调试钩取技术 - 记事本WriteFile() API钩取
@author: dlive
0x01 简介
本章将讲解前面介绍过的调试钩取技术,钩取记事本的kernel32!WriteFile() API
调试钩取技术能进行与用户更具有交互性(interactive)的钩取操作,这种技术会向用户提供简单的接口,使用户能够控制目标进程的运行,并且可以自由使用进程内存。
调试钩取技术涉及的重要API: DebugActiveProcess,GetThreadContext,SetThreadContext
0x02 调试器工作原理
调试进程经过注册之后,每当被调试者触发调试事件,OS就会暂停其运行,并向调试器报告相应事件。调试器对相应事件做适当处理后,使被调试者继续运行。
- 一般的异常Exception也属于调试事件
 - 若相应进程处于非调试,调试事件会在其自身的异常处理或OS的异常处理机制中被处理掉
 - 调试器无法处理或不关心的调试事件最终由OS处理
 
0x03 调试事件
Windows的DebugEvent:https://msdn.microsoft.com/en-us/library/windows/desktop/ms679302(v=vs.85).aspx
我们关注的比较重要的调试事件是EXCEPTION_DEBUG_EVENT,该事件对应了多种异常事件(异常事件列表见书)
各种异常中,调试器必须处理的是EXCEPTION_BREAKPOINT异常。断点对应的汇编指令为INT 3,IA-32指令为0xCC。
代码调试遇到INT3指令即中断执行,EXCEPTION_BREAKPOINT异常事件被传送到调试器,此时调试器可以做多种处理。
调试器实现断点的方法非常简单,找到要设置断点的代码在内存中的起始地址,只要把1字节修改为0xCC就可以了。想继续运行时将它恢复原值。
0x04 调试WirteFile函数
(运行调试器需要使用管理员权限)
测试环境为Win7 x86
在OD的CPU窗口右键->查找->所有模块中的名称,然后搜索WriteFile函数,找到kernel32中的导出函数WriteFile的位置下断点,使用notepad.exe保存文件程序断在kernel32!WriteFile的入口处

