前面一直在写dll hook技术的学习心得,但是现在又来写API hook的体会,很多人都不理解,为什么要学习API hook,dll hook已经那么强大,为什么还要把API hook单独拿出来学习?在我学习完这些内容之后,我深刻的认识到二者的差别,请听我说。
下面完整的看一下API hook 的概念和优势
举个例子
下面看一下API HOOK 所用到的所有方法
HOOK API 主要分为调试法和注入法,注入法又包括dll注入和代码注入。
下面讲解 使用调试的方法来HOOK API
RIP_EVENT
关于这种调试事件的文档资料非常少,即使提到也只是用“系统错误”或者“内部错误”一笔带过。既然如此,我们也不需要对其进行什么处理,只要输出一条信息或者干脆忽略它即可。
OUTPUT_DEBUG_STRING_EVENT
当被调试进程调用OutputDebugString时就会引发该类调试事件,OUTPUT_DEBUG_STRING_INFO结构体描述了关于该事件的详细信息。在MSDN中,对该结构体各字段的解释是:lpDebugStringData字段是字符串在被调试进程的进程空间内的地址;nDebugStringLength字段是以字符为单位的字符串的长度;fUnicode指示字符串是否Unicode编码的。根据我个人的实验观察,发现只有第一个字段的解释是对的。实际上,无论调用OutputDebugStringA还是OutputDebugStringW,字符串都会以ANSI编码来表示。如果是调用OutputDebugStringW,那么会先将字符串转换成ANSI编码之后再调用OutputDebugStringA(这个过程在MSDN内有描述)。所以fUnicode的值永远都是0,而nDebugStringLength是以字节为单位的字符串长度,而不是以字符为单位。
LOAD_DLL_DEBUG_EVENT
加载一个DLL模块之后引发该类调试事件,LOAD_DLL_DEBUG_INFO结构体描述了它的详细信息。lpImageName这个字段可能会使你想在调试器中输出DLL的文件名,然而这行不通。MSDN上的解释是,lpImageName的值是文件名字符串在被调试进程的进程空间内的地址,但是这个值可能为NULL,即使不为NULL,通过ReadProcessMemory读取到的内容也可能是NULL。所以,想通过这个字段获取DLL的文件名并不可靠。
那么,通过hFile字段来获取文件名如何?没有Windows API可以直接通过文件句柄获取文件名,想要这么做的话必须绕一个大圈子,实际上hFile是与dwDebugInfoFileOffset和nDebugInfoSize一起使用的,用于获取DLL文件的调试信息。一般情况下我们不需要这么做,所以只要调用CloseHandle关闭这个句柄即可。记住!关闭这个句柄非常重要,如果不这么做的话会引起资源泄漏。
我的想法是,先通过EnumProcessModules枚举被调试进程的模块,然后通过GetModuleInformation获取模块的基地址,将这个基地址与LOAD_DLL_DEBUG_INFO结构体的lpBaseOfDll字段进行比较,如果相等的话就通过GetModuleFileNameEx获取DLL的文件名。可是我在实验这个方法的时候EnumProcessModules总是返回FALSE,GetLastError返回299,这是什么原因呢?
这可能是因为当调试器在处理这类调试事件时,被调试进程还没有启动完毕,所需要的模块还未全部加载完成,所以无法获取它的模块信息。
UNLOAD_DLL_DEBUG_EVENT
卸载一个DLL模块的时候引发该类调试事件。一般情况下只要输出一条信息或者忽略它即可。
CREATE_PROCESS_DEBUG_EVENT
创建进程之后的第一个调试事件,CREATE_PROCESS_DEBUG_INFO结构体描述了该类调试事件的详细信息。该结构体有三个字段是句柄,分别是hFile,hProcess和hThread,同样要记得使用CloseHandle关闭它们!
EXIT_PROCESS_DEBUG_EVENT
被调试进程结束时引发此类调试事件,EXIT_PROCESS_DEBUG_INFO结构体描述了它的详细信息。或许你能做的只有输出dwExitCode这个字段的值。
CREATE_THREAD_DEBUG_EVENT
创建一个线程之后引发此类调试事件,CREATE_THREAD_DEBUG_INFO结构体描述了它的详细信息。同样要记住用CloseHandle关闭hThread字段!
EXIT_THREAD_DEBUG_EVENT
一个线程结束之后引发此类调试事件,EXIT_THREAD_DEBUG_INFO结构体描述了它的详细信息。对此同样也只能输出dwExitCode的值。
EXCEPTION_DEBUG_EVENT
发生异常时引发此类调试事件,EXCEPTION_DEBUG_INFO结构体描述了它的详细信息。对这种调试事件的处理是最麻烦的,因为异常的种类非常多,对每种异常的处理也不相同。另外,此类调试事件也是实现断点和单步执行的关键。
下面介绍HOOK API 的总体流程(简单的来说就是在API段首下CC断点,然后程序调用函数时会在停在函数段首,然后就可以对API进行各种操作了)
首先查看notepad.exe的PID
运行HOOKdbg.exe
这时钩子已经安装完毕,我们输入小写的hello i am shin然后保存
可以发现,保存出来的是大写的HELLO I AM SHIN
接下来开始讲解工作原理
在WriteFile() API段首下CC断点
查看堆栈缓冲区
可以看出ESP+8是www.xuenixiang.com的数据缓冲区,只要把ESP+8的内容换成大写再保存就实现了保存后转换为大写
初学者很容易认为是WriteFile()API的起始地址为7C8112FF,但是EIP的值应该是WriteFile()API的起始地址7C8112FF+1=7C811300
像OD这类应用范围很广的调试器,EIP值与设置断点的地址是相同的,并不现实INT3(0XCC)指令,这是OD为了向用户展示更方便的界面而提供的功能,也就是说,复写了INT3指令之后,若执行该命令,则EIP值增1,此时OD会将0XCC恢复为原来的字节,并调整EIP。
源代码如下(源码花了一天敲备注,非常详细,请仔细阅读!!!)
//shin-2018.10.11
#include "pch.h"
#include "windows.h"
#include "stdio.h"
LPVOID g_pfWriteFile = NULL;
CREATE_PROCESS_DEBUG_INFO g_cpdi;
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0;
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
// WriteFile() API 地址
g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");
// API Hook - WriteFile()
// 更改第一个字节为0xCC (INT 3)
// orginal byte(orginal意思是原始的)是g_chOrgByte
memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));//从CreateProcessInfo结构体中拷贝
//出CREATE_PROCESS_DEBUG_INFO大小的字节给g_cpdi
ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chOrgByte, sizeof(BYTE), NULL);
/*hProcess[in]远程进程句柄。 被读取者
pvAddressRemote[in]远程进程中内存地址。 从具体何处读取
pvBufferLocal[out]本地进程中内存地址.函数将读取的内容写入此处
dwSize[in]要传送的字节数。要写入多少
pdwNumBytesRead[out]实际传送的字节数.函数返回时报告实际写入多少 返回值是布尔类型*/
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);
//把0xCC覆盖第一个字节
return TRUE;
}
BOOL OnExceptionDebugEvent(LPDEBUG_EVENT pde)
{
CONTEXT ctx;//用context结构体定义一个变量
PBYTE lpBuffer = NULL;
DWORD dwNumOfBytesToWrite, dwAddrOfBuffer, i;
PEXCEPTION_RECORD per = &pde->u.Exception.ExceptionRecord;//把pde结构体中的ExceptionRecord(异常记录)结构体给per
// 判断异常记录的内容是否是断点异常(断点异常里面包括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);//将函数修改后的首字节0xCC恢复为原首字节(6A)
// #2. Thread Context 获取线程上下文,在使用该结构之前 要在ContextFlags中指定哪些寄存器组用来读写
ctx.ContextFlags = CONTEXT_CONTROL;//即将使用如下寄存器
//DWORD Ebp;
//DWORD Eip;
//DWORD SegCs; // MUST BE SANITIZED
//DWORD EFlags; // MUST BE SANITIZED
//DWORD Esp;
//DWORD SegSs;
GetThreadContext(g_cpdi.hThread, &ctx);//获取线程的各种状态
// #3. WriteFile()的param2、3 值
// 函数参数存在于相应进程的栈
// param 2 : ESP + 0x8
// param 3 : ESP + 0xC
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0x8),
&dwAddrOfBuffer, sizeof(DWORD), NULL);//Buffer 是一个字符串指针,把buffer的地址写到dwAddrOfBuffer
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)(ctx.Esp + 0xC),
&dwNumOfBytesToWrite, sizeof(DWORD), NULL);//字符串长度读到dwNumOfBytesToWrite中
/*hProcess[in]远程进程句柄。 被读取者
pvAddressRemote[in]远程进程中内存地址。 从具体何处读取
pvBufferLocal[out]本地进程中内存地址.函数将读取的内容写入此处
dwSize[in]要传送的字节数。要写入多少
pdwNumBytesRead[out]实际传送的字节数.函数返回时报告实际写入多少 返回值是布尔类型*/
// #4.分配临时缓冲区
lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite + 1);//加1是因为当前断在WriteFile() + 1位置
//在调用malloc动态申请内存块时,一定要进行返回值的判断(这里没有判断),
memset(lpBuffer, 0, dwNumOfBytesToWrite + 1);//把lpBuffer的内容全部赋值为0,赋值的长度为dwNumOfBytesToWrite + 1
// #5. 恢复WriteFile()缓冲区到临时缓冲区
ReadProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);//把buffer的内容存到lpBuffer
printf("\n### 原字符串的内容 ###\n%s\n", lpBuffer);
// #6. 将小写字母转换为大写字母
for (i = 0; i < dwNumOfBytesToWrite; i++)
{
if (0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A)//如果字符ASCll大于等于97小于等于122
lpBuffer[i] -= 0x20;//将小写字母转换为大写字母
}
printf("\n### 转换成大写后的内容 ###\n%s\n", lpBuffer);//输出到CMD
printf("------------------------------------\n技术详情访问www.xuenixiang.com\n");
// #7. 将转换后的缓冲区复制到WriteFile()缓冲区
WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
lpBuffer, dwNumOfBytesToWrite, NULL);//把lpBuffer的内容写到dwAddrOfBuffer(buffer)中
/*hProcess
由OpenProcess返回的进程句柄。
如参数传数据为 INVALID_HANDLE_VALUE 【即 - 1】目标进程为自身进程
lpBaseAddress
要写的内存首地址
再写入之前,此函数将先检查目标地址是否可用,并能容纳待写入的数据。
lpBuffer
指向要写的数据的指针。
nSize
要写入的字节数*/
// #8.释放临时缓冲区
free(lpBuffer);
// #9. 将线程上下文的EIP更改为WriteFile()首地址
// (当前为WriteFile() + 1位置,int3命令之后)
ctx.Eip = (DWORD)g_pfWriteFile;//将WriteFile首地址赋值给eip
SetThreadContext(g_cpdi.hThread, &ctx);//将指定线程g_cpdi.hThread的context存到ctx结构体变量,为运行被调试进程做准备
//g_cpdi.hThread是被调试者的主线程句柄
// #10. 运行Debuggee(被调试进程)
ContinueDebugEvent(pde->dwProcessId, pde->dwThreadId, DBG_CONTINUE);
Sleep(0);//释放当前线程的剩余时间片,调用sleep(0)之后,cpu会立即执行其他线程,经过一定时间再获得控制权
//(避免notepad正在调用writefile()API的过程中,后面的钩子代码在调用成功之前执行完毕,这样会导致内存访问异常)
// #11. API Hook
WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
&g_chINT3, sizeof(BYTE), NULL);//将0xCC写到g_pfWriteFile(函数首地址)——继续设置断点
return TRUE;
}
}
return FALSE;
}
void DebugLoop()
{
DEBUG_EVENT de;//用DEBUG_EVENT结构体生成一个变量de,de继承结构体的属性和内容
DWORD dwContinueStatus;//继续状态
// 等待被调试者发生事件
while (WaitForDebugEvent(&de,INFINITE))//infinite=0xFFFFFFFF 二者可以替换,都代表无穷等待
//第一个参数指向event结构,这个结构描述了一个调试事件,第二个参数为等待事件的毫秒数。
//该函数会将调试事件写入de这个调试事件结构体中,并返回BOOL类型值(0或1)
{
dwContinueStatus = DBG_CONTINUE;
// 被调试进程生成或者附加事件
if (CREATE_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)//如果DEBUG_EVENT结构体中的调试事件是CREATE_PROCESS_DEBUG_EVENT就继续执行
//CREATE_PROCESS_DEBUG_EVENT(创建进程之后发送此类调试事件,这是调试器收到的第一个调试事件)
{
OnCreateProcessDebugEvent(&de);//进程创建成功之后会把DEBUG_EVENT当参数传给该函数
}
// 异常事件
else if (EXCEPTION_DEBUG_EVENT == de.dwDebugEventCode)
{
if (OnExceptionDebugEvent(&de))//出现异常之后会执行该函数
continue;
}
// 被调试进程终止事件
else if (EXIT_PROCESS_DEBUG_EVENT == de.dwDebugEventCode)//如果事件是进程终止,就跳出调试器循环(while)
{
// 被调试者终止 -> 调试器终止
break;
}
// 再次运行被调试者
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, dwContinueStatus);
}
}
int main(int argc, char*argv[])//argc是参数个数,argv是参数内容的数组指针
{
system("title www.xuenixiang.com");
DWORD dwPID;//声明一个双字空间用来存输入进来的PID
if (argc != 2)//hookdbg必须有2个参数
{
printf("\nUSAGE : hookdbg.exe \n");
return 1;
}
// Attach Process(附加进程部分)
dwPID = atoi(argv[1]);//argv[0]=程序名 argv[1]=PID 把输入的PID从char类型转换为int类型
if (!DebugActiveProcess(dwPID))//如果附加失败,会返回0,if语句会执行
{
printf("DebugActiveProcess(%d) failed!!!\n"
"Error Code = %d\n", dwPID, GetLastError());//获取错误参数
return 1;
}
// 调试器循环
DebugLoop();
return 0;
}
下面是源码分析,会整体性的分析hook过程
一:Main()
Main函数以程序运行参数的形式接收要钩取API的进程的PID,然后通过DebugActiveProcess()API将调试器附加到该运行的进程上,开始调试。然后进入DebugLoop()函数,处理来自被调试者的调试事件。(也可以通过CreateProcess()API,从一开始就直接以调试模式运行进程)
二:DebugLoop()
WaitForDebugEvent(
__in LPDEBUG_EVENT lpDebugEvent,
__in DWORD dwMilliseconds
)
typedef struct _DEBUG_EVENT {
DWORD dwDebugEventCode;
DWORD dwProcessId;
DWORD dwThreadId;
union {
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;
UNLOAD_DLL_DEBUG_INFO UnloadDll;
OUTPUT_DEBUG_STRING_INFO DebugString;
RIP_INFO RipInfo;
} u;
} DEBUG_EVENT, *LPDEBUG_EVENT;
ContinueDebugEvent是一个使调试器继续运行的API
定义如下
ContinueDebugEvent(
__in DWORD dwProcessId,
__in DWORD dwThreadId,
__in DWORD dwContinueStatus
);
ContinueDebugEvent() API 的 最 后 一 个 参 数 dwContinueStatus 的 值 为 DBG_CONTINUE 或 DBG_EXCEPTION_NOT_HANDLED。若处理正常,则其值设置为DBG_CONTINUE;若无法处理,或希望在应用程序的SEH中处 理,则其值设置为DBG EXCEPTION NOT HANDLED
DebugLoopO函数处理3种调试事件,如下所示。
□ EXIT_PROCESS_DEBUG_EVENT
□ CREATE_PROCESS_DEBUG_EVENT
□ EXCEPTION_DEBUG_EVENT
下面分别看看这3个事件。
EXIT_PROCESS_DEBUG_EVENT
被调试进程终止时会触发该事件。本章的示例代码中发生该事件时,调试器与被调试者将一 起终止。
CREATE_PROCESS_DEBUG_EVENT-OnCreateProcessDebugEvent()
OnCreateProcessDebugEvent()是 CREATE_PROCESS_DEBUG_EVENT 事件句柄,被调试进程启动(或者附加)时即调用执行该函数。
typedef struct _CREATE_PROCESS_DEBUG_INFO {
HANDLE hFile;
HANDLE hProcess;
HANDLE hThread;
LPVOID lpBaseOfImage;
DWORD dwDebugInfoFileOffset;
DWORD nDebugInfoSize;
LPVOID lpThreadLocalBase;
LPTHREAD_START_ROUTINE lpStartAddress;
LPVOID lpImageName;
WORD fUnicode;
} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;
g_chOrgByte变量中存储的是WriteFile()API的第一个字节,后面进行恢复时会用到,然后使用WriteProcessMemory()API将WriteFile()API第一个字节更改为0xCC
EXCEPTION_DEBUG_EVENT-OnExceptionDebugEvent()
EXCEPTION_DEBUG_EVENT-OnExceptionDebugEvent()是EXCEPTION_DEBUG_EVENT事件句柄,它处理被调试者的Int3指令,下图为代码实现过程
2.获取线程上下文(通过对Context结构体的读取来获得当前的线程信息,例如esp,ebp等寄存器的内容)
在写脱壳脚本时,经常用到context结构体中的esp值充当返回值
在使用CONTEXT结构之前 要在ContextFlags中指定哪些寄存器组用来读写
我们要获取WriteFile的缓冲区内容,要用到esp,所以需要获取CONTEXT_CONTROL结构体
(如果要用eax等寄存器,则需要让ContextFlags =CONTEXT_INTEGER)
WINBASEAPI
BOOL
WINAPI
GetThreadContext(
__in HANDLE hThread,
__inout LPCONTEXT lpContext
);
获取WriteFile()的param值
存储在dwAddrOfBuffer中的数据缓冲区地址是被调试者(notepad.exe)虚拟内存空间中的地址
把小写字母转换为大写字母后覆写在WriteFile()缓冲区
把线程上下文的EIP修改为WriteFile()起始地址
将线程上下文的EIP更改为WriteFile()首地址(当前为WriteFile() + 1位置,int3命令之后)
修改好CONTEXT.Eip成员后,调用SetThreadContext()API来修改
SetThreadContext(
__in HANDLE hThread,
__in CONST CONTEXT *lpContext
);
运行调试进程
全部准备完成后,接下来就正常调用WriteFile()API了,调用ContinueDebugEvent()API就可以重启被调试进程,使之继续运行。
继续HOOK(达到循环获取的目的)
在 OnExceptionDebugEvent()函 数 中 调 用 了 ContinueDebugEvent()函 数 后 ,为 什 么 还 要 调 用Sleep(0)函 数 ?
调 用Sleep(0)函 数 可 以 释 放 当 前 线 程 的 剩 余 时 间 片 , 即 放 弃 当 前 线 程 执 行 的CPU时 间 片。也 就 是 说 , 调 用Sleep(0)函 数 后 ,CPU会 立 即 执 行 其 他 线 程 。 被 调 试 进 程(Notepad.exe) 的 主 线 程 处 于 运 行 状 态 时 , 会 正 常 调 用WriteFiIe()API。然 后 经 过 一 定 时 间 , 控 制 权 再 次 转 移 给HookDbg.exe, Sleep(0)后 面 的 “ 钩子 ” 代 码(WriteProcessMemory()API)会 被 调 用 执 行。若 没 有Sleep(0)语 句 ,Notepad.exe调 用WriteFile()API的 过 程 中 ,HookDbg.exe会尝试将WriteFile()API的首字节修改为OxCC。若运气不佳,这可能会导致内存访问异常。(避免notepad正在调用writefile()API的过程中,后面的钩子代码在调用成功之前执行完毕)
最近比较忙,一个月才搞定~~~
2018.11.1-shin