一次性进群,长期免费索取教程,没有付费教程。
教程列表见微信公众号底部菜单
进微信群回复公众号:微信群;QQ群:460500587
微信公众号:计算机与网络安全
ID:Computer-network
在学习编程的过程中,需要阅读大量的源代码才能提高我们的编程能力。可是通常情况下,在使用软件的过程中会发现某个软件的功能实现得非常不错,希望该功能能够在自己的软件中实现,但又得不到它的源代码进行参考。遇到这种情况,通常能够做的就是通过反汇编工具进行逆向分析,或者借助调试工具进行调试分析。
何为逆向工程?简单说就是将已经编译好的二进制程序通过反汇编分析来还原相应的源代码。本文介绍C语言代码的逆向基础知识,主要讲解C语言中流程控制代码对应的反汇编代码。
一、函数的识别
做逆向分析的第一步是先要对函数进行识别,也就是说确定函数的开始位置、结束位置、函数调用方式及函数的参数个数。在逆向分析的过程中,不会把单个指令当作是最基本的逆向单位,因为一条汇编指令很难说明任何问题,就像在C语言中,很难通过一行代码来说明问题一样。
大多情况下是针对自己比较感兴趣的部分进行重点分析,分析部分功能或者部分关键函数。因此,确定函数的开始位置及结束位置就很重要了。不过通常情况下,函数的起始位置和结束位置都可以通过反汇编工具自动识别,从而省去了我们自己的识别。使用强大的反汇编工具IDA更是如此,不过在某些特殊情况下仍然需要我们自己去识别。简单介绍一下如何识别函数的开始与结束的位置。写一个简单的C语言代码,用VC6进行编译连接,再用IDA打开。C语言代码如下:
在程序中由主函数调用了自定义函数test(),test()函数的返回值为int类型。在test()函数中调用了printf()函数和MessageBox()两个函数。逆向分析是在DEBUG编译方式下进行的。用IDA打开我们编译连接好的程序文件。用IDA打开后,将直接找到main()函数,如果没有找到main()的话,通过简单的操作也能找到。下面先来通过运行时启动函数定位找到main()函数。
首先在IDA上单击选项卡中的“Exports”项,在这里可以看到mainCRTStartup,如图1所示。
图1
双击mainCRTStartup就可以运行启动函数了。记住main()函数不是程序运行的第一个函数,而是程序员编写程序时的第一个函数。main()函数是由运行时启动函数来调用的。启动函数部分反汇编代码如下:
从反汇编代码中可以看到,main()函数在004012B4位置处,启动函数获得版本号、获得命令行参数、获得环境变量字符串……等一系列的操作后才调用了main()函数。由于使用的是调试版且有pdb文件,因此在反汇编代码中能直接显示出程序中的符号。通常情况是没有pdb文件的,这样_main是一个地址,而不是直接给出的符号了,不过仍然可以按照规律来找到主函数。
我们已经顺利找到了主函数,直接双击_main,到达如下反汇编代码处:
我们看到了4行0040100A,其实不是有4行,真正的那一行是jmp main,其余的都是IDA为了方便查看而生成的。这时发现这并不是我们编写的主函数,这是以Debug方式编译生成的一个跳表,双击main处,就到了真正的主函数处了,代码如下:
观察反汇编代码就可以确定这是我们的主函数了,代码中有一个对test()的调用,也看到了printf()。下面介绍一下这段反汇编代码。
大多数函数的入口处都是push ebp / mov ebp, esp / sub esp, xxx这样一个形式,这几句是用来保存栈帧并开辟栈空间用的。push ebx/pushesi /pushedi是保存的几个关键寄存器值,以便函数返回后这几个寄存器中的值还能在调用函数处继续使用而没有被破坏。lea edi, [ebp+ var_44] / movecx,11h/mov eax, 0CCCCCCCCh/rep stosd,这几句是将开辟的内存空间全部初始化为0xcc,0xcc如果当机器码来解释的话,其对应的汇编指令为int3,也就是断点中断指令。这样做是方便调试,尤其是对指针变量的调试好处非常多。以上代码是一个固定的形式,唯一会发生变化的部分是subesp, xxx,这里是subesp,44h。在VC6下的Debug编译方式下,如果当前函数没有变量,那么就是sub esp, 40h;如果有一个变量的情况下是sub esp,44h;两个变量时为sub esp, 48h。也就是说,从Debug方式编译时总是在预留局部变量后将栈顶多抬高40h字节。局部变量都在栈空间中,栈空间进入函数后临时开辟的空间。函数入口处的代码基本上就这些,看一下函数返回处的反汇编代码:
这是函数返回时的固定格式,这个格式跟入口的格式基本是对应的。首先是pop edi/popesi /popebx,这里是将上面保存的几个关键寄存器的值进行恢复。然后是add esp,xxx,这里是与subesp,xxx对应的,将临时开辟的栈空间释放掉(这个释放只是改变寄存器)。Mov esp,ebp/pop ebp是恢复栈帧,retn就返回到上层函数了。在该反汇编代码中还有一步没有讲,是cmp ebp, esp /call __chkesp,这两句是对__chkesp函数的一个调用,在Debug编译方式下,对几乎所有的函数调用完成后调用一次__chkesp,该函数的功能是用来检查栈是否平衡,以保证程序的正确性。如果栈不平,会给出错误提示。我们做个简单的测试,在主函数后面加一条内联汇编__asmpushebx,然后编译连接运行,在输出过后会看到一个错误的提示,如图2所示。
图2 调用__chkesp后对栈平衡进行检查时出错的提示
整个的主函数还有最后一部分,反汇编代码如下:
首先是push6 /pushoffsetaHello /callj_test/add esp,8/ mov [ebp+ var_4],eax是对test()函数的一个调用,pusheax /pushoffset aD /callprintf / addesp,8是对printf()函数的调用,xoreax,eax是对eax进行清0。我们重点来看一下对test()函数的调用。双击j_test来到如下反汇编代码处:
这里仍然是个跳表,双击test来到如下反汇编代码处:
函数开头和结尾的部分我们都已经熟悉了,直接看其他部分吧,反汇编代码如下:
另外一段反汇编代码如下:
从这两段代码中可以看出分别是对printf()函数和MessageBoxA()函数的调用,在对printf()函数调用后有一条add esp,0Ch,而在MessageBoxA()后面则没有。这是为什么呢?这就是调用约定。在VC下常见到两种调用约定,分别是stdcall和cdecl两种。
stdcall是Windows下的标准调用约定,Windows提供的API函数及WDK中提供的函数都是使用stdcall,当然也有例外,就是API中变参函数是使用cdecl调用约定。C语言默认的是使用cdecl调用约定。
stdcall参数的入栈方式是从右往左,平衡栈是在被调用函数的内部。cdecl参数的入栈方式也是从右往左,平衡栈是在调用函数方。这就是两种函数调用的约定方式,没有好与不好,只是调用方式不同而已,唯一一点是stdcall不能支持变参函数。
二、if……else……分支结构
下面用一个相对简单的if……else……分支实例来说明其对应的反汇编,C代码如下:
代码很简单,用IDA看其关键的反汇编代码:
以上3句分别是对3个变量进行赋值初始化。
观察上面的反代码发现,代码中使用的比较关联符号是“大于号”,而反汇编代码却使用了相反的比较跳转指令,使用的是“小于等于”则跳转,否则不跳转。请注意观察00401043和0040105E这两个地址,jle会跳过紧接着的下面的部分代码,跳转的目的地址上面是一条无条件跳转指令jmp,也就是说,jle和jmp之间的部分是代码中比较表达式成功后执行的代码。在反汇编代码中,如果比较指令后的跳转指令没有发生跳转的话,则在执行完一系列指令后,会紧跟着一条无条件跳转指令jmp跳到某个地址。请注意观察00401056和00401071这两个地址,这两个跳转指令跳转到了同一个目的地址。在C源代码中,当比较表达式成功后会执行其后面的代码块,当执行完这些代码块后就跳过与之配对的elseif或else后面的代码。
上面C源代码中if……else……对应反汇编结构如下:
这基本上就是if……else……分支结构的执行流程了,当反汇编时遇到这样的结构就可以马上知道这是一个if……else……分支结构了,大的流程定下以后就可以具体地分析每个分支要执行的代码了。
三、switch分支结构
前面讲了if……else……的分支结构,接下来介绍switch分支结构。该结构是一个比较有趣的结构,它的反汇编代码和我们想的反汇编代码不同,而且switch的反汇编形式特别的丰富,这里只介绍它的其中一种反汇编形式。C语言代码如下:
我们来看以上代码对应的反汇编代码:
根据对if……else……的经验判断,分支是由跳转指令完成的。在上面的反汇编代码中发现有两条跳转指令,一条是跳转到default分支的,另一条是一个看不太明白的跳转。我们把那条跳转指令单独看一下:
此时说明eax的值不大于3,也就是在0~3之间的任意一个数,看一下4010bb是什么内容:
4010bb像是一个数组,数组中的内容是40105f、40106e、40107d和40108c,这4个值就是switch中各个分支的目标地址,也就是说该处是根据一个地址表来跳转到各个分支流程的,根据各个取值的不同而不同。比如输入的内容是4,而经过反汇编的一番处理后变成了3,处理的反汇编代码如下:
当输入变为3时,在jmp ds::off_4010bb[3 *4]中得到的地址为4010bb +3* 4= 4010c7,而4010c7中保存的则是到case 4:分支的地址。
switch分支的反汇编形式比较有用,这里只介绍了其中的一种反汇编形式,其余的反汇编形式就不进行介绍了,大家可以自行学习。
四、for循环结构
介绍完分支结构后,相应的就要介绍C语言中的循环结构,下面先来介绍一下for循环,C语言代码如下:
这是一个很经典的求1加到100的累加和的例子,我们看其反汇编代码:
这段for循环的反汇编代码很清晰,也很简单,就不对反汇编代码做具体介绍了,下面只对for循环结构做一个反汇编的总结,具体如下所示:
; 初始化循环变量
jmp循环变量比较处
修改循环变量处:
循环变量比较处:
; 循环变量的比较
jx不满足条件则跳出循环
…… ;循环体
jmp修改循环变量处
五、do……while与while……循环结构
最后再介绍两种循环结构,分别是do……while和while循环结构,同样使用简单的例子来整理其反汇编对应的结构。先来看do……while循环,C语言代码如下:
再看其反汇编代码:
循环体的开始:
jx条件成立跳转循环体的开始处。
下面来看一个while循环的代码:
看其反汇编代码:
整个反汇编代码与do……while类似,只是把判断放到前面了,while循环的结构如下:
; 初始化循环变量
循环体开始:
循环体结束:
对于for、do……while和while这3种循环来说,do……while的效率显然更高一些。编程时可以尽量选择do……while循环来使用。
对于C语言的逆向知识就介绍这么多。大家可以写一些C语言代码,然后用IDA来进行分析学习。
微信公众号:计算机与网络安全
ID:Computer-network
【推荐书籍】