在学编程的历程中,必须浏览很多的源码才可以增强自己的程序编程工作能力。一样,在做设备的过程中也要很多参照同行业的软件才可以改进自身商品的不够。假如发觉某一软件的作用十分非常好,是自身急缺融进自身软件商品的作用,而这时又沒有源码可以参照,那麼程序猿唯一能做的仅有根据逆向分析来认识其完成方法。此外,当采用的某一软件存有 Bug,而该软件早已不会升级时,程序猿能做的并并不是寻找类似的别的软件,反而是可以根据逆向分析来自主调整其软件的 Bug,进而有效地再次应用该软件。逆向分析程序流程的因素许多,有一些状况迫不得已开展逆向分析,例如病毒分析、系统漏洞剖析等。
很有可能病毒分析、系统漏洞剖析等深奥技术性针对有的人而言现阶段还没法做到,可是其基本知识一部分都离不了逆向专业知识。下边依靠IDA来剖析由VC6编译程序联接C语言的编码,进而来学习培训把握逆向的基本知识。
1. 简易的C语言调用函数程序流程
为了更好地便捷详细介绍有关函数公式的鉴别,这儿写一个简易的C语言程序流程,用VC6开展编译程序联接。C语言的源代码如下所示:
在编程代码中,自定义函数test()由主函数main()所启用,test()函数公式的传参为int类型。在test()函数公式中获取了printf()函数公式和MessageBox()函数。将编码在VC6下应用DEBUG方法开展编译程序联接来形成一个可执行程序,对该可执行文件根据IDA开展逆向分析。
以上编码的拓展名叫“.c”,而不是“.cpp”。这儿用于开展逆向分析的案例均应用DEBUG方法在VC6下开展编译程序联接。
2. 函数公式逆向分析
大部分情形下程序猿全是对于自身较为有兴趣的程序流程一部分开展逆向分析,剖析一部分作用或是一部分重要函数公式。因而,明确函数公式的逐渐部位和完毕部位特别关键。但是一般来说,函数公式的开始部位和完毕部位都能够根据反汇编工具自动检索,仅有在编码被有意更改后才必须程序猿自身开展鉴别。IDA可以有效地鉴别函数公式的开始部位和完毕部位,假如在逆向分析的历程中看到有剖析不确切的情况下,可以根据Alt P键盘快捷键开启“Edit function”(编写函数公式)提示框来调节函数公式的开始部位和完毕部位。“Edit function”提示框的页面如下图1所显示。在图1中,被选定的一些可以设置函数公式的开始详细地址和终止详细地址。
图1 “Edit function”提示框
用IDA开启VC6编译程序好的程序流程,在开启的情况下,IDA会有一个提醒,如下图2所显示。该图了解是不是应用PDB文档。PDB文件是程序流程数据文件,是c语言编译器产生的一个文档,便捷调试程序应用。PDB包含函数详细地址、局部变量的姓名和详细地址、主要参数和静态变量的名称与在局部变量的偏移等许多信息内容。这儿挑选“Yes”按键。
图2 提醒是不是应用PDB文档
在剖析别的程序流程的情况下,通常沒有PDB文档,那麼这儿会挑选“No”按键。在有PDB和无PDB文档时,IDA的研究結果是有所不同的。请大伙儿在自身剖析时,试着比照不载入c语言编译器产生的PDB文档和载入了PDB文档IDA转化成的反汇编代码的差别。
当IDA进行对系统的解析后,IDA立即找到main()函数公式的跳表项,如下图3所显示。
图3 main()函数公式的跳表
所说main()函数公式的跳表项,意思是这儿并并不是main()函数公式的真正意义上的开始部位,反而是该部位是一个跳表,用于统一管理方法每个函数公式的详细地址。从图3中见到,有一条jmp _main的汇编代码,这条编码用于跳向真正意义上的main()函数公式的详细地址。在IDA中查询图3左右部位,很有可能只有寻找这样一条自动跳转命令。在图3的靠下一部分有一句注解为“[00000005 BYTES: COLLAPSED FUNCTION j._test. PRESS KEYPAD " " TO EXPAND]”。这儿是可以进行的,在该注解上单击右键,发生右键后挑选“Unhide”项,则能够看见被隱藏的跳表项,如下图4所显示。
图4 进行后的跳表
在具体的反汇编代码时,jmp _main和jmp _test是紧挨着的两根命令,并且jmp后边是2个详细地址。这儿的表明函数公式方式、_main和_test是由IDA开展加工处理的。在OD下观查跳表的方式,如下图5所显示。
图5 OD中跳表的命令部位
并非每一个流程都能被IDA鉴别出自动跳转到main()函数公式的跳表项,并且程序流程的进口点也并不是main()函数公式。最先来说一下程序流程的通道函数公式部位。在IDA上点击对话框菜单栏,挑选“Exports”对话框(Exports窗口是导出来对话框,用以查询导出来函数公式的详细地址,可是针对EXE程序流程而言通常是沒有导出来函数公式的,这儿将表明EXE程序流程的通道函数公式),在“Exports”对话框中能够看见_mainCRTStartup,如下图6所显示。
图6 Exports对话框
双击鼠标_mainCRTStartup就可以抵达运行函数公式的地方了。在C语言中,main()并不是程序执行的第一个函数公式,反而是程序猿程序编写时的第一个函数公式,main()函数是由运行函数公式来启用的。如今看一下
从反汇编代码中能够看见,main()函数的调用在004012B4部位处。运行函数从004011D0详细地址处逐渐,期内调用GetVersion()函数得到了系统软件版本号、调用._heap_init函数复位了系统所采用的堆室内空间、调用GetCommandLineA()函数获得了命令行参数、调用__.crtGetEnviro nmentStringsA函数得到了环境变量字符串数组……在进行一系列运行需要的工作任务后,总算在004012B4处调用了_main_0。因为这儿应用的是调节版且有PDB文档,因而在反汇编代码中同时展现出系统中的标记,在剖析别的程序流程时是沒有PDB文档的,那样_main_0便会表明为一个详细地址,而不是一个标记。但是依旧可以根据基本规律来寻找_main_0所属的部位。
沒有PDB文档,怎样寻找_main_0所属的地方呢?在VC6中,运行函数会先后调用GetVersion()、GetCommandLineA()、GetEnvironmentStringsA()等函数,而这一系列函数就是一串显著的特点。在调用完GetEnvironmentStringsA()后,不远的地方会出现3个push实际操作,分别是main()函数的3个主要参数,编码如下所示:
该反汇编代码相匹配的C编码如下所示:
该一部分编码是以CRT0.C中取得的,能够看见运行函数在调用main()函数时有3个主要参数。
然后上边的內容,在3个push实际操作后的第1个call处,就是_main_0函数的详细地址。往_main_0下边看,_main_0后详细地址为004012C3的命令为call _exit。明确了程序流程是由VC6撰写的,那麼寻找对_exit的调用后,往上找一个call命令就寻找_main_0所相应的详细地址。大伙儿可以根据该方式 实现检测。
在成功寻找_main_0函数后,立即双击鼠标反编译的_main_0,抵达函数自动跳转表处。在跳转表格中双击鼠标_main,就可以到真真正正的_main函数的反汇编代码处。_main函数的返汇编代码如下所示:
短短的几行C语言代码,在编译程序联接转化成可执行程序后,再开展反编译居然转化成了比C语言代码多许多的代码。认真观察上边的反编译代码,根据特点可以确认这也是写的主函数,最先代码中有一个对test()函数的启用在004010BF详细地址处,次之有一个对printf()函数的启用在004010D3详细地址处。_main函数的入口一部分代码如下所示:
大部分函数的入口处全是push ebp/mov ebp, esp/sub esp, ×××那样的方式,这一两句代码完成了储存栈帧,并开拓了现阶段函数需要的栈室内空间。push ebx/push esi/push edi是用于储存好多个重要存储器的值,便于函数回到后这好多个存储器中的值还能在启用函数处再次采用而并没有被毁坏掉。lea edi, [ebp var_44]/mov ecx, 11h/move ax , 0CCCCCCCCh/rep stosd,这一两句代码是开拓的内存空间,所有重置为0xCC。0xCC被作为序列号来表述时,其相应的汇编语言指令为int 3,也就是启用3号中断点终断来造成一个软件终断。将新开拓的栈室内空间复位为0xCC,那样做的益处是便捷调节,尤其是给指针变量的调试产生了便捷。
以上反编译代码是一个稳定的方式,唯一会产生变化的是sub esp, ×××一部分,在现阶段反编译代码处是sub esp, 44h。在VC6下应用Debug方法编译程序,假如现阶段函数沒有自变量,那麼该句代码是sub esp, 40h;假如有一个自变量,其代码是sub esp, 44h;有两个变量时,为sub esp, 48h。换句话说,根据Debug方法编译程序时,函数分派栈室内空间一直开拓了静态变量的室内空间后又预埋了40h字节数的室内空间。静态变量都是在栈室内空间中,栈空间是在进到函数后临时性开拓的室内空间,因而静态变量在函数完毕后就荡然无存了。与函数入口代码相匹配的代码自然是出入口代码,其代码如下所示:
函数的出入口一部分(或是是函数回到时的一部分)也归属于固定不动文件格式,这一文件格式跟入口的格式基本上是相应的。最先是pop edi/pop esi/pop ebx,这儿是将入口一部分储存的一些重要存储器的值开展修复。push和pop是对局部变量开展使用的命令。局部变量构造的特性是后进先出,或先进后出。因而,在函数的入口一部分的入栈次序是push ebx/push esi/push edi,出栈顺序则是倒序pop edi/pop esi/pop ebx。修复完存储器的值后,必须修复esp表针的部位,这儿的命令是add esp, 44h,将临时性开拓的栈室内空间释放出来掉(这儿的释放出来仅仅更改存储器的值,在其中的数据信息并没有消除掉),在其中44h也是与入口处的44h相匹配的。从入口和出入口更改esp存储器的具体情况可以看得出,栈的方位是由高详细地址向低地址方位延长的,开拓室内空间是将esp做加减法实际操作。mov esp, ebp/pop ebp是修复栈帧,retn就回到顶层函数了。在该反编译代码中也有一步沒有讲到,也就是cmp ebp, esp/call ._chkesp,这几句是对._chkesp函数的一个启用。在Debug方法下编译程序,对几乎任何的函数启用进行后都是会启用一次._chkesp。该函数的功用是用于查验栈是不是均衡,以确保系统的准确性。假如栈高低不平,会得出报错。这儿做一个简易的检测,在主函数的return句子前面一条内联选编._asm push ebx(只需是更改esp或ebp存储器值的使用都能够达到效果),随后编译程序联接运作,在輸出后会见到一个失误的提醒,如下图7所显示。
图7 启用._chkesp后对栈均衡开展查验后的出差错提醒
图7便是._chkesp函数在监测到ebp与esp不平常得出的弹出框。该作用只在DEBUG版本号中存有。
主函数的反编译代码中再有一部分沒有详细介绍,反编译代码如下所示:
最先几个反编译代码是push 6/push offset aHello/call j_test/add esp, 8/mov [ebp var_ 4], eax,这几个反编译代码是主函数对test()函数的启用。函数主要参数的传播可以挑选存储器或是运行内存。因为存储器总数比较有限,几乎绝大多数函数启用全是根据运行内存开展传送的。当主要参数应用结束后,必须把主要参数所采用的运行内存开展回收利用。针对VC开发工具来讲,其默认设置的启用承诺方法是cdecl。这类函数启用承诺对主要参数的传送借助栈运行内存,在启用函数前,会根据压栈实际操作将主要参数从右往左先后送进栈中。在C代码中,对test()函数的启用方式如下所示:
而相匹配的反编译代码为push 6 / push offset aHello / call j_test。从压栈实际操作的push命令看来,主要参数是以右往左先后入栈的。当函数回到时,必须将主要参数应用的室内空间回收利用。这儿的回收利用,指的是修复esp存储器的值到函数启用前的值。而针对cdecl启用方法来讲,均衡局部变量的使用是由函数启用方来做的。从以上的反编译代码中能够看见反编译代码add esp, 8,它是用以均衡局部变量的。该代码相匹配的语言表达为启用函数前的2个push实际操作,即函数主要参数入栈的实际操作。
函数的传参通常存放在eax存储器中,这儿的传参是以return句子来进行的返回值,并不是以主要参数接受的返回值。004010C7详细地址处的反汇编代码mov [ebp var_4], eax是将对j_test启用后的返回值储存在[ebp var_4]中,这儿的[ebp var_4]就等同于C语言代码中的nNum自变量。逆向分析时,可以在IDA中根据键盘快捷键N来实现对var_4的重新命名。
在对j_test启用进行并将返回值储存在var_4之后,随后push eax/push offset aD/call _printf/add esp, 8的反汇编代码应当都不生疏了。而最终面的xor eax, eax这一句代码是将eax开展清0。由于在C语言代码中,main()函数公式的返回值为0,即return 0;,因而这儿对eax开展了清0实际操作。
双击鼠标004010BF详细地址处的call j._test,会挪到j_test的函数公式跳表处,反汇编代码如下所示:
双击鼠标跳表中的_test,到如下所示反编译处:
该反汇编代码的开始一部分和末尾一部分,这儿不会再反复,关键看一下正中间的反汇编代码一部分。正中间的一部分主要是printf()函数公式和MessageBoxA()函数的反汇编代码。
启用printf()函数公式的反汇编代码如下所示:
启用MessageBoxA()函数公式的反汇编代码如下所示:
较为以上简易的2段代码会看到许多不同点,最先在调用完_printf后会出现add esp, 0Ch的代码开展均衡局部变量,而调用MessageBoxA后沒有。为何对MessageBoxA函数的调用则没有呢?缘故取决于,在Windows系统下,对API函数的调用都遵循的函数调用承诺是stdcall。针对stdcall这类调用承诺来讲,参数仍然是以右往左先后被送进局部变量,而参数的平栈是在API函数内结束的,而不是在函数的调用方进行的。在OD中看一下MessageBoxA函数在回到时的平栈方法,如下图8所显示。
图8 MessageBoxA函数的平栈实际操作
从图8中可以看得出,MessageBoxA函数在调用retn命令后跟了一个10。这儿的10是一个16进制数,16进制的10相当于10进制的16。而在为MessageBoxA传送参数时,每一个参数是4字节数,4个参数相当于16字节数,因而retn 10除开有回到的功效外,还包括了add esp, 10的功效。
上边2段反汇编代码中除开均衡局部变量的不一样外,也有此外一个显著的差别。在调用printf时的命令为call _printf,而调用MessageBoxA时的指令为call ds:._imp__MessageBoxA@16。printf()函数在stdio.h库函数中,该函数归属于C语言的静态数据库,在联接的时候会将其代码联接入二进制文件中。而MessageBoxA函数的完成在user32.dll这一动态性联接库文件。在代码中,这儿只留了进到MessageBoxA函数的一个详细地址,并没详细的代码。MessageBoxA的详细地址储放在数据信息节中,因而在反汇编代码中提供了提醒,应用了作为前缀“ds:”。“._imp__”表明导进函数。MessageBoxA后边的“@16”表明该API函数有4个参数,即16 / 4 = 4。
多参的API函数依然在调用方开展平栈,例如wsprintf()函数。缘故取决于,被调用的函数没法实际确立调用方会传送好多个参数,因而多参函数没法在函数内进行参数的堆栈平衡工作中。
stdcall是Windows下的规范函数调用承诺。Windows给予的网络层及核心层函数均应用stdcall的调用承诺方法。cdecl是C语言的调用函数承诺方法。
3. 总结
在逆向分析函数时,最先必须明确函数的开始部位,这通常会由IDA全自动完成鉴别(识别不确切得话,就只有手动式鉴别了);次之必须把握函数的调用承诺和明确函数的参数数量,明确函数的调用承诺和参数数量全是根据平栈的方法和平栈时对esp实际操作的值来开展判定的;最终便是观查函数的传参,这一部分通常便是观查eax的值,因为return通常只回到布尔类型、标值种类有关的值,因而仔细观察eax的值可以明确传参的种类,明确了传参的种类后,可以进一步考虑到函数调用方下一步的姿势。