首先感谢小生我怕怕大神,这个是他的随笔,作为一个业余爱好者,确实挺喜欢这门技术,所以做了这个系列的转载,后面会慢慢更新!
不过时间有限,一边考研,为了给自己枯燥的生活带来点乐趣,故选择随便看些东西!
————————————————————————————————————————————————
分析环境:WIN7sp1所用工具:VC++6.0/OllyDBG/IDA
适用人群:有一定计算机基础,熟悉C/C++编程,熟悉X86系列汇编/了解OD/IDA等调试工具使用,对逆向安全有极大兴趣者!
————————————————————————————————————————————————
开篇前言:
1.何为反汇编?简单来说就是通过读取可执行文件的二进制代码,将其还原为汇编代码的过程。
2.何为逆向分析? 再简单来说就是在只有可执行文件汇编代码的情况下,利用逆向思维来还原程序本身的意图和行为等。
3.两者的用处?主要用于软件破解、WG分析、病毒分析、漏洞分析、软件逆向、软件汉化、底层技术等领域。
(注:逆向工程分析不仅仅是一门学科,还是一门艺术,逆向的艺术!想战胜它往往需要极大费精力、时间、毅力等。这其中还涉及密码学、社会工程学、数学算法,以及软件工程、网络通讯、操作系统底层及硬件等,但付出总是和回报成正比...)
————————————————————————————————————————————————
正文部分:
本帖隐藏的内容
首先了解下VC的两种编译方式:Debug和Relese 也就是我们常说的调试版和发布版,那具体这两种编译方式有什么区别呢?往下面看:Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
上面的解释可能太生硬,那我们就先做个例子来对比下!程序就选择我们最熟悉的Hello World,下面我们就看下两个世界的Hello World...
源码:
————————————————————————————————————————————————
- #include<stdio.h>
- void main()
- {
- printf("Hello World");
- }
————————————————————————————————————————————————
我们使用两种不同的编译方式编译生产后载入到OD查看,先看Debug版本的:载入OD后,停留在00401120处,也就是OEP,先看反汇编代码,注意着色地方
————————————————————————————————————————————————
00401120 >/$ 55 push ebp ; 程序OEP(Initial CPU selection)
00401121 |. 8BEC mov ebp,esp
00401123 |. 6A FF push -0x1
00401125 |. 68 38214200 push Test.00422138
0040112A |. 68 D0414000 push Test._except_handler3 ; SE 处理程序安装
0040112F |. 64:A1 0000000>mov eax,dword ptr fs:[0]
00401135 |. 50 push eax
00401136 |. 64:8925 00000>mov dword ptr fs:[0],esp
0040113D |. 83C4 F0 add esp,-0x10
00401140 |. 53 push ebx
00401141 |. 56 push esi
00401142 |. 57 push edi
00401143 |. 8965 E8 mov [local.6],esp
00401146 |. FF15 4CA14200 call dword ptr ds:[<&KERNEL32.GetVersion>; kernel32.GetVersion
0040114C |. A3 707C4200 mov dword ptr ds:[_osver],eax
00401151 |. A1 707C4200 mov eax,dword ptr ds:[_osver]
00401156 |. C1E8 08 shr eax,0x8
00401159 |. 25 FF000000 and eax,0xFF
0040115E |. A3 7C7C4200 mov dword ptr ds:[_winminor],eax
00401163 |. 8B0D 707C4200 mov ecx,dword ptr ds:[_osver]
00401169 |. 81E1 FF000000 and ecx,0xFF
0040116F |. 890D 787C4200 mov dword ptr ds:[_winmajor],ecx
00401175 |. 8B15 787C4200 mov edx,dword ptr ds:[_winmajor]
0040117B |. C1E2 08 shl edx,0x8
0040117E |. 0315 7C7C4200 add edx,dword ptr ds:[_winminor]
00401184 |. 8915 747C4200 mov dword ptr ds:[_winver],edx
0040118A |. A1 707C4200 mov eax,dword ptr ds:[_osver]
0040118F |. C1E8 10 shr eax,0x10
00401192 |. 25 FFFF0000 and eax,0xFFFF
00401197 |. A3 707C4200 mov dword ptr ds:[_osver],eax
0040119C |. 6A 00 push 0x0
0040119E |. E8 BD2D0000 call Test._heap_init
004011A3 |. 83C4 04 add esp,0x4
004011A6 |. 85C0 test eax,eax
004011A8 |. 75 0A jnz short Test.004011B4
004011AA |. 6A 1C push 0x1C
004011AC |. E8 CF000000 call Test.fast_error_exit
004011B1 |. 83C4 04 add esp,0x4
004011B4 |> C745 FC 00000>mov [local.1],0x0
004011BB |. E8 A0270000 call Test._ioinit
004011C0 |. FF15 48A14200 call dword ptr ds:[<&KERNEL32.GetCommand>; [GetCommandLineA
004011C6 |. A3 04964200 mov dword ptr ds:[_acmdln],eax
004011CB |. E8 70250000 call Test.__crtGetEnvironmentStringsA
004011D0 |. A3 487C4200 mov dword ptr ds:[_aenvptr],eax
004011D5 |. E8 56200000 call Test._setargv
004011DA |. E8 011F0000 call Test._setenvp
004011DF |. E8 1C1B0000 call Test._cinit
004011E4 |. 8B0D 8C7C4200 mov ecx,dword ptr ds:[_environ]
004011EA |. 890D 907C4200 mov dword ptr ds:[__initenv],ecx
004011F0 |. 8B15 8C7C4200 mov edx,dword ptr ds:[_environ]
004011F6 |. 52 push edx ;第一个参数
004011F7 |. A1 847C4200 mov eax,dword ptr ds:[__argv]
004011FC |. 50 push eax ;第二个参数
004011FD |. 8B0D 807C4200 mov ecx,dword ptr ds:[__argc]
00401203 |. 51 push ecx ;第三个参数
00401204 |. E8 FCFDFFFF call Test.00401005 ;main函数入口点
00401209 |. 83C4 0C add esp,0xC ; (Initial CPU selection)
0040120C |. 8945 E4 mov [local.7],eax
0040120F |. 8B55 E4 mov edx,[local.7]
00401212 |. 52 push edx ; /status
00401213 |. E8 281B0000 call Test.exit ; \exit
————————————————————————————————————————————————
初学者,看到这些是不是很晕?不要紧我们细细品味:
1.这是VC++6.0典型的入口标志,大家最好熟记,感兴趣的同学可以对比下其他语言编译器的入口特征,例如:VB,E语言,Win32Asm等,熟悉入口特征在以后的逆向脱壳中,对于寻找程序oep也很有帮助!
2.我们分析的重点在main函数,上面的汇编代码主要是VC++6.0初始化启动的过程,我们大概了解下即可,看下我标绿的几个函数,即启动函数,这里借用官方语言了解下:
1.GetVersion函数:获取当前运行平台的版本号。
2._heap_init函数:用于初始化堆空间。在函数实现中使用HeapCreate申请堆空间
3.GetCommandLineA函数:获取命令行参数信息的首地址
4._crtGetEnvironmentStringA函数:获取环境变量信息的首地址
5._setargv函数:此函数根据GetCommandLineA获取命令行参数信息的首地址并进行参数分析
6._setenvp函数:此函数根据_crtGetEnvironmentStringA函数获取环境变量信息的首地址进行分析。
7._cinit函数:用于全局变量数据和浮点数寄存器的初始化。
以上都是编译器帮我们自动完成,有兴趣的同学so一下,越过初始化启动过程来到main函数,即 00401204 处,很多人问,你怎么知道这里是main函数?一般在完成初始化启动操作以后,下面有3个连续push操作的call就应该是main函数,理由很简单,main函数有三个参数
下面开始OD调试,首先从OEP处一步一步F8跟下去,不要F7(除非你想了解启动函数具体过程),否则你将循环在无尽的call与retn中...我们F8一直执行到main函数处,即00401204处:
————————————————————————————————————————————————
004011F6 |. 52 push edx ;第一个参数
004011F7 |. A1 847C4200 mov eax,dword ptr ds:[__argv]
004011FC |. 50 push eax ;第二个参数
004011FD |. 8B0D 807C4200 mov ecx,dword ptr ds:[__argc]
00401203 |. 51 push ecx ;第三个参数
00401204 |. E8 FCFDFFFF call Test.00401005 ;main函数入口点
————————————————————————————————————————————————
上面的三个连续push就是mian函数的三个参数,这里不多解释,这时我们注意2个寄存器的值,首先EIP指向00401204,esp值为0012FF50,OK,这时可以F7了,进去以后,如下:
————————————————————————————————————————————————
00401005 /$ /E9 06000000 jmp Test.main
0040100A | |CC int3
0040100B | |CC int3
0040100C | |CC int3
0040100D | |CC int3
0040100E | |CC int3
0040100F | |CC int3
00401010 >|> \55 push ebp
00401011 |. 8BEC mov ebp,esp
00401013 |. 83EC 40 sub esp,40
00401016 |. 53 push ebx
00401017 |. 56 push esi
00401018 |. 57 push edi
00401019 |. 8D7D C0 lea edi,[local.16]
0040101C |. B9 10000000 mov ecx,10
00401021 |. B8 CCCCCCCC mov eax,CCCCCCCC
00401026 |. F3:AB rep stos dword ptr es:[edi]
00401028 |. 68 1C204200 push 42201C
00401032 |. 83C4 04 add esp,4
00401035 |. 5F pop edi
00401036 |. 5E pop esi
00401037 |. 5B pop ebx
00401038 |. 83C4 40 add esp,40
0040103B |. 3BEC cmp ebp,esp
0040103D |. E8 9E000000 call 004010E0
00401042 |. 8BE5 mov esp,ebp
00401044 |. 5D pop ebp
00401045 \. C3 retn————————————————————————————————————————————————
F7跟进来以后,直接停留到:00401005 /$ /E9 06000000 jmp Test.main
好奇的人肯定会问,为什么不直接指向00401010呢,多一处无条件跳转为何?这里应该是考虑到冗余的问题,毕竟这里是debug版本,当然利用这个空隙可做很多很多事情...至于为何用int 3来填充,是 因为需要保护这段空间,int 3即中断
开始详细分析,先来到第一句汇编指令00401005处,此处是一个无条件跳转:jmp _main 此句什么意思呢?其实这句等价于 jmp 00401010 因为此处的_main只是作为一个地址标识符,他指向的地址就是00401010处,相当于我们使用寄存器跳转,如:
mov eax,00401010
jmp eax
反汇编注释版:
————————————————————————————————————————————————
00401005 /$ /E9 06000000 jmp Test.main ;这里无条件跳转到00401010处
0040100A | |CC int3
0040100B | |CC int3
0040100C | |CC int3
0040100D | |CC int3
0040100E | |CC int3
0040100F | |CC int3 ;以上空间用int3填充
00401010 >|> \55 push ebp ; 保存ebp
00401011 |. 8BEC mov ebp,esp ; ebp指向栈顶
00401013 |. 83EC 40 sub esp,0x40 ; 开辟局部变量空间
00401016 |. 53 push ebx ; ebx入栈保存
00401017 |. 56 push esi ; esi入栈保存
00401018 |. 57 push edi ; edi入栈保存,以上几句用于保护现场
00401019 |. 8D7D C0 lea edi,[local.16] ; 设置局部变量初始化起始地址
0040101C |. B9 10000000 mov ecx,0x10 ; 循环次数
00401021 |. B8 CCCCCCCC mov eax,0xCCCCCCCC ; 将4个int 3指令,即4个CC放入eax中
00401026 |. F3:AB rep stos dword ptr es:[edi] ; 循环拷贝填充局部空间,以上4句用来CC初始化
00401028 |. 68 1C204200 push Test.0042201C ; /Hello World
0040102D |. E8 2E000000 call Test.printf ; \调用printf函数
00401032 |. 83C4 04 add esp,0x4 ; 调用完printf,恢复esp
00401035 |. 5F pop edi ; 还原edi
00401036 |. 5E pop esi ; 还原esi
00401037 |. 5B pop ebx ; 还原ebx
00401038 |. 83C4 40 add esp,0x40 ; 恢复局部变量所用空间
0040103B |. 3BEC cmp ebp,esp ; 检测栈平衡
0040103D |. E8 9E000000 call Test._chkesp ; 调试信息,F7跟进去看看
00401042 |. 8BE5 mov esp,ebp ; 恢复esp
00401044 |. 5D pop ebp ; 恢复ebp
00401045 \. C3 retn ; 返回主程序,等同ret | add esp,4
————————————————————————————————————————————————
文字叙述:
push ebp push入栈指令 ,ebp帧指针,意思就是把ebp入栈保存,(此时esp-4)
mov ebp,esp mov传送指令,esp为栈指针,就是把esp保存到ebp中,因为下面要用到esp
sub esp, 40h 申请局部变量空间而用,40h的大小根据调用子程序中变量多少而不同
连续三句push 也就是我们常说的保护现场,因为子程序中有可能要用到这三个寄存器,需要先保存起来
lea edi,[local.16] lea是地址传送指令,此句的意思就是把ebp的地址加上40h放到edi中
mov ecx, 10h ecx一般用于循环次数,这里就是设置循环次数
mov eax, 0CCCCCCCCh eax赋值为8个C,也就是初始化CC操作,CC汇编指令等于int 3中断指令
rep stosd 循环拷贝字符串操作,把整个区域设置为CC
push Test.0042201C 把字符串地址指针做为参数入栈
call _printf 调用printf函数
add esp, 4 调用完毕,恢复栈操作
连续三个pop 出栈,和之前的3个push相对应,回复相关寄存器
add esp, 40h 对应之前的sub esp,40h 恢复esp的值
cmp ebp,esp 判断现在的esp值是否与之前保存在ebp中的值吻合
call __chkesp 栈出错信息调试,下面根据查看
mov esp, ebp 把ebp的值还给esp,还原esp
mov esp, ebp 函数调用结束,恢复ebp的值
retn 返回到主函数
这里有一个call __chkesp 函数来显示调试信息, 跟进去看下:
————————————————————————————————————————————————
004010E0 >/$ /75 01 jnz short Test.004010E3
004010E2 |. |C3 retn
004010E3 |> \55 push ebp
004010E4 |. 8BEC mov ebp,esp
004010E6 |. 83EC 00 sub esp,0x0
004010E9 |. 50 push eax
004010EA |. 52 push edx
004010EB |. 53 push ebx
004010EC |. 56 push esi
004010ED |. 57 push edi
004010EE |. 68 5C204200 push Test.0042205C ; The value of ESP was not properly saved across a function call. This is usually a result of calling a function declared with one calling convention with a function pointer declared with a different calling convention.
004010F3 |. 68 58204200 push Test.00422058
004010F8 |. 6A 2A push 0x2A
004010FA |. 68 48204200 push Test.00422048 ; i386\chkesp.c
004010FF |. 6A 01 push 0x1
00401101 |. E8 5A150000 call Test._CrtDbgReport
00401106 |. 83C4 14 add esp,0x14
00401109 |. 83F8 01 cmp eax,0x1
0040110C |. 75 01 jnz short Test.0040110F
0040110E |. CC int3
0040110F |> 5F pop edi
00401110 |. 5E pop esi
00401111 |. 5B pop ebx
00401112 |. 5A pop edx
00401113 |. 58 pop eax
00401114 |. 8BE5 mov esp,ebp
00401116 |. 5D pop ebp
00401117 \. C3 retn
————————————————————————————————————————————————
首先是一个jnz跳转,此跳转根据zp标志位来决定,也就是之前的CMP比较命令,如果esp不等于ebp就发生跳转,弹出调试信息窗口,如果相等就直接retn返回到main函数。想知道程序是如何弹出调试信息窗口的同学,可以再跟一下,都是一些系统内部函数,也好理解,好了,Debug版本解析到此为止!
————————————————————————————————————————————————
分析过debug版本,再来看看Release 版本,你会感觉到震惊...直接Release 编译生成后载入OD,来到main入口处:
————————————————————————————————————————————————
004010E3 |. 50 push eax
004010E4 |. FF35 20994000 push dword ptr ds:[0x409920]
004010EA |. FF35 1C994000 push dword ptr ds:[0x40991C]
004010F0 |. E8 0BFFFFFF call Test.00401000 ;main函数入口
————————————————————————————————————————————————
可以看出,main函数的三个参数被直接简化为三个直接的push,F7跟进去看看:
————————————————————————————————————————————————
00401000 /$ 68 30704000 push Test.00407030 ; Hello World
00401005 |. E8 06000000 call Test.00401010
0040100A |. 59 pop ecx
0040100B \. C3 retn
————————————————————————————————————————————————
怎么样,是不是感觉很精简,push入栈printf函数的一个参数,直接使用call调用printf,这里的pop ecx 等价于 add esp,4,再回头看下两种版本的对比:
Debug 通常称为调试版本,它包含调试信息,并且不作任何优化,便于程序员调试程序。Release 称为发布版本,它往往是进行了各种优化,使得程序在代码大小和运行速度上都是最优的,以便用户很好地使用。
一切尽在不言中,慢慢品味...