在前面的章节中曾经实现过一个枚举进程中被加载DLL文件的函数,接下来要实现一个枚举进程的函数。枚举进程不能在用户态下进行,需要到内核态下进行,这样就必须使用驱动程序来完成。先用WinDbg完成一次手动的枚举过程,再通过代码来完成。
图7-3 Dbgview显示的输出
使用WinDbg调试驱动程序或内核,需要双机进行调试,所谓双机就是两台电脑。通常情况下,大部分人都没有两台电脑,往往只有一台电脑。那么,解决的方法就是安装虚拟机,然后对虚拟机进行一些设置,也是可以通过WinDbg来进行调试的。虚拟机选择使用VMware,下面讨论一下如何对虚拟机进行配置。
安装好VMware,并在VMware中安装好操作系统,然后对安装好的虚拟机进行一些设置,通过此设置可以达到调试器与虚拟机的连接。单击菜单“VM” → “Settings”命令,弹出“Virtual Machine Settings”窗口,如图7-4所示。
图7-4 “Virtual Machine Settings”对话框
单击“Add”按钮,打开“Add Hardware Wizard”(添加硬件向导)对话框,如图7-5所示。
图7-5 “Add Hardware Wizard”对话框 1
在该对话框中选择“Serial Port”选项,也就是串口,然后单击“Next”按钮,弹出“Add Hardware Wizard”对话框的第二个界面,如图7-6所示。
图7-6 “Add Hardware Wizard ”对话框 2
在该界面中选择“Output to named pipe”单选钮,也就是命名管道,命名管道是Windows下进程通信的一种方法。选中该项后继续单击“Next”按钮进入下一个界面,也是设置的最后一个界面,如图7-7所示。
图7-7 “Add Hardware Wizard ”对话框 3
在这个界面中对命名管道进行设置,然后单击“Finish”按钮即可。至此,已经完成了一半的设置了。接着,启动虚拟机配置Windows的Boot.ini文件。Boot.ini文件原内容如下:
将最后一行复制一下,然后放到最后面,并进行修改,修改后的内容如下:
去掉Boot.ini文件的只读属性,然后保存Boot.ini文件。在下次需要对驱动进行调试,或者对内核进行调试时选择启动Debug模式的Windows。
注:这里只介绍了针对Windows XP系统的配置方法,关于其他版本系统的配置方法请自行参考相关内容。
至此,所有的配置工作就做好了,但是使用WinDbg进行连接时,还是要有连接参数的。我们先在桌面上创建一个WinDbg的快捷方式,然后在WinDbg快捷方式上单击右键,在弹出的快捷菜单中选择“属性”命令,弹出“属性”对话框,将“目标”位置改为:
F:\WinDDK\7600.16385.0\Debuggers\windbg.exe -b -k com:port=\\.\pipe\com_l,baud= 115200,pipe
这样就可以用WinDbg连接虚拟机中调试状态下的Windows XP了。
Windows中有一个非常大的与进程有关的结构体——EPROCESS。每个进程对应一个 EPROCESS结构,但EPROCESS是一个系统未公开的结构体,我们在WDK中只能找到说明,而找不到其结构体的具体定义,因此需要通过WinDbg来查看,这次使用WinDbg和 WMware进行调试。按照前面的方法,使WinDbg和WMware可以连接,当WinDbg出现调试界面时,在其命令处输入dt_eprocess命令来查看一下该结构体,如图7-8所示。
图7-8 WinDbg显示的部分EPROCESS结构体
从图中可以看出,EPROCESS结构体显示出了非常多的内容,从WinDbg调试界面只能看到部分的成员变量,而且偏移已经到了 0×258,非常多。看一下WinDbg的全部内容。
上面就是EPROCESS结构体的全部,对于遍历进程列表来说,有用的只有几个内容,首先是偏移0×84处的进程ID;然后是偏移0×88处的进程链表;最后一个是偏移0×174的进程名。下面手动进行一次遍历。
在WinDbg的命令输入提示处输入!Process 0 0命令,得到进程的列表,如图 7-9所示。
图7-9 进程信息
PROCESS后面给出的值就是当前进程中EPROCESS的地址,选择explorer.exe进程给出的地址0×ff364708来解析EPROCESS。输入命令dt _eprocess ff364708,输出如下:
可以看到,按照EPROCESS结构体解析ff364708这个地址,输出了我们需要的内容。接着,通过ActiveProcessLinks来获取下一个进程的信息。输入命令dd ff364708 + 0×88,输出如下:
在ff364790地址处保存了下一个EPROCESS结构体ActiveProcessLinks的地址,要得到下一个EPROCESS的地址,必须减去0×88才能得到。输入命令dt_eprocess (ff2b44bO-0×88),输出如下:
将输出结果和图7-9中的结果对比,在explorer.exe的下一个进程为vmwaretray.exe,可见遍历方法是正确的。
上面介绍的手动遍历过程就是指导我们如何编写代码的,只要上面的手动遍历过程能够掌握,那么代码的编写也就不是问题了。下面直接看代码,代码如下:
在代码中用到了一个函数,就是PsGetCurrentProcess(),这个函数是用来获取当前进程的 EPROCESS指针的,该函数的定义如下:
通过PsGetCurrentProcess()函数获得的是system进程的EPROCESS,大多数内核模式系统线程都在system进程中。除了这个函数没有接触过以外,剩下的部分就是对EPROCESS结构体的操作了,这里就不做过多的介绍了。在前面的章节中介绍过如何实现进程内DLL文件的隐藏,方法是将指定DLL在DLL链表中“脱链”。为了隐藏进程,同样可以将指定进程的EPROCESS结构体在进程链表中“脱链”,以达到隐藏的目的。
前面介绍了如何编写简单的驱动程序,这节将介绍内核下的一张非常有用的表。在很多游戏保护系统中,或在一些杀毒软件中,都会对该表进行修改,从而改变系统函数调用流程来起到反外挂、反病毒的作用。同样,病毒也在修改该表,从而修改系统函数调用流程来完成其自身的目的。这张非常关键的表叫做SSDT,即System Service Descriptor Table (系统服务描述表)。这张表的作用是把用户层的Win32 API和内核层的API建立一个关联。在该表中维护非常多Native API,或称本地API。下面通过WinDbg来查看一下该表。
依照前面的方法,使用WinDbg连接到虚拟机上,然后在命令提示符处输入dd KeServiceDescriptorTable命令,会得到一些十六进制的输出。KeServiceDescriptorTable是 Ntoskrnl.exe导出的一个指针,用来指向SSDT表的。我们查看一下命令的输出结果:
在该输出中,第一行就是 SSDT表,该表中的 80502b8c是一个函数指针数组,该指针数组保存了所有Native API的函数地址,0000011c是数组的大小,80503000里面保存的是一个参数个数数组,与Native API相对应。将SSDT定义成一个结构体,如下:
要想在驱动中获得该表,需要使用Notokrnl.exe导出的KeServiceDescriptorTable,将其定义如下:
有了上面的SSDT表和KeServiceDescriptorTable的定义,就可以编写与SSDT相关的程序了,不过似乎还少点什么。表里面对应的Native API到底是什么?用WinDbg来看一下,输入dd 80502b8c,输出结果如下:
全都是一些地址值比较接近的函数地址,为什么说是函数地址,因为这是函数指针数组。输入u8059a948命令,输出如下:
从输出可以看出,8059a948是NtAcceptConnectPort()函数的地址。再来看一个地址,输入u 805e7db6命令,输出如下:
这次输出的是NtAccessCheck()函数的反汇编代码。在SSDT表中,第3个参数告诉我们,这个数组的大小是0×11c,也就是数组最后一项的下标是0×11b。再看一下下标为0×11b中保存的地址是多少,输入命令dd 80502b8c + 11b * 4,80502b8c是数组的起始地址,11b是数组下标,那么乘4的是什么原因呢?数组地址的定位是通过数组首地址+下标×数组元素字节数得出的。一个函数的地址占用4个字节,因此要做乘4的操作。该命令输出如下:
再用u命令来查看805c2798处的反汇编代码,输入命令u 805c2798,输出如下:
数组中最后一项保存的是NtQueryPortInformationProcess()函数的地址。
从上一节的介绍中我们知道,SSDT把用户层的Win32 API与内核层的Native API做了一个关联,而整个Native API都保存在SSDT中的一个函数指针数组中,只要修改函数指针数组中的某一项,就相当于HOOK了某个Native API函数。比如,修改SSDT中函数指针数组中的最后一个函数指针,那么就相当于HOOK了 NtQueryPortInformationProcess()函数。
下面 HOOK一个我们比较熟悉的函数,即创建进程函数NtCreateProcessEx()。该函数在指针数组的第 0×30项(该编号根据系统版本的不同而不同,是系统相关的)。通过编程获取 SSDT表,然后找到Native API的函数指针数组,然后修改其中第0×30项的内容为我们的函数地址,为了不影响进程的正常创建,在函数中调用NtCreateProcessEx()函数。代码如下:
在DriverEntry()中,调用了 HookCreateProcess()函数,该函数的作用就是将指针数组中 NtCreateProcessEx()函数的地址替换为 MyNtCreateProcess()函数的地址。而 MyNtCreateProcess()函数是用来取代NtCreateProcess()函数的函数,在我们的函数中调用了一条KdPrint()用于输出的代码。整个Hook的过程非常简单,只要找到指针数组的位置,保存原地址后修改为新的地址即可。在代码中出现了两个函数,分别是UN_PROTECT()和 RE_PROTECT()。这两个函数的作用是,禁止和开启CPU向标志为只读的内存页进行写入的操作。执行了 UN_PROTECT后CPU可以向标志为只读的内存页进行写入操作。当写入完成好后,调用RE_PROTECT()函数恢复到原来的状态。把它放到虚拟机中,打开DebugView然后加载该驱动,加载成功后随便运行一个可执行程序,可以看到在DebugView中显示了在 MyNtCreateProcess()中的输出了,如图7-10所示,说明我们的HOOK成功了。
图7-10 MyCreateProcess()函数的输出
前面介绍了 HOOK SSDT,下面介绍一下Inline HOOK SSDT,为什么要介绍Inline HOOK SSDT呢?在有些情况下,反病毒软件会保护SSDT不受篡改,其保护方式是检查Native API SSDT呢?在有些情况下,反病毒软件会保护SSDT不受篡改,其保护方式是检查Native API的函数指针数组中的值是否正确。如果其中某项或某几项被修改,那么反病毒软件会将被修改的项恢复到原来的值,这样HOOK SSDT就不成功了。还记得前面章节介绍Inline HOOK的原理吗?Inline Hook是修改被HOOK的入口字节为一个跳转,从而跳转入函数中。如果换作是Inline Hook SSDT的话,那么就不会去修改Native API的函数指针数组中的值了,这样可以避免反病毒软件的检测(注:这只是一个简单的说明,现在使用Inline HOOK SSDT同样会被反病毒软件查杀)。
实现Inline Hook SSDT的代码如下:
关于代码的部分就不多介绍了,测试一下,其运行结果如图7-11所示。
图7-11 Inline Hook
虽然应用程序处在操作系统的用户态,而驱动程序处在操作系统的内核态,但是它们之间是必须通信的,应用程序的执行都是依赖于操作系统内核的。
在编写驱动程序的时候,主函数的入口函数是DriverEntry()函数,该函数有一个参数为 PDRIVER_OBJECT类型,是指向驱动对象的指针的。为了能够使驱动程序被卸载掉,我们为DRIVER_OBJECT结构体中的DriverUnload成员进行了赋值。DriverUnload是驱动对象中的一个程序,其保存了驱动卸载例程(Routine,也就是函数的意思)的指针,其实保存的就是一个函数指针。在DRIVER_OBJECT中,除了 DriverUnload以外,还有一个数组也存放了函数指针,这个数组名是MajorFunction,该数组的大小为IRP_MJ_MAXIMUM_FUNCTION + 1,IRP_MJ_MAXIMUM_FUNCTION是一个宏,该宏的定义为:
在MajorFunction中可以指定非常多的例程指针,比如IRP_MJ_CREATE、IRP_MJ_READ、IRP_MJ_WRITE、IRP_MJ_CLOSE……这些例程都有一个非常相似的定义,定义如下:
这些是摘自WDK提供的文件中的定义,函数名可以随意修改,但是函数的定义是不能修改的,也就是说函数的返回值和参数是已经被确定的。在这些函数的定义中,第一个参数是PDEVICE_OBJECT的类型,是一个指向设备对象的指针。第二个参数是一个指向IRP结构体的指针,IRP是I/O请求包。应用程序与驱动程序的通信就是依靠IRP进行的。因此,要在DriverEntry()下指定MajorFunction数组中的各种例程。在没有指定MajorFunction的情况下,有默认的例程来执行。
既然在所有的例程中都需要设备对象指针,那么自然要创建一个设备,创建设备的函数为IoCreateDevice(),其定义如下:
该函数用来创建一个设备,参数说明如下。
(1)DriverObject:该参数是一个指向驱动对象的指针,指定设备所属的驱动,使用 DriverEntry()的第一个参数即可。
(2) DeviceExtensionSize:指定设备扩展的大小,如果没有设备扩展的话该参数为0。
(3) DeviceName:指定设备的名称,设备名通常是\Device\xxx。
(4) DeviceType:指定设备的类型,由于创建的设备没有具体对应的设备类型,因此此处指定 FILE_DEVICE_UNKNOWN。
(5) DeviceCharacteristics:设置设备对象的特征,这里赋值为0即可。
(6) Exclusive:该参数指定设备是否为互斥设备,这里赋值为TRUE即可。
(7) DeviceObject:该参数是一个指向设备对象的指针。
为了能与设备对象进行通信,必须为设备对象指定一个符号链接,用来在应用程序中打开。创建符号链接的函数为IoCreateSymbolicLink(),其定义如下:
该函数的第一个参数用来指定符号链接的名称,该名称通常的格式为\??\xxx,第二个参数为设备名称。
应用程序与驱动程序的通信方式通常有打开、关闭、读、写、控制这5种。这5种方式对应的 Win32 API函数分别为 CreateFile()、CloseHandle()、ReadFile()、WriteFile()和 DeviceIoControl()。而这5个Win32 API函数分别对应着5个派遣例程,分别是 IRP_MJ_CREATE、IRP_MJ_CLOSE、IRP_MJ_READ、IRP_MJ_WRITE和 IRP_MJ_DEVICE_ CONTROL。下面主要来介绍一下 DeviceIoControl()函数与 IRP_MJ_DEVICE_CONTROL派遣例程。
DeviceIoControl()函数是用来向指定设备发送控制码的,当指定的设备接收到 DeviceIoControl()发来的控制码后会调用IRP_MJ_DEVICE_CONTROL对应的派遣例程,针对不同的控制码进行不同的处理。DeviceIoControl()函数的定义如下:
(1) hDevice:该参数指明设备句柄。
(2) dwIoControlCode:该参数是控制码,该值由CTL_CODE生成。
(3) lpInBuffer:该参数是输入的缓冲区。
(4) nInBufferSize:该参数是输入缓冲区大小。
(5) IpOutBuffer:该参数是输出的缓冲区。
(6) nOutBufferSize:该参数是输出缓冲区大小。
(7) lpBytesReturned:该参数是返回字节的个数。
(8) lpOverlapped:该参数针对重叠方式使用,通常为0即可。
应用程序与驱动程序之间的通信实例以前面写的HOOK NtCreateProcessEx()来进行介绍。在应用程序中通过发送控制码来启动和关闭HOOK,对NtCreateProcessEx()的HOOK进行有效地控制。下面直接给出完整的源代码,代码如下:
下面来看一下应用层的代码,在于驱动进行通信时,首先要打开驱动程序:
在使用CreateFile()函数时,该函数的第一个参数为驱动程序的符号链接。定义驱动的符号链接为:L“\\??\\HookNtCreateProcessEx”,当在应用层中使用时应改为: \\\\.\\HookNtCreateProcessEx。
打开驱动程序后,通过DeviceIoControl()来对驱动程序发送控制码,使驱动程序开启和关闭HOOK。代码如下:
在应用程序中使用CTL_CODE宏时应包含头文件winioctl.h。运行看一下效果,当开启 HOOK时NtCreateProcessEx()会直接返回,也就是不进行进程的创建。当关闭HOOK后, NtCreateProcessEx()又会正常地创建进程。用KmdManager加载驱动,然后用我们编写的应用程序开启HOOK后运行的任意程序,创建进程都会失败,如图7-12所示。
图7-12 开启HOOK后创建进程失败
关于驱动的开发有非常多的东西需要学习,驱动的开发时下也非常流行,很多安全厂商(比如,反病毒、反外挂公司)都对驱动开发人员有大量的需求。
在学习编程的过程中,需要阅读大量的源代码才能提高我们的编程能力。可是通常情况下,在使用软件的过程中会发现某个软件的功能实现得非常不错,希望该功能能够在自己的软件中实现,但又得不到它的源代码进行参考。遇到这种情况,通常能够做的就是通过反汇编工具进行逆向分析,或者借助调试工具进行调试分析。
何为逆向工程?简单说就是将已经编译好的二进制程序通过反汇编分析来还原相应的源代码。本节就来介绍C语言代码的逆向基础知识,主要讲解C语言中流程控制代码对应的反汇编代码。
做逆向分析的第一步是先要对函数进行识别,也就是说确定函数的开始位置、结束位置、函数调用方式及函数的参数个数。在逆向分析的过程中,不会把单个指令当作是最基本的逆向单位,因为一条汇编指令很难说明任何问题,就像在C语言中,很难通过一行代码来说明问题一样。
大多情况下是针对自己比较感兴趣的部分进行重点分析,分析部分功能或者部分关键函数。因此,确定函数的开始位置及结束位置就很重要了。不过通常情况下,函数的起始位置和结束位置都可以通过反汇编工具自动识别,从而省去了我们自己的识别。使用强大的反汇编工具IDA更是如此,不过在某些特殊情况下仍然需要我们自己去识别。简单介绍一下如何识别函数的开始与结束的位置。写一个简单的C语言代码,用VC6进行编译连接,再用IDA打开。C语言代码如下:
在程序中由主函数调用了自定义函数test(),test()函数的返回值为int类型。在test()函数中调用了printf()函数和MessageBox()两个函数。逆向分析是在DEBUG编译方式下进行的 (RELEASE方式留给大家自己去研究)。用IDA打开我们编译连接好的程序文件。用IDA打开后,将直接找到main()函数(多么方便的工具),如果没有找到main()的话,通过简单的操作也能找到。下面先来通过运行时启动函数定位找到main()函数。
首先在IDA上单击选项卡中的“Exports”项,在这里可以看到mainCRTStartup,如图 7-13所示。
图7-13 “Exports”选项卡
双击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/push esi/push edi是保存的几个关键寄存器值,以便函数返回后这几个寄存器中的值还能在调用函数处继续使用而没有被破坏。lea edi,[ebp + var_44]/mov ecx, 11h/mov eax, OCCCCCCCCh/rep stosd,这几句是将开辟的内存空间全部初始化为0×cc,0×cc如果当机器码来解释的话,其对应的汇编指令为int 3,也就是断点中断指令。这样做是方便调试,尤其是对指针变量的调试好处非常多。以上代码是一个固定的形式,唯一会发生变化的部分是sub esp,xxx,这里是sub esp,44h。在VC6下的Debug编译方式下,如果当前函数没有变量,那么就是sub esp,40h;如果有一个变量的情况下是sub esp, 44h;两个变量时为sub esp,48h。也就是说,从Debug方式编译时总是在预留局部变量后将栈顶多抬高40h字节。局部变量都在栈空间中,栈空间进入函数后临时开辟的空间。函数入口处的代码基本上就这些,看一下函数返回处的反汇编代码:
这是函数返回时的固定格式,这个格式跟入口的格式基本是对应的。首先是pop edi/pop esi/pop ebx,这里是将上面保存的几个关键寄存器的值进行恢复。然后是add esp,xxx,这里是与sub esp,xxx对应的,将临时开辟的栈空间释放掉(这个释放只是改变寄存器)。Mov esp, ebp/pop ebp是恢复栈帧,retn就返回到上层函数了。在该反汇编代码中还有一步没有讲,这是cmp ebp,esp/call_chkesp,这两句是对_chkesp函数的一个调用,在Debug编译方式下,对几乎所有的函数调用完成后调用一次_chkesp,该函数的功能是用来检查栈是否平衡,以保证程序的正确性。如果栈不平,会给出错误提示。我们做个简单的测试,在主函数后面加一条内联汇编—asm push ebx,然后编译连接运行,在输出过后会看到一个错误的提示,如图 7-14所示。
图7-14 调用_chkesp后对栈平衡进行检查时出错的提示
整个的主函数还有最后一部分,反汇编代码如下:
首先是push 6/push offset aHello/call j_test/add esp,8/mov [ebp + var_4],eax是对test()。函数的一个调用,push eax/push offset aD/call printf/add esp,8是对printf()函数的调用,xor eax,eax是对eax进行清0。我们重点来看一下对test()函数的调用。双击j_test来到如下反汇编代码处:
这里仍然是个跳表,双击test来到如下反汇编代码处:
函数开头和结尾的部分我们都已经熟悉了,直接看其他部分吧,反汇编代码如下:
另外一段反汇编代码如下:
从这两段代码中可以看出分别是对printf()函数和MessageBoxA()函数的调用,在对printf()函数调用后有一条add esp,OCh,而在MessageBoxA()后面则没有。这是为什么呢?这就是调用约定。在VC下常见到两种调用约定,分别是stdcall和cdecl两种。
stdcall是Windows下的标准调用约定,Windows提供的API函数及WDK中提供的函数都是使用stdcall,当然也有例外,就是API中变参函数是使用cdecl调用约定。C语言默认的是使用cdecl调用约定。
stdcall参数的入栈方式是从右往左,平衡栈是在被调用函数的内部。cdecl参数的入栈方式也是从右往左,平衡栈是在调用函数方。这就是两种函数调用的约定方式,没有好与不好,只是调用方式不同而已,唯一一点是stdcall不能支持变参函数。
下面用一个相对简单的if……else……分支实例来说明其对应的反汇编,C代码如下:
代码很简单,用IDA看其关键的反汇编代码:
以上3句分别是对3个变量进行赋值初始化。
观察上面的反代码发现,代码中使用的比较关联符号是“大于号”,而反汇编代码却使用了相反的比较跳转指令,使用的是“小于等于”则跳转,否则不跳转。请注意观察00401043和0040105E这两个地址,jle会跳过紧接着的下面的部分代码,跳转的目的地址上面是一条无条件跳转指令jmp,也就是说,jle和jmp之间的部分是代码中比较表达式成功后执行的代码。在反汇编代码中,如果比较指令后的跳转指令没有发生跳转的话,则在执行完一系列指令后,会紧跟着一条无条件跳转指令jmp跳到某个地址。请注意观察00401056和00401071这两个地址,这两个跳转指令跳转到了同一个目的地址。在C源代码中,当比较表达式成功后会执行其后面的代码块,当执行完这些代码块后就跳过与之配对的else if或else后面的代码。
上面C源代码中if……else……对应反汇编结构如下:
这基本上就是if……else……分支结构的执行流程了,当反汇编时遇到这样的结构就可以马上知道这是一个if……else……分支结构了,大的流程定下以后就可以具体地分析每个分支要执行的代码了。
前面讲了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分支的反汇编形式比较有用,这里只介绍了其中的一种反汇编形式,其余的反汇编形式就不进行介绍了,大家可以自行学习。
介绍完分支结构后,相应的就要介绍C语言中的循环结构,下面先来介绍一下for循环, C语言代码如下:
这是一个很经典的求1加到100的累加和的例子,我们看其反汇编代码:
这段for循环的反汇编代码很清晰,也很简单,就不对反汇编代码做具体介绍了,下面只对for循环结构做一个反汇编的总结,具体如下所示:
修改循环变量处:
循环变量比较处:
最后再介绍两种循环结构,分别是do……while和while循环结构,同样使用简单的例子来整理其反汇编对应的结构。先来看do……while循环,C语言代码如下:
再看其反汇编代码:
do……while循环对应的反汇编结构如下:
下面来看一个while循环的代码:
看其反汇编代码:
整个反汇编代码与do……while类似,只是把判断放到前面了,while循环的结构如下:
对于for、do……while和while这3种循环来说,do……while的效率显然更高一些。编程时可以尽量选择do……while循环来使用。
对于C语言的逆向知识就介绍这么多。大家可以写一些C语言代码,然后用IDA来进行分析学习。
1.段钢.加密与解密(第三版).北京:电子工业出版社。
2. 王艳平.Windows程序设计.北京:人民邮电出版社。
3. (美)Jeffrey Richter著.王建华,张焕生,侯丽坤等译.Windows核心编程.北京:机械工业出版社。
4. Eldad Eilam著作.韩琪,杨艳,王玉英,李娜等译.Reversing:逆向工程解密.北京:电子工业出版社。
5. (美)Greg hoglund James Butler著.韩智文译.ROOTKITS-Windows内核的安全防护.北京:清华大学出版社。
Table of Contents
1.1 编程语言和开发环境的选择
1.1.1 何为SDK、API和MFC
1.1.2 VC6和SDK的配置
1.2 应用程序的调试
1.2.1 编写我们的第一个程序
1.2.2 用VC6调试第一个程序
1.2.3 专业的应用程序调试工具——OllyDbg
1.3 简单API的介绍
1.3.1 复制自身程序到Windows目录和系统目录下
1.3.2 获得系统的相关信息
1.3.3 Debug和Release的编译方式
1.3.4 查看函数定义
1.4 总结
2.1 网络通信基础
2.1.1 IP地址的作用与分类
2.1.2 端口的作用与分类
2.2 网络编程基础知识
2.2.1 通信模型
2.2.2 Winsock
2.2.3 Winsock的相关函数
2.2.4 字节顺序
2.3 简单的通信程序
2.3.1 基于TCP协议的“Hello World!”
2.3.2 基于UDP协议的“Hello World!”
2.4 实现一个C/S模式的简单木马
2.4.1 木马服务器端的实现
2.4.2 木马客户端的实现
2.5 总结
3.1 文件
3.1.1 打开文件
3.1.2 文件操作
3.2 AutoRun免疫程序的编写
3.2.1 AutoRun免疫原理
3.2.2 AutoRun免疫程序的代码实现
3.2.3 界面设置
3.2.4 代码相关部分
3.3 注册表操作
3.3.1 注册表
3.3.2 与注册表操作相关的常用API函数
3.3.3 注册表启动项的管理
3.3.4 程序的界面设置及相关代码
3.3.5 启动项的枚举
3.3.6 添加启动项的代码
3.3.7 删除启动项的代码
3.4 服务相关的编程
3.4.1 如何查看系统服务
3.4.2 服务控制管理器的开发
3.4.3 枚举服务的相关API函数
3.4.4 服务的停止
3.4.5 停止服务的相关API函数
3.4.6 服务的启动
3.5 进程与线程
3.5.1 进程
3.5.2 进程的创建
3.5.3 “下载者”的简单演示
3.5.4 CreateProcess()函数介绍与程序创建
3.5.5 进程的结束
3.5.6 进程的枚举
3.5.7 调整当前进程的权限
3.5.8 进程的暂停与恢复
3.5.9 多线程
3.6 DLL编程
3.6.1 什么是DLL
3.6.2 编写一个简单的DLL程序
3.6.3 对DLL程序的调用方法一
3.6.4 对DLL程序的调用方法二
3.7 远程线程
3.7.1 DLL注入
3.7.2 DLL卸载
3.7.3 无DLL的代码注入
3.8 总结
4.1 PE文件结构
4.1.1 PE文件结构全貌
4.1.2 MZ头部
4.1.3 PE头部
4.1.4 节表
4.1.5 节表数据
4.2 详解PE文件结构
4.2.1 DOS头部详解 IMAGE_D〇S_HEADER
4.2.2 PE头部详解IMAGE_NT_HEADERS
4.2.3 IAMGE_FILE_HEADER
4.2.4 IMAGE_OPTIONAL_HEADER
4.2.5 节区详解IMAGE_SECTION_HEADER
4.2.6 与PE结构相关的3种地址
4.2.7 3种地址的转换
4.3 PE查看器
4.4 简单的查壳工具
4.5 地址转换器
4.6 添加节区
4.6.1 手动添加一个节区
4.6.2 通过编程添加节区
4.7 破解基础知识及调试API函数的应用
4.7.1 CrackMe程序
4.7.2 用OD破解CrackMe
1.破解方法一
2.破解方法二
4.8 文件补丁及内存补丁
4.8.1 文件补丁
4.8.2 内存补丁
4.9 调试API函数的使用
4.9.1 常见的3种断点方法
4.9.2 调试API函数及相关结构体介绍
4.9.3 判断是否处于被调试状态
4.9.4 断点异常函数
4.9.5 调试事件
4.9.6 调试循环
4.9.7 内存的操作
4.9.8 线程环境相关API及结构体
4.10 打造一个密码显示器
4.11 总结
5.1 HOOK知识前奏
5.2 内联钩子——Inline Hook
5.2.1 Inline Hook的原理
5.2.2 Inline Hook的实现
5.2.3 HOOK MessageBoxA
5.2.4 HOOK CreateProcessW
5.2.5 7字节Inline Hook
5.2.6 Inline Hook的注意事项
5.3 导入地址表钩子——IATHOOK
5.3.1 导入表简介
5.3.2 导入表的数据结构定义
5.3.3 手动分析导入表
5.3.4 枚举导入地址表
5.3.5 IAT HOOK介绍
5.3.6 IAT HOOK之 CreateFileW ()
5.4 Windows钩子函数
5.4.1 钩子原理
5.4.2 钩子函数
5.4.3 键盘钩子实例
5.4.4 使用钩子进行DLL注入
5.5 总结
6.1 恶意程序剖析
6.1.1 恶意程序的自启动
6.1.2 木马的配置生成与反弹端口
6.1.3 代码实现剖析
6.2 简单病毒剖析
6.2.1 病毒的感染剖析
6.2.2 缝隙搜索的实现
6.2.3 感染目标程序文件剖析
6.2.4 添加感染标志
6.2.5 自删除功能的实现
6.3 隐藏DLL文件
6.3.1 启动WinDBG
6.3.2 调试步骤
6.3.3 编写枚举进程中模块的函数
6.3.4 指定模块的隐藏
6.4 安全工具开发基础
6.4.1 行为监控工具开发基础
6.4.2 专杀工具
6.4.3 U盘防御软件
6.4.4 目录监控工具
6.5 引导区解析
6.5.1 通过WinHex来手动解析引导区
6.5.2 通过程序解析MBR
6.5.3 自定义MBR的各种结构体
6.5.4 解析MBR的程序实现
6.6 加壳与脱壳
6.6.1 手动加壳
6.6.2 编写简单的加壳工具
7.1 驱动版的 “Hello World”
7.2 驱动下的进程遍历
7.2.1 配置VMware和WinDbg进行驱动调试
7.2.2 EPROCESS和手动遍历进程
7.2.3 编程实现进程遍历
7.3 HOOK SSDT(系统服务描述表)
7.3.1 SSDT简介
7.3.2 HOOK SSDT
7.3.3 Inline HOOK SSDT
7.4 应用程序与驱动程序的通信
7.4.1 创建设备
7.4.2 应用程序与驱动程序的通信方式
7.4.3 应用程序与驱动程序的通信实例
7.5 C语言代码逆向基础
7.5.1 函数的识别
7.5.2 if……else……分支结构
7.5.3 switch分支结构
7.5.4 for循环结构
7.5.5 do while与 while循环结构