编写调试器。如何调试通过 LoadLibray 访问的 DLL?

Writing a debugger. How to debug a DLL accessed through LoadLibray?

我主要使用 CreateProcess 编写了自己的调试器并相应地访问 DEBUG_EVENT 结构以加载 DLL、异常、线程等设置断点(来自源代码)

到目前为止,调试器没问题。当我在 .EXE 文件上设置断点时,以及当我调试调用主机作为进程目标的 DLL 时(类似于 IDAPro 所做的)一切正常。

例如:DLL 包含一个名为 "random" 的导出,其伪代码如下:

DLL 名称:RND.dll

Proc random::
   mov eax 1 ; (return 1) <---- I set a breakpoint here on the dll.
EndP

问题出在从 LoadLibrary.
调用 DLL 例如:

案例一)


调试器正常:

主机(EXE)有这个伪代码。
EXE 名称:test.exe

Main:

call 'RND.Random' ; On a regular call to IAT the debug stops nicelly, since RND dll is part of the IAT table on the executable.
call 'KERNEL32.FreeLibrary' D$hLib
call 'Kernel32.ExitProcess' 0

因此,当加载 RND.dll 并激活调试器时,会打开一个 OpenDialog 告诉用户选择主机 (EXE) 来加载它。在这种情况下 test.exe.

因此,当打开我在 "Random" 导出函数上设置断点的 DLL 时,调试器会正确地停止在 DLL 上执行。

但是.....如果我的主机包含 LoadLibrary,调试器上的断点不会被激活。
像这样:

案例2)
不工作。

EXE(主机)现在有这个伪代码。
例如:test2.exe

Main:

call 'KERNEL32.LoadLibraryA' {'RND.DLL',0} | mov D$hLib eax
call 'kernel32.GetProcAddress' eax, { B$ "Random", 0}
call eax
call 'KERNEL32.FreeLibrary' D$hLib
call 'Kernel32.ExitProcess' 0

当我打开 DLL 并将断点设置在 "random" 函数时,调试器不工作,因为导出的函数不是主机 IAT 的一部分。

如何以调试器可以"see"间接调用的 DLL 函数上的断点的方式将 DLL 附加到主机?

我尝试将 DLL 注入进程,但没有成功。
创建进程的主要功能有这些设置:

call 'KERNEL32.CreateProcessA' DebuggeeExe,
                               CommandLineString, &NULL, &NULL, &FALSE,
                               &CREATE_DEFAULT_ERROR_MODE+&NORMAL_PRIORITY_CLASS+&DEBUG_PROCESS+&DEBUG_ONLY_THIS_PROCESS,
                               &NULL, DebuggeePath, STARTUPINFO, PROCESS_INFORMATION

如何解决?
在 IDAPro 上,它具有相同的功能。我的意思是我可以打开一个DLL,在一个地址上设置断点并调试它。
但在这种情况下,会打开一个对话框,告诉我选择主机 (EXE)。

IDAPro 在这两种情况下都能正常工作。

  1. 当宿主(EXE)直接调用DLL时,意味着它是IAT的一部分
  2. 当主机间接调用通过LoadLibrary访问的DLL时。

我的调试器只能执行上面的第一种情况。
如何解决?

注意:我习惯用汇编编码,这部分代码来自我正在开发的名为 RosAsm 的汇编器。但是我无法让调试器在这些情况下工作。
如果有人可以使用 WinAPI 在 C 中提供此类功能的示例,我们将不胜感激。 (请不要使用 C++ 或 .Net,因为我可以阅读 C,但我无法使用 .Net 或 C++ 重现它,因为我无法阅读)

非常感谢,提前。

如果用户试图为未加载的 DLL 设置断点,请记下它。稍后,当被调试者加载 DLL 时,您的调试器循环会收到模块加载通知。当时从它的注释中可以看出它有一个断点需要在那个模块中设置,并且它在恢复被调试之前的工作。

