April 27, 2022

关于逆向工程核心原理-调试式API钩取

(简称逆向工程第三十章学习)

先贴下调试的技术的图表

image-20220427220724582

调试技术

调试器能逐一执行被调试者的指令,拥有对寄存器与内存的所有访问权限

平常的软件断点其实就是INT 3,IA-32指令为0xCC,刚好在OALABS里看过

一般调试的一个流程是

  1. 对想钩取的函数附加,使目标进程变成被调试者
  2. 钩子:将API起始地址的第一个字节变成0xCC
  3. 调用相应API,控制权转到调试器
  4. 执行需要的操作
  5. 脱钩:将0xCC恢复原值(为了正常运行被改的API)
  6. 运行该API(无INT3状态)
  7. 钩子:再次改为0xCC,反复而使(根据需求而来)

实验环节

查找PID然后挂钩,随便输入点小写字母,然后保存

image-20220427222048423

保存完会发现,全部转为大写字符!

image-20220427222157803

工作原理

用od打开notepad,按F9执行,在Kernel32!WriteFile下个断点,随便输入点的东西再保存

就会在这断下,这时候我们可以观察栈,第二个参数就是指向我们的输入

image-20220427223427974

执行流

  1. 当我们在WriteFile设置了一个断点(INT3)
  2. 向目标进程(notepad.exe)保存文件时
  3. EXCEPTION_BREAKPOINT事件就会发到调试器(hookdbg.exe)

于是此时有个有趣的点就是notepad.exe的EIP是什么

乍一想会觉得是API的起始地址是7551754E,但其实是 7551754E + 1 = 7551754F

因为运行了0xCC执行,使得EIP + 1,然后控制权给了调试器hookdbg.exe(因为EXCEPTION_BREAKPOINT异常需要由调试器处理)

修改覆盖好数据缓冲区内容后,EIP重新设置为API的起始地址7551754E

脱钩 & 钩子

另一个问题是,我们起始地址还是0xCC!这样不就死循环了吗

所以我们要把0xCC再恢复成0x6A,再调整EIP即可(恢复成WriteFile()API的起始地址)

开审!

main

DebugActiveProcess()可以将此exe附加到指定目标进程

随后DebugLoop处理目标进程的调试事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
int main(int argc, char* argv[])
{
DWORD dwPID;

if( argc != 2 )
{
printf("\nUSAGE : hookdbg.exe <pid>\n");
return 1;
}

// Attach Process
dwPID = atoi(argv[1]);
// 将调试器(此exe)附加到进程
if( !DebugActiveProcess(dwPID) )
{
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwPID, GetLastError());
return 1;
}

// 随后进入DebugLoop,处理目标进程的调试事件
DebugLoop();

return 0;
}

DebugLoop

当经过main函数的DebugActiveProcess附加上目标进程后,就会调用DebugLoop函数

程序处理了三种事件

  1. OnCreateProcessDebugEvent,创建进程所对应的操作
  2. OnExceptionDebugEvent,发生异常所对应的操作
  3. 还有一个就是发生退出事件,就直接break了

关于ContinueDebugEvent,就是设置发生了事件,希望目标进程该怎么处理,要么照常执行要么SEH

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
void DebugLoop()
{
// DEBUG_EVENT结构体
DEBUG_EVENT de;
DWORD dwContinueStatus;

// 等待目标进程者发生的调试事情函数,若发生调试事件,就会设置到第一个参数的变量
while( WaitForDebugEvent(&de, INFINITE) )
{
dwContinueStatus = DBG_CONTINUE;

// 共有9种调试事件,DEBUG_EVENT.dwDebugEventCode其中的一种(九大巨人之一!)
if( CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
OnCreateProcessDebugEvent(&de);
}
// 如果发生异常,结构体会被设置EXCEPTION_DEBUG_EVENT
// 这时候有两个选择
// 若处理正常,在ContinueDebugEvent()的最后一个参数dwContinueStatus,设置DBG_CONTINUE
// 若无法处理,或希望在应用程序中的SEH中处理,则可以设置DBG_EXCEPTION_NOT_HANDLED
else if( EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode )
{
if( OnExceptionDebugEvent(&de) )
continue;
}
// 目标进程终止时会触发该事件
else if( EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode )
{
// 目标进程终止事件
break;
}

// 这时候的控制权在hookdbg.exe上,想返回给目标进程notepad.exe就可以调用这个函数继续运行
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}

DEBUG_EVENT

首先了解一下九大调试事件(九大巨人!雾

其实都是字如其名,慢慢了解就好了

WaitForDebugEvent() API是等带目标进程发生调试事件的函数,当发生了就会把相关事件放置到第一个参数的变量

image-20220428121717524

OnCreateProcessDebugEvent

当目标进程启动(或附加)即在DebugLoop对应,于是调用执行该函数

(也就是DebugLoop的第一个if语句所对应)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// OnCreateProcessDebugEvent是CREATE_PROCESS_DEBUG_EVENT事件句柄,即目标进程启动(或附加)时即调用执行该函数
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
// 获取WriteFile() API地址
g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");

// 获取WriteFile() API的起始地址,虽然不是目标进程,不过Windows OS,系统dll地址都一样
// g_cpdi是CREATE_PROCESS_DEBUG_INFO结构体
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
// 通过结构体的目标进程句柄,备份原起始地址的第一个字节
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL);
// 再把INT3写到目标进程的地址设置断点
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);

