zoukankan      html  css  js  c++  java
  • 调试钩取技术

    @author: dlive

    0x01 简介

    本章将讲解前面介绍过的调试钩取技术,钩取记事本的kernel32!WriteFile() API

    调试钩取技术能进行与用户更具有交互性(interactive)的钩取操作,这种技术会向用户提供简单的接口,使用户能够控制目标进程的运行,并且可以自由使用进程内存。

    调试钩取技术涉及的重要API: DebugActiveProcess,GetThreadContext,SetThreadContext

    0x02 调试器工作原理

    调试进程经过注册之后,每当被调试者触发调试事件,OS就会暂停其运行,并向调试器报告相应事件。调试器对相应事件做适当处理后,使被调试者继续运行。

    1. 一般的异常Exception也属于调试事件
    2. 若相应进程处于非调试,调试事件会在其自身的异常处理或OS的异常处理机制中被处理掉
    3. 调试器无法处理或不关心的调试事件最终由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("
    USAGE : hookdbg.exe <pid>
    ");
            return 1;
        }
    
        // Attach Process
        dwPID = atoi(argv[1]);
        if( !DebugActiveProcess(dwPID) )
        {
            printf("DebugActiveProcess(%d) failed!!!
    "
                   "Error Code = %d
    ", 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("
    ### original string ###
    %s
    ", lpBuffer);
    
                // #6. 小写字母转换成大写字母
                for( i = 0; i < dwNumOfBytesToWrite; i++ )
                {
                    if( 0x61 <= lpBuffer[i] && lpBuffer[i] <= 0x7A )
                        lpBuffer[i] -= 0x20;
                }
    
                printf("
    ### converted string ###
    %s
    ", 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

  • 相关阅读:
    博客作业01-日期抽象数据类型的设计与实现
    C语言博客作业06--结构体&文件
    C语言博客作业05--指针
    C语言博客作业04--数组
    C语言博客作业03---函数
    C语言博客作业02----循环结构
    DS博客作业08--课程总结
    DS博客作业07--查找
    DS博客作业06--图
    DS博客作业05--树
  • 原文地址:https://www.cnblogs.com/dliv3/p/6385210.html
Copyright © 2011-2022 走看看