【问题标题】:Writing a debugger. How to debug a DLL accessed through LoadLibray?编写调试器。如何调试通过 LoadLibray 访问的 DLL?
【发布时间】:2017-07-27 00:45:57
【问题描述】:

我编写了自己的调试器,主要使用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。
例如:

案例 1)


调试器没问题:

主机(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

因此,当打开我在“随机”导出函数上设置断点的 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 并在“随机”函数处设置断点时,调试器无法工作,因为导出的函数不是主机 IAT 的一部分。

如何以调试器可以“看到”被间接调用的 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++ 重现它,因为我无法阅读)

非常感谢,提前。

【问题讨论】:

  • 我不明白; IAT 与设置断点有什么关系?通常,调试器通过将断点处的指令替换为int 3 来设置断点,你在做一些不同的事情吗?
  • 这就是为什么 WaitForDebugEvent() 在加载 DLL 时返回 LOAD_DLL_DEBUG_EVENT 的原因。让你武装断点。
  • 我知道。它确实包含所有 DEBUG_EVENT 结构(除了尚未实现的 RIP_INFO),但它没有找到间接加载的库的正确地址。
  • 关于IAT,似乎当模块已经在同一进程(exe)的内存上加载时触发了调试器。由于可执行文件已在 IAT 上包含要使用的必要 dll,因此一旦使用可执行文件,它们就会加载到进程中。问题取决于在运行时加载模块时。 Adrian McCarthy 提出了一个很好的观点,但调试器仍然只在模块已经加载并附加到进程时才激活断点。我正在分析 MS detours 库,看看是否可以在间接加载 dll 时启用这些情况。

标签: debugging winapi assembly breakpoints createprocess


【解决方案1】:

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

【讨论】:

  • 好点。我检查了“DEBUG_EVENT+DEBUG_EVENT.dwDebugEventCodeDis = &LOAD_DLL_DEBUG_EVENT”,其中包含对 Debugger_OnLoadDll 函数的调用。但是,在它填充了所有必要的数据并继续调试(ContinueDebugEvent)之后。正在卸载 dll,到达 DEBUG_EVENT+DEBUG_EVENT.dwDebugEventCodeDis = &UNLOAD_DLL_DEBUG_EVENT(包含对卸载函数的调用)。当exe直接调用dll时,UNLOAD_DLL_DEBUG_EVENT没有被激活允许断点,但是当使用loadlibrary时,这个标志被激活了。
  • 嗯..忘记之前的评论。它正在卸载另一个 dll 而不是 rnd.dll。模块仍处于活动状态,但断点未到达;
【解决方案2】:

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

原始源代码一团糟,有点慢。所以,我不得不重写整个 DetourCreateProcessWithDll 来尝试让它在我的调试器上工作。

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

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

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

【讨论】:

  • Detour 库创建的钩子不是断点。
  • 断点在我的调试器上解决,它在源代码上激活了它们(它的工作方式类似于源调试器)。问题是,在调试 dll 时,仅当主机(需要加载的 dll 的目标 exe - 如在 ollydbg 或 idapro 中)仅在主主机调用 dll 函数时才有效,才会激活断点。换句话说,如果 IAT 包含正在调试的 dll。在主机从 loadlibrary api(或其他间接调用 dll 的方式)访问 dll 的情况下,调试器失败,因为主机没有(直接)加载 api。
  • DetourCreateProcessWithDll 在这种情况下似乎是 CreateProcess Api 的一个很好的替代品。顺便说一句..即使 OllyDbg 也无法调试 DLL。它需要一个名为 LoadDll 的应用程序才能使其工作,但是,您需要提供正在调试的导出函数的参数。使用 Detours 库,您不需要解决这些参数...所需要的只是将 dll 注入主机上,使其看起来像 IAT 的一部分。通过这种方式,您可以直接调试 DLL,而无需知道它的参数或从主机访问它的方式。
  • 调试器允许在任何地方设置断点,而不仅仅是在 DLL 导出的函数的入口处。
  • 可以,但仅限于调试可执行文件时。例如,Ollydbg 无法调试 dll。 (我的意思是没有 loaddll 应用程序)。另一个可以直接调试 dll 的调试器是 idapro,但我无法理解他如何允许在从主机间接调用函数的 dll 上设置断点。对于我所看到的它也使用 Createprocess,但我无法弄清楚 Idapro 如何克服调试通过主机的 loadLibrary 激活的 dll 的问题。这就是为什么我在 Detour Library 上进行了测试,到目前为止,它正在工作,我希望 :)
【解决方案3】:

如何在使用 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] = '\0';
    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 中设置断点。值得注意的是,真正的调试器需要能够移除断点并恢复原始指令,以便在到达断点后程序可以继续运行。相反,这个程序只是终止被调试的程序并退出。

【讨论】:

  • 非常感谢,罗斯。确实问题是我创建的断点例程不在 LOAD_DLL_DEBUG_EVENT 内激活/停用断点列表(和 WriteProcessMem 调用)的主要例程在 DEBUG_EVENT 结构的 jmp 链之外(特别是来自 LOAD_DLL_DEBUG_EVENT 的那些)。我按照你的例子做了,它似乎工作正常。我将对代码进行细微调整,并在调试更大的文件时对其进行测试以查看其行为,但到目前为止,它按预期工作。非常非常感谢:)
  • @guga 您需要在对WaitForDebugEventContinueDebugEvent 的调用之间安装断点,因为被调试进程中的所有线程都在该点停止。如果您在调用ContinueDebugEvent 之后这样做,那么您将遇到竞争条件,被调试进程中的线程将恢复执行,因此您尝试设置断点的指令可能已经执行.
  • 是的,我就是这样做的。现在调试器能够调试像 Sony vegas 或 VirtualDub 中的插件文件,并完全加载从主机使用的 dll。另外,我做了一个例程来检查正在加载的模块是否正常,我的意思是,如果正在加载的 dll 中的一个不包含导出或没有代码库,或者因为它正在被使用而无法加载系统或进程重新映射图像库等,一个日志文件,警告这种情况。您在 LOAD_DLL_DEBUG_EVENT 中使用断点例程的提示/代码非常有用:)
  • 顺便说一句,当您在主代码之外遇到异常时,调试器现在似乎可以按预期工作,而在 Idapro 上没有问题,或者只有当您使用函数的参数提供它时才调试 dll 的 Olly .我做了一个例程,尽可能绕过应用程序的异常,并且仅在被调试的 dll/exe 是挂起的原因而不是由最终加载的外部 dll 引起的任何人时才显示它们。更有用的是必须手动修复其他模块上的错误,就像 Idapro 上发生的那样。现在主机尝试完全加载,无论外部异常。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2022-01-13
  • 1970-01-01
  • 2016-03-16
  • 1970-01-01
  • 1970-01-01
  • 2011-01-30
相关资源
最近更新 更多