C/C++之函数体hack(下)

电子说

1.3w人已加入

描述

2、条件转移指令

指令的汇编格式及功能根据条件码的值转移:

ja 大于时跳转
jae 大于等于
jb 小于
jbe 小于等于
je 相等
jna 不大于
jnae 不大于或者等于
jnb 不小于
jnbe 不小于或等于
jne 不等于
jg 大于(有符号)
jge 大于等于(有符号)
jl 小于(有符号)
jle 小于等于(有符号)
jng 不大于(有符号)
jnge 不大于等于(有符号)
jnl 不小于
jnle 不小于等于
jns 无符号
jnz 非零
js 如果带符号
jz 如果为零

例子:

mov eax, 1
    cmp eax, 100
    jle xiao_deng_yu_100
    sub eax, 20

xiao_deng_yu_100:
    add eax, 1
    ret

这个例子中就是让eax中存储的值和100比较,如果小于等于则跳转到xiao_deng_yu_100处。 从这个例子也可以看出:

    1. 条件转移一般会和cmp比较指令配合使用,因为比较指令会改变状态寄存器中的标志位,而jle等跳转指令回去状态寄存器中读取这些标志位
  • 2.跳转指令不会返回到原来的指令地址处,后面讲解函数跳转的时候可以看到会返回到原来的指令地址处,根据返回地址。

条件跳转指令这么多,怎么记住呢?这里面是有套路的: 首先,跳转指令的前面都是字母j,关键是j后面的的字母,比如j后面是ne,对应的是jne跳转指令,n和e分别对应not和equal,也就是“不相等”,也就是说在比较指令的结果为“不想等”的时候,就会跳转。

a: above
e: equal
b: below
n: not
g: greater
l: lower
s: signed
z: zero

好了,这里列出来了j后面的字母所对应的含义。根据这些字母的组合,和上述大概的规则,你就能清楚怎么写出这些跳转指令了。当然,这里有“有符号”和“无符号”之分,后面有机会再扯,读者也可以自行了解。

5.堆栈操作指令

我们知道,在一个函数作用域中会使用到一些寄存器,如果在这个函数中又调用了另外一个函数,那这些寄存器信息可能会被覆盖掉,怎么办呢? 首先CPU会在内存中开辟一块空间,叫栈空间,CPU将这些寄存器压入到栈中,叫入栈,待另外一个函数返回时,再将当前栈中的信息恢复回来,叫出栈。

1.入栈指令push

格式:push src
功能: 把数据src压入到栈中
注意:src可以是寄存器或者内存中的数据。

2.出栈指令pop

格式:pop dst
功能: 把数据弹出到到栈中
注意:dst可以是通用寄存器和段寄存器,但不能是CS,可以是字存储单元。

6.函数调用跳转指令

格式:call   标号
功能:跳转到另外一个函数去执行。

举例:

eax_plus_1s:
    add eax, 1
    ret
main:
    mov eax, 0
    call eax_plus_1s
    ret

这里的eax_plus_1s就是一个函数,使用call跳转

call方法执行之前CPU还会做一个动作,就是将当前eip保存起来,然后再去跳转,这是为了函数执行完成后可以找到回来执行的地址。

好了。有了以上基础后,我们再来看C/C++中的hack环节。

hack

那我们就拿前面的举得例子来讲解下hack过程

1.前增++i和后增i++

我们打开vs中输入下面一段代码:

int  main()
{
    int a = 0;
    int b = ++a;
    return 0;
}

如何反汇编呢, 在main的两个括号{}处打两个断点,然后执行程序,右击选择反汇编 。 得到的反汇编代码如下:

int  main()
//段1:初始化
{
001947C0  push        ebp  
001947C1  mov         ebp,esp  
001947C3  sub         esp,0D8h  
001947C9  push        ebx  
001947CA  push        esi  
001947CB  push        edi  
001947CC  lea         edi,[ebp-18h]  
001947CF  mov         ecx,6  
001947D4  mov         eax,0CCCCCCCCh  
001947D9  rep stos    dword ptr es:[edi]  
001947DB  mov         ecx,offset _BCEF6C65_20221229-demo-cAndCpp@cpp (01A8052h)  
001947E0  call        @__CheckForDebuggerJustMyCode@4 (0173F1Eh)  
//段2:
    int a = 0;
001947E5  mov         dword ptr [a],0  
    int b = ++a;
001947EC  mov         eax,dword ptr [a]  
001947EF  add         eax,1  
001947F2  mov         dword ptr [a],eax  
001947F5  mov         ecx,dword ptr [a]  
001947F8  mov         dword ptr [b],ecx  
    return 0;
001947FB  xor         eax,eax  
//段3:反初始化
}
001947FD  pop         edi  
001947FE  pop         esi  
001947FF  pop         ebx  
00194800  add         esp,0D8h  
00194806  cmp         ebp,esp  
00194808  call        __RTC_CheckEsp (0173AC8h)  
0019480D  mov         esp,ebp  
0019480F  pop         ebp  
00194810  ret

这段汇编代码怎么读呢?由于main方法中没有其他函数调用,所以CPU会按顺序执行这段代码。 我们将代码分为3段: 段1:首先来看前面这段代码:

001947C0  push        ebp  
001947C1  mov         ebp,esp  
001947C3  sub         esp,0D8h  
001947C9  push        ebx  
001947CA  push        esi  
001947CB  push        edi  
001947CC  lea         edi,[ebp-18h]  
001947CF  mov         ecx,6  
001947D4  mov         eax,0CCCCCCCCh  
001947D9  rep stos    dword ptr es:[edi]  
001947DB  mov         ecx,offset _BCEF6C65_20221229-demo-cAndCpp@cpp (01A8052h)  
001947E0  call        @__CheckForDebuggerJustMyCode@4 (0173F1Eh)

