开始学习逆向,从熟悉的js、php跳跃到汇编,有点小凌乱 ...
先来OD一段简单的C++代码:
#include <string.h>
#include <windows.h>
#include <stdio.h>
int funcA(int a, int b);
void main()
{
int a;
int b;
int n;
a = strlen("abc");
b = 10;
n = funcA(a, b);
}
int funcA(int a, int b)
{
int c;
char sBuff[10];
c = (a + b) * a * b;
sprintf(sBuff, "%d", c);
MessageBox(NULL, sBuff, NULL, MB_OK);
return c;
}
编译环境:VC6 ,release
这段代码逻辑上很简单,一共两个函数,main和funcA,funcA会在main中被调用一次。main函数的入口会被编译成call 00401000,00401000是虚拟的偏移量。虚拟地址也可写作“段:偏移量”的形式 ,一般来说可以不用关注段,因为同一个程序在不同的系统下,段值可能不相同。但是偏移量却是有着一些共性,对于同一个程序的某条指令,在不同系统中它的偏移量一般是相同的。按照VS的默认设置,编译出的exe文件基地址是00400000,dll文件基地址是10000000。
先来直接在main的入口处下断,然后逐步跟进。
00401154 |. 50 push eax
00401155 |. FF35 0C994000 push dword ptr [40990C]
0040115B |. FF35 08994000 push dword ptr [409908]
00401161 |. E8 9AFEFFFF call 00401000
00401166 |. 83C4 0C add esp, 0C
这是一个典型的函数调用,只不过调的是main罢了。三个参数压栈,然后call,完了消栈,格式很清晰。
main中的汇编代码如下:
00401000 /$ 57 push edi
00401001 |. BF 30704000 mov edi, 00407030 ; ASCII "abc"
00401006 |. 83C9 FF or ecx, FFFFFFFF
00401009 |. 33C0 xor eax, eax
0040100B |. F2:AE repne scas byte ptr es:[edi]
0040100D |. F7D1 not ecx
0040100F |. 49 dec ecx
00401010 |. 6A 0A push 0A
00401012 |. 51 push ecx
00401013 |. E8 08000000 call 00401020 ; 该处调用funcA函数
00401018 |. 83C4 08 add esp, 8
0040101B |. 5F pop edi
0040101C \. C3 retn
strlen函数并没有向funcA一样被编译为另外一个call,而是直接编译在了main函数之中。strlen的实现非常之精妙:
这是strlen()在VC优化编译模式下编译后的代码:
00401000 /$ 57 push edi ; 因为后面repne的时候要发生更改,所以先暂存edi
00401006 |. 83C9 FF or ecx, FFFFFFFF
00401009 |. 33C0 xor eax, eax
0040100B |. F2:AE repne scas byte ptr es:[edi]
0040100D |. F7D1 not ecx
0040100F |. 49 dec ecx
edi是指向字符串“abc”的指针,repne scas byte ptr es:[edi] 指令会扫描字符串“abc”(在内存中为 61 62 63 00),从字母a开始,一直到字符串结束符,一共4个字符。
REPNE :用在CMPS、SCAS指令前,每执行一次串操作指令ECX减1,并判断ZF标志是否为1,只要CX=0或ZF=1,则重复执行结束。
SCAS :将附加段中的字节或字内容与AL/AX寄存器内容进行比较,根据比较的结果设置标志,每次比较后修改EDI寄存器的值,使之指向下一个元素。
这里需要扫描4次,每次扫描的字符与eax中的低八8位即00相比较,如果相同,则ZF标志位变成1,扫描结束;如果不同,则ecx减1并继续扫描。因此需要事先将eax置0,并且ecx置为-1。扫描完之后的ecx变为FFFFFFFB。取反后变成00000004,这是包含了字符串结束标志的长度,因此还需要减1.
随后将10、3入栈,准备调用funcA函数。函数的调用基本上都满足_stdcall约定 ,即参数从右向左,顺次压栈,函数调用的返回值保存在eax函数中。funcA的汇编码如下:
虽然有点长,但是代码还是很好理解的。
开始的两句mov是去栈中取操作数3、10,并且分别存放到eax、ecx中。
随后,sub esp,0xC是将栈顶向上移动,从而开辟出一块空的区域,可以用作预留用。
0040102C |. 8D3408 lea esi,dword ptr ds:[eax+ecx]
0040102F |. 0FAFF0 imul esi,eax
00401032 |. 0FAFF1 imul esi,ecx
这三句完成了 c = (a + b) * a * b ,运算结果的值保存在esi中。
随后调用的MessageBox的过程暂时忽略。
因为最后需要返回c的值,因此在retn之前,需要执行一句mov eax,esi。
这里的call调用都没有被编译成经典的:
PUSH ebp
MOV ebp esp
······
这个无所谓,值得注意的是需要确保栈的平衡性,即入栈一定要与消栈相对应。