可以看到被保存的字符串首地址写在esp+8的位置
0x05 钩子代码分析
main
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]);
    if( !DebugActiveProcess(dwPID) )
    {
        printf("DebugActiveProcess(%d) failed!!!\n"
               "Error Code = %d\n", dwPID, GetLastError());
        return 1;
    }
    DebugLoop();
    return 0;
}
main中使用DebugActiveProcess将调试器附加到指定PID的进程上
也可以使用CreateProcess API从一开始就直接以调试模式运行相关进程
DebugLoop
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);
    }
}
DebugLoop处理了三个DebugEvent,分别是
CREATE_PROCESS_DEBUG_EVENT: 被调试进程启动/附加时触发该事件,调试器调用OnCreateProcessDebugEvent()
EXCEPTION_DEBUG_EVENT:被调试进程遇到iNT 3指令时触发该事件,调试器调用OnExceptionDebugEvent()
EXIT_PROCESS_DEBUG_EVENT:被调试进程终止时触发,在本代码中,调试器在被调试器终止时退出
OnCreateProcessDebugEvent
LPVOID g_pfWriteFile = NULL; //global_pointer_function_WirteFile
CREATE_PROCESS_DEBUG_INFO g_cpdi; //global_create_process_debug_info
BYTE g_chINT3 = 0xCC, g_chOrgByte = 0; 
BOOL OnCreateProcessDebugEvent(LPDEBUG_EVENT pde)
{
    g_pfWriteFile = GetProcAddress(GetModuleHandleA("kernel32.dll"), "WriteFile");
    memcpy(&g_cpdi, &pde->u.CreateProcessInfo, sizeof(CREATE_PROCESS_DEBUG_INFO));
  	//g_cpdi.hProcess为被调试程序的句柄
    ReadProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
                      &g_chOrgByte, sizeof(BYTE), NULL);
    WriteProcessMemory(g_cpdi.hProcess, g_pfWriteFile,
                       &g_chINT3, sizeof(BYTE), NULL);
    return TRUE;
}
代码首先获得WriteFile的内存地址,
然后将函数地址处的第一个字节数据存放在g_chOrgByte变量中,之后将函数地址处第一个字节改为0xCC
由于调试器拥有被调试进程的句柄(带有调试权限,DLL注入时也是首先将进程提升为调试权限[SE_DEBUG_NAME])所以可以使用ReadProcessMemory和WriteProcessMemory对被调试进程的内存空间自由进行读写操作。
ReadProcessMemory:
https://msdn.microsoft.com/en-us/library/ms680553(VS.85).aspx
OnExceptionDebugEvent
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. 获取Thread Context
            ctx.ContextFlags = CONTEXT_CONTROL;
            GetThreadContext(g_cpdi.hThread, &ctx);
            // #3. 获取WriteFile()的第二和第三个参数
            //   参数在栈上的位置
            //   param 2 : ESP + 0x8
            //   param 3 : ESP + 0xC
            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. 开辟临时缓冲区
            lpBuffer = (PBYTE)malloc(dwNumOfBytesToWrite+1);
            memset(lpBuffer, 0, dwNumOfBytesToWrite+1);
            // #5. 将WriteFile()的第二个参数指向的缓冲区内容读出来
            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. 字母转换成大写后将临时缓冲区中的内容写入WriteFile的缓冲区
            WriteProcessMemory(g_cpdi.hProcess, (LPVOID)dwAddrOfBuffer,
                               lpBuffer, dwNumOfBytesToWrite, NULL);
            // #8. 释放动态申请的缓冲区
            free(lpBuffer);
            // #9. 将EIP修改为WriteFile()的起始地址
            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;
}
KK学长之前说写过一个程序获得当前系统所有运行的进程的EIP,现在想想好像可以用调试目标进程的方法获得
这里需要重点关注的地方是CONTEXT结构体,该结构体保存了线程的上下文信息,即线程的CPU寄存器信息
在调试进程的时候,被调试进程遇到INT 3会中断执行,执行流程调到调试器。被调试进程中断执行时的CPU寄存器信息就保存在CONTEXT结构体中。
从Windows XP开始可以调用DebugSetProcessKillOnExit函数,可以不销毁被调试进程就退出调试器,需要注意的是调试器退出前需要脱钩。
这本书有个好处,在前面的笔记中也提到过,就是每章后面有Q&A,好多看代码时自己的疑问都能在Q&A中找到解释。
在OnExceptionDebugEvent中调用ContinueDebugEvent之后,代码调用了Sleep(0),当时看到这里的时候不解Sleep(0)有什么作用,在Sleep(0)之后紧接着又下了钩子。当时就想如果下钩子的操作跑到了被调试进程WriteFile执行的前面,WriteFile不就又被钩取住了么。看了Q&A之后明白,Sleep(0)的作用是释放当前线程的剩余时间片,也就是说执行Sleep(0)后,CPU会立即执行其他线程。被调试进程的主线程处于运行状态时会正常调用WriteFile。一段时间后,控制权再次转移给调试器线程,Sleep(0)后面的钩子代码会被调用执行。
0x06 推荐文章
这几天在知乎上推荐的一系列关于自己编写调试器的文章,附上知乎链接
https://www.zhihu.com/question/52553014/answer/136312479
调试钩取技术 - 记事本WriteFile() API钩取的更多相关文章
- x64 下记事本WriteFile() API钩取
		
<逆向工程核心原理>第30章 记事本WriteFile() API钩取 原文是在x86下,而在x64下函数调用方式为fastcall,前4个参数保存在寄存器中.在原代码基础上进行修改: 1 ...
 - 通过调试对WriteFile()API的钩取
		
通过调试对WriteFile()API的钩取 0x00 目标与思路 目标:钩取指定的notepad.exe进程writeFile()API函数,对notepad.exe进程的写入的字符保存时保存为大写 ...
 - C++11中的技术剖析(萃取技术)
		
从C++98开始萃取在泛型编程中用的特别多,最经典的莫过于STL.STL中的拷贝首先通过萃取技术识别是否是已知并且支持memcpy类型,如果是则直接通过内存拷贝提高效率,否则就通过类的重载=运算符,相 ...
 - 抓取摩拜单车API数据,并做可视化分析
		
