PS:本书将指引你进入美妙又刺激的代码逆向分析世界,开启一段神奇之旅!
第二章 逆向分析 Hello World!程序
OD常用快捷键
- 执行到返回:ctrl+F9
- 重新开始:ctrl+F2
- 执行到光标处:F4
- 编辑数据:ctrl+E
- 编写汇编代码:空格
- 注释:;
- 设置标签::
设置程序大本营
1.先对所要设置的语句设置一个标签,再通过查看已经设置的标签找到。同时标签可以让代码变得非常直观。
2.与标签相同的方法,只不过使用 ;,再查看已经设置的注释。
3.设置断点。
4.使用命令跳转命令:ctrl+G
快速查找指定代码
1.API检索法(1):在调用代码中设置断点。
- 在事先推测出代码要使用API后,鼠标右键菜单-查找-所有模块间之间的调用。找到指定函数后双击查看。
2.API检索法(2):在API代码中设置断点。
- 鼠标右键菜单-查找-所有模块名称,列出被加载的DLL文件中提供的所有API。找到函数,进入下断点,F9执行,在该函数停下后,使用ctrl+F9执行到ret,最后F7回到被调用函数的下一条语句处。
第三章 小端序标记法
介绍
字节序:多字节数据在计算机内存中存储或网络传输时各字节的存储顺序。主要二大类:大端与小端。
大端:高地址存放数据的低位,低地址存放数据的高位。
小端:高地址存放数据的高位,低地址存放数据的地位。
应用
- 大端序常用于大型UNIX服务器的RISC系列的CPU与网络传输协议中。
- 小端序,Intel x86 Cpu采用的序列。
第四章 IA-32寄存器基本详解
介绍
寄存器:CPU内部用来存放数据的一些小型存储区域。它集成在CPU内部,拥有非常高的读写速度。
基本程序运行寄存器
1.通用寄存器(32位 8个):EAX,EBX,ECX,EDX,ESI,EDI,EBP,ESP
ps:为了实现对低16位寄存器的兼容,各寄存器又可分为高:H(high),低(L:low)几个独立的寄存器。
- EAX:(0-31)32位
- AX:(0-15)EAX的低16位
- AH:(8-15)AX的高8位
- AL:(0-7)AX的低8位
EAX:(针对操作数和结果数据的)累加器
EBX:(DS段中的数据指针)基址寄存器
ECX:(字符串和循环操作的)计数器
EDX:(I/O指针)数据寄存器
ps:以上4个寄存器主要用在算术运算之路中,常常用于保存常量与变量的值,EAX一般用在函数返回值中。
EBP:(SS段中栈内数据指针)拓展基址指针寄存器
ESI:(字符串操作源指针)源变址寄存器
EDI:(字符串操作目标指针)目的变址寄存器
ESP:(SS段中栈指针)栈指针寄存器
ps:以上四个寄存器常用作保存内存地址的指针。
2.段寄存器(16位 6个):CS,DS,SS,ES,FS,GS
CS:代码段寄存器
SS:栈段寄存器
DS:数据段寄存器
ES:附加数据段寄存器
FS:数据段寄存器
GS:数据段寄存器
3.程序状态与控制寄存器:ELFLAGS寄存器的每位都有意义,每位的值为0或1。
常见的三个:
- ZF:若运算结果为0,则其值为1,否则为0
- OF:有符号整数溢出时,OF值被置为1。此外,MSB(最高有效位)改变时,其值也被设为1
- CF:无符号整数溢出时,其值被置为1
4.指令指针寄存器(EIP):保存着CPU要执行指令的地址,每执行完一条指令就会通过EIP寄存器读取下一条指令。
第五章 栈
栈:其实是一种数据结构,它按照FILO(先进后出)的原则存储数据。用于存储局部变量,传递函数参数,保存函数的返回地址。
这里拿之前在PWN学习过程中对栈学习后的图做补充。
第六章 分析abex’ crackme#1
- 使用VC++,VC,Delphi等开发工具编写的程序,除了自己编写的代码外,还有一部分启动函数由编译器添加的。但是如果使用汇编语言编写程序,汇编代码直接变为反汇编代码,main()函数直接出现在EP中。
- GetDriveType()函数:获取C驱动器的类型(大部分返回的是HDD类型)。
- 注意调用Win32 API后,某些特定寄存器的值就会改变。
第七章 栈帧
介绍
栈帧:利用EBP(栈帧指针)寄存器访问栈内的局部变量,参数,函数返回地址的手段。
函数开始时,会先通过
push ebp;mov ebp, esp;
生成与其对应的栈帧。函数结束时会通过mov esp, ebp;pop ebp;(leave)
删除栈帧。最新的编译器都有一个优化选项,使用该选项编译简单的函数将不会产生栈帧。
知识点
函数中局部变量使用sub esp xx;开辟空间;函数结束后,参数传递使用的栈空间使用add esp xx;进行清理。
在栈窗口中,点击鼠标右键,选择地址后可以选择栈相对于那个寄存器的偏移来显示,可以让栈空间更直观。
对于原来以EBP表示的函数局部变量,可以通过使用快捷键alt+o,然后选择分析1,将显示函数中的局部变量与参数的选项勾上。那我们的在OD中看到的局部变量将会被表示成:LOCAL.n,参数表示成:ARG.n。
函数调用规则:cdecl:函数调用者(Caller)负责清理储存在栈中的参数。
stdcall:被调用者(Callee)负责清理保存在栈中的参数。
XOR命令:异或运算,由于XOR命令比MOV EAX,0命令执行速度快,所以常用于寄存器初始化操作。
第八章 abex’ crackme #2
第一次分析vb写的程序,开始很不适应,后面从对象出发还是好很多,但始终还是不如VC写的简单,还是自己能力不够。
VB编写程序的介绍
事件处理程序:VB主要用来编写GUI程序。由于VB程序采用windows操作系统的系统的事件驱动方式工作,所以在main()或WinMain()中并不存在用户代码(即我们希望调试的代码),用户代码存在于各个事件处理程序中。
未文档化的结构体:VB中使用的各种信息以结构体形式保存在文件内部。由于微软未正式公开这种结构体信息,所以调试VB文件会难一些。
分析程序
首先调用VB引擎主函数(ThunRTMain)
这里直接从事件处理程序开始
获取输入的name字符串
长度比较
使用name生成密码
最后,比较密码
难点:程序中处理的每个数据都是对象,调试时不易看,且程序中函数太多。
第九章 Process Explorer
积累一个新工具:进程管理。
可以显示电脑中运行每个进程哦PID,CPU占有率,注册信息,终止进程。
第十章 函数调用约定
三种主要的函数调用约定:cdecl(C语言默认调用),stdcall(使用_stdcall关键字来使用stdcall方式编译),fastcall
它们通过栈来传递参数的方式都是一样的,区别在于清理栈的过程。
cdecl:函数调用者清理压入栈的参数。在调用函数之后通过:add esp xxx;
stdcall:被调用者清理栈。如RETN 8;命令,它的含义就是:RETN+POP 8字节,返回后使esp增加指定大小。(Win 32 API 使用stdcall方式,这个方式可以获得更好的兼容性,使C语言之外的其他语言也能直接调用API)
fastcall:前2个参数会使用ECX与EDX寄存器传递参数,实现对函数的快速调用。其他与stdcall相似。
第十一章 视频讲座
收获两点:1.查看传入一个函数的参数个数。2.neg指令与sbb指令。
1.查看传入函数参数个数:首先判断函数调用约定,再在函数调用前后查看栈情况即可。
函数调用后,两次的esp只差即为使用栈空间传递参数大小。
2.neg指令的操作对象是0则CF=0,否则CF=1
第十三章 PE文件格式
之前已经学习总结在另一个帖子:http://www.bxnop.cn/2020/05/30/PE%E5%AD%A6%E4%B9%A0/
第十五章 调试UPX压缩的notepad程序
知识点
- GetModuleHandleA()函数:获取程序的imagebase。
- GetProcAdress()函数:从EAT中获取指定名称的API的实际地址。
- pushad命令:将8个通用寄存器(EAX-EDI)的值保存到栈;popad是将栈中的各个值再次回复到各个寄存器。
- 在内存复制命令中,ESI指Source,EDI指Destination。调试时,同时设置ESI与EDI时就应该猜想从ESI所指缓冲区到EDI所指缓冲区的内存发生复制。
- ctrl+f11:反复执行step into,ctrl+f12:反复执行step over(都是不显示画面)
- 脱壳后修正IAT的原因:压缩后,文件INT(记录API名称,序数)已经损坏。
操作
1.从一个循环一个循环的跳出解压缩过程最后找到EP。
2.利用堆栈平衡的原理,在程序刚开始的的esp处的数据下硬件断点,F9执行,断下的地方即是解压缩完成的地方。
断点->硬件访问->字节
第十六章 基址重定位表
介绍
PE文件在重定位过程中用到基址重定位表。
当向进程的虚拟内存加载PE文件(EXE/DLL/SYS)时,文件会被加载到PE头的imagebase所指的地址处,若加载的是DLL(SYS)文件且在imagebase位置处已经加载了DLL(SYS)文件,那么PE装载器就会将其加载到其他未被占用的空间,这就会引起一系列重定位问题。
创建好进程,EXE文件会首先加载到内存,所以EXE文件无需考虑重定位问题,但windows vista之后的版本引入了ASLR的安全机制。
基址重定位表
PE头的DataDirectory[5]记录了它的RVA与SIZE。
1 | typedef struct _IMAGE_BASE_RELOCATION |
以查看notepad程序的重定位表为例:
程序实例分析
可以看到,程序中的内存地址以硬编码形式存在,如果加载的基地址不同又不进行重定位,程序将内存地址引用错误退出。
重定位操作的基本操作原理
在应用程序中查找硬编码的地址位置
读取值后,减去imagebase(VA->RVA)
加上实际加载的地址(RVA->VA)
找到第一个硬编码位置:
加载OD,查看指定RVA:
这里经过的过程:(1)由于imagebase为:1000000,所以开始读到的值为:10010C4,(2)减去imagebase:10010C4-1000000 = 10C4,(3)加上实际的加载基地址:10C4+F20000 = F210C4
一直重复上述过程,直到TypeOffset的值为0,则表明一个结构体块结束。对所有结构体块如此,直到遇到NULL(即最后一个结构体以NULL结束)。
第二十章 “内嵌补丁练习”
介绍
内嵌补丁:内嵌代码补丁的简称,对难以修改指定代码时,在程序中插入并运行被称为“洞穴代码”的补丁代码后对程序打补丁的技术。
实操
首先单步调式,很快来到2处解密代码的地方,记录好解密代码区域:
继续单步:
继续:
EDX寄存器为4个字节大小,像这样向其中不断加上4个字节的值,就会发生溢出。一般的校验和计算中常常忽略该溢出问题,使用最后一个保存在EDX中的值。
继续单步步入:
最后解密完成,来到OEP:40121E
打内嵌补丁操作:找到写入补丁代码的区域:(1)设置到文件空白区域。(2)扩展最后的节区后设置。(3)添加新节区后设置。
这里直接找空白区域:在.txt节区的:401280开始写入代码:
最后在之前的jmp oep的地方,把oep修改为我们的内嵌补丁。但是注意:jmp oep指令也是加密了的,所以我们要写入异或过的数据。且这里要写入文件中才可以,不能是内存中。
最后,打开文件成功。
第二十五章 通过修改PE加载DLL
介绍
通过“直接修改目标文件的可执行文件”,使其运行时强制加载指定的DLL文件。每当进程开始运行时就会自动加载指定的DLL文件,而加载了某DLL文件会自动执行其DLLMain。
本技术关键就是对PE文件头的修改,把之前学习了PE知识应用起来(特别是IMAGE_IMPORT_DESCRIPTOP)。
实操
对于要强制加载dll文件源代码就是下载一个网页的功能,本技术的关键不在这里,所以我只将我查过其中的一些函数记录下来:
1 | DWORD WINAPI GetModuleFileName( |
由于PE文件中导入的DLL信息以结构体列表形式存储在IDT。
首先查看IDT的所占空间,找到地址,发现以NULL结尾的IID后存在其他数据。所以要添加一个IID的话,我们就要移动IDT到一个新的足够大空间,再在其尾部添加一个IID。
确定移动目标位置:(3种方式)
- 查找文件的空白区域。
- 增加文件最后一个节区的大小。
- 在文件末尾添加新节区。
首先直接查看本节区中的空白区域,发现有足够的空间,但要计算该区域中加载到进程的虚拟内存的区域,因为只有节区头明确记录的区域才会被加载。
所以还有未被使用的区域大小为:1AA,这是足够的。
节区在磁盘文件中的大小比加载到内存的大小大的原因:
文件的大小是经过文件对齐后的。
开始修改要记载指定dll的文件:
修改导入表的RVA值及大小。(新移动的区域)
删除绑定导入表。(可有可无,提高DLL加载速度的技术)
创建新的IDT,将原IDT复制到新区域并在尾巴添加新的IID。
设置Name,INT,IAT。(PE学习已经详细记录了,这里就就只是实操了下)。
修改IAT节区的属性值,增加可写属性。IMAGE_SCN_MEM_WRITE(80000000)。所以从40000040 -> C0000040。
最后本章主要是还是对PE文件熟悉了下。
第二十七章 代码注入
之前学习了DLL注入,而代码注入与之最大的区别就是只向目标进程注入要运行的代码与数据,注入完成后之后消失。原理类似。
使用dll注入优点:1.占用内存少。2.难以查找痕迹。3.不需要另外的dll文件,只要有代码注入程序即可。
整个流程:
改变进程的权限进行提取。
代码注入:
- 使用GetModuleHandleA()函数获得指定模块(kernel.dll)的句柄,为了获得要用函数的地址。
- 使用OpenProcess()函数获得指定PID号的进程的句柄。
- 设置THREAD_PARAM结构体变量并对其赋值(所要使用的函数地址,字符串),在目标进程申请内存,将该结构体写入目标进程。
- 计算出线程函数的大小,在目标进程申请内存,将线程函数的代码写入目标进程。
- 使用CreateRemoteThread()函数在目标进程创建一个远程线程(利用已经写入目标进程内存的数据)。
tips:
Window OS中,加载到进程的kernel32.dll的地址都相同,所以我们在注入程序中获得的API(“LoadLibraryA”,“GetProcAddress”)的地址与目标程序进程中获取的同然的API的地址是相同的。因此可以直接在注入程序中获得函数的地址。
其实整个流程也很简单,但熟练,用的好还是要多实战经历。
完整代码:(注释可以看的很清楚)
1 |
|
向notepad注入显示一个对话框的代码并调试注入代码:
首先查看将notepad加载入OD并运行,然后使用Process Explorer进程管理查看notepad的PID;OD中设置事件终止在新的线程(即我们在另一个程序使用CreateRemoteThread()在notepad中创建的线程)。
开始注入,OD中成功暂停在新的线程。
成功。
另外有一个注意的就是,使用VS编译上面的代码时一定要选release编译选项,不然会注入失败。。原因之一是:release编译选项的编译的exe中二进制代码函数的顺序与源代码中的一致,这样就能使用后一个函数减前一个函数获得函数的大小。其他原因暂时不知道。。。
最后,win32编程要多熟悉熟悉才行。。
第四十一章 ASLR
总结在其他帖子:http://www.bxnop.cn/2020/05/12/ASLR/
第四十五章 TLS回调函数
介绍
TLS是各线程的独立的数据存储空间,用来保存变量或回调函数。使用TLS技术可在线程内部独立使用或修改进程的全局数据或静态数据,就像对待自身的局部变量一样。
TLS回调函数是指,每当创建或终止进程时,TLS回调函数都会自动调用执行,前后共2次,执行进程的主线程(运行进程的EP代码)前,TLS回调函数会先别调用执行,这使得该特征应用于反调试技术。
若编程中启用了TLS功能,PE头文件就会设置TLS表(TLS Table)项目,即可选头中的IMAGE_DATA-DRRECTORY[9]记录了TLS表的RVA与SIZE。
1 | typedef struct _IMAGE_TLS_DIRECTORY32 |
IMAGE_TLS_DIRECTORY结构体有2种版本,分别为32位(大小:18h)与64位,但只是成员字节大小不一样。
1 | TLS Callback回调函数的定义: |
调试TLS回调函数
因为TLS回调函数在EP代码之前被调用执行了,直接使用调试器打开是无法调试的。
修改ollydbg的选项,让调试器暂停的位置是系统断点(System Startup Breakpoint)来调试TLS回调函数。
在Ollydbg调试器的默认设置下,调试器会在EP处暂停,而WinDbg调试器默认在系统启动断点暂停。
使用CFF explorer查看tls回调表回调函数的地址。可以看到有2个回调函数。
使用olldbg advanced插件,打开暂停在回调函数的选项。F9运行。可以看到,和上面查看的回调函数地址一样。
手工添加TLS回调函数
首先确定IMAGE_TLS_DIRECTORY结构体与TLS回调函数放到文件的那个位置。向PE文件添加代码或数据时,有三种方法:
1.添加到节区的末尾的空白区域。2.增加最后一个节区的大小。3.在最后增加新节区。
之前的内嵌打补丁使用的第1种方法,这里使用第二种。
查看最后一个节区的属性:注意section alignment:1000,file alignment:200
知道文件对齐单位是200,所以这里将最后一个节区大小增加200(即文件文件大小变为9200+200 = 9400),那需要修改最后一个节区的Raw Size从200增加到400。但是Virtual Size可以不修改,因为文件对齐单位是1000,加上200后也远小于1000的。
修改Raw Size:400。增加3个属性:1.节中包含代码;2.可执行;3.可写。
20 IMAGE_SCN_CNT_CODE 节中包含代码
20000000 IMAGE_SCN_MEM_EXECUTE 可执行
80000000 IMAGE_SCN_MEM_WRITE 可写
下面设置TLS表:
写入IMAGE_TLS_DIRECTORY结构体:
写入回调函数:
成功:
第四十六章 TEB
介绍
TEB:线程环境块,该结构体包含进程中运行线程的各种信息,进程中的每个线程都对应一个TEB结构体。
TEB结构体中的重要成员:
1 | +0x000 NtTib :_NT_TIB |
NtTib成员:
1 | typedef struct _NT_TIB |
ExceptionList成员指向EXCEPTION_REGISIRATION_RECORD结构体组成的链表,它用于Windows OS的SEH。
Self成员是_NT_TIB结构体的自引用指针,也是TEB结构体的指针(因为TEB结构体的第一个成员是_NT_TIB结构体)
实操
Ntdll.NtCurrentTeb()API用来放回当前线程的TEB结构体的地址。
OD中载入一个程序,搜索该API并进入:
TEB结构体的地址与FS段寄存器所指的段内存的基址是一样的。
FS段寄存器
FS段寄存器用来指示当前线程的TEB结构体。实际上,FS寄存器并非直接指向TEB结构体的地址,它持有SDT的索引,而该索引持有实际的TEB地址。
SDT位于内核内存区域,其地址存储在特殊的寄存器GDTR(全局描述符表寄存器)中。
上述示意图:
由于段寄存实际存储的是SDT的索引,所以它也被称为“段选择符”,TEB结构体位于FS段选择符所指的段内存的起始地址处。
FS:[0x18] = TEB起始地址:
FS:[0x18] = TEB.NtTib.Self = address of TIB = address of TEB = FS:0
FS:[0x30] = PEB起始地址:
FS:[0x30] = TEB.ProcessEnvironmentBlock = address of PEB
FS:[0] = SEH起始地址:
FS:[0] = TEB.NtTIb.ExceptionList = address of SEH
第四十七章 PEB
介绍
PEB:进程环境块,存放进程信息的结构体。
TEB结构体位于FS段选择符所指的段内存的起始地址处,而ProcessEnvironmentBlock成员位于距TEB结构体Offset 30位置,所以:FS:[30] = TEB.ProcessEnvironmentBlock = address of PEB
获取PEB的两种方法:
1.直接获取PEB地址:mov eax, dword ptr fs:[30]; fs:[30] = address of PEB
2。先获取TEB地址,再通过ProcessEnvironmentBlock成员+30偏移处获取:mov eax, dword pte fs:[18]; mov eax, dword ptr ds:[eax+30]
PEB中重要成员:
1 | +002 BeingDebugged :UChar |
实操
PEB.BeingDebugged
Kernel32.dll中的Kernel32!IsDebuggerPresent()API:该API通过检查PEB.BeingDebugged成员确定是否正在调试进程(是返回1,否返回0):Bool WINAPI IsDebuggerPresent(void);
Windows 7中,IsDebuggerPresent()API是在Kernelbase.dll中实现。而在Windows XP及以前的版本的操作系统中,它在Kernel32.dll中实现。
PEB.ImageBaseAddress
PEB.ImageBaseAddress成员用来表示进程的Imagebase:GetModuleHandle()API用来获取ImageBase
PEB.Ldr
PEB.Ldr成员是指向_PEB_LDR_DATA结构体的指针:
1 | +000 Length :Uint4B |
当模块(DLL)被加载到进程,通过PEB.Ldr成员可以直接获取该模块的加载基地址。
_PEB_LDR_DATA结构体成员中有3个_LIST_ENTRY类型的成员(上面已标出):_LIST_ENTRY结构体提供了双向链表的机制。
1 | typedef struct _LIST_ENTRY |
该链表中保存的信息:_LDR_DATA_TABLE_ENTRY结构体:
1 | typedef struct _LDR_DATA_TABLE_ENTRY |
每个加载到进程中的DLL模块都有与之对应的_LDR_DATA_TABLE_ENTRY结构体,这些结构体相互链接,最终形成_LIST_ENTRY双向链表。
PEB.ProcessHeap & PEB.NtGlobalFlag
PEB.ProcessHeap和PEB.NtGlobalFlag(像PEB.BeingDebugged一样)应用于反调试结束。若进程处于调试状态,则ProcessHeap于NtGlobalFlag成员就持有特定值。