return TRUE;
}

g_cpdi

g_cpdi是CREATE_PROCESS_DEBUG_INFO的结构体,这边用到hProcess即目标进程的句柄

image-20220428123037822

OnExceptionDebugEvent

当目标进程发生EXCEPTION_BREAKPOINT异常运行此函数(除此之外还有19种异常)

(也就是对应DebugLoop的else if)

于是一整套流程就是

  1. 脱钩(将0xCC改回原来的API函数的值)
  2. 获取线程上下文,这里面存放着CPU的信息,保证之后继续运行不会出错(因为会有时间片)
  3. 获取WriteFile的第二三个参数
  4. 4-8就是将小写字母改成大写字母
  5. 把线程上下文的EIP改为WriteFile的其实EIP(因为执行了0xCC,现在的EIP为+1状态)
  6. 一切弄好之后就是继续运行目标进程,一定要注意Sleep(0),这是释放当前线程的剩余时间片,防止内放访问异常(详细见书304页,还没彻底理解)
  7. 最后设置API钩子,方便下次钩取操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
// 发生EXCEPTION_DEBUG_EVENT事件句柄,这个处理目标进程的INT3指令
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
CONTEXT ctx;
PBYTE lpBuffer = NULL;
DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;

// 是断点异常(INT3)时
if( EXCEPTION_BREAKPOINT == per->ExceptionCode )
{
// 断点地址是WriteFile() API时
if( g_pfWriteFile == per->ExceptionAddress )
{
// #1. Unhook
// 将0xCC恢复为original byte
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL);

// #2. 获取线程上下文
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(g_cpdi.hThread, &ctx);

// #3. 获取WriteFile()'s param 2, 3
// 函数参数存在于相应进程的栈
// param 2 : ESP + 0x8(Buffer)
// param 3 : ESP + 0xC(nByteToWrite)
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),
&dwAddrOfBuffer, sizeof(DWORD), NULL);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),
&dwNumOfBytesToWrite, sizeof(DWORD), NULL);

// #4. 分配临时缓冲区,再初始为0
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
memset(lpBuffer, 0, dwNumOfBytesToWrite+1);

// #5. 从WriteFile的缓冲区到临时缓冲区IpBuffer,长度为dwNumOfBytesToWrite
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);
printf("\n### original string ###\n%s\n", lpBuffer);

// #6. 小写字母转大写字母
for( i = 0; i < dwNumOfBytesToWrite; i++ )
{
if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
lpBuffer[i] -= 0x20;
}

printf("\n### converted string ###\n%s\n", lpBuffer);

// #7. 将变换后数据再写到目标进程的缓冲区
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);

// #8. 释放临时缓冲区
free(lpBuffer);

// #9. 将线程上下文的EIP改为WriteFile的首地址
// (因为执行完INT3后地址 + 1,要修改回去)
ctx.Eip = (DWORD)g_pfWriteFile;
SetThreadContext(g_cpdi.hThread, &ctx);

// #10. 继续运行目标进程
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0);

// #11. API钩子
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);

return TRUE;
}
}

return FALSE;
}

调试TIME

Scr1pt师傅直接让我GET到一个小tips,xdbg的参数设置

本此内容是我们要调试调试器调试目标进程的细节(有点绕

所以我们要dbg hookdbg.exe,所以我们直接先拖进xdbg,由于需要参数,我们选择改变命令行

img

然后在后面跟上notepad的PID

image-20220428140620270

可以通过字符串查找,分别在我们想要调试的地方设置断点(我分别在主函数和异常处理函数的函数开头设了断点)

image-20220428140824927

接着就去异常处理的地方看看细节,再F9几下让记事本跑起来,再输入数据保存文件(中间可能会断住一下,应该是其他的异常主持但不是我们要的WriteFile)

注意此时我们打开PZ.TXT还是全空的

image-20220428140954771

然后就可以尽情调试函数细节!

image-20220428141652026

DASCTF X SU
🍬
HFCTF2022
🍪

About this Post

This post is written by P.Z, licensed under CC BY-NC 4.0.