1

I wonder what is the "correct" way for the debugger to read debuggee's memory. Since debugger provides the ability to view and alter debuggee's memory, the user can modify the code. The code section is frequently read by the debugger to produce disassembly, also we should be able to detect jump to the middle of instruction and disassemble again.

While ReadProcessMemory and WriteProcessMemory seem appropriate for reading and writing data, I think it will be easier to use file mapping for code, so debugger will be able to work with code as if it was in its own address space. Also, we need to insert imported/exported function names in our disassembly output, to achieve this we need to walk image import/export directory (file mapping is more convenient here).

There is one problem though. MapViewOfFileEx(..., ImageBase) will fail, since ImageBase is used by Windows (it is a start of unnamed SEC_IMAGE section created by loader). We have the option to use MapViewOfFile and copy image, however, if an executable is large (like 100Mb Nvidia Driver) it will rob debuggee out of some heap.

//--------------------------------------------------------------------------
// debugger
//--------------------------------------------------------------------------

struct MY_PROCESS_INFORMATION
{
    HANDLE hProcess;
    DWORD dwProcessId;
};

struct FILEMAP_INFORMATION
{
    HANDLE hFileMap;
    void* MapAddr;
};

struct INJECT_INFORMATION
{
    HMODULE hModule;
    BOOL Success;
};

struct MODULE_INFORMATION
{
    ptrdiff_t BaseDelta;
    void* AddressOfEntryPoint;
};

BOOL DbgLoad(HANDLE hProcess, char* DllName, INJECT_INFORMATION* InjectInfo)
{
    BOOL Loaded;
    size_t Size;
    void* pAlloc;
    HMODULE hModule;
    HANDLE hNewThread;
    LPTHREAD_START_ROUTINE Start;
    HANDLE hEvent;

    if (InjectInfo)
    {
        hEvent = CreateEventA(NULL, FALSE, FALSE, "dbg_event");

        if (!hEvent) return FALSE;
    }

    Loaded = FALSE;
    hModule = GetModuleHandleA("kernel32.dll");

    if (hModule)
    {
        Start = (LPTHREAD_START_ROUTINE)GetProcAddress(hModule, "LoadLibraryA");

        if (Start)
        {
            Size = strlen(DllName) + sizeof(char);
            pAlloc = VirtualAllocEx(hProcess, NULL, Size, MEM_COMMIT, PAGE_READWRITE);

            if (pAlloc)
            {
                if (WriteProcessMemory(hProcess, pAlloc, DllName, Size, NULL))
                {
                    hNewThread = CreateRemoteThread(hProcess, NULL, 0, Start, pAlloc, 0, NULL);

                    if (hNewThread)
                    {
                        if (InjectInfo)
                        {
                            if (!WaitForSingleObject(hEvent, INFINITE)) InjectInfo->Success = TRUE;
                            else InjectInfo->Success = FALSE;

                            if (!WaitForSingleObject(hNewThread, INFINITE))
                            {
#ifdef _WIN32
                                if (GetExitCodeThread(hNewThread, (DWORD*)&hModule))
                                {
                                    if (hModule)
                                    {
                                        InjectInfo->hModule = hModule;
                                        Loaded = TRUE;
                                    }
                                }
#else
                                //...
#endif
                            }
                        }
                        else
                        {
                            if (!WaitForSingleObject(hNewThread, INFINITE)) Loaded = TRUE;
                        }

                        CloseHandle(hNewThread);
                    }
                }

                if (InjectInfo) VirtualFreeEx(hProcess, pAlloc, 0, MEM_DECOMMIT);
            }
        }
    }

    if (InjectInfo) CloseHandle(hEvent);

    return Loaded;
}

