《有趣的二进制:软件安全与逆向分析》读书笔记:自由控制程序运行方式的编程技巧
Posted 思源湖的鱼
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了《有趣的二进制:软件安全与逆向分析》读书笔记:自由控制程序运行方式的编程技巧相关的知识,希望对你有一定的参考价值。
目录
前言
本篇继续阅读学习《有趣的二进制:软件安全与逆向分析》,本章是自由控制程序运行方式的编程技巧,主要介绍调试器的原理、代码注入和API钩子
一、调试器
本节给出了一个简单的调试器源码,通过实践来学习一些基本知识
1、调试器是怎样工作的
一段最简单的调试器代码如下:
// wdbg01a.cpp : 定义命令行应用程序入口点
#include "stdafx.h"
#include <Windows.h>
int _tmain(int argc, _TCHAR* argv[])
PROCESS_INFORMATION pi;
STARTUPINFO si;
if(argc < 2)
fprintf(stderr, "C:\\\\>%s <sample.exe>\\n", argv[0]);
return 1;
memset(&pi, 0, sizeof(pi));
memset(&si, 0, sizeof(si));
si.cb = sizeof(STARTUPINFO);
//程序通过 CreateProcess 函数启动调试目标进程,调试目标进程也叫调试对象或者被调试程序(debuggee)
BOOL r = CreateProcess(
NULL, argv[1], NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS | CREATE_SUSPENDED | DEBUG_PROCESS,
NULL, NULL, &si, &pi);
if(!r)
return -1;
//直接调用 ResumeThread 函数,这时调试对象的所有线程就会恢复运行
ResumeThread(pi.hThread);
while(1)
DEBUG_EVENT de; //保存调试事件信息的结构体指针
//调试事件会通过 WaitForDebugEvent 函数来进行接收
if(!WaitForDebugEvent(&de, INFINITE)) // INFINITE 表示一直等待
break;
DWORD dwContinueStatus = DBG_CONTINUE;
//这里是根据事件将事件的内容显示出来,具体可参见下面的 DEBUG_EVENT 结构体
switch(de.dwDebugEventCode)
case CREATE_PROCESS_DEBUG_EVENT:
printf("CREATE_PROCESS_DEBUG_EVENT\\n");
break;
case CREATE_THREAD_DEBUG_EVENT:
printf("CREATE_THREAD_DEBUG_EVENT\\n");
break;
case EXIT_THREAD_DEBUG_EVENT:
printf("EXIT_THREAD_DEBUG_EVENT\\n");
break;
case EXIT_PROCESS_DEBUG_EVENT:
printf("EXIT_PROCESS_DEBUG_EVENT\\n");
break;
case EXCEPTION_DEBUG_EVENT:
if(de.u.Exception.ExceptionRecord.ExceptionCode !=
EXCEPTION_BREAKPOINT)
dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
printf("EXCEPTION_DEBUG_EVENT\\n");
break;
case OUTPUT_DEBUG_STRING_EVENT:
printf("OUTPUT_DEBUG_STRING_EVENT\\n");
break;
case RIP_EVENT:
printf("RIP_EVENT\\n");
break;
case LOAD_DLL_DEBUG_EVENT:
printf("LOAD_DLL_DEBUG_EVENT\\n");
break;
case UNLOAD_DLL_DEBUG_EVENT:
printf("UNLOAD_DLL_DEBUG_EVENT\\n");
break;
if(de.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT)
break;
//当处理被交给调试器时,调试对象会暂停运行。因此,在我们的调试器显示消息的过程中,调试对象是处于暂停状态的
//调用 ContinueDebugEvent 函数可以让调试对象恢复运行,这时调试器又回到 WatiForDebugEvent 函数等待下一条调试事件
ContinueDebugEvent(
de.dwProcessId, de.dwThreadId, dwContinueStatus);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
CreateProcess
函数如下:
BOOL CreateProcess(
LPCTSTR lpApplicationName, // 可执行模块名称
LPTSTR lpCommandLine, // 命令行字符串
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, // 句柄继承选项
DWORD dwCreationFlags, // 创建标志
LPVOID lpEnvironment, // 新进程的环境变量块
LPCTSTR lpCurrentDirectory, // 当前路径
LPSTARTUPINFO lpStartupInfo, // 启动信息
LPPROCESS_INFORMATION lpProcessInformation // 进程信息
);
- 如果设置了
DEBUG_PROCESS
或DEBUG_ONLY_THIS_PROCESS
标志,则启动的进程(调试对象)中所产生的异常都会被调试器捕捉到 - 通过
CREATE_SUSPENDED
标志可以让进程在启动后进入挂起状态。 当设置这一标志时,CreateProcess
函数调用完成之后,新进程中的所有线程都会暂停
DEBUG_EVENT
结构体如下:
typedef struct _DEBUG_EVENT
DWORD dwDebugEventCode; //调试事件编号
DWORD dwProcessId; //进程 ID
DWORD dwThreadId; //线程 ID
union //接下来的数据会随 dwDebugEventCode 的不同而发生变化
EXCEPTION_DEBUG_INFO Exception; //发生异常
CREATE_THREAD_DEBUG_INFO CreateThread; //创建线程
CREATE_PROCESS_DEBUG_INFO CreateProcessInfo; //创建进程
EXIT_THREAD_DEBUG_INFO ExitThread; //线程结束
EXIT_PROCESS_DEBUG_INFO ExitProcess; //进程结束
LOAD_DLL_DEBUG_INFO LoadDll; //加载 DLL
UNLOAD_DLL_DEBUG_INFO UnloadDll; //卸载 DLL
OUTPUT_DEBUG_STRING_INFO DebugString; //调用 OutputDebugString 函数
RIP_INFO RipInfo; //发生系统调试错误
u;
DEBUG_EVENT, *LPDEBUG_EVENT;
这个最简单的调试器可以捕捉到创建进程、线程以及加载、卸载 DLL 等事件
2、实现反汇编功能
本小节添加反汇编功能,希望能实现一下功能:
- 显示出发生异常的地址以及当前寄存器的值
- 显示发生异常时所执行的指令
// wdbg02a.cpp : 定义命令行应用程序入口点
#include "stdafx.h"
#include <Windows.h>
#include "udis86.h" // udis86 是一个开源的反汇编器 https://github.com/vmt/udis86
#pragma comment(lib, "libudis86.lib")
//disas 函数负责对机器语言进行反汇编,使用了 udis86
int disas(unsigned char *buff, char *out, int size)
ud_t ud_obj;
ud_init(&ud_obj);
ud_set_input_buffer(&ud_obj, buff, 32);
ud_set_mode(&ud_obj, 32);
ud_set_syntax(&ud_obj, UD_SYN_INTEL);
if(ud_disassemble(&ud_obj))
sprintf_s(out, size, "%14s %s",
ud_insn_hex(&ud_obj), ud_insn_asm(&ud_obj));
else
return -1;
return (int)ud_insn_len(&ud_obj);
//exception_debug_event 函数会在发生异常时运行
int exception_debug_event(DEBUG_EVENT *pde)
DWORD dwReadBytes;
//在 Windows 中,即便我们的程序不是作为调试器挂载在目标进程上, 只要能够获取目标进程的句柄,就可以随意读写该进程的内存空间
HANDLE ph = OpenProcess(
PROCESS_VM_WRITE | PROCESS_VM_READ | PROCESS_VM_OPERATION, // 访问标志
FALSE, // 句柄继承选项
pde->dwProcessId); // 进程ID
if(!ph)
return -1;
// 用 OpenThread 打开线程之后,可通过 GetThreadContext 和 SetThreadContext 来读写寄存器
HANDLE th = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT, // 访问标志
FALSE, // 句柄继承选项
pde->dwThreadId); // 线程ID
if(!th)
return -1;
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_ALL;
GetThreadContext(th, &ctx); // 参数如下:拥有上下文的线程句柄,接收上下文的结构体地址
char asm_string[256];
unsigned char asm_code[32];
ReadProcessMemory(ph, (VOID *)ctx.Eip, asm_code, 32, &dwReadBytes); //参数如下:进程句柄,读取起始地址,用于存放数据的缓冲区,要读取的字节数,实际读取的字节数
if(disas(asm_code, asm_string, sizeof(asm_string)) == -1)
asm_string[0] = '\\0';
printf("Exception: %08x (PID:%d, TID:%d)\\n",
pde->u.Exception.ExceptionRecord.ExceptionAddress,
pde->dwProcessId, pde->dwThreadId);
printf(" %08x: %s\\n", ctx.Eip, asm_string);
printf(" Reg: EAX=%08x ECX=%08x EDX=%08x EBX=%08x\\n",
ctx.Eax, ctx.Ecx, ctx.Edx, ctx.Ebx);
printf(" ESI=%08x EDI=%08x ESP=%08x EBP=%08x\\n",
ctx.Esi, ctx.Edi, ctx.Esp, ctx.Ebp);
SetThreadContext(th, &ctx);
CloseHandle(th);
CloseHandle(ph);
return 0;
int _tmain(int argc, _TCHAR* argv[])
STARTUPINFO si;
PROCESS_INFORMATION pi;
if(argc < 2)
fprintf(stderr, "C:\\\\>%s <sample.exe>\\n", argv[0]);
return 1;
memset(&pi, 0, sizeof(pi));
memset(&si, 0, sizeof(si));
si.cb = sizeof(STARTUPINFO);
BOOL r = CreateProcess(
NULL, argv[1], NULL, NULL, FALSE,
NORMAL_PRIORITY_CLASS | CREATE_SUSPENDED | DEBUG_PROCESS,
NULL, NULL, &si, &pi);
if(!r)
return -1;
ResumeThread(pi.hThread);
int process_counter = 0;
do
DEBUG_EVENT de;
if(!WaitForDebugEvent(&de, INFINITE))
break;
DWORD dwContinueStatus = DBG_CONTINUE;
switch(de.dwDebugEventCode)
case CREATE_PROCESS_DEBUG_EVENT:
process_counter++;
break;
case EXIT_PROCESS_DEBUG_EVENT:
process_counter--;
break;
case EXCEPTION_DEBUG_EVENT:
if(de.u.Exception.ExceptionRecord.ExceptionCode !=
EXCEPTION_BREAKPOINT)
dwContinueStatus = DBG_EXCEPTION_NOT_HANDLED;
exception_debug_event(&de);
break;
ContinueDebugEvent(
de.dwProcessId, de.dwThreadId, dwContinueStatus);
while(process_counter > 0);
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 0;
3、运行改良版调试器
进行一个测试
一个简单的会发生异常的程序如下:
int main(int argc, char *argv[])
char *s = NULL;
*s = 0xFF;
return 0;
测试结果如下:
可以看到在 mov byte [eax], 0xff
的地方发生了第 2 个异常
对应源代码中的 *s = 0xFF
二、代码注入
本节介绍了3种代码注入的手法
1、用 SetWindowsHookEx 劫持系统消息
先简单看下三个 Windows 官方 API 函数:
//SetWindowsHookEx 的功能是将原本传递给窗口过程的消息劫持下来,交给第 2 参数中所指定的函数来进行处理
HHOOK SetWindowsHookEx(
int idHook, // 钩子类型
HOOKPROC lpfn, // 钩子过程
HINSTANCE hMod, // 应用程序实例的句柄
DWORD dwThreadId // 线程ID
);
LRESULT CallNextHookEx(
HHOOK hhk, // 当前钩子的句柄
int nCode, // 传递给钩子过程的代码
WPARAM wParam, // 传递给钩子过程的值
LPARAM lParam // 传递给钩子过程的值
);
BOOL UnhookWindowsHookEx(
HHOOK hhk // 要解除的对象的钩子过程句柄
);
利用这 SetWindowsHookEx
这个API,书里给出了 loging.h 和 loging.cpp 如下:
//loging.h
#ifdef LOGING_EXPORTS
#define LOGING_API extern "C" __declspec(dllexport)
#else
#define LOGING_API extern "C" __declspec(dllimport)
#endif
LOGING_API int CallSetWindowsHookEx(VOID);
LOGING_API int CallUnhookWindowsHookEx(VOID);
// loging.cpp
#include "stdafx.h"
#include "loging.h"
HHOOK g_hhook = NULL;
// GetMsgProc 中调用了 CallNextHookEx 函数,这时消息会继续传递给下一个钩子过程
static LRESULT WINAPI GetMsgProc(int code, WPARAM wParam, LPARAM lParam)
return(CallNextHookEx(NULL, code, wParam, lParam));
LOGING_API int CallSetWindowsHookEx(VOID)
if(g_hhook != NULL)
return -1;
MEMORY_BASIC_INFORMATION mbi;
if(VirtualQuery(CallSetWindowsHookEx, &mbi, sizeof(mbi)) == 0)
return -1;
HMODULE hModule = (HMODULE) mbi.AllocationBase;
g_hhook = SetWindowsHookEx(
WH_GETMESSAGE, GetMsgProc, hModule, 0); //将 GetMsgProc 设为钩子过程,因此系统消息在传递给目标线程原有的窗口过程之前,会先由 GetMsgProc 来进行处理
if(g_hhook == NULL)
return -1;
return 0;
LOGING_API int CallUnhookWindowsHookEx(VOID)
if(g_hhook == NULL)
return -1;
UnhookWindowsHookEx(g_hhook);
g_hhook = NULL;
return 0;
将 loging.cpp 编译成 DLL
// dllmain.cpp
#include "stdafx.h"
//DLL 成功加载之后, 向 %TEMP% 目录输出一个名为 loging.log 的日志文件。日志的内容包括进程 ID 和模块路径
int WriteLog(TCHAR *szData)
TCHAR szTempPath[1024];
GetTempPath(sizeof(szTempPath), szTempPath);
lstrcat(szTempPath, "loging.log");
TCHAR szModuleName[1024];
GetModuleFileName(GetModuleHandle(NULL),
szModuleName, sizeof(szModuleName));
TCHAR szHead[1024];
wsprintf(szHead, "[PID:%d][Module:%s] ",
GetCurrentProcessId(), szModuleName);
HANDLE hFile = CreateFile(
szTempPath, GENERIC_WRITE, 0, NULL,
OPEN_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if(hFile == INVALID_HANDLE_VALUE)
return -1;
SetFilePointer(hFile, 0, NULL, FILE_END);
DWORD dwWriteSize;
WriteFile(hFile, szHead, lstrlen(szHead), &dwWriteSize, NULL);
WriteFile(hFile, szData, lstrlen(szData), &dwWriteSize, NULL);
CloseHandle(hFile);
return 0;
BOOL APIENTRY DllMain( HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
switch (ul_reason_for_call)
case DLL_PROCESS_ATTACH:
WriteLog("DLL_PROCESS_ATTACH\\n");
break;
case DLL_THREAD_ATTACH:
break;
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
WriteLog("DLL_PROCESS_DETACH\\n");
break;
return TRUE;
将 dllmain.cpp、loging.cpp 和 loging.h 进行编译得到 loging.dll
然后再利用 CallSetWindowsHookEx
加载这个DLL,见 setwindowshook.cpp:
// setwindowshook.cpp
#include "stdafx.h"
#include <Windows.h>
int _tmain(int argc, _TCHAR* argv[])
if(argc < 2)
fprintf(stderr, "%s <DLL Name>\\n", argv[0]);
return 1;
HMODULE h = LoadLibrary(argv[1]);
if(h == NULL)
return -1;
int (__stdcall *fcall) (VOID);
fcall = (int (WINAPI *)(VOID))
GetProcAddress(h, "CallSetWindowsHookEx");
if(fcall == NULL)
fprintf(stderr, "ERROR: GetProcAddress\\n");
goto _Exit;
int (__stdcall *ffree) (VOID);
ffree = (int (WINAPI *)(VOID))
GetProcAddress(h, "CallUnhookWindowsHookEx");
if(ffree == NULL)
以上是关于《有趣的二进制:软件安全与逆向分析》读书笔记:自由控制程序运行方式的编程技巧的主要内容,如果未能解决你的问题,请参考以下文章
《有趣的二进制:软件安全与逆向分析》读书笔记:自由控制程序运行方式的编程技巧
《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击
《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击
《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击