这段代码不用管太多,只是对函数栈的一个初始化的操作,ebp指向栈底,esp指向栈底,还有一些就是当前栈的基地址,返回地址等等信息。因为函数中可能还会调用函数,所以每个函数调用都会有自己的栈,也叫函数的栈帧,函数的深度就代表栈深,但是CPU中的寄存器又是共享的,函数a在使用这些寄存器的时候,函数b又要使用了,那怎么办?方法就是 将寄存器中的数值压入栈帧中保存起来,等函数b结束后,再从栈帧中恢复起来就好了

下面看重点代码:

段2:
    int a = 0;
001947E5  mov         dword ptr [a],0  //1
    int b = ++a;
001947EC  mov         eax,dword ptr [a]  //2
001947EF  add         eax,1  //3
001947F2  mov         dword ptr [a],eax  //4
001947F5  mov         ecx,dword ptr [a]  //5
001947F8  mov         dword ptr [b],ecx  //6
    return 0;
001947FB  xor         eax,eax  //7

分析:

  • 1.mov指令将0赋值到内存中的一个a变量地址中。
  • 2.mov指令将变量a地址中的值赋值给eax,此时eax变为0.
  • 3.add指令给eax+1,此时eax变为了1.
  • 4.mov指令将eax的值传递到变量a中,此时a变为了1.
  • 5.mov指令将a中的值传递给寄存器ecx,此时ecx值变为1.
  • 6.mov指令将ecx的值传递给变量b地址,这样b就变为了1。

以上过程需要注意点:

a的值变化是在b的值变化之前,所以++a是先a+1,然后再将a+1赋值给b。

下面我们再来看下这段代码:

int  main()
{
    int a = 0;
    int b = a++;
    return 0;
}

反汇编后:同样找到关键代码:

int a = 0;
00AB47E5  mov         dword ptr [a],0  //1
    int b = a++;
00AB47EC  mov         eax,dword ptr [a]  //2
00AB47EF  mov         dword ptr [b],eax  //3
00AB47F2  mov         ecx,dword ptr [a]  //4
00AB47F5  add         ecx,1  //5
00AB47F8  mov         dword ptr [a],ecx  //6
    return 0;
00AB47FB  xor         eax,eax  
}

分析:

  • 1.给a赋值为0
  • 2.将a传递给eax,此时eax变为了0
  • 3.将eax传递给b,此时b变为了0
  • 4.将a值传递给ecx,此时ecx变为了0
  • 5.给ecx+1,此时ecx变为了1
  • 6.将ecx值传递给a,此时a变为了1

这个过程可以看到CPU是先赋值给b,然后再让a进行+1的操作。

通过对a++和++a的分析,也可以看到hack可以让我们了解底层是如何执行我们写的代码的。

下面我们再来看一个函数调用过程:

2.函数体hack过程

int maxab(int a,int b) {
    return a > b ? a : b;
}
int  main()
{
    maxab(3, 4);
    return 0;
}

反汇编后:

maxab(3, 4);
00A347E5  push        4  //1
00A347E7  push        3  //2
00A347E9  call        maxab (0A13794h)  //3

00A347EE  add         esp,8  //4


int maxab(int a,int b) { 
    return a > b ? a : b;
00A32685  mov         eax,dword ptr [a]  //5
00A32688  cmp         eax,dword ptr [b]  //6
00A3268B  jle         __$EncStackInitStart+2Ch (0A32698h)  //7
00A3268D  mov         ecx,dword ptr [a]  //8
00A32690  mov         dword ptr [ebp-0C4h],ecx  //9
00A32696  jmp         __$EncStackInitStart+35h (0A326A1h)  //10
00A32698  mov         edx,dword ptr [b]  //11
00A3269B  mov         dword ptr [ebp-0C4h],edx  //12
00A326A1  mov         eax,dword ptr [ebp-0C4h]  //13
}

分析:

  • 1和2处将立即数4和3压入到栈内。注意他是先push的右边参数4,再push的左边参数3。
  • 3处调用call跳转到maxab函数处,注意5处以后这个时候是处于另外一个函数栈内了。
  • 5处将变量a的值3传递给eax,a的值是怎么得到的呢?看前面压栈操作是先压入右边再压入左边,根据FILO规则,此时先找到的参数就是先出左边3,再出右边4.
  • 6处让eax也就是前面的a的值和b的值进行比较,比较结果会写入到状态寄存器中。
  • 7处的jle是小余等于就跳转,这个比较结果是在状态寄存器中获取到的,a是3,b是4,所以结果就是小余咯。条件成功跳转到指定的0A32698h位置。0A32698h指向11处:00A32698 mov edx,dword ptr [b] //11
  • 11处使用mov指令将b中的值传递给edx。
  • 然后在12处将edx存入到ebp-0C4h位置,ebp是栈底,根据栈的特点,ebp-C4H其实是一个往栈顶走的一个操作,如果你观察仔细,可以看到其实就是栈顶。
  • 最终在13处将ebp-0C4h处的值传递给eax,最终返回eax。 前面也说过所有函数都使用eax进行返回。

上面讲解了一个max函数的hack过程,如果你对hack过程还比较模糊,还可以使用vs提供的 内存监视器查看具体内存变化

hacker

好了,关于函数的hack分析过程就这里了,大部分操作其实还是要理解汇编一些基本指令以及CPU和内存,栈的模型等。

总结

本篇文章主要讲解了hack的定义以及hack过程中需要了解的几个基本知识: 如 寄存器,内存,CPU以及一些基本汇编指令 。并使用两个例子来讲解如何在C/C++中进行hack的过程。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分