4. 逆向分析技术(上)

一、前言

前面两个章节,我们大致实现了通过动态逆向分析静态逆向分析这两种方式对一个简单的程序实现爆破登录功能。

逆向破解只是逆向工程中非常初级的部分,对于win32程序的逆向工程核心在于:将可执行文件反汇编,通过分析反汇编代码来理解代码功能,然后再用高级语言(比如c/c++)重新描述这段代码。

逆向分析首选工具是IDA,它虽然复杂、界面有时候可能感觉也不美观,但却真的强大。

二、32位软件逆向技术

想要逆向分析某个软件,去了解它底层实现原理就是很有必要的一件事了。

一个win32应用程序,想要运行起来,就必须要写WinMain函数,不明白的可以参考这篇文章:C++实战入门到精通-9.windows编程入门-酷编程 (kucoding.com)

但要注意的是,Windows程序执行并不是从WinMain函数开始的,它首先会执行一个启动函数,在这个启动函数初始化进程完成后,才会来调用这个WinMain函数。

而这个启动函数,就是由编译器自动生成的。

对于vc++编译器来说,它调用的是C/C++运行时启动函数,这个函数负责对C/C++运行库进行初始化操作。

所有C/C++程序运行时,启动函数的作用基本相同:

  1. 检索指向新进程的命令行指针
  2. 检索指向新进程的环境变量指针
  3. 全局变量初始化和内存栈初始化

当所有的初始化操作完成后,启动函数才会调用我们的应用程序入口函数,也就是我们常见的mainWinMain函数。

前面我们动态逆向分析时,提到的要修改设置选项,然后让程序自动断在入口处,那个入口处其实就是我们写的mainWinMain函数所在的函数位置。

它之前执行的代码,就是启动代码,因为其是固定的,所以正常来说我们无需关心,只需要从入口代码开始分析。

1.函数

对于函数的分析非常重要,事实上在前面的动态逆向分析静态逆向分析中,我们都看到了大量的call指令,这其实就是在调用函数,其后紧跟的数字,其实就是这个函数的地址。

只要调用这个指令,计算机就会调转到这个地址上去执行该代码。

函数执行完是需要返回的,也就是我们常见到的那个ret指令,它的作用其实就是跳转回来而已。

image-20231022083421449

比如上图,还是以前的那个traceme32程序,并在启动后停在了入口点的位置。

这个call指令,一旦执行它,就会跳转到它后面紧跟的那个地址去,也就是traceme32模块上的D7D8F1位置。

这里的traceme32是这里分析的可执行文件名字。

只要你步进这个函数执行,就可以看到:

image-20231022083701893

此时它就跳转到了这个地址D7D8F1上来执行代码了。

你还可以看到它这句机器码前面就有个ret指令,它往往预示着上一个函数的结束,那么它的下一行一般也就是新函数的开始位置。

一旦执行到这个函数的ret指令:

image-20231022083954308

你就可以看到右下角的栈顶,其数字就是我们前面跳转位置的下一条指令的地址。

这个指令的作用就是取出栈顶这个地址,然后跳转回去。

前面没有提到过,这里也提一下,右下角那个窗口就是的窗口,其基本逻辑就是先入后出,后入先出,每次都只能使用栈顶的数据。

这个栈中的地址其实就是前面调用call指令时,它推入栈的,不信的话可以重新启动程序,仔细观察栈窗口,然后步进执行这个call指令,看它是不是真的会将下一条指令的地址推入栈中。

栈的栈顶所在地址是通过右边寄存器窗口中的ESP寄存器存放的:

image-20231022084330606

比如这里它存放的地址为7FFAD4,所以右下角显示的栈顶地址为:7FFAD4,并且这个地址上的数据为00D7CCA9

一旦执行它,它就会取出栈顶这个地址,并跳转到该地址:

image-20231022084955923

你就可以看到,现在它又跳转回来了,并且栈顶的位置也发送了变化。

而栈底部的地址则是由另一个EBP寄存器存放的:

image-20231022085335077

这里你甚至可以看到,栈底部之下的数据就全是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的调试功能:

image-20231022094510102

注意一定要是在Release模式下,因为在Debug模式下,vs会自动生成许多其它无关代码,会导致得到的汇编代码很难观察。

但此时还有个问题,那就是现代编译器优化能力非常强大。

比如这里,它可能会直接把这个函数add全部给优化掉,直接返回结果:ret=30并将其输出。

所以我们还需要在项目属性中关闭优化:

image-20231022094637484

并且一定是x86,即32位,64位生成的汇编代码与32位会有点不同,这个后面再提。

然后打开返汇编窗口:

image-20231022091730516

然后就可以看到这个调用这个函数时的反汇编代码:

image-20231022094904425

注意这里要去除符号名,不然后面不方便观察。

从这里就能看到,1020这两个参数,是通过push指令传送进函数内部的,而这个push指令,实际上其作用就是向栈中推入数据。

这里的14h20的十六进制,0Ah10的十六进制。

并且还有一个特点就是,参数是从右向左推入栈中的:先推入最右边的20,再推入左边的10

推入参数完成后,他就会调用这个add函数了,也就是这里的call指令,同样的,点击步进,我们进入这个函数内看看:

image-20231022095019571

这个函数只有4条汇编指令,不过先不急着分析它们,首先我们先想想我们目前栈中的数据有哪些。

分析栈,主要关注的其实就是ESPEBP这两个寄存器,前者是栈的顶部地址,后者是栈的底部地址,其中间就是栈的内容。

所以我们得先看看这个函数外面它是怎么做的:

image-20231022095815664

在调用add函数前,它用到了三个指令:

  • push ebp:将ebp,即栈底部地址推入栈中
  • mov ebp,espmov指令的作用是将后者赋值给前者,也就是让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寄存器的值

最后再来看看结尾的两句指令:

  1. mov eax,dword ptr [ebp+8]mov指令前面说过了,是将后者赋值给前者,所以这里就是将地址为dbp+8位置上的dword宽度的指针(ptr)数据赋值给eax寄存器。
  2. add eax,dword ptr [ebp+0Ch]add指令则是将后者加到前者身上,也就是将eax的值加上ebp+0Ch地址上的值,最后将结果保存到eax寄存器上。

看起来似乎有些复杂,但其实只要理解了指令的逻辑也并不难。

这里的ebp不就是栈底地址吗?

所以这里实际上就是在取栈中的数据,dword可以等价于int类型,就是占4个字节的意思,ptr代表后面是地址,所以使用[]符号进行取值,得到该地址上的值。

注意!栈的地址是从高位往低位扩展的,也就是栈底,其实地址更高:

image-20231022085335077

比如前面我们看到的,这里的栈顶为***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寄存器中。

最后,函数调用结束:

image-20231022102528577

通过pop指令,从栈中取出栈顶部的数据,存放在寄存器ebp中,然后ret指令再取出栈顶部数据作为地址,跳转回去。

这里的pop指令就是在恢复栈,即回到上一个函数的栈,因为本函数已经结束了,本栈就没必要存在了,这也是为什么编程时,函数内的局部变量不能拿到外部使用的原因。

那么此时栈中的数据失去了两个,还剩下后面三个:

1.数字10 //栈底+8 然后通过dword 4字节长度将其取出参数10
2.数字20 //栈底+12,即+C, 然后通过dword 4字节长度将其取出参数20
3.ecx寄存器的值

然后回去后,就将eax内的结果保存到本函数的栈上(也就是局部变量ret上):

image-20231022103004481

注意它的前面还有一句指令:add esp,8,这是在恢复本函数的栈,即去除掉了两个参数,此时栈中的数据就只剩下了ecx

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux