一、前言
前面两个章节,我们大致实现了通过动态逆向分析、静态逆向分析这两种方式对一个简单的程序实现爆破登录功能。
但逆向破解只是逆向工程中非常初级的部分,对于win32程序的逆向工程核心在于:将可执行文件反汇编,通过分析反汇编代码来理解代码功能,然后再用高级语言(比如c/c++)重新描述这段代码。
逆向分析首选工具是IDA
,它虽然复杂、界面有时候可能感觉也不美观,但却真的强大。
二、32位软件逆向技术
想要逆向分析某个软件,去了解它底层实现原理就是很有必要的一件事了。
一个win32应用程序,想要运行起来,就必须要写WinMain
函数,不明白的可以参考这篇文章:C++实战入门到精通-9.windows编程入门-酷编程 (kucoding.com)
但要注意的是,Windows程序执行并不是从WinMain
函数开始的,它首先会执行一个启动函数,在这个启动函数初始化进程完成后,才会来调用这个WinMain
函数。
而这个启动函数,就是由编译器自动生成的。
对于vc++编译器来说,它调用的是C/C++运行时启动函数,这个函数负责对C/C++运行库进行初始化操作。
所有C/C++程序运行时,启动函数的作用基本相同:
- 检索指向新进程的命令行指针
- 检索指向新进程的环境变量指针
- 全局变量初始化和内存栈初始化
当所有的初始化操作完成后,启动函数才会调用我们的应用程序入口函数,也就是我们常见的main
、WinMain
函数。
前面我们动态逆向分析时,提到的要修改设置选项,然后让程序自动断在入口处,那个入口处其实就是我们写的main
、WinMain
函数所在的函数位置。
它之前执行的代码,就是启动代码,因为其是固定的,所以正常来说我们无需关心,只需要从入口代码开始分析。
1.函数
对于函数的分析非常重要,事实上在前面的动态逆向分析、静态逆向分析中,我们都看到了大量的call
指令,这其实就是在调用函数,其后紧跟的数字,其实就是这个函数的地址。
只要调用这个指令,计算机就会调转到这个地址上去执行该代码。
函数执行完是需要返回的,也就是我们常见到的那个ret
指令,它的作用其实就是跳转回来而已。
比如上图,还是以前的那个traceme32
程序,并在启动后停在了入口点的位置。
这个call
指令,一旦执行它,就会跳转到它后面紧跟的那个地址去,也就是traceme32
模块上的D7D8F1
位置。
这里的
traceme32
是这里分析的可执行文件名字。
只要你步进这个函数执行,就可以看到:
此时它就跳转到了这个地址D7D8F1
上来执行代码了。
你还可以看到它这句机器码前面就有个
ret
指令,它往往预示着上一个函数的结束,那么它的下一行一般也就是新函数的开始位置。
一旦执行到这个函数的ret
指令:
你就可以看到右下角的栈顶,其数字就是我们前面跳转位置的下一条指令的地址。
这个指令的作用就是取出栈顶这个地址,然后跳转回去。
前面没有提到过,这里也提一下,右下角那个窗口就是栈的窗口,其基本逻辑就是先入后出,后入先出,每次都只能使用栈顶的数据。
这个栈中的地址其实就是前面调用
call
指令时,它推入栈的,不信的话可以重新启动程序,仔细观察栈窗口,然后步进执行这个call
指令,看它是不是真的会将下一条指令的地址推入栈中。
栈的栈顶所在地址是通过右边寄存器窗口中的ESP
寄存器存放的:
比如这里它存放的地址为7FFAD4
,所以右下角显示的栈顶地址为:7FFAD4
,并且这个地址上的数据为00D7CCA9
。
一旦执行它,它就会取出栈顶这个地址,并跳转到该地址:
你就可以看到,现在它又跳转回来了,并且栈顶的位置也发送了变化。
而栈底部的地址则是由另一个EBP
寄存器存放的:
这里你甚至可以看到,栈底部之下的数据就全是0
了。
这种是没有参数、没有返回值的情况下函数的调用逻辑,但大多数函数是有参数、有返回值的,所以下面我们再来看看有参数、有返回值的情况下,函数是如何被调用的:
首先在vs中写下这段代码:
#include<iostream>
using namespace std;
int add(int a, int b) {
return a + b;
}
int main() {
int ret = add(10,20);
cout << ret;
}
代码逻辑很简单,就是调用了一个add
函数,并且有参数也有返回值。
此时随便找个地方打上断点,然后启用vs的调试功能:
注意一定要是在Release
模式下,因为在Debug
模式下,vs会自动生成许多其它无关代码,会导致得到的汇编代码很难观察。
但此时还有个问题,那就是现代编译器优化能力非常强大。
比如这里,它可能会直接把这个函数add
全部给优化掉,直接返回结果:ret=30
并将其输出。
所以我们还需要在项目属性中关闭优化:
并且一定是x86
,即32位,64位生成的汇编代码与32位会有点不同,这个后面再提。
然后打开返汇编窗口:
然后就可以看到这个调用这个函数时的反汇编代码:
注意这里要去除符号名,不然后面不方便观察。
从这里就能看到,10
与20
这两个参数,是通过push
指令传送进函数内部的,而这个push
指令,实际上其作用就是向栈中推入数据。
这里的
14h
是20
的十六进制,0Ah
是10
的十六进制。
并且还有一个特点就是,参数是从右向左推入栈中的:先推入最右边的20
,再推入左边的10
。
推入参数完成后,他就会调用这个add
函数了,也就是这里的call
指令,同样的,点击步进,我们进入这个函数内看看:
这个函数只有4条汇编指令,不过先不急着分析它们,首先我们先想想我们目前栈中的数据有哪些。
分析栈,主要关注的其实就是ESP
、EBP
这两个寄存器,前者是栈的顶部地址,后者是栈的底部地址,其中间就是栈的内容。
所以我们得先看看这个函数外面它是怎么做的:
在调用add
函数前,它用到了三个指令:
push ebp
:将ebp
,即栈底部地址推入栈中mov ebp,esp
:mov
指令的作用是将后者赋值给前者,也就是让ebp=esp
,这一般就意味着开了一个新的栈,因为此时栈底与栈顶地址相同,那么栈内就没有任何数据。push ecx
:将ecx
寄存器的值推入栈中。
现在也不用管这个ecx
是干嘛的,只需要知道现在栈中有一个数据,那就是ecx
的值。
然后来到后面的add
函数汇编代码,因为20
先入栈,所以它在其后,然后是10
,最后调用call
指令时,前面也说过,它会将下一条指令的地址也推入栈。
所以目前栈顶的数据为跳转回去的地址,栈中的数据如下:
1.跳转回去的地址
2.数字10
3.数字20
4.ecx寄存器的值
然后我们再来看看add
函数内部开头的两个指令:
00541000 push ebp
00541001 mov ebp,esp
是不是有点熟悉的感觉?这不就是上面调用add
前的那三条指令中的前两句嘛!
所以不用多说也知道了,这两个指令组合是很常见的,其作用就是新开辟一个栈空间!
进一步总结来说就是,大部分函数,进入函数内部后,第一件事就是调用这两个指令,给自己这个函数开辟一个新的栈空间,一般是用于存放函数内部局部变量的。
所以此时栈中内容为:
1.栈底部地址
2.跳转回去的地址
3.数字10
4.数字20
5.ecx寄存器的值
最后再来看看结尾的两句指令:
mov eax,dword ptr [ebp+8]
:mov
指令前面说过了,是将后者赋值给前者,所以这里就是将地址为dbp+8
位置上的dword
宽度的指针(ptr
)数据赋值给eax
寄存器。add eax,dword ptr [ebp+0Ch]
:add
指令则是将后者加到前者身上,也就是将eax
的值加上ebp+0Ch
地址上的值,最后将结果保存到eax
寄存器上。
看起来似乎有些复杂,但其实只要理解了指令的逻辑也并不难。
这里的ebp
不就是栈底地址吗?
所以这里实际上就是在取栈中的数据,dword
可以等价于int
类型,就是占4个字节的意思,ptr
代表后面是地址,所以使用[]
符号进行取值,得到该地址上的值。
注意!栈的地址是从高位往低位扩展的,也就是栈底,其实地址更高:
比如前面我们看到的,这里的栈顶为***DB
、而栈底地址为***F4
。
因此这里是通过栈底+8
以及+C
的方式来取得上一个栈的数据,也就是函数外传入的参数的。
从前面我们观察到的开辟栈的那两个指令也能看出来,其实栈是不断向下连续进行扩展的。
因此+8
,刚好跳过两个数据,来到第一个参数数字10
,+C
,刚好跳过三个数据,来到第二个参数20
:
1.栈底部地址 //此时栈顶与栈底都在这里,每个数据4字节长度
2.跳转回去的地址 //栈底+4
3.数字10 //栈底+8 然后通过dword 4字节长度将其取出参数10
4.数字20 //栈底+12,即+C, 然后通过dword 4字节长度将其取出参数20
5.ecx寄存器的值
那么此时的计算结果就保存在了eax
寄存器中。
最后,函数调用结束:
通过pop
指令,从栈中取出栈顶部的数据,存放在寄存器ebp
中,然后ret
指令再取出栈顶部数据作为地址,跳转回去。
这里的
pop
指令就是在恢复栈,即回到上一个函数的栈,因为本函数已经结束了,本栈就没必要存在了,这也是为什么编程时,函数内的局部变量不能拿到外部使用的原因。
那么此时栈中的数据失去了两个,还剩下后面三个:
1.数字10 //栈底+8 然后通过dword 4字节长度将其取出参数10
2.数字20 //栈底+12,即+C, 然后通过dword 4字节长度将其取出参数20
3.ecx寄存器的值
然后回去后,就将eax
内的结果保存到本函数的栈上(也就是局部变量ret
上):
注意它的前面还有一句指令:add esp,8
,这是在恢复本函数的栈,即去除掉了两个参数,此时栈中的数据就只剩下了ecx
: