逆核 _06_ 高级逆向分析技术

0x00 TLS回调函数

TLS (Thread Local Storage, 线程局部储存) 回调函数常用于反调试, 程序运行EP代码前会先调用它。

00. TLS

  1. 通过pe文件的可选头中的IMAGE_DATA_DIRECTORY[9]可以找到IMAGE_TLS_DIRECTORY, 其中重要的成员的是AddressOfCallbacks, 该值指向含有TLS回调函数地址(VA)地址, 这意味着可以向同一程序注册多个TLS回调函数(数组以NULL值结束)。

  2. 回调函数地址数组。进程启动运行时, (执行EP代码前)系统会逐一调用存储在该数组中的函数。

01. TLS回调函数

01.0.所有TLS回调函数是指, 每当创建/终止进程的线程时会自动调用执行的函数。也就是说, 创建或终止某线程时, TLS回调函数都会自动调用执行,前后共2次。

01.1.IMAGE_TLS_CALLBACK

1
2
3
4
5
6
typedef VOID
(NTAPI *PIMAGE_TLS_CALLBACK)(
PVOID DllHandle, //模块句柄 (即加载地址)
DWORD Reason, //调用TLS回调函数的原因
PVOID Reserved
);

01.2.Reason

1
2
3
4
#define DLL_PROCESS_ATTACH 1
#define DLL_THREAD_ATTACH 2
#define DLL_THREAD_DETACh 3
#define DLL_PROCESS_ATTACh 0

顾名思义, DLL_PROCESS在main()函数创建和终止后作为原因调用TLS回调函数; DLL_THREAD在线程创建和终止后作为原因调用TLS回调函数。

02. 调试TLS回调函数

将OD的Make first pause of切到System break point, 调试器最初暂停的位置是系统启动断点, 之后参考TLS回调函数的地址(通过PE文件可以找到), 在回调函数的起始地址下断点后, 变可以调试了。

0x01 TEB

TEB (Thread Environment Block, 线程环境块), 该结构体包含进程中运行线程的各种信息, 进程中的每个线程都对应·一个TEB结构体。

00. TEB的重要成员

1
2
3
+0x000 NtTib :_NT_TIB
...
+0x030 ProcessEnvironmentBlock : Ptr32_PEB
  1. Nt_Tib成员
    TIB (Thread Information Block, 线程信息块), 其中的ExceptionList成员指向_EXCEPTION_REGISTRATION_RECORD结构体组成的链表, 它用于Windows OS的SEH; Self成员是_NT_TIB结构体的自引用指针, 也是TEB结构体的指针(+0x018)。

  2. ProcessEnvironmentBlock成员
    ProcessEnvironmentBlock成员值指向PEB (进程环境块)结构体的指针, 每个进程对应一个PEB结构体。

01. TEB的访问方法

利用Ntdll!NtCurrentTeb()API返回当前线程的TEB结构体地址, API返回FS:[18]的值, 也就是上文提到的Self成员的指针值, 而该值也恰好是FS段寄存器的起始地址。FS段寄存器用来指示当前线程的TEB结构体, 实际上, FS段寄存器持有SDT的索引, 而该索引持有实际TEB地址。

注: SDT位于内核内存区域, 其地址存储在特殊的寄存器GDTR (Global Descriptor Table Register, 全局描述符表寄存器中)。

02. TEB重要成员的地址表示

  1. FS:[0x00] == SEH起始地址
    FS:[0x00] = TEB.NtTib.ExceptionList = address of SEH
  2. FS:[0x18] == TEB起始地址
    FS:[0x18] = TEB.NtTib.Self = address of TEB = FS:[0x00]
  3. FS:[0x30] == PEB起始地址
    FS:[0x30] = TEB.ProcesssEnvironmentBlock = address of PEB

0x02 PEB

PEB (Process Environmnet Block, 进程环境块), 是存放进程信息的结构体。

00. PEB的重要成员

1
2
3
4
5
6
7
8
9
+0x002 BeingDebugged : UChar
+0x008 ImageBaseAddress : Ptr32 Void
+0x00c Ldr : Ptr _PEB_LDR_DATA
+0x018 ProcessHeap : Ptr32 Void
+0x068 NtGlobalFlag : Uint48
  1. BeingDebugged成员
    该成员用于检测进程是否处于被调试的状态,: 是为01, 否为00。

  2. ImageBaseAddress成员
    该成员用于表示进程的ImageBase。

  3. Ldr成员
    0.该成员是指向_PEB_LDR_DATA结构体的指针, 当模块(DLL)加载到进程后, 通过PEB.Ldr成员可以直接获取该模块的加载基地址。
    1 _PEB_LDR_DATA结构体含有3个_LIST_ENTRY类型的成员, 而_LIST_ENTRY结构体提供了双向链表机制, 链表保存的是_LDR_DATA_TABLE_ENTRY结构体的信息。
    2.每个加载到进程中的DLL模块都有与之对应的_LDR_DATA_TABLE_ENTRY结构体, 这些结构体相互链接, 形成_LIST_ENTRY双向链表。

  4. ProcessHeap & NtGlobalFlag成员
    该成员与PEB.BeingDebugged成员类似, 处于调试状态时会持有特定值, 可用于反调试技术。

01. PEB的访问方法

  1. 直接获取PEB地址

    1
    MOV EAX, DWORD PTR FS:[30] ; FS:[30] = address of PEB
  2. 先获取TEB地址, 再通过+0x30的偏移获取PEB的地址

    1
    2
    MOV EAX, DWORD PTR FS:[18] ; FS:[18] = address of TEB
    MOV EAX, DWORD PTR DS:[EAX+30] ; DS:[EAX+30] = address of PEB

0x03 SEH

SEH (Structured Exception Handling, 结构化异常处理)是Windows OS默认的异常处理机制。

00. OS的异常处理方法

  1. 正常运行时的异常处理方法
    进程运行过程中若发生异常, OS会委托进程处理。若进程代码中存在具体的异常处理机制(如SEH异常处理器)代码, 则能顺利处理相关异常, 程序继续运行; 但如果进程内部没有具体实现SEH, 则相关机场无法处理, OS会启动默认的异常处理机制, 终止进程运行。
  2. 调试运行时的异常处理方法
    若被调试进程内部发生异常, OS会首先把异常抛给调试进程处理。调试器几乎拥有被调试者的所有权限, 它不仅可以运行、终止被调试者, 还拥有被调试进程的虚拟内存、寄存器的读写权限。所以调试过程中发生的所有异常都要先交由调试器管理 (被调试者的SEH依据优先顺序推给调试器)。
    于是, 被调试者发生异常时, 调试器暂停运行, 采取某种措施处理异常, 完成后继续调试。
    遇到异常可以采取的几种方法:
    (1)直接修改异常: 代码, 寄存器, 内存
    (2)将异常抛给被调试者处理
    如果被调试者内部存在SEH能够处理异常, 那么异常通知会发送给被调试者, 由被调试者自行处理。这与程序正常运行时的异常处理方式是一样的。
    (3)OS默认的异常处理机制
    终止被调试进程, 同时结束调试。

01. 常见异常

0.EXCEPTION_ACCESS_VIOLATION(C0000005)
试图访问不存在或不具备访问权限的内存区域时, 就会发生非法访问异常。

1.EXCEPTION_BREAKPONIT(80000003)
在运行代码中设置断点后, CPU尝试执行该地址处的指令, 将发生此异常。调试器利用该异常实现断点功能。具体实现为把1字节替换成CC (INT3)。

2.EXCEPTION_ILLEGAL_INSTRUCTION(C000001D)
CPU遇到无法解析的指令时引发该异常。

3.EXCEPTION_INT_DIVIDE_BY_ZERO(C0000094)
Integer除法运算中, 若分母为0, 则引发该异常。

4.EXCEPTION_SINGLE_STEP(80000004)
Single1 Step(单步)的含义是执行一条指令, 然后暂停。CPU进入单步模式后, 每执行一条指令就会引发该异常, 然后暂停运行。将EFLAGS寄存器的TF (Trap Flag, 陷阱标志)位设置为1后, CPU就会进入单步工作模式

02. SEH具体实现

0. SEH链