BOOL DbgMap(MY_PROCESS_INFORMATION* ProcessInfo, FILEMAP_INFORMATION* FileMapInfo)
{
    HANDLE hFileMap;
    void* MapAddr;
    INJECT_INFORMATION InjectInfo;

    if (DbgLoad(ProcessInfo->hProcess, "D:\\Data\\New\\DllMap\\Debug\\DllMap.dll", &InjectInfo))
    {
        if (InjectInfo.Success)
        {
            hFileMap = OpenFileMappingA(FILE_MAP_ALL_ACCESS, FALSE, "file_map");

            if (hFileMap)
            {
                MapAddr = MapViewOfFile(hFileMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);

                if (MapAddr)
                {
                    FileMapInfo->hFileMap = hFileMap;
                    FileMapInfo->MapAddr = MapAddr;

                    return TRUE;
                }

                CloseHandle(hFileMap);
            }
        }

        // unload...
    }

    return FALSE;
}

BOOL DbgGetModuleInfo(FILEMAP_INFORMATION* FileMapInfo, MY_PROCESS_INFORMATION* ProcessInfo, MODULE_INFORMATION* ModuleInfo)
{
    BYTE* ImageBase;
    BYTE* __ImageBase;
    ptrdiff_t BaseDelta;
    BYTE* AddressOfEntryPoint;

    ImageBase = (BYTE*)FileMapInfo->MapAddr;
    assert(((PIMAGE_DOS_HEADER)ImageBase)->e_magic == IMAGE_DOS_SIGNATURE);

    AddressOfEntryPoint = ImageBase + GetImageEntryRVA(ImageBase);

    // BaseDelta:
    // 1) determine whether we have 32bit or 64bit image (using ImageBase)
    // 2) NtQueryInformationProcess to get PEB
    // 3) use PEB32 or PEB64 (depends on 1st step) to get __ImageBase
    // 4) BaseDelta = ImageBase - __ImageBase

    // parse imports and exports (using ImageBase)...

    return TRUE;
}

//--------------------------------------------------------------------------
// DllMap
//--------------------------------------------------------------------------

HANDLE g_hFileMap;
void* g_MapAddr;

DWORD GetImageSize(void* Image)
{
    PIMAGE_NT_HEADERS header;

    header = (PIMAGE_NT_HEADERS)((BYTE*)Image + ((PIMAGE_DOS_HEADER)Image)->e_lfanew);

    return header->OptionalHeader.SizeOfImage;
}

void* HModuleToAddr(HMODULE hModule)
{
    return (void*)((size_t)hModule & ~(HANDLE_FLAG_INHERIT | HANDLE_FLAG_PROTECT_FROM_CLOSE));
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
    )
{
    HANDLE hEvent;
    HMODULE hExe;
    void* ImageBase;
    DWORD ImageSize;

    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
        hEvent = OpenEventA(EVENT_MODIFY_STATE, FALSE, "dbg_event");

        if (hEvent)
        {
            hExe = GetModuleHandleA(NULL);

            if (hExe)
            {
                ImageBase = HModuleToAddr(hExe);
                ImageSize = GetImageSize(ImageBase);

                g_hFileMap = CreateFileMappingA(INVALID_HANDLE_VALUE, NULL, PAGE_READWRITE, 0, ImageSize, "file_map");

                if (g_hFileMap)
                {
                    g_MapAddr = MapViewOfFile(g_hFileMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);

                    if (g_MapAddr)
                    {
                        memcpy(g_MapAddr, ImageBase, ImageSize);
                        SetEvent(hEvent);
                    }
                }
            }

            CloseHandle(hEvent);
        }

        break;
    case DLL_THREAD_ATTACH:
        break;
    case DLL_THREAD_DETACH:
        break;
    case DLL_PROCESS_DETACH:
        if (!lpReserved)        // FreeLibrary
        {
            if (g_hFileMap)
            {
                if (g_MapAddr)
                {
                    UnmapViewOfFile(g_MapAddr);
                    g_MapAddr = 0;
                }

                CloseHandle(g_hFileMap);
                g_hFileMap = 0;
            }
        }
        break;
    }

    return TRUE;
}

Then we do this:

if (DbgMap(&ProcessInfo, &FileMapInfo))
{
    if (DbgAttach(&ProcessInfo))
    {
        if (DbgGetModuleInfo(&FileMapInfo, &ProcessInfo, &ModuleInfo))
        {
            // Good
        }
    }
}