我只是使用 MS detours 库中的 DetourCreateProcessWithDll 函数成功地使其工作。 api 似乎工作正常,除了它在内部保存的内存分配指针上有一些小问题。 某些结构 (DETOUR_CLR_HEADER) 在原始 Microsoft 源代码的函数 DetourUpdateProcessWithDll 中被错误地指向。

原版源码乱七八糟,速度有点慢。因此,我不得不重写整个 DetourCreateProcessWithDll 以尝试使其在我的调试器上运行。

到目前为止,除了源代码和调试数据的同步存在一些小问题外,一切正常,但这似乎更容易修复。

如果有人在你自己的调试器上遇到同样的问题,无法直接在主机通过 Loadlibrary(或其他方法)调用函数的 DLL 上设置断点,我建议尝试使用 MS Detours 库.

我还不确定我是否可以通过其他方式解决问题,但是,Detour Api 似乎按预期工作。

如何在使用 LoadLibrary

加载的 DLL 中设置 真实 断点

以下代码显示如何在使用 LoadLibrary 加载的 DLL 中使用 x86 断点指令 INT 3 设置真正的断点。它处理 LOAD_DLL_DEBUG_EVENT 并将断点指令写入加载的 DLL。该命令有两个参数,一个 DLL 的名称和该 DLL 中要在其开头设置断点的导出函数的名称。 DLL 的名称必须包括扩展名,但不能包括目录或驱动器号。如果程序运行,它将打印 BREAKPOINT REACHED.

#include <stdio.h>
#include <windows.h>

int
winperror(char const *prefix) {
    DWORD errid = GetLastError();
    PVOID *buf;
    fprintf(stderr, "%s: ", prefix);
    if (FormatMessage(FORMAT_MESSAGE_FROM_SYSTEM 
              | FORMAT_MESSAGE_IGNORE_INSERTS
              | FORMAT_MESSAGE_ALLOCATE_BUFFER,
              NULL, errid, 0, (LPTSTR) &buf, 0, NULL) != 0) {
        fprintf(stderr, "%s\n", (TCHAR *) buf);
    } else {
        fprintf(stderr, "unknown windows error %08lx\n", errid);
    }
    return -1;
}

static int 
install_breakpoint(HANDLE process, DWORD_PTR addr) {
    static char const int3 = 0xcc;
    if (WriteProcessMemory(process, (LPVOID) addr, &int3, 1, NULL) == 0) {
        return winperror("WriteProcessMemory");
    }
    printf("breakpoint set at address %p\n", (void *) addr);
    return 0;
}

static int
install_dll_breakpoint(HANDLE process, HMODULE module,
               char const *dll, char const *function) {
    HMODULE lmodule = LoadLibrary(dll);
    if (lmodule == NULL) {
        return winperror("LoadLibrary");
    }
    void *lproc = GetProcAddress(lmodule, function);
    if (lproc == NULL) {
        return winperror("GetProcAddress");
    }
    FreeLibrary(lmodule);
    /* The debugged process might load the DLL at a different
       address than the DLL in this process, but the offset of the
       function from base of the DLL remains the same in both
       processes.
    */
    DWORD_PTR offset = (DWORD_PTR) lproc - (DWORD_PTR) lmodule;
    DWORD_PTR proc = (DWORD_PTR) module + offset;

    return install_breakpoint(process, proc);
}

static int
get_file_name_from_handle(HANDLE file, char *buf, size_t len) {
    DWORD tmp[1 + 1024 / 2];
    if (GetFileInformationByHandleEx(file, FileNameInfo,
                     tmp, sizeof tmp) == 0) {
        return winperror("GetFileInformationByHandleEx");
    }
    FILE_NAME_INFO *info = (FILE_NAME_INFO *) tmp;
    int n = WideCharToMultiByte(CP_ACP, WC_NO_BEST_FIT_CHARS,
                    info->FileName, info->FileNameLength / 2,
                    buf, len - 1, NULL, NULL);
    if (n == 0) {
        return winperror("WideCharToMultiByte");
    }
    buf[n] = '[=10=]';
    return 0;
}