SEH以链的形式存在。第一个异常处理器中若未处理相关异常, 它就会被传递到下一个异常处理器, 直到得到处理。从技术层面看, SEH是由_EXCEPTION_REGISTRATION_RECORD结构体组成的链表。

1
2
3
4
tydef _EXCEPTION_REGISTRATION_RECORD{
PEXCEPTION_REGISTRATION_RECORD Next;
PEXCEPTION_DISPOSITION Handler;
} _EXCEPTION_REGISTRATION_RECORD, *PEXCEPTION_REGISTRATION_RECORD;

Nex成员是指向下一个此结构体的指针;
Handler成员是异常处理函数(异常处理器)。

注: 若Next成员为FFFFFFFF, 则表示它是链表的最后一个结点。

1. 异常处理函数

1.0.
异常处理器的定义:

1
2
3
4
5
6
EXCEPTION_DISPOSITION _except_handler(
EXCEPTION_RECORD *pRecord,
PEXCEPTION_REGISTRATION_RECORD *pFrame,
CONTEXT *pContext,
PVOID *pValue
);

该异常处理函数由系统调用, 是一个回调函数, 系统调用它时会给出这4个参数, 这4个参数保存着异常相关的的信息。

参数0— EXCEPTION_RECORD结构体的定义

1
2
3
4
5
6
7
8
9
typedef struct _EXCEPTION_RECORD{
DWORD ExceptionCode; // 异常代码
DWORD ExceptionFlags;
struct _EXCEPTION_RECORD *ExceptionRecord;
PVOID ExceptionAddress; // 异常发生地址
DWORD NumberParameters;
ULONG_PTR ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
// 15
}

参数1— PEXCEPTION_REGISTRATION_RECORD结构体
此结构体指针, 指向SEH链的起始地址。

参数2— CONTEXT结构体
CONTEXT结构体用来备份CPU寄存器的值。每个线程内部都拥有1个CONTEXT结构体。

注: CPU离开当前线程时, 会将此时寄存器的值保存在当前线程的CONTEXT结构体; CPU再次运行该线程, 会使用保存在CONTEXT结构体的值来覆盖寄存器的值, 然后从之前暂停的代码处继续执行。通过这种方式, OS可以在多线程环境下安全运行各线程。

1.1.
异常发生时, 执行异常代码的线程就会中断运行, 转而运行SEH, 此时OS会把线程的CONTEXT结构体的指针传递给异常处理器的相应参数。

CONTEXT结构体成员中有一个Eip成员, 在异常处理函数中将参数传递过来的CONETXT.Eip设置为其他地址, 然后返回异常处理函数。这样, 之前暂停的线程会执行新设置的EIP地址处的代码。

1
2
3
4
5
6
typedef enum _EXCEPTION_DISPOSITION{
ExceptionContinueExecution = 0, // 继续执行异常代码
ExceptionContinueSearch = 1, // 运行下一个异常处理器
ExceptionNestedException = 2, // 在OS内部使用
ExceptionCollidedUnwid = 3 // 在OS内部使用
} EXCEPTION_DISPOSITON;

2. TEB.NtTib.ExceptionList

通过TEB结构体的Ntlib成员可以访问进程的SEH链。
TEb.NtTib.Exception = FS:[0]

3. SEH安装方法

1
2
3
PUSH @MyHandler ; 异常处理器
PUSH DWORD PTR FS:[0] ; Head of SEH Linked List
MOV DWORD PTR FS:[0], ESP ; 添加链表

将自身的EXCEPTION_REGISTRATION_RECORD结构体链接到此结构体链表。

detail:
第1步, 把异常处理器的地址PUSH进栈 (作为参数Handler);
第2步, 把原本SEH链第一个结构体的地址PUSH进栈 (作为参数Next);
第3步, 将ESP此时的值([ESP+0]为Next, [ESP+4]为Handler)复制到FS:[0], 成为新的SEH链的第一个结构体的地址。

4. SEH删除方法

1
2
POP DWORD PTR FS:[0]
ADD ESP,4

detail:
第1步, 将ESP+0pop至FS:[0], 还原为安装前的SEH链的第一个结构体的地址。
第2步, 清理栈, 将Handler和Next参数消除。