Note that when we create debuggee process we create it in suspended state without debugging flags (otherwise we would be unable to run LoadLibrary thread to perform file mapping). After we have created or opened debuggee process and performed mapping we attach with the debugger (in case we have created debuggee process we also need to call ResumeThread on its main thread).

I never wrote debugger before. So my question is whether we should use file mapping or ReadProcessMemory/WriteProcessMemory (for data, code, imports, exports, etc)? Is my approach reasonable or it is nonsense? Thanks.

Nithinlal
  • 4,845
  • 1
  • 29
  • 40
igntec
  • 1,006
  • 10
  • 24
  • The debugger is built into the system, exported from *kernel32.dll*. Why don't you use that? [The Debugging Application Programming Interface](https://msdn.microsoft.com/en-us/library/ms809754.aspx) provides a (somewhat outdated) introduction. – IInspectable Dec 24 '16 at 11:31
  • 1
    File mapping can't work because the debugee doesn't habitually map its address space. – David Heffernan Dec 24 '16 at 11:34
  • The article is helpful, but it does not have information relevant to this question. I want to prepare so I can run disassembler on debuggee's code and ask whether using ReadProcessMemory will be reasonable to fetch code and disassemble it, or we should use some file mapping trick (as OS loader likely does). Since image resides in SEC_IMAGE section created by loader, we might be able to map view of this section without debuggee support. – igntec Dec 24 '16 at 14:28
  • @IInspectable - system really have built-in debugging interface, but not in `kernel32.dll` - in `dbgeng.dll` - https://msdn.microsoft.com/en-us/library/windows/hardware/ff539131(v=vs.85).aspx – RbMm Dec 24 '16 at 14:48
  • @igntec - use PE mapping not a way. how about global vars in PE which is changed in debuggee process ? or code modification ? only way - direct read virtual memory. but it (reads) can be custom cashed in your app (while debuggee in suspended state). after it begin run - cache need reset – RbMm Dec 24 '16 at 14:53
  • Is there a problem with ReadProcessMemory? – David Heffernan Dec 24 '16 at 14:54
  • @RbMm: You have linked to the debugger **extension** API. The debugger is implemented in *kernel32.dll* (e.g. [WaitForDebugEvent](https://msdn.microsoft.com/en-us/library/windows/desktop/ms681423.aspx)). – IInspectable Dec 24 '16 at 14:59
  • @IInspectable - what you mean under "debugger" ? `WaitForDebugEvent` this is only core function for wait for debug events - all debuggers call it. but this is not engine. and my link not for **extension API** but for independed **dbg engine** API – RbMm Dec 24 '16 at 15:06
  • @David Heffernan: Since we will read memory frequently I am not sure about ReadProcessMemory performance. – igntec Dec 24 '16 at 15:09
  • @IInspectable - also if use this engine we call IDebugControl::WaitForEvent (https://msdn.microsoft.com/en-us/library/windows/hardware/ff561229(v=vs.85).aspx) instead `WaitForDebugEvent` – RbMm Dec 24 '16 at 15:09
  • @igntec - ReadProcessMemory have enough performance, can say this exactly. – RbMm Dec 24 '16 at 15:10
  • @RbMm: In my code changes to global variables and code are indeed undetectable (since we have made image copy). That's why there might be some way to map existing image section. – igntec Dec 24 '16 at 15:11
  • @RbMm: If you say ReadProcessMemory is ok I will not wander in some "native api solution". There is more stuff to do. – igntec Dec 24 '16 at 15:17
  • @igntec - when image section page modified in process (writecopy pages) - system break binding this page with section view. this by design not reflect to any other sections view. simply example - ntdll.dll - same section mapped in all processes at the same address - but global vars in the different processes is different – RbMm Dec 24 '16 at 15:23
  • @igntec - "we need to walk image import/export directory " - yes. when debugger got `LOAD_DLL_DEBUG_EVENT` - we need load dll (as data file) in self process for create list of export/symbols – RbMm Dec 24 '16 at 15:25
  • 1
    ReadProcessMemory has the distinct advantage of actually solving your problem. Memory mapping does not. – David Heffernan Dec 24 '16 at 15:28

0 Answers0