int 
main(int argc, char **argv) {
    if (argc != 3) {
        fprintf(stderr, "usage: %s dll function\n", argv[0]);
        return 1;
    }

    static STARTUPINFO startup;
    PROCESS_INFORMATION process_info;

    startup.cb = sizeof startup;
    startup.lpReserved = NULL;
    startup.lpDesktop = NULL;
    startup.lpTitle = NULL;
    startup.dwFlags = 0;
    startup.cbReserved2 = 0;
    startup.lpReserved2 = NULL;

    static char const rundll32[] = "rundll32";
    char buf[1024];
    if (sizeof rundll32 + 1 + strlen(argv[1]) + 1 + strlen(argv[2])
        > sizeof buf) {
        fprintf(stderr, "DLL and/or function name too long\n");
        return 1;
    }
    strcpy(buf, rundll32);
    strcat(buf, " ");
    strcat(buf, argv[1]);
    strcat(buf, ",");
    strcat(buf, argv[2]);

    if (CreateProcess(NULL, buf, NULL, NULL, TRUE,
              DEBUG_PROCESS | DEBUG_ONLY_THIS_PROCESS, 0, NULL, 
              &startup, &process_info) == 0) {
        winperror("CreateProcess");
        return 1;
    }

    HANDLE process = process_info.hProcess;
    int first_breakpoint = 1;
    while(1) {
        DWORD continue_flag = DBG_EXCEPTION_NOT_HANDLED;
        DEBUG_EVENT event;

        if (WaitForDebugEvent(&event, INFINITE) == 0) {
            winperror("WaitForDebugEvent");
            return 1;
        }

        continue_flag = DBG_EXCEPTION_NOT_HANDLED;
        switch(event.dwDebugEventCode) {
        case EXCEPTION_DEBUG_EVENT:
            EXCEPTION_DEBUG_INFO *info = &event.u.Exception;
            EXCEPTION_RECORD *exp = &info->ExceptionRecord;
            if (exp->ExceptionCode == EXCEPTION_BREAKPOINT) {
                if (first_breakpoint) {
                    printf("PROCESS STARTED\n");
                    first_breakpoint = 0;
                    continue_flag = DBG_CONTINUE;
                } else {
                    printf("BREAKPOINT REACHED %p\n",
                           exp->ExceptionAddress);
                    TerminateProcess(process, 0);
                    return 0;
                }
            } 
            break;

        case CREATE_PROCESS_DEBUG_EVENT:
            CloseHandle(event.u.CreateProcessInfo.hFile);
            break;

        case EXIT_PROCESS_DEBUG_EVENT:
            printf("process exited without encoutering breakpoint"
                   " exit code = %d\n", 
                   (int) event.u.ExitProcess.dwExitCode);
            return 0;

        case LOAD_DLL_DEBUG_EVENT:
            HMODULE module = (HMODULE) event.u.LoadDll.lpBaseOfDll;
            HANDLE file = event.u.LoadDll.hFile;
            if (get_file_name_from_handle(file,
                              buf, sizeof buf) == -1) {
                return 1;
            }
            printf("LOAD_DLL   %p %s\n", module, buf);
            char *s = strrchr(buf, '\');
            if (s == NULL) {
                s = buf;
            } else {
                s++;
            }
            if (stricmp(s, argv[1]) == 0
                && install_dll_breakpoint(process, module, argv[1],
                              argv[2]) == -1) {
                return 1;
            }
            CloseHandle(file);
            break;

        case UNLOAD_DLL_DEBUG_EVENT:
            printf("UNLOAD_DLL %p\n",
                   event.u.UnloadDll.lpBaseOfDll);
            break;
        }
        if (ContinueDebugEvent(event.dwProcessId, event.dwThreadId,
                       continue_flag) == 0) {
            winperror("ContinueDebugEvent");
            return 1;
        }
    }
}

要编译程序,您需要做的就是cl test.c。如果您使用 test d3d9.dll CreateDirect3D9 调用它,您将看到如下输出:

LOAD_DLL   76EE0000 \Windows\SysWOW64\ntdll.dll
UNLOAD_DLL 76AE0000
UNLOAD_DLL 766B0000
UNLOAD_DLL 76AE0000
UNLOAD_DLL 76C00000
LOAD_DLL   766B0000 \Windows\SysWOW64\kernel32.dll
LOAD_DLL   76A50000 \Windows\SysWOW64\KernelBase.dll
LOAD_DLL   75DD0000 \Windows\SysWOW64\user32.dll
LOAD_DLL   76890000 \Windows\SysWOW64\gdi32.dll
LOAD_DLL   76EB0000 \Windows\SysWOW64\lpk.dll
LOAD_DLL   767F0000 \Windows\SysWOW64\usp10.dll
LOAD_DLL   75FD0000 \Windows\SysWOW64\msvcrt.dll
LOAD_DLL   75D20000 \Windows\SysWOW64\advapi32.dll
LOAD_DLL   76420000 \Windows\SysWOW64\sechost.dll
LOAD_DLL   758B0000 \Windows\SysWOW64\rpcrt4.dll
LOAD_DLL   749D0000 \Windows\SysWOW64\sspicli.dll
LOAD_DLL   749C0000 \Windows\SysWOW64\cryptbase.dll
LOAD_DLL   767C0000 \Windows\SysWOW64\imagehlp.dll
PROCESS STARTED
LOAD_DLL   74960000 \Windows\SysWOW64\apphelp.dll
LOAD_DLL   6E070000 \Windows\AppPatch\AcLayers.dll
LOAD_DLL   74C60000 \Windows\SysWOW64\shell32.dll
LOAD_DLL   765E0000 \Windows\SysWOW64\shlwapi.dll
LOAD_DLL   74B00000 \Windows\SysWOW64\ole32.dll
LOAD_DLL   76380000 \Windows\SysWOW64\oleaut32.dll
LOAD_DLL   748B0000 \Windows\SysWOW64\userenv.dll
LOAD_DLL   748A0000 \Windows\SysWOW64\profapi.dll
LOAD_DLL   73540000 \Windows\SysWOW64\winspool.drv
LOAD_DLL   73510000 \Windows\SysWOW64\mpr.dll
LOAD_DLL   58B50000 \Windows\AppPatch\acwow64.dll
LOAD_DLL   72EC0000 \Windows\SysWOW64\version.dll
LOAD_DLL   75F60000 \Windows\SysWOW64\imm32.dll
LOAD_DLL   74A30000 \Windows\SysWOW64\msctf.dll
LOAD_DLL   6EF10000 \Windows\SysWOW64\d3d9.dll
breakpoint set at address 6EF70A62
UNLOAD_DLL 6EF10000
LOAD_DLL   6EF10000 \Windows\SysWOW64\d3d9.dll
breakpoint set at address 6EF70A62
UNLOAD_DLL 6EF10000
LOAD_DLL   6EF10000 \Windows\SysWOW64\d3d9.dll
breakpoint set at address 6EF70A62
UNLOAD_DLL 6EF10000
LOAD_DLL   6EF10000 \Windows\SysWOW64\d3d9.dll
breakpoint set at address 6EF70A62
LOAD_DLL   6FF20000 \Windows\SysWOW64\d3d8thk.dll
LOAD_DLL   736F0000 \Windows\SysWOW64\dwmapi.dll
BREAKPOINT REACHED 6EF70A62

该程序仅实现了最低限度,以演示如何在使用 LoadLibrary 加载的 DLL 中设置断点。值得注意的是,真正的调试器需要能够移除断点并恢复原始指令,以便程序可以在到达断点后继续运行。相反,该程序只是终止被调试的程序并退出。