(简称逆向工程第三十章学习)
先贴下调试的技术的图表
调试技术
调试器能逐一执行被调试者的指令,拥有对寄存器与内存的所有访问权限
平常的软件断点其实就是INT 3,IA-32指令为0xCC,刚好在OALABS里看过
一般调试的一个流程是
- 对想钩取的函数附加,使目标进程变成被调试者
- 钩子:将API起始地址的第一个字节变成0xCC
- 调用相应API,控制权转到调试器
- 执行需要的操作
- 脱钩:将0xCC恢复原值(为了正常运行被改的API)
- 运行该API(无INT3状态)
- 钩子:再次改为0xCC,反复而使(根据需求而来)
实验环节
查找PID然后挂钩,随便输入点小写字母,然后保存
保存完会发现,全部转为大写字符!
工作原理
用od打开notepad,按F9执行,在Kernel32!WriteFile下个断点,随便输入点的东西再保存
就会在这断下,这时候我们可以观察栈,第二个参数就是指向我们的输入
执行流
- 当我们在WriteFile设置了一个断点(INT3)
- 向目标进程(notepad.exe)保存文件时
- 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; }
dwPID = atoi(argv[1]); if( !DebugActiveProcess(dwPID) ) { printf("DebugActiveProcess(%d) failed!!!\n" "Error Code = %d\n", dwPID, GetLastError()); return 1; }
DebugLoop();
return 0; }
|
DebugLoop
当经过main函数的DebugActiveProcess附加上目标进程后,就会调用DebugLoop函数
程序处理了三种事件
- OnCreateProcessDebugEvent,创建进程所对应的操作
- OnExceptionDebugEvent,发生异常所对应的操作
- 还有一个就是发生退出事件,就直接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 de; DWORD dwContinueStatus;
while( WaitForDebugEvent(&de, INFINITE) ) { dwContinueStatus = DBG_CONTINUE;
if( CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode ) { OnCreateProcessDebugEvent(&de); } else if( EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode ) { if( OnExceptionDebugEvent(&de) ) continue; } else if( EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode ) { break; }
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus); } }
|
DEBUG_EVENT
首先了解一下九大调试事件(九大巨人!雾
其实都是字如其名,慢慢了解就好了
WaitForDebugEvent() API是等带目标进程发生调试事件的函数,当发生了就会把相关事件放置到第一个参数的变量
OnCreateProcessDebugEvent
当目标进程启动(或附加)即在DebugLoop对应,于是调用执行该函数
(也就是DebugLoop的第一个if语句所对应)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde) { g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO)); ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL); WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
return TRUE; }
|
g_cpdi
g_cpdi是CREATE_PROCESS_DEBUG_INFO的结构体,这边用到hProcess即目标进程的句柄
OnExceptionDebugEvent
当目标进程发生EXCEPTION_BREAKPOINT异常运行此函数(除此之外还有19种异常)
(也就是对应DebugLoop的else if)
于是一整套流程就是
- 脱钩(将0xCC改回原来的API函数的值)
- 获取线程上下文,这里面存放着CPU的信息,保证之后继续运行不会出错(因为会有时间片)
- 获取WriteFile的第二三个参数
- 4-8就是将小写字母改成大写字母
- 把线程上下文的EIP改为WriteFile的其实EIP(因为执行了0xCC,现在的EIP为+1状态)
- 一切弄好之后就是继续运行目标进程,一定要注意Sleep(0),这是释放当前线程的剩余时间片,防止内放访问异常(详细见书304页,还没彻底理解)
- 最后设置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
| BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde) { CONTEXT ctx; PBYTE lpBuffer = NULL; DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i; PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;
if( EXCEPTION_BREAKPOINT == per->ExceptionCode ) { if( g_pfWriteFile == per->ExceptionAddress ) { WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chOrgByte, sizeof(BYTE), NULL);
ctx.ContextFlags = CONTEXT_CONTROL; GetThreadContext(g_cpdi.hThread, &ctx);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8), &dwAddrOfBuffer, sizeof(DWORD), NULL); ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC), &dwNumOfBytesToWrite, sizeof(DWORD), NULL);
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1); memset(lpBuffer, 0, dwNumOfBytesToWrite+1);
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL); printf("\n### original string ###\n%s\n", lpBuffer);
for( i = 0; i < dwNumOfBytesToWrite; i++ ) { if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A ) lpBuffer[i] -= 0x20; }
printf("\n### converted string ###\n%s\n", lpBuffer);
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer, lpBuffer, dwNumOfBytesToWrite, NULL); free(lpBuffer);
ctx.Eip = (DWORD)g_pfWriteFile; SetThreadContext(g_cpdi.hThread, &ctx);
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE); Sleep(0);
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile, &g_chINT3, sizeof(BYTE), NULL);
return TRUE; } }
return FALSE; }
|
调试TIME
Scr1pt师傅直接让我GET到一个小tips,xdbg的参数设置
本此内容是我们要调试调试器调试目标进程的细节(有点绕
所以我们要dbg hookdbg.exe,所以我们直接先拖进xdbg,由于需要参数,我们选择改变命令行
然后在后面跟上notepad的PID
可以通过字符串查找,分别在我们想要调试的地方设置断点(我分别在主函数和异常处理函数的函数开头设了断点)
接着就去异常处理的地方看看细节,再F9几下让记事本跑起来,再输入数据保存文件(中间可能会断住一下,应该是其他的异常主持但不是我们要的WriteFile)
注意此时我们打开PZ.TXT还是全空的
然后就可以尽情调试函数细节!