《有趣的二进制:软件安全与逆向分析》读书笔记:自由控制程序运行方式的编程技巧

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_PROCESSDEBUG_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)
		以上是关于《有趣的二进制:软件安全与逆向分析》读书笔记:自由控制程序运行方式的编程技巧的主要内容,如果未能解决你的问题,请参考以下文章

《有趣的二进制:软件安全与逆向分析》读书笔记:自由控制程序运行方式的编程技巧

《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击

《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击

《有趣的二进制:软件安全与逆向分析》读书笔记:利用软件的漏洞进行攻击

《有趣的二进制:软件安全与逆向分析》读书笔记:使用工具探索更广阔的世界

《有趣的二进制:软件安全与逆向分析》读书笔记:使用工具探索更广阔的世界