• 10.3 调试事件转存进程内存


    我们继续延申调试事件的话题,实现进程转存功能,进程转储功能是指通过调试API使获得了目标进程控制权的进程,将目标进程的内存中的数据完整地转存到本地磁盘上,对于加壳软件,通常会通过加密、压缩等手段来保护其代码和数据,使其不易被分析。在这种情况下,通过进程转储功能,可以将加壳程序的内存镜像完整地保存到本地,以便进行后续的分析。

    在实现进程转储功能时,主要使用调试API和内存读写函数。具体实现方法包括:以调试方式启动目标进程,将其暂停在运行前的位置;让目标进程进入运行状态;使用ReadProcessMemory函数读取目标进程内存,并将结果保存到缓冲区;将缓冲区中的数据写入文件;关闭目标进程的调试状态。

    首先老样子先来看OnException回调事件,当进程被断下时首先通过线程函数恢复该线程的状态,在进程被正确解码并运行起来时直接将该进程的EIP入口地址传递给MemDump();内存转存函数,实现转存功能;

    void OnException(DEBUG_EVENT *pDebug, BYTE *bCode)
    {
        CONTEXT context;
        DWORD dwNum;
        BYTE bTmp;
    
        // 打开当前进程与线程
        HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, pDebug->dwProcessId);
        printf("[+] 当前打开进程句柄: %d 进程PID: %d \n", hProcess, pDebug->dwProcessId);
        HANDLE hThread = OpenThread(THREAD_ALL_ACCESS, FALSE, pDebug->dwThreadId);
        printf("[+] 当前打开线程句柄: %d 线程PPID: %d \n", hThread, pDebug->dwThreadId);
        // 暂停当前线程
        SuspendThread(hThread);
    
        // 读取出异常产生的首地址
        ReadProcessMemory(hProcess, pDebug->u.Exception.ExceptionRecord.ExceptionAddress, &bTmp, sizeof(BYTE), &dwNum);
        printf("[+] 当前异常产生地址为: 0x%08X \n", pDebug->u.Exception.ExceptionRecord.ExceptionAddress);
    
        // 设置当前线程上下文,获取线程上下文
        context.ContextFlags = CONTEXT_FULL;
        GetThreadContext(hThread, &context);
    
        printf("[-] 恢复断点前: EAX = 0x%08X  EIP = 0x%08X \n", context.Eax, context.Eip);
        // 将刚才的CC断点取消,也就是回写原始的指令集
        WriteProcessMemory(hProcess, pDebug->u.Exception.ExceptionRecord.ExceptionAddress, bCode, sizeof(BYTE), &dwNum);
    
        // 当前EIP减一并设置线程上下文
        context.Eip--;
        SetThreadContext(hThread, &context);
        printf("[+] 恢复断点后: EAX = 0x%08X  EIP = 0x%08X \n", context.Eax, context.Eip);
        printf("[+] 获取到动态入口点: 0x%08x \n", pDebug->u.CreateProcessInfo.lpBaseOfImage);
        // 转储内存镜像
        MemDump(pDebug, context.Eip, (char *)"dump.exe");
        // 恢复线程
        ResumeThread(hThread);
        CloseHandle(hThread);
        CloseHandle(hProcess);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38

    MemDump函数中,首先通过调用CreateFile函数打开me32.szExePath路径也就是转存之前的文件,通过使用VirtualAlloc分配内存空间,分配大小是PE头中文件实际大小,接着OpenProcess打开正在运行的进程,并使用ReadProcessMemory读取文件的数据,此处读取的实在内存中的镜像数据,当读取后手动修正,文件的入口地址,及文件的对齐方式,接着定位PE节区数据,找到节区首地址,并循环将当前节区数据赋值到新文件缓存中,最后当一切准备就绪,通过使用WriteFile函数将转存后的文件写出到磁盘中;

    void MemDump(DEBUG_EVENT *pDe, DWORD dwEntryPoint, char *DumpFileName)
    {
        // 得到当前需要操作的进程PID
        DWORD dwPid = pDe->dwProcessId;
        MODULEENTRY32 me32;
    
        // 对系统进程拍摄快照
        HANDLE hSnap = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, dwPid);
    
        me32.dwSize = sizeof(MODULEENTRY32);
        // 得到第一个模块句柄,第一个模块句柄也就是程序的本体
        BOOL bRet = Module32First(hSnap, &me32);
        printf("[+] 当前转储原程序路径: %s \n", me32.szExePath);
    
        // 打开源文件,也就是dump之前的文件
        HANDLE hFile = CreateFile(me32.szExePath, GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
        if (hFile == INVALID_HANDLE_VALUE)
            exit(0);
    
        // 判断PE文件的有效性
        IMAGE_DOS_HEADER imgDos = { 0 };
        IMAGE_NT_HEADERS imgNt = { 0 };
    
        DWORD dwReadNum = 0;
    
        // 读入当前内存程序的DOS头结构
        ReadFile(hFile, &imgDos, sizeof(IMAGE_DOS_HEADER), &dwReadNum, NULL);
        // 判断是否是一个合格的DOS头
        if (imgDos.e_magic != IMAGE_DOS_SIGNATURE)
            return;
        // 设置文件指针到NT头上
        SetFilePointer(hFile, imgDos.e_lfanew, 0, FILE_BEGIN);
        ReadFile(hFile, &imgNt, sizeof(IMAGE_NT_HEADERS), &dwReadNum, NULL);
        // 判断是否是合格的NT头
        if (imgNt.Signature != IMAGE_NT_SIGNATURE)
            return;
    
        // 得到EXE文件的大小
        DWORD BaseSize = me32.modBaseSize;
        printf("[+] 当前内存文件大小: %d --> NT结构原始大小: %d 一致性检测: True \n", BaseSize, imgNt.OptionalHeader.SizeOfImage);
    
        // 如果PE头中的大小大于实际内存大小,则以PE头中大小为模板
        if (imgNt.OptionalHeader.SizeOfImage > BaseSize)
        {
            BaseSize = imgNt.OptionalHeader.SizeOfImage;
        }
    
        // 分配内存空间,分配大小是PE头中文件实际大小,并打开进程
        LPVOID pBase = VirtualAlloc(NULL, BaseSize, MEM_COMMIT, PAGE_READWRITE);
        printf("[+] 正在分配转储空间 句柄: %d \n", pBase);
    
        HANDLE hProcess = OpenProcess(PROCESS_ALL_ACCESS, FALSE, dwPid);
    
        // 读取文件的数据,此处读取的实在内存中的镜像数据
        bRet = ReadProcessMemory(hProcess, me32.modBaseAddr, pBase, me32.modBaseSize, NULL);
    
        // 判断PDOS头的有效性
        PIMAGE_DOS_HEADER pDos = (PIMAGE_DOS_HEADER)pBase;
        if (pDos->e_magic != IMAGE_DOS_SIGNATURE)
            return;
    
        // 计算出NT头数据
        PIMAGE_NT_HEADERS pNt = (PIMAGE_NT_HEADERS)(pDos->e_lfanew + (PBYTE)pBase);
        if (pNt->Signature != IMAGE_NT_SIGNATURE)
            return;
    
        // 设置文件的入口地址
        pNt->OptionalHeader.AddressOfEntryPoint = dwEntryPoint - pNt->OptionalHeader.ImageBase;
        printf("[*] 正在设置Dump文件相对RVA入口地址: 0x%08X \n", pNt->OptionalHeader.AddressOfEntryPoint);
    
        // 设置文件的对齐方式
        pNt->OptionalHeader.FileAlignment = 0x1000;
        printf("[*] 正在设置Dump文件的对齐值: %d \n", pNt->OptionalHeader.FileAlignment);
    
        // 找到节区首地址,并循环将当前节区数据赋值到新文件缓存中
        PIMAGE_SECTION_HEADER pSec = (PIMAGE_SECTION_HEADER)((PBYTE)&pNt->OptionalHeader + pNt->FileHeader.SizeOfOptionalHeader);
        for (int i = 0; i < pNt->FileHeader.NumberOfSections; i++)
        {
            pSec->PointerToRawData = pSec->VirtualAddress;
            printf("[+] 正在将虚拟地址: 0x%08X --> 设置到文件地址: 0x%08X \n", pSec->VirtualAddress, pSec->PointerToRawData);
            pSec->SizeOfRawData = pSec->Misc.VirtualSize;
            printf("[+] 正在将虚拟大小: %d --> 设置到文件大小: %d \n", pSec->Misc.VirtualSize, pSec->SizeOfRawData);
            pSec++;
        }
        CloseHandle(hFile);
    
        // 打开转储后的文件.
        hFile = CreateFile(DumpFileName, GENERIC_WRITE, FILE_SHARE_READ, 0, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0);
        if (hFile == INVALID_HANDLE_VALUE)
            exit(0);
        printf("[*] 转储 %s 文件到本地 \n", DumpFileName);
    
        DWORD dwWriteNum = 0;
    
        // 将读取的数据写入到文件
        bRet = WriteFile(hFile, pBase, me32.modBaseSize, &dwWriteNum, NULL);
        if (dwWriteNum != me32.modBaseSize || FALSE == bRet)
            printf("写入错误 !");
        // 关闭于释放资源
        CloseHandle(hFile);
        VirtualFree(pBase, me32.modBaseSize, MEM_RELEASE);
        CloseHandle(hProcess);
        CloseHandle(hSnap);
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104

    读者可自行运行这段程序,当程序运行后即可将指定的一个文件内存数据完整的转存到磁盘中,输出效果如下图所示;

  • 相关阅读:
    猿创征文|OLAP之apache pinot初体验
    QT QSpinBox 整数计数器控件 使用详解
    在工业机器视觉领域中应用钡铼技术有限公司的EtherCAT网关
    高等数学(第七版)同济大学 习题4-2(后半部分) 个人解答
    企业架构LB-服务器的负载均衡之LVS实现
    浏览器的 5 种 Observer
    宏定义天坑记录
    合取范式可满足性问题:CDCL(Conflict-Driven Clause Learning)算法详解
    Doris实践——同程数科实时数仓建设
    编译tolua——3、以pbc为例子,添加第三方库
  • 原文地址:https://blog.csdn.net/lyshark_csdn/article/details/133577641