各位读者,总结如有错误请指正,谢谢。
2.1 常见指令
2.1.1堆栈相关命令
Push:把一个32位操作压入堆栈中。Esp -4,我们认为栈顶是最小的区域;
Pop:与push相反,一个数据出栈;
sub:减法:第一个是被减数的寄存器,第二个参数是减数;
Add:加法:
Ret:返回,自动的弹出返回地址;
Call:调用函数。将下一条指令的地址压入堆栈中,然后指向它调用的函数的开头;
区别:call相当于Push 下条指令 + jump 进入函数;ret相当于 pop + jump;
2.1.2数据传输指令
Mov: 数据移动,第一个是目的,第二个是来源;
Xor:异或,xor eax,eax 常用来代替mov eax,0.
Lea: 取地址(第二个参数)后放入到前面的寄存器(第一个参数)中。
因此:lea edi,[ebp - 0cch] 相当于 mov edi,ebp - 0cch 但是mov不支持后一个操作数写成寄存器减去数字,但是lea支持,所以用lea 代替了mov。
Stos:串存储指令,处理4个字节;将eax中的数据存放如edi所指的地址中,同时edi会增加4字节;
Stow:处理word 2个字节;
Stob:处理byte 1个字节;
Rep:指指令重复执行ecx 中填写的次数。
2.1.2数据传输指令:
Mov: 数据移动,第一个是目的,第二个是来源;
Xor:异或,xor eax,eax 常用来代替mov eax,0.
Lea: 取地址(第二个参数)后放入到前面的寄存器(第一个参数)中。
因此:lea edi,[ebp - 0cch] 相当于 mov edi,ebp - 0cch 但是mov不支持后一个操作数写成寄存器减去数字,但是lea支持,所以用lea 代替了mov。
Stos:串存储指令,处理4个字节;将eax中的数据存放如edi所指的地址中,同时edi会增加4字节;
Stow:处理word 2个字节;
Stob:处理byte 1个字节;
Rep:指指令重复执行ecx 中填写的次数。
其他命令:
Shr: 逻辑右移 相反的shl
Sar: 算数右移
两者区别:SAR右移的时候保留操作数的符号,即使用符号位来补足,而SHR右移时总是用0来补足;我们在编码的过程中,右移动时高位补零还是补1是变量的类型决定的;
2.2 例子: 局部变量赋值对应的汇编代码
#include “stdafx.h”
int main(int argc, _TCHAR* argv[])
{
int Var1 = 0;
return 0;
}
对应的汇编:
逐句汇编的解释:
#include "stdafx.h"
int _tmain(int argc, _TCHAR* argv[])
{
00D81370 push ebp //保存ebp,
00D81371 mov ebp,esp //esp 栈顶位置赋值给ebp;ebp = esp
ebp保存这个函数执行之前的esp
00D81373 sub esp,0CCh //分配栈空间,esp - 0xcc保存局部变量
00D81379 push ebx //保存ebx
00D8137A push esi //保存esi
00D8137B push edi //保存edi
00D8137C lea edi,[ebp-0CCh] //edi = ebp -0xcc ,其实edi = esp
本来是用mov,但是mov不支持-操作。
00D81382 mov ecx,33h //ecx = 33 分配33个4字节空间。
00D81387 mov eax,0CCCCCCCCh //eax = 0xCCCCCCCC 每四字节赋值为OXcccccccc
00D8138C rep stos dword ptr es:[edi] //rep重复33次,写入occh指令(中断指令)
int Var1 = 0;
00D8138E mov dword ptr [Var1],0 //变量赋值
return 0;
00D81395 xor eax,eax //异或清零
}
00D81397 pop edi //恢复edi、esi、ebx
00D81398 pop esi
00D81399 pop ebx
00D8139A mov esp,ebp //函数执行完后,用ebp来恢复esp
00D8139C pop ebp
00D8139D ret
总结:
1、从这里可以看出函数调用是压入了好多寄存器值,返回的时候做逆向操作。
2、先做保存现场,再做堆栈的初始化(这里初始化为CC),然后再去做局部变量的复制操作,最后退出来;
3、堆栈的初始化过程,是先esp - 0xcc(栈顶向上减0xCC),然后用rep来操作edi (esp的地址)向下每四个字节初始化为0xCCCCCCCC(eax) ,初始化33次(ecx).
小技巧:函数头饰固定的,这个可以作为我们在IDA中确定函数的头;
2.3 C语言流程到对应的汇编
2.3.1 for循环
对应的反汇编:
for(i = 0; i < 50 ;i++)
00192DD7 mov dword ptr [i],0 //mov初始化变量i
00192DDE jmp myfunction2+39h (192DE9h) //jmp跳过修改循环变量的代码
00192DE0 mov eax,dword ptr [i] //局部变量
00192DE3 add eax,1 //i++
00192DE6 mov dword ptr [i],eax //eax的值赋值给局部变量i
00192DE9 cmp dword ptr [i],32h //cmp 实现条件判断I 与 50比较
00192DED jge myfunction2+4Ah (192DFAh) //大于等于50就跳转到结束,否者顺序执行
{
c = c + i;
00192DEF mov eax,dword ptr [c]
00192DF2 add eax,dword ptr [i] //
00192DF5 mov dword ptr [c],eax //改变局部变量C的值
}
00192DF8 jmp myfunction2+30h (192DE0h)
return c;
00192DFA mov eax,dword ptr [c]
基本结构图为:
总结:使用jmp来实现跳转循环;
2.3.2 do循环
反汇编为:
do
{
c = c +i;
001C17EE mov eax,dword ptr [c]
001C17F1 add eax,dword ptr [i]
001C17F4 mov dword ptr [c],eax
}while(c < 50);
001C17F7 cmp dword ptr [c],32h
001C17FB jl myfunctionDo+2Eh (1C17EEh)
2.3.3 while 循环
对应的反汇编:
while(c < 50)
00B42E3E cmp dword ptr [c],32h
00B42E42 jge myfunctionWhile+3Fh (0B42E4Fh)
{
c = c +i;
00B42E44 mov eax,dword ptr [c]
00B42E47 add eax,dword ptr [i]
00B42E4A mov dword ptr [c],eax
}
00B42E4D jmp myfunctionWhile+2Eh (0B42E3Eh)
return c;
00B42E4F mov eax,dword ptr [c]
2.3.4 if -else
对应的反汇编:
if (c == 10 && c < 11)
00A43067 cmp dword ptr [c],0Ah
00A4306B jne myfunctionIfElse+40h (0A43080h)
00A4306D cmp dword ptr [c],0Bh
00A43071 jge myfunctionIfElse+40h (0A43080h)
{
c = c/2;
00A43073 mov eax,dword ptr [c]
00A43076 cdq
00A43077 sub eax,edx
00A43079 sar eax,1
00A4307B mov dword ptr [c],eax
00A4307E jmp myfunctionIfElse+5Ch (0A4309Ch)
}
else if (c == 8 || c == 6)
00A43080 cmp dword ptr [c],8
00A43084 je myfunctionIfElse+4Ch (0A4308Ch)
00A43086 cmp dword ptr [c],6
00A4308A jne myfunctionIfElse+55h (0A43095h)
{
c = 4/2;
00A4308C mov dword ptr [c],2
00A43093 jmp myfunctionIfElse+5Ch (0A4309Ch)
}
else c = 100;
00A43095 mov dword ptr [c],64h
return c;
00A4309C mov eax,dword ptr [c]
总结:
1、 If判断是使用了cmp 再加上跳转指令;
2、 在开始的地方都有一条无条件跳转指令,跳转到判断的结束处,阻止情面的分支执行结束后,直接进入到这个分支的可能;(黄色的部分)
3、 Else 则是在jmp之后直接执行操作。
2.3.5 switch case
反编译结果:
int d,e;
switch(c)
00E93477 mov eax,dword ptr [c]
00E9347A mov dword ptr [ebp-0E8h],eax
00E93480 cmp dword ptr [ebp-0E8h],0
00E93487 je myfunctionSwitchCase+44h (0E93494h)
00E93489 cmp dword ptr [ebp-0E8h],1
00E93490 je myfunctionSwitchCase+55h (0E934A5h)
00E93492 jmp myfunctionSwitchCase+64h (0E934B4h)
{
case 0:
d = c++;
00E93494 mov eax,dword ptr [c]
00E93497 mov dword ptr [d],eax
00E9349A mov ecx,dword ptr [c]
00E9349D add ecx,1
00E934A0 mov dword ptr [c],ecx
break;
00E934A3 jmp myfunctionSwitchCase+6Dh (0E934BDh) //如果有break,那么就多一条jmp
case 1:
e = ++c;
00E934A5 mov eax,dword ptr [c]
00E934A8 add eax,1
00E934AB mov dword ptr [c],eax
00E934AE mov ecx,dword ptr [c]
00E934B1 mov dword ptr [e],ecx
default:
c = c+1;
00E934B4 mov eax,dword ptr [c]
00E934B7 add eax,1
00E934BA mov dword ptr [c],eax
};
return c;
00E934BD mov eax,dword ptr [c]
总结:
1、 switch 的特点是有多个判断,且因为switch 只能用je 做判断;
2、 连续的比较与条件就让人联想到switch;
2.3.6 C语言的数组和结构体访问
对应的汇编语句是:
unsigned char *buf[100];
mystruct *strs = (mystruct *) buf;
008A34EE lea eax,[buf]
008A34F4 mov dword ptr [strs],eax
int i;
for(i =0;i<5;i++)
008A34FA mov dword ptr [i],0
008A3504 jmp myfunctionStruct+45h (8A3515h)
008A3506 mov eax,dword ptr [i]
008A350C add eax,1
008A350F mov dword ptr [i],eax
008A3515 cmp dword ptr [i],5
008A351C jge myfunctionStruct+94h (8A3564h)
{
strs[i].a = 0;
008A351E mov eax,dword ptr [i] //目的是把i*0ch放入到eax中。
008A3524 imul eax,eax,0Ch //i*och 是结构的大小,由编译器生成
008A3527 mov ecx,dword ptr [strs] //把strs 的地址(基址)存入到ecx中
008A352D mov dword ptr [ecx+eax],0 //计算得到strs[i]的地址,并赋0
strs[i].b = 1;
008A3534 mov eax,dword ptr [i]
008A353A imul eax,eax,0Ch
008A353D mov ecx,dword ptr [strs]
008A3543 mov dword ptr [ecx+eax+4],1 //这里增加偏移量获取b的地址;
strs[i].c = 2;
008A354B mov eax,dword ptr [i]
008A3551 imul eax,eax,0Ch
008A3554 mov ecx,dword ptr [strs]
008A355A mov dword ptr [ecx+eax+8],2
}
008A3562 jmp myfunctionStruct+36h (8A3506h) //典型的循环结束
return 0;
008A3564 xor eax,eax
总结:
1、imul指令让人联想到结构体数组,这是个特征;
某个元素的起始地址 = 下标*单个元素的长度 + 数组的起始地址;
2、常量很重要;结构体的大小可以通过常量来确定;
2.3.7 共用体和枚举
其实和结构体访问是类似的;
2.4 函数约定:C函数的参数传递 到汇编
C语言程序通过堆栈把参数从函数外部传入到函数内部;在堆栈中划分区域来容纳函数的内部变量;
使用指令:Push、Pop
7.3.1 C语言的调用方式:堆栈总是调用方把参数反序(从右往左)地压入堆栈中,被调用方把堆栈复原。Call指令和ret指令只是为了调用的方便而已,绝对不是函数存在的绝对证据,即使我们仅仅使用jmp 并自己操作堆栈,一样可以实现函数的功能,一般有三种方式:
_cdecl C调用规则:
1、 参数从右往左进入堆栈;
2、 函数返回后,调用者要负责清除堆栈,所以这种调用方式常会生成较大的可执行程序;
适合可变参数函数:例如printf
_stdcall 又称为WINAPI:
1、 参数从右往左进入堆栈;
2、 函数返回后,被调用者要负责清除堆栈(栈平衡),所以生成 的代码比cdecl 小;
适合参数固定的函数,体积小,速度快;
最后一句一定是:ret 12;返回并在栈区清除12字节;
_fastcal 快速调用方式:….
是stdcal的一种变种,传递给函数的前两个参数存于寄存器 Ecx、edx 中,剩余的按照stdcal的方式来进行入栈;也是由被调用者负责清除堆栈;
_thiscall 方式(C++的调用方式):…
与_stdcall大致相同,Ecx 传递this指针;
This指针作为任何非静态成员的第一个隐含参数;
Pascall:
1、 参数从左往右进入堆栈;
2、 函数返回后,被调用者要负责清除堆栈
3、 不支持可变参数的函数调用;
注意:pascal 在Win16里面的规则如上,但是在Win32里面和stdCall调用方式一样;
重要:调用方式在写Hook工具 和 IDA识别约定错误 时候是十分要注意的;
总结注意点:
1、 在windows中,不管哪种调用方式都是返回值放在eax中,然后放返回,外部从eax中获取;
2、 重点观察涉及call、ret、push、pop、ebp、esp 的指令,就能看到C语言函数的调用过程;
3、 定位函数头:必有存储环境,初始化的内容;