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处。 从这个例子也可以看出:
-
- 条件转移一般会和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提供的 内存监视器查看具体内存变化 :
好了,关于函数的hack分析过程就这里了,大部分操作其实还是要理解汇编一些基本指令以及CPU和内存,栈的模型等。
总结
本篇文章主要讲解了hack的定义以及hack过程中需要了解的几个基本知识: 如 寄存器,内存,CPU以及一些基本汇编指令 。并使用两个例子来讲解如何在C/C++中进行hack的过程。
-
C++
+关注
关注
22文章
2108浏览量
73618 -
汇编代码
+关注
关注
0文章
23浏览量
7548 -
hacker
+关注
关注
0文章
4浏览量
1361
发布评论请先 登录
相关推荐
评论