抓取摩拜单车API数据,并做可视化分析 纵聊天下 百家号|04-19 15:16 关注 警告:此篇文章仅作为学习研究参考用途,请不要用于非法目的. 摩拜是最早进入成都的共享单车,每天我从地铁站下来的时 ...
 - 百度音乐API抓取
		
百度音乐API抓取 前段时间做了一个本地音乐的播放器 github地址,想实现在线播放的功能,于是到处寻找API,很遗憾,不是歌曲不全就是质量不高.在网上发现这么一个APIMRASONG博客,有“获取 ...
 - C++的类型萃取技术
		
应该说,迭代器就是一种智能指针,因此,它也就拥有了一般指针的所有特点——能够对其进行*和->操作.但是在遍历容器的时候,不可避免的要对遍历的容器内部有所了解,所以,设计一个迭代器也就自然而然的变 ...
 - 基于Casperjs的网页抓取技术【抓取豆瓣信息网络爬虫实战示例】
		
CasperJS is a navigation scripting & testing utility for the PhantomJS (WebKit) and SlimerJS (Ge ...
 - Python数据抓取技术与实战 pdf
		
Python数据抓取技术与实战 目录 D11章Python基础1.1Python安装1.2安装pip1.3如何查看帮助1.4D1一个实例1.5文件操作1.6循环1.7异常1.8元组1.9列表1.10字 ...
 - 和风api爬取天气预报数据
		
''' 和风api爬取天气预报数据 目标:https://free-api.heweather.net/s6/weather/forecast?key=cc33b9a52d6e48de85247779 ...
 
随机推荐
- 笔记-pyton内置数据类型
			
笔记-pyton内置数据类型 1. 简介 The principal built-in types are numerics, sequences, mappings, classes, i ...
 - Oozie 实战之 shell
			
说明:使用 shell action 执行 shell 脚本 hive-select-test.sh 来通过已经配置好的 Hive -f 来执行 HQL 查询脚本文件 select.sql 1.创建脚 ...
 - hadoop,hbase,hive
			
linux上安装hadoop,然后安装hbase,然后安装zookeeper,最后安装hive.hbase安装在hdfs下.hive是纯逻辑表,hbase是物理表.hdfs是hadoop上的一个组件.
 - 多个".h"文件中声明及定义 全局变量和函数
			
一.".h"文件必须以如下格式书写 例:文件<CZ_efg_hi.h"> ------------文件内容----------- #ifndef CZ_Efg ...
 - 安测云验证有CTA问题
			
背景: 现在所有的app 都需要通过工信部的审核.用户不同意之前,不能联网. 那么,我怎么知道自己的应用有没有联网呢?那么多sdk ,那么多代码?我怎么测试呢? 哈哈,我们测试给的方法真的很管用. l ...
 - Android 图片放错位置会拉伸变形
			
今天做了一个很小的需求,然后需要图片,我给ui要图片.直接给了我三套,还命名 x . xx. 2k 真的一开始都不知道.没有玩过这么正规的.我就用了一张,放到了hdpi下面. 后来同事帮我才知道, 图 ...
 - ElasticSearch学习笔记(三)-- 查询
			
1. URISearch详解与演示 2. QueryDSL简介 3. 字段类查询简介及match-query 4. 相关性算分 5. match-phrase-query 6. query-strin ...
 - Java继承的缺点
			
转载自:https://www.cnblogs.com/xz816111/archive/2018/05/24/9080173.html JAVA中使用到继承就会有两个无法回避的缺点: 1.打破了封装 ...
 - Python基础——安装运行
			
Python是如何运行的? 像绝大多数编程语言一样,要在计算机上能够运行python程序,至少需要安装一个最小的Python包:一个Python解释器和支持的库. 安装Python 安装包下载:htt ...
 - 快速登录机器&数据库
			
本文来自网易云社区. 作者:盛国存 背景 我们日常在使用ApiDoc维护管理api文档,提高了api文档的整体维护性.但在老旧接口中,补充接口注解无疑是一次繁重的体力劳动.仔细查看,大多数接口的格式 ...