re-core-principle-4
逆向工程核心原理笔记-4
0x00 优秀分析工具标准
即便是普通的工具,对其认真研习并运用到极致,它也能成为天下独一无二的优秀工具。
0x01 64位计算
windows 95是32位的,但也能运行16位DOS。windows 2000/XP也是32位的。IA-64(Itanium)是专用在大型服务器与超算中,不支持32位。而现在的PC中的x64则支持32位。如果要用64位支持32位程序,首先在数据的定义上,如下所示:
1 | ILP32: Integer、Long、Pointer-32位 |
微软提供了WOW64(windows on windows 64)的机制,使得32位程序在64位程序中正常运行。64位应用程序会加载kernel32.dll
(64位)与ntdll.dll
(64位)。而32位应用程序则会加x64载kernel32.dll
(32位)与ntdll.dll
(32位),WOW64会在中间将ntdll.dll
(32位)的请求(API调用)重定向到ntdll.dll
(64位)。如下图所示:
windows下有SysWOW64
与System32
两个文件夹,SysWOW64
中存放32位dll,当运行32位程序时,就用这个,映射到System32
文件夹下的dll中。System32
存放64位dll。因此,64位应用程序中使用GetSystemDirectory
查找系统文件夹,正常返回System32
文件夹,32位应用程序中调用GetSystemDirectory
返回的文件夹名称也为System32
,文件夹的实际内容与SysWOW64
文件夹是一样的,这是WOW64在中间截获了API调用并进行操作后返回的结果,这使32位应用程序可以正常运行。
对于注册表而言,使用32位进程请求访问HKLM/SOFTWARE
下的键时,WOW64会将其重定向到32位的HKLM/SOFTWARE/Wow6432Node
下的键。当使用64位进程请求访问HKLM/SOFTWARE
下的键时,就正常进行访问。注意,与文件系统不同,注册表无法完全分离32位与64位,经常出现32/64位共用的情形。有时候向 32 位部分写入的值会自动写入 64 位部分。
0x02 x64处理器
x64相比于x86新增了很多东西,我们在此只说一小部分。
- 内存地址变为64位,因此寄存器大小以及栈都变为64位。
- x64进程虚拟内存大小为16TB,32位进程虚存仅为4GB。
- x64系统中通用寄存器大小为64位,数量增加到18个,此外,还有16个XMM寄存器,每个XMM寄存器为128位。64位寄存器的字母以
R
开头,x86以E
开头。64位系统中不使用段寄存器。
- call/jmp解析方式不同。
32位如下所示,其中FF15后面的00405000解析为绝对地址。
64位如下所示,其中FF15后面的3FFA解析为相对地址。
计算方法:00000001 0040100 + 3FFA + 6 = 00000001 00405000
。而又因为00000001 00405000
存储着00000000 75CE1E12
,因此最后被解析为call 00000000 75CE1E12
。
- 32位调用函数包括
cdecl
、stdcall
、fastcall
三种,而64位系统中只有fastcall
,它把函数的4个参数传递到寄存器中,进行参数传递,64位系统中由调用者清理栈。(复习:被调函数执行完毕后,函数的调用者(Caller)负责清理存储在栈中的参数,这种方式称为cdecl
方式。反之,被调用者(Callee)负责清理保存在栈中的参数,这种方式称为stdcall
方式。fastcall
方式与stdcall
类似,但是使用寄存器传递前两个参数,而不是使用栈。其调用速度快。)如下表所示:
- 调用子函数时,不再使用push命令传递参数,而是通过mov指令操作寄存器与预定的栈来传递。创建栈帧时也不再使用RBP寄存器,而是直接使用RSP寄存器来实现。使用 Visual C++ 编译32位程序时,若开启了编译器的优化功能,则几乎看不到使用EBP寄存器的栈帧。
该方式的优点:调用子函数无需改变栈指针RSP,函数返回也无需清理RSP,大幅度提升速度。下面来验证一下,代码如下,分别用win32与x64进行编译:
1 |
|
win32的main函数情况:
(1)不使用栈帧。由于代码比较简单,变量又少,开启编译器的优化选项后,栈帧会被省略。
(2)调用子函数(CreateFileA
、CloseHandle
)时使用栈传递参数特征。
(3)使用PUSH指令压入栈的函数参数不需要main函数清理。在32位环境中采用stdcall
方式调用Win32API时,由被调用者清理栈。
(4)调用CreateFileA
后,CreateFileA
会自动调CreateFileW
。
x64的程序则需要使用windbg调试。其main代码如下所示:
(1)使用变形的栈帧。在代码起始部分分配58h字节大小的栈,最后在RET命令之前释放。栈操作并未使用RBP寄存器,而直接使用RSP寄存器。
(2)几乎没有使用PUSH/POP指令。设置参数的代码(00000001 4000101d-00000001 40001044
)。前4个参数使用寄存器(RCX、RDX、R8、R9)。后3个参数使用栈。有意思的是并未看到调用者清理栈(这原来是64位fastcall
的特征),原因在于子函数使用的是分配给main的栈,子函数本身不会分配栈。
(3)第5个参数在栈中的存储位置有些奇怪。如下所示:
1 | 00000001-40001028 mov dword ptr [rsp+20h], 3; |
从以上代码可以看到,第5个参数在栈中的存储位置为[rsp+20h]
,[rsp]
并未指向栈顶。原因在于,虽然x64系统中前4个参数使用寄存器传递,但栈中仍然为它们预留了同等大小的空间(20h=4*8字节)。
0x03 PE32+
PE32+是64位windows的可执行文件格式。前面说过,64位windows的进程虚拟内存为16TB,低8TB给用户模式,高8TB给内核模式。本节主要讲解PE32+与PE32的不同点。
PE32+
的IMAGE_NT_HEADERS
如下:
PE32+
的IMAGE_FILE_HEADER
的Machine值为0x8664
,PE32
的Machine值为0x014C
。
与PE32
相比,PE32+
的IMAGE_OPTIONAL_HEADER
变化最大。具体变化简述如下:
1 | Magic。PE32中Magic值为010B,PE32+中Magic值为020B。 |
与PE32
相比,IMAGE_THUNK_DATA
结构体的大小由原来的4个字节变为8个字节。下图是IMAGE_IMPORT_DESCRIPTOR
示意图(其中OriginalFirstThunk
与FirstThunk
是IMAGE_THUNK_DATA
结构体,装载PE文件时,OS的PE装载器会向IAT
中写入真正的API入口地址):
注:
IMAGE_TLS_DIRECTORY
:在Windows PE格式中,TLS代表线程本地存储(Thread Local Storage),用于在多线程应用程序中为每个线程分配独立的内存空间。IMAGE_TLS_DIRECTORY
是PE结构中的一个数据结构,用于描述TLS数据的位置和大小。CFF Explorer
与PE view
都是观察PE格式的工具,但是CFF Explorer
可以查看64位PE的格式,而PE view
只能观察32位PE格式。
0x04 windbg学习
符号指的是调试信息文件(*.pdb
)。使用Visual C++编译程序时,除了生成PE文件外,还会一起生成(*pdb:Program Data Base
)程序数据库文件,该文件包含PE文件的各种调试信息(变量/函数名、函数地址等)。
windbg
不仅可以对64位程序进行调试,还可以进行内核调试。使用WinDbg
进行内核调试时,一般要使用2台PC(调试器-被调试者)调试前需要将2台PC连接起来。使用虚拟机也可以,被称为本地内核调试(Local Kernel Debugging),但是调试器的许多功能在这种调试方式下都受到限制。
为什么要用两台PC?内核调试中,被调试者为系统内核,所以 OS 系统自身会暂停。
下面我们想进行本地内核调试。
1 | 1. bcdedit // 查看系统信息 |
0x05 64位调试
示例文件源代码:
1 | // WOW64Test.cpp -> WOW64Test.exe |
分别编译成x86与x64程序,并运行,其输出如下:
对于x86程序,VC++2010工具生成的(基于控制台的)PE32
文件的EP
代码的特征,ollydbg一开始就停在了entry处:
如何查找main函数:
1 | (1) 在GetCommandLine上设置断点。被调试者(WOW64Test_x86.exe)是一个基于控制台的应用程序。因此,调用main函数前会先调用GetCommandLine,需要先把main函数的参数存储到栈中。 |
对于x64程序,使用windbg,程序一开始停在ntdll.dll
的系统断点处。因此,我们先要程序停在entry处。
1 | (1) 输入!dh WOW64Test_x64,查看文件头信息,从而获得 entry point 地址,发现为142C(RVA)。 |
如下所示:
由于GetCommandLineW
没有参数,因此rsp
指向函数返回地址,也就是00000001 40001381
。
1 | (7) g 00000001 40001381,之后输入u eip L20 |
如下所示:
1 | (8) g 00000001 400013ea,转到main函数,使用r查看其寄存器 |
上述,main(int argc, char* argv[])
,其中rcx==argc==1
,argv==rdx
是参数数组,r8
是一个指针数组,所有元素都指向栈区域,它是使用Visual C++ 2010
工具编译代码时由编译器自动添加的参数。
注:
(1)bp与bu的区别:bp就是正常断点,bu命令也用于设置断点,但它是一个不稳定的断点,即一旦命中后就自动失效。
0x06 ASLR
ASLR
是一种针对缓冲区溢出的安全保护技术,微软从windows vista
(内核版本6.0开始)开始使用此技术。
ASLR使得PE文件每次加载到内存的起始地址都会随机变化,并且每次运行程序时相应进程的栈以及堆的起始地址也会随机改变。也就是说,每次EXE文件运行时加载到进程内存的实际地址都不同,最初加载DLL文件时装载到内存中的实际地址也是不同的。
可以看到,采用ASLR的程序有.reloc
节区,而不采用ASLR的程序没有.reloc
节区。两者Number of Sections
也不一样。
两个重要的不同字段:IMAGE_FILE_HEADER\Characteristics
、IMAGE_OPTIONAL_HEADER\DLL Characteristics
。
练习-删除ASLR功能
跟着P428来操作就可以。仅需要删除IMAGE_OPTIONAL_HEADER\DLL Characteristics
的部分即可。而添加ASLR功能比较麻烦,既需要在文件头添加ASLR相应条目,还要增加重定位节区(.reloc
)。
0x07 内核版本为6中的会话
0x06中说了,windows vista
使用的windows
版本为6的内核。其采用了一种新的会话管理机制。
一个在XP中可运行的服务程序在Vista中无法正常运行,且这些服务程序主要是与用户存在交互行为的程序。这是为什么呢?Kernel 6
中使用的会话管理机制引起的。会话管理机制意味着:使用CreateRemoteThread
进行DLL注入的方法不再适用于kernel6
中的服务进程(对一般进程仍然适用)。
会话指的是登录后的用户环境。以Windows操作系统为例,”切换用户”可以创建本地用户会话,”远程桌面连接”可以创建远程用户会话。
XP与vista的系统进程与服务进程都在ID为0的会话中。但是,对于XP来说,第一个登陆系统的用户会话ID为0;而对于vista来说,第一个登录系统的用户会话ID为1。相当于将会话ID为0与其他会话隔离开,能够提高安全性,这叫做会话0隔离机制。
虽然微软的ASLR与会话0隔离机制能够提高安全性,但是也有攻破的方法。例如,对于会话0隔离机制,会话1中的进程可以强行终止会话0中的进程;对于ASLR,ReadProcessMemory
、WriteProcessMemory
、VirtualAllocEx
API也能正常运行(绕过了ASLR)。
0x08 内核版本为6中的DLL注入
由于使用了新的会话管理机制,因此使用CreateRemoteThread
注入DLL的旧方法对某些进程(服务进程)不再适用。这是为什么呢?该如何解决呢?
首先,先给出一个失败的例子:
1 | // InjectDll.cpp -> InjectDll.exe |
1 | // dummy.cpp -> dummp.dll |
可以发现,向正常进程notepad.exe
注入是成功的,然而向服务进程svchost.exe
注入则是失败的。
首先,调试InjectDll.exe
发现,运行完CreateRemoteThread()
,显示错误ERROR_ACCESS_DENIED
。
再次进行调试,可以发现CreateRemoteThread()
调用了CreateRemoteThreadEx()
,发现参数差不多(只是多了一个lpAttributeList
),其又调用了ntdll!ZwCreateThreadEx()
,其参数也是差不多,重要参数都传过来了。书上说,最终调用了sysenter
,但是我没有找到进入sysenter
的证据,程序会在某个环节卡住。
实际上,CreateRemoteThreadEx()
与ntdll!ZwCreateThreadEx()
都是vista新增的API,之前没有。下图展示了在XP与win7中调用CreateRemoteThread
的流程:
那我们得到结论,由于增加的API,让注入失败。
注:kernelbase.dll
是从Vista开始新增的DLL
文件,负责包装(wrapper)kernel32.dll
。
由于kernelbase!CreateRemoteThreadEx
只是kernel32!CreateRemoteThread
的包装器(wrapper),他俩没啥区别,因此问题就在ntdll!ZwCreateThreadEx()
中。
ZwCreateThreadEx()
定义如下:
下图是注入失败时ZwCreateThreadEx()
的参数:
Google后发现,在vista中,直接调用ZwCreateThreadEx()
就能注入成功,不受会话影响,但是第7个参数(CreateSuspended
)要设置为0,而不是1。Windows XP开始,CreateRemoteThread
内部的实现算法采用了挂起模式,即先创建出线程,再使用”恢复运行”方法继续执行(CreateSuspended=1
)。
我改了第7个参数发现没用。
发现了一个问题,就是ZwCreateThreadEx()
与NtCreateThreadEx()
地址一样,且我看不到ZwCreateThreadEx()
的代码,且调试时,CreateRemoteThreadEx()
调用的是ntdll!NtCreateThreadEx()
,而不是ntdll!ZwCreateThreadEx()
。经过思考,可能是用ollydbg进行调试的问题,ollydbg只能调试用户模式下的应用。因此,换成windbg调试一波(但是要进行两个主机之间的连接,win7虚拟机一直报dll缺失,懒了,搁置)。
注:
ZwCreateThreadEx()
与NtCreateThreadEx()
的区别是什么?ZwCreateThreadEx()
是内核调用的。NtCreateThreadEx()
由用户模式代码调用。
除了改第7个参数以外,书中还说了一种解决注入失败的方法。可以看到,ZwCreateThreadEx()
的第一个参数是线程句柄,说明此时远程线程已经被创建,但是无法工作。原因可能就是ZwResumeThreadEx
调用失败(注意是resume)导致的。经过调试发现,程序将ZwResumeThreadEx
跳过去了,通过改变程序执行路径,就能实现正确的DLL注入。(但是复现的时候有问题)
存在的问题如下:
1 | 1. 找不到NtCreateThreadEx(),只有ZwCreateThreadEx() |
总结一下无法注入的原因:在API内部创建远程线程时采用了挂起模式,若远程进程属于会话0,则不会恢复运行,而是直接返回错误。(一般来说,在创建远程线程时,先采用挂起模式创建,再”恢复运行”。)
之后,我们编写一个新的InjectDll.exe
程序:
1 |
|
发现在win7下官方给的示例也不行,win11下也不行,只不过报错变了。离谱啊?为啥啊?
经过分析,是如下代码出了问题,其中pFunc
总为空(问题搁置):
0x09 InjDll.exe:DLL注入专用工具
InjDll.exe
是之前我们使用的DLL注入的程序,若目标进程为32位,那么InjDll.exe
和要注入的DLL都是32位。若目标进程为64位,那么InjDll.exe
和要注入的DLL都是64位。(突然想到,之前的注入问题是不是64位与32位的问题??)
高级逆向分析技术
咱什么时候也弄上了高级逆向啊?我好牛蛙。
0x10 TLS回调函数
TLS(Thread Local Storage,线程局部存储)回调函数(Callback Function)常用于反调试。TLS回调函数的调用运行要先于EP代码的执行,该特征使它可以作为一种反调试技术使用。
下面跟着书做实验:
1 | 打开HelloTls.exe,显示Hello。但用Ollydbg调试之后,显示Debugger Detected。(为什么不同呢?是因为程序运行EP代码前先调用了TLS回调函数,而该回调函数中含有反调试代码,使程序在被调试时弹出"Debugger Detected!"消息对话框。) |
TLS是各线程的独立的数据存储空间,使用TLS技术可在线程内部使用或修改进程的全局数据,就像对待自身的局部变量一样。
如果在编程中启用了TLS功能,PE头文件就会设置TLS表,在IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> IMAGE_DATA_DIRECTORY[9]
中,它存的值指向了IMAGE_TLS_DIRECTORY
。其结构如下:
结构体中AddressOfCallBacks
比较重要,它指向含有TLS回调函数的数组(VA),这意味着同一程序可以注册多个TLS回调函数。
TLS回调函数是指,每当创建或终止进程的线程时会自动调用执行的函数。创建进程的主线程时也会自动调用回调函数,且其调用执行先于EP代码。反调试技术利用的就是TLS回调函数的这一特征。
TLS回调函数的定义如下:
再来看DLL的main函数的定义:
是不是差不多?在TLS回调函数的定义中,DllHandle
为模块句柄(加载地址);Reason
表示调用TLS回调函数的原因,包括DLL_PROCESS_ATTACH(1)
、DLL_THREAD_ATTACH(2)
、DLL_THREAD_DETACH(3)
、DLL_PROCESS_DETACH(0)
。
下面看一个程序TlsTest.exe
:
1 |
|
程序运行如下:
可以看到,在进程初始与结束,会运行TLS。在线程初始与结束,也会运行TLS。程序中没有使用printf
而是使用了WriteConsole
,是因为TLS函数先于主线程来运行,此时可能C语言的某些库还没有加载,所以使用printf
会报错。
使用ollydbg
调试此程序时需要改变first pause at Tls callback
。调试Hellotls.exe
,发现回调函数:
其使用kernel32.IsDebuggerPresent
来判断是否有调试器。
下面对于简单的Hello.exe
添加TLS功能。
1 | // Hello.cpp -> Hello.exe |
添加思路如下:IMAGE_OPTIONAL_HEADER
中添加TLS Table
(IMAGE_TLS_DIRECTORY
)位置,之后在相应位置添加IMAGE_TLS_DIRECTORY
;IMAGE_TLS_DIRECTORY
中有Address of Callbacks
,定义好它的位置后,在相应位置添加TLS函数内容即可。
其中IMAGE_TLS_DIRECTORY
与Address of Callbacks
放到哪儿呢?有几种可选位置:
1 | 1. 添加到某节区末尾的空白区域 |
书中采用方法2,跟着书上的实验做,是可以进行的。具体步骤如下:
首先,查看PE文件相关信息:Section Alignment=1000
,File Alignment=200
。那么,由于我们选择方法2,即增加最后一个节区的大小,那么我们就把最后一个节区.rsrc
后面添加200字节。(原来.rsrc
节区的VirtualSize
为1B4
,PE装载器会按照Section Alignment
值对齐该值,即加载到内存中的大小为 1000。所以将节区的文件大小增加200后,实际VirtualSize
值变为3B4
,它比加载到内存中的尺寸1000要小,所以不需要再单独增大VirtualSize
的值。)
其次,更改.rsrc
节区头信息,SizeofRawData=400
(原来是200)、Characteristics=E0000060
。Characteristics
的属性如下:
之后,设置TLS表(位置在IMAGE_NT_HEADERS -> IMAGE_OPTIONAL_HEADER -> IMAGE_DATA_DIRECOTRY[9]
)的RVA与Size。接下来,设置TLS表指向的IMAGE_TLS_DIRECTORY
,在IMAGE_TLS_DIRECTORY
中的AddressOfCallbacks
中定义TLS函数的地址。并在相应地址填入回调函数,如下:
程序的解释如下:Reason参数值为1(DLL_PROCESS_ATTACH
)时,MOV EAX, DWORD PTR FS:[30]
用于检查PEB.BeingDebugged
成员,若处于调试状态,则弹出消息框(MessageBoxA
)后终止并退出进程(ExitProcess
)。注意,0x49c270
与0x40c280
存储MessageBoxA
所需的字符串。
0x11 TEB
TEB(Thread Environment Block
)指线程环境块,该结构体包含进程中运行线程的各种信息,进程中的每个线程都对应1个TEB结构体。TEB结构体如下:
上述对于TEB的定义很简单(实际很复杂),windows 7与XP的TEB定义是不同的,也很复杂。使用windbg查看win7的TEB如下(内核模式下,在用户模式下使用此命令只能看到wow64_teb
):
在TEB中,有几个成员比较重要。
在用户调试中,比较重要的两个成员是NtTib
与ProcessEnvironmentBlock
。如下图所示:
NtTib
(TIB: Thread Information Block
)线程信息块定义如下所示:
ExceptionList
成员指向EXCEPTION_REGISTRATION_RECORD
结构体组成的链表,它用于Windows的SEH(异常处理机制)。Self成员是 NT_TIB
结构体的自引用指针,也是TEB结构体的指针(因为TEB结构体的第一个成员就是NT_TIB
)。
那么如何在用户模式下访问TEB结构体呢?除了使用windbg,还有就是通过API(Ntdll.NtCurrentTeb()
)进行访问。经过调试发现,TEB与FS段寄存器有某种关联。
FS寄存器除了可以访问TLS,还指向当前线程的TEB。32位系统中进程的虚拟内存大小为4GB,因而需要32位的指针才能访问整个内存空间。但是FS寄存器的大小只有16位,那么它如何表示进程内存空间中的TEB结构体的地址呢?实际上,FS寄存器并非直接指向TEB结构体的地址,它持有SDT的索引,而该索引持有实际TEB地址。如下所示,TEB结构体位于FS段寄存器所指的段内存的起始地址处。:
那么,就有:FS:[0x18] = TEB.NtTib.Self = address of TIB = address of TEB
,其中,self成员在TEB中的偏移是0x18。
还有:FS:[0x30] = TEB ProcessEnvironmentBlock = address of PEB
,PEB是进程环境块。
最后,FS:[0] = TEB.NtTib.ExceptionList = address of SEH
,SEH代表异常处理机制。
补充:
1. SDT是Segment Descriptor Table
(段描述符表)的缩写,其位于内核内存区域,其地址存储在特殊的寄存器 GDTR(Global Descriptor Table Resiger
,全局描述符表寄存器)中。
2. FS段寄存器作用:FS段寄存器提供一个用于访问线程局部存储(TLS)的基址。TLS是一种内存管理机制,允许多个线程在共享内存的情况下独立地访问自己的数据。当一个线程访问TLS时,它将使用FS段寄存器中指定的地址作为数据的基址。这个地址通常被初始化为指向一个线程私有的数据区域,这个数据区域可以存储线程专有的数据,例如线程的栈、线程局部变量等等。
0x12 PEB
PEB,进程环境块,它是存放进程信息的结构体,尺寸非常大。获得PEB的两种方式如下:
- 直接获取PEB地址。
1 | MOV EAX,DWORD PTR FS:[30] ; FS[30] = address of PEB |
- 先获取TEB地址,再通过ProcessEnvironmentBlock成员(+30偏移)获取PEB地址。
1 | MOV EAX,DWORD PTR FS:[18] ; FS[18] = address of TEB |
PEB结构如下,与TEB一样,实际复杂得多:
其中的几个重要成员有:BeingDebugged
、ImageBaseAddress
、Ldr
、ProcessHeap
、NtGlobalFlag
:(这样就能解释通0x10处的代码了)
下面是一个PEB的dump:
对于BeingDebugged
成员,windows API中有一个Kernel32!IsDebuggerPresent()
,代码如下,来判断是否被调试,其中调用了此成员:
对于ImageBaseAddress
成员,其可以表示进程的基址。GetModuleHandle
的API可以用来获取ImageBase
,其中就调用了此成员。
对于Ldr
成员,它是指向_PEB_LDR_DATA
结构体的指针,其结构如下:
Ldr
的作用是:当模块(DLL)加载到进程后,通过PEB.Ldr
成员可以直接获取该模块的加载基地址。 _PEB_LDR_DATA
结构体中,有3个_LIST_ENTRY
类型的成员,分别为InLoadOrderModuleList
、InMemoryOrderModuleList
、InInitializationOrderModuleList
。下面是_LIST_ENTRY
的结构:
可以看到_LIST_ENTRY
是一个双向链表,这个双线链表保存着_LDR_DATA_TABLE_ENTRY
结构体的信息(我没看出来)。每一个加载到进程中的DLL模块都有对应的_LDR_DATA_TABLE_ENTRY
结构体。需要注意的是,_PEB_LDR_DATA
中存在3种链表(InLoadOrderModuleList
、InMemoryOrderModuleList
、InInitializationOrderModuleList
),即,存在多个_LDR_DATA_TABLE_ENTRY
结构体,并且有3种链接方法可以将它们链接起来。
对于ProcessHeap
与NtGlbalFlag
成员,可以应用于反调试技术。若进程处于调试状态,它们就持有特定值,类似于BeingDebugged
。
0x13 SEH
SEH(Structure Exception Handler)是Windows操作系统默认的异常处理机制。逆向分析中,SEH可以用于反调试程序。在程序源码中,使用__try
、__except
、__finally
等关键字来具体实现。注意,SEH与C++中的try
、catch
相比有不同的结构。
有一个练习示例seh.exe
。该程序故意触发了内存非法访问(Memory Access Violation)异常,然后通过SEH机制来处理该异常。并且使用PEB信息向程序添加简单的反调试代码,使程序在正常运行与调试运行时表现出不同的行为动作。此程序是通过编译空的main函数,之后用ollyDbg向其中加入汇编代码来实现的,因此没有源代码。
内存非法访问如下:
红框语句就是向内存地址为0的单位放入1,但是内存地址0是未分配的,因此会报异常。之后按shift+F9
将异常抛给程序,它就会显示检测出调试器
。
OS处理异常的方法:
1 | 1. 运行时出现异常: |
下面介绍几种常见的异常:
1 | 1. EXCEPTION_ACCESS_VIOLATION(C0000005) |
注:PE tools可以将运行的进程转储,可以作为ollydbg保存调试文件的另一种方式。
SEH以链的形式存在。第一个异常处理器中若未处理相关异常,它就会被传递到下一个异常处理器,直到得到处理。SEH是由_EXCEPTION_REGISTRATION_RECORD
结构体组成的链表,_EXCEPTION_REGISTRATION_RECORD
的结构如下,若next为全1,那么就代表是最后一个节点:
SEH链如下:
而异常处理函数(上图HANDLER
)定义如下:
可以看到,异常处理函数接收4个参数,返回一个PEXCEPTION_DISPOSITION
。
异常处理函数的第1个参数,类型为EXCEPTION_RECORD
,定义如下(其中ExceptionCode
指明异常类型,ExceptionAddress
指明异常发生的地址):
异常处理函数的第2个参数pFrame
,代表SEH链的起始地址。
异常处理函数的第3个参数,类型为CONTEXT
,它用于存储CPU各寄存器的值,也就是上下文。这种情况在多线程中有用,每个线程都有一个CONTEXT
结构体。CPU暂时离开当前线程去运行其他线程时,CPU寄存器的值就会保存当前线程的CONTEXT
结构体;CPU再次运行该线程时,会使用保存在CONTEXT
结构体的值来覆盖CPU寄存器的值,然后从之前暂停的代码处继续执行。
第4个参数pValue
供系统内部使用,可忽略。
异常处理函数的返回值(PEXCEPTION_DISPOSITION
)是一个枚举类型,其定义如下:
异常处理函数如果能正确处理异常,则会返回ExceptionContinueExecution(0)
,之后从发生异常的代码处继续运行。若异常处理函数无法处理异常,则返回ExceptionContinueSearch(1)
,将异常派送到SEH链的下一个处理函数。
通过TEB(线程)的NtTib
可以访问SEH链,其中TEB.NtTib.ExceptionList
是第一个SEH成员。FS段寄存器指向段内存的起始地址,FS:[0]
即指向SEH链开头。
SEH定义安装
C语言中使用__try
、__except
、finally
就可以向代码添加SEH。使用汇编的话,则需要加入如下汇编指令:
1 | PUSH @MyHandler ; 第2个参数 |
前面的seh.exe
就是通过修改汇编指令来做的,如下图红框所示:
安装好后,发现SEH代码存放在0040105A
中,代码如下所示:
上述代码可以检测是否为调试器。
1 | MOV ESI, DWORD PTR SS:[ARG3] // 将pContext放入到ESI中 |
在0040104D
代码中,为POP DWORD PTR FS:[0]
(可以分解为MOV EAX, [ESP]; ADD ESP, 4;
)。之后为ADD ESP,4
,使用这两条语句,就能将刚才定义的SEH函数删除,并将FS:[0]
定为SEH的第二个函数。
SEH大量应用于压缩器、保护器、恶意程序(Malware),用来反调试。
什么是枚举类型?
枚举类型通过列出枚举器(Enumerator)的方式定义,每个枚举器代表一个具体的取值。例如:
1 | enum Color { |
这定义了一个名为Color
的枚举类型,它包含三个枚举器:RED
、GREEN
和BLUE
。这些枚举器的取值分别为0、1和2(默认情况下,第一个枚举器的取值为0,后续枚举器的取值依次递增)。使用上面的枚举类型,可以声明一个变量表示颜色:
1 | Color myColor = GREEN; |
这比使用数字0或1更容易理解,也更容易修改和维护。
FPU(Floating Point Unit,浮点运算单元)是专门用于浮点数运算的处理器,它有一套专用指令,与普通x86指令的形态结构不同。
0x14 IA-32(x86)指令
指令格式如下:
指令解析练习
了解即可。P524之前的内容。
为什么要掌握IA-32解析?
检测变形病毒(Polymorphic Virus
)时必须探测出Polymorphic
引擎中产生的指令类型,这时就需要分析人员具有丰富的指令知识。掌握IA-32指令相关知识对于编写打补丁代码、分析漏洞Shell代码都非常有帮助。
留言
- 文章链接: https://wd-2711.tech/
- 版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-ND 4.0 许可协议。转载请注明出处!