加密与解密 调试篇(二) Windows调试器实现(一)

加密与解密 调试篇(二) Windows调试器实现(一)

概述

软件调试对于程序员与逆向工程师来说都是工作中不可缺少的好伙伴。调试器能让我们动态的跟踪程序的运行状态,获取某一瞬间整个程序进程空间中的各类信息及程序的操作运行指令。但是调试器本身是如何实现的,我之前没有深入跟踪分析过。调试篇(二)的主要内容就是从0开始,逐渐完成一个Windows下调试器的Demo程序的开发过程。目前,参考的内容有软件调试软件加密技术内幕Python灰帽子Zplutor的一个调试器的实现Writing a basic Windows Debugger – Part 1WinDebugger等。

在开始套路之前,首先要明确调试的两个步骤:

  1. 调试器创建或者附加到一个进程中
  2. 设置调试器的循环来处理调试事件

几个事实:

  1. 调试器是一个程序/进程,我们用这个进程来调试另一个进程
  2. 被调试程序是一个将要被调试器调试的进程
  3. 仅能有一个调试器被挂载到被调试进程中,然而一个调试器可以调试多个进程(用不同线程)
  4. 只有创建被调试进程的线程才能调试被调试进程,所以创建和后续的操作都需要在同一个线程中
  5. 当调试器线程结束,被调试进程也会结束,但是调试器进程可能继续运行
  6. 当调试器的调试线程忙于处理一个事件时,被调试进程的所有线程都会处于挂起状态

新建一个进程或者附加到一个进程

在我们使用调试器进行调试时,常用的方法有两个,一个是针对可执行程序,利用调试器打开一个新的运行实例,然后针对新建进程进行调试;另一个是针对当前已经运行的进程,采用附加(attach)方法进行调试,现在让我们从这两个地方入手,看一下调试器是如何实现这两个功能的。

新建一个新的进程

在Windows中,新建进程的API是CreateProcess。根据MSDN文档,该API的原型为:

BOOL CreateProcessA(
  LPCSTR                lpApplicationName,
  LPSTR                 lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL                  bInheritHandles,
  DWORD                 dwCreationFlags,
  LPVOID                lpEnvironment,
  LPCSTR                lpCurrentDirectory,
  LPSTARTUPINFOA        lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

其中,dwCreationFlags可以被用来控制新创建进程的优先级以及其属性。根据Windows核心编程,该参数影响新进程创建方式的标志(flag)。多个标志可以使用按位或起来,以便同时指定多个标志组合。关于调试方面,存在两个参数

  • DEBUG_PROCESS,该标识向系统表明父进程希望调试子进程以及子进程将来生成的所有进程。这就表示所有debugee中发生的特定事件都要向父进程(debugger)汇报
  • DEBUG_ONLY_THIS_PROCESS,类似于DEBUG_PROCESS,但是它不会跟踪子进程的子进程信息
VOID CreateProcessDemo() {
        STARTUPINFO si = { 0 };
        PROCESS_INFORMATION pi = { 0 };
        BOOL bRet = false;

        si.cb = sizeof(si);
        bRet = CreateProcess(
                TEXT("C:\\Windows\\System32\\notepad.exe"),
                NULL,
                NULL,
                NULL,
                FALSE,
                DEBUG_PROCESS | CREATE_NEW_CONSOLE, 
                NULL,
                NULL,
                &si,
                &pi
        );
        if (!bRet) {
                _tprintf(TEXT("Error create process: %d.\n"), GetLastError());
                return;
        }

        CloseHandle(pi.hProcess);
        CloseHandle(pi.hThread);
        _tprintf(TEXT("The pid of new process is %d.\n"), pi.dwProcessId);
        return;
}

以上代码显示的是一个创建新的调试进程的代码片段,其中关键是使用CreateProcessAPI以及使用DEBUG_PROCESS或者DEBUG_ONLY_THIS_PROCESS标识。但是在这段代码中发现在创建进程时除了调试标识以外,它或操作了一个CREATE_NEW_CONSOLE标识,在Zplutor的一个调试器的实现中,作者标识推荐使用该标识。表示如果被调试进程是一个控制台程序,如果不使用CREATE_NEW_CONSOLE的话,调试器的IO与被调试的IO会同时显示到一个终端中,显得整个输入输出很混乱。

附加到一个进程中

在Windows API中,提供了一个对应的API函数,DebugActiveProcess,其函数原型为:

BOOL DebugActiveProcess(
  DWORD dwProcessId
);

根据MSDN,该API的功能是“Enables a debugger to attach to an active process and debug it.”。 然后这个功能类似于使用DEBUG_ONLY_THIS_PROCESS来创建目标进程。
在下面的详细描述中提到,如果想要停止调试进程,需要调用DebugActiveProcessStop,退出调试器进程也会停止被调试进程,除非使用了DebugSetProcessKillOnExit
在权限方面,调试器进程需要拥有针对目标进程的合适权限,这个权限必须是PROCESS_ALL_ACCESS。然后如果调试进程拥有SE_DEBUG_NAME权限,那么它就可以调试任何进程。

VOID DebugActiveProcessDemo() {
    BOOL bRet = FALSE;
    DWORD pid = -1;

    _tcscanf_s(TEXT("%d.\n"), &pid);

    bRet = DebugActiveProcess(pid);
    if (!bRet) {
        _tprintf(TEXT("Attach to %d error: %d"), pid, GetLastError());
        return;
    }
    _tprintf(TEXT("Attach to %d ok!"), pid);
    return;
}

调试器循环

作为一个调试器,监视目标进程的执行,对目标进程发生的每一件调试事件进行处理是它的主要工作。所以,对于一个调试器而言,调试器循环是调试器的核心部位。被调试进程在完成了某些操作或者发生某些异常时,会发送通知给调试器,然后自身挂起,直到调试器命令它继续运行。这点类似于Windows窗口的消息机制。在调试器这个消息处理循环中,WaitForDebugEvent函数发挥了核心功能,因为它会持续获取目标进程的相关环境信息,获取需要处理的调试事件。

BOOL WaitForDebugEvent(
  LPDEBUG_EVENT lpDebugEvent,
  DWORD         dwMilliseconds
);

MSDN中描述,WaitForDebugEvent的作用是等待被调试进程产生调试事件。MSDN中提到一个事件,说是以前的操作系统不支持通过OutputDebugStringW来输出Unicode编码的信息,为了使其支持,需要调用WaitForDebugEventEx来实现。但是我在看其他的调试器源码时,包括python的WinAppDebug以及x64dbg使用的TitanEngine,其中都使用的WaitForDebugEvent。后续的搜索发现WaitForDebugEventEx是Win10新引进的API,然后在细节角度,两者其实都是调用的同一个内部函数,不过在参数上做了些调整。

API区别

在上图中,发现有两个调用,都跳转到了同样的kernelbase中的一个地址,区别在于一个将r8d清零,另一个将r8b置位1。

调试器事件

另外,在API中,调试信息的结构体为DEBUG_EVENT,其结构为

typedef struct _DEBUG_EVENT {
  DWORD dwDebugEventCode;
  DWORD dwProcessId;
  DWORD dwThreadId;
  union {
    EXCEPTION_DEBUG_INFO      Exception;
    CREATE_THREAD_DEBUG_INFO  CreateThread;
    CREATE_PROCESS_DEBUG_INFO CreateProcessInfo;
    EXIT_THREAD_DEBUG_INFO    ExitThread;
    EXIT_PROCESS_DEBUG_INFO   ExitProcess;
    LOAD_DLL_DEBUG_INFO       LoadDll;
    UNLOAD_DLL_DEBUG_INFO     UnloadDll;
    OUTPUT_DEBUG_STRING_INFO  DebugString;
    RIP_INFO                  RipInfo;
  } u;
} DEBUG_EVENT, *LPDEBUG_EVENT;

该结构保存记录了一个发生的调试事件,其中dwDebugEventCode用来标识发生的调试事件类型,dwProcessId记录了发生调试事件的PID,dwThreadId则记录了发生调试事件的线程ID信息,最后的联合体u则是根据dwDebugEventCode的不同选择不同的类型进行保存。
调试事件类型总共有9种:

调试事件含义
CREATE_PROCESS_DEBUG_EVENT进程被创建,当调试的进程刚被创建(还未运行)或调试器开始调试已经激活的进程时,就会产生这个事件
CREATE_THREAD_DEBUG_EVENT在调试进程中创建一个新的线程或者调试器开始调试已经激活的进程时,生成该调试事件,值得注意的是主线程创建时不会生成该事件
EXCEPTION_DEBUG_EVENT在调试进程中产生了异常,就会产生该事件
EXIT_PROCESS_DEBUG_EVENT每当退出调试进程中的最后一个线程时,产生该事件
EXIT_THREAD_DEBUG_EVENT调试中的线程退出时事件发生,主线程退出时不会发生
LOAD_DLL_DEBUG_EVENT每当调试进程装载DLL文件时,生成该事件。当PE装载器第一次解析出与DLL文件有关的链接时,将收到该事件。调试进程使用LoadLibrary时也会发生。每当DLL文件装载到地址空间时,都要调用该事件
OUTPUT_DEBUG_STRING_EVENT每当调试进程调用OutputDebugString向程序发送消息字符时该事件发生
UNLOAD_DLL_DEBUG_EVENT每当调试进程使用FreeLibrary函数卸载DLL文件时,就会生成该调试事件。仅当最后一次从过程的地址空间卸载DLL文件时,才出现该调试事件
RIP_EVENT报出一个RIP-debugging事件(系统调试错误),软件加密技术内幕中表示只有win98检查过的构建才会生成该调试事件

调试循环

上面部分讲到了如何创建或者附加到一个调试进程,调试进程会发出哪些类型的调试事件,这部分将说明调试器是如何监视以及处理事件的。这一部分会展示一个通用的代码结构,该代码结构由循环、WaitForDebugEventswitch语句以及ContinueDebugEvent构成。
其中,循环的目的是持续判断调试进程是否产生调试事件,这个是由WaitForDebugEvent来完成的,返回为TRUE时为在等待时间内发生了事件,否则为不存在或者出现了错误。
当获取到调试事件后,根据调试事件类型通过switch语句来分别跳转到对应的处理例程上。
因为在调试器接受到调试事件并进行处理的过程中调试进程处于挂起装填,在处理完调试事件后,使用ContinueDebugEvent来恢复进程的运行,从挂起状态转换到运行状态中。

VOID MainLoop() {
    DEBUG_EVENT debug_event;
    BOOL bRunning = FALSE;
    while (bRunning) {
        if (!WaitForDebugEvent(&debug_event, INFINITE)) {
            _tprintf(TEXT("Error : %d.\n"), GetLastError());
            break;
        }
        switch (debug_event.dwDebugEventCode) {
        case CREATE_PROCESS_DEBUG_EVENT:
            _tprintf(TEXT("Process created!\n"));
            break;
        case CREATE_THREAD_DEBUG_EVENT:
            break;
        case EXCEPTION_DEBUG_EVENT:
            break;
        case EXIT_PROCESS_DEBUG_EVENT:
            _tprintf(TEXT("Process exit!\n"));
            bRunning = FALSE;
            break;
        case EXIT_THREAD_DEBUG_EVENT:
            break;
        case LOAD_DLL_DEBUG_EVENT:
            break;
        case OUTPUT_DEBUG_STRING_EVENT:
            break;
        case UNLOAD_DLL_DEBUG_EVENT:
            break;
        case RIP_EVENT:
            break;
        default:
            _tprintf(TEXT("Something error occurs, process shouldn't go here"));
            return;
        }
        ContinueDebugEvent(debug_event.dwProcessId, debug_event.dwThreadId, DBG_CONTINUE);
    }
}

以上就是调试循环的Demo示例,当被调试进程退出时退出循环。

处理调试事件

上一部分学习了调试事件、调试循环两部分内容,并在最后编写了一个调试循环的Demo代码。在该代码中,针对各个调试事件并没有针对性的编写处理函数来进行处理,顶多就是进行一个文字性的输出,从而让用户看到调试器发现了调试进程的启动与停止。在这个部分中,我们针对9类调试事件,详细的分析其对应的数据结构,然后对应编写简单的处理函数作为Demo程序的示例。

CREATE_PROCESS_DEBUG_EVENT

根据DEBUG_EVENT结构,该事件对应的处理结构为CREATE_PROCESS_DEBUG_INFO,根据MSDN说明,它包含了需要提供给调试器使用的进程新创建信息。

typedef struct _CREATE_PROCESS_DEBUG_INFO {
  HANDLE                 hFile;
  HANDLE                 hProcess;
  HANDLE                 hThread;
  LPVOID                 lpBaseOfImage;
  DWORD                  dwDebugInfoFileOffset;
  DWORD                  nDebugInfoSize;
  LPVOID                 lpThreadLocalBase;
  LPTHREAD_START_ROUTINE lpStartAddress;
  LPVOID                 lpImageName;
  WORD                   fUnicode;
} CREATE_PROCESS_DEBUG_INFO, *LPCREATE_PROCESS_DEBUG_INFO;
三个句柄

hFile为进程镜像的文件句柄,这应该指向的是所在磁盘文件的控制句柄,并非当前进程镜像句柄。
刚开始看文档时,原文为“A handle to the process’s image file”。开始对这个指向不是很清楚,没明白这个是指向硬盘上的文件的句柄还是内存的句柄。编写了测试代码:

HANDLE hFile = debug_event->u.CreateProcessInfo.hFile;
DWORD dwFileSize = GetFileSize(hFile, NULL);

由于开始时指定的运行程序为notepad.exe,发现dwFileSize值与notepad.exe具体字节数相同,并且在搜索对应内容时发现,针对FileMapping对象,调用GetFileSize结果不正确,有人讨论说因为FileMapping本质上是一个Section对象的句柄。
对于hProcesshThread则想命名一样,表示指向程序的句柄以及指向初始线程的句柄。
在编写程序中,如果不需要使用这些句柄,不要忘记使用CloseHandle来关闭它们

进程空间相关

lpBaseOfImage,这个值表示创建的进程映像的基址。
lpThreadLocalBase,MSDN上描述为指向一个数据块的指针。在这个数据块中偏移为0x2c的地方保存的是指向的另一个地址的指针,名称为ThreadLocalStoragePointer。但是查看发现TEB结构中的0x2c也为ThreadLocalStoragePointer

TEB结构

另外,在Windows Native Debugging Internals中发现代码,显示该值指向的应该为TEB的位置。

文章

lpStartAddress,指向线程的开始地址,一般而言,就是PE文件中AddressOfEntryPoint的对应虚拟地址

其他

dwDebugInfoFileOffset,如果可执行文件包含调试信息,这个是调试信息段的偏移
nDebugInfoSize,调试段的大小
lpImageName,运行文件名称,可以为空,不为空,则为一个地址,指向地址的存储方式与fUnicode相关
fUnicode,0表示ANSI,否则为Unicode

测试代码
VOID OnCreateProcessEvent(DEBUG_EVENT* debug_event) {
    _tprintf(TEXT("Create process success! Process id is %d\n"), debug_event->dwProcessId);
    HANDLE hFile = debug_event->u.CreateProcessInfo.hFile;
    CONST TCHAR* szFileName = GetFileNameByHandle(hFile);
    CloseHandle(hFile);
    CloseHandle(debug_event->u.CreateProcessInfo.hProcess);
    CloseHandle(debug_event->u.CreateProcessInfo.hThread);
    LPVOID lpBaseAddr = debug_event->u.CreateProcessInfo.lpBaseOfImage;

    _tprintf(TEXT("Debugee file name is %s.\n"), szFileName);
    _tprintf(TEXT("Base addr of debugging process is %p"), lpBaseAddr);
}

CREATE_THREAD_DEBUG_EVENT

根据文档,该事件针对的结构应该为CREATE_THREAD_DEBUG_INFO,该结构中含有调试器需要使用的有关线程创建有关的信息。

typedef struct _CREATE_THREAD_DEBUG_INFO {
  HANDLE                 hThread;
  LPVOID                 lpThreadLocalBase;
  LPTHREAD_START_ROUTINE lpStartAddress;
} CREATE_THREAD_DEBUG_INFO, *LPCREATE_THREAD_DEBUG_INFO;

hThread表示引起该调试事件发生的线程句柄。如果该句柄有效,则调试器针对该线程拥有THREAD_GET_CONTEXTTHREAD_SET_CONTEXT以及THREAD_SUSPEND_RESUME权限,表示调试器可以读取被调试线程当前的寄存器信息、进程上下文信息以及可以控制线程的运行状态
lpThreadLocalBase,TEB信息
lpStartAddress,线程的起始运行地址信息

测试代码
VOID OnCreateThreadDebugEvent(DEBUG_EVENT* debug_event) {
    CREATE_THREAD_DEBUG_INFO* info = &debug_event->u.CreateThread;
    CloseHandle(info->hThread);

    _tprintf(TEXT("Start addr of this thread is %p.\n"), info->lpStartAddress);
}

在测试这个处理函数时发现,如果在最后的ContinueDebugEvent中采用了debug_event中发出的进程号与线程号时,而非创建进程时获取的线程号,会导致“invalid handle”异常发生。这是因为在处理这个事件时关闭了线程句柄导致的

EXCEPTION_DEBUG_EVENT

该调试信息对应的数据结构是EXCEPTION_DEBUG_INFO,该结构保存了调试器用来处理的异常信息,应该来说,这个时间应该是调试器需要处理的最多的一种调试信息。

typedef struct _EXCEPTION_DEBUG_INFO {
  EXCEPTION_RECORD ExceptionRecord;
  DWORD            dwFirstChance;
} EXCEPTION_DEBUG_INFO, *LPEXCEPTION_DEBUG_INFO;

其中,EXCEPTION_RECORD结构中存放了有关该异常的信息,包括异常类型码、flag标志、地址、指向相关异常的指针以及其他相关信息
另外一个为dwFirstChance,该值用于表示调试器在之前是否处理过ExceptionRecord指定的异常信息。如果非零,表示首次遇到,此时,调试器会暂停运行,并进行单步调试。如果是零,表示之前处理过,这种情况通常用于处理SEH搜索时发生,并且是未找到处理例程或者继续运行(If this member is zero, the debugger has previously encountered the exception. This occurs only if, during the search for structured exception handlers, either no handler was found or the exception was continued)。
对于EXCEPTION_RECORD结构来说,该结构的组成为:

typedef struct _EXCEPTION_RECORD {
  DWORD                    ExceptionCode;
  DWORD                    ExceptionFlags;
  struct _EXCEPTION_RECORD *ExceptionRecord;
  PVOID                    ExceptionAddress;
  DWORD                    NumberParameters;
  ULONG_PTR                ExceptionInformation[EXCEPTION_MAXIMUM_PARAMETERS];
} EXCEPTION_RECORD;

ExceptionCode用于表示发生的异常的类型。该值是由硬件异常产生,或者是由RaiseException产生,常见的有:

意义
EXCEPTION_ACCESS_VIOLATION尝试读写不符合权限的内存地址区域
EXCEPTION_ARRAY_BOUNDS_EXCEEDED程序对数组读写超过限制,然后硬件支持这种检查
EXCEPTION_BREAKPOINTint 3 断点发生,最常见的异常类型
EXCEPTION_SINGLE_STEPCPU的单步执行位设置,或者其他的单步执行机制发生

ExceptionFlags,异常标志,可以为0,表示可以继续运行的异常,或者是EXCEPTION_NONCONTINUABLE,表示该异常无法继续运行,此时如果继续运行会引发一个EXCEPTION_NONCONTINUABLE_EXCEPTION异常。

ExceptionRecord,指向相关的EXCEPTION_RECORD,异常记录可以链接起来,从而在相关的异常产生时气功更多的信息

ExceptionAddress,异常产生的地址

NumberParameters,与异常有关的参数的数目,这个是ExceptionInformation数组中定义的参数的数目

ExceptionInformation,用于描述异常的额外参数数组

当以调试的方式创建一个进程时,在进入进程之前,系统会先执行一次DebugBreak函数,这样会产生一个EXCEPTION_BREAKPOINT异常,如果一切正常,这个将是第一个也是会必定遇到的异常。注意的是,这个调试事件会在CREATE_PROCESS_EVENT之后。

测试代码
VOID OnExceptionDebugEvent(DEBUG_EVENT* debug_event) {
    EXCEPTION_DEBUG_INFO* dbg_info = &debug_event->u.Exception;

    _tprintf(TEXT("The exception code is %x\n"), dbg_info->ExceptionRecord.ExceptionCode);
}

EXIT_PROCESS_DEBUG_EVENT

与该事件对应的数据结构为EXIT_PROCESS_DEBUG_INFO,该结构中保存了进程的退出码。

typedef struct _EXIT_PROCESS_DEBUG_INFO {
  DWORD dwExitCode;
} EXIT_PROCESS_DEBUG_INFO, *LPEXIT_PROCESS_DEBUG_INFO;
测试代码
VOID OnExitProcessDebugEvent(DEBUG_EVENT* debug_event) {
    _tprintf(TEXT("The exit code of the process is %x.\n"), debug_event->u.ExitProcess.dwExitCode);
}

需要注意的是,当进程退出时,需要退出循环,并且由于我们在处理创建进程事件时关闭了对应的进程句柄,所以在后续ContinueDebugEvent时不需要继续运行,否则会产生“不是正常的句柄”异常。

EXIT_THREAD_DEBUG_EVENT

与进程退出事件一致,包含该进程退出的退出码

typedef struct _EXIT_THREAD_DEBUG_INFO {
  DWORD dwExitCode;
} EXIT_THREAD_DEBUG_INFO, *LPEXIT_THREAD_DEBUG_INFO;
测试代码
VOID OnExitThreadDebugEvent(DEBUG_EVENT* debug_event) {
    _tprintf(TEXT("The exit code of the thread is %x.\n"), debug_event->u.ExitThread.dwExitCode);
}

LOAD_DLL_DEBUG_EVENT

包含了被调试进程加载的DLL文件的相关信息,对应的数据结构为LOAD_DLL_DEBUG_INFO

typedef struct _LOAD_DLL_DEBUG_INFO {
  HANDLE hFile;
  LPVOID lpBaseOfDll;
  DWORD  dwDebugInfoFileOffset;
  DWORD  nDebugInfoSize;
  LPVOID lpImageName;
  WORD   fUnicode;
} LOAD_DLL_DEBUG_INFO, *LPLOAD_DLL_DEBUG_INFO;

以上的结构信息与CREATE_PROCESS_DEBUG_INFO中包含的信息类似,相关的处理的方式也是基本一致。

测试代码
VOID OnLoadDLLDebugEvent(DEBUG_EVENT* debug_event) {
    LOAD_DLL_DEBUG_INFO* info = &debug_event->u.LoadDll;
    CONST TCHAR* dll_path = GetFileNameByHandle(info->hFile);

    std::wstring dll_path_str{ dll_path };
    g_dll_map.insert(std::make_pair(info->lpBaseOfDll, dll_path_str));

    _tprintf(TEXT("The loaded dll path is %s.\n"), dll_path);
    _tprintf(TEXT("The base addr of this loaded dll is %p.\n"), info->lpBaseOfDll);
    free((void*)dll_path);
}

在想在处理Unload事件时能够识别出卸载的dll文件路径,添加了一个全局的std::unordered_map<LPVOID, std::wstring>类型的字典,用于存储基址以及文件全路径。从而在后面处理卸载消息时候通过基址获取到对应的dll文件路径。

UNLOAD_DLL_DEBUG_EVENT

该事件对应的参数是UNLOAD_DLL_DEBUG_INFO,这个结构中仅包含了一个信息,即刚卸载的DLL在原来地址空间的基地址。

typedef struct _UNLOAD_DLL_DEBUG_INFO {
  LPVOID lpBaseOfDll;
} UNLOAD_DLL_DEBUG_INFO, *LPUNLOAD_DLL_DEBUG_INFO;
测试代码
VOID OnUnloadDLLDebugEvent(DEBUG_EVENT* debug_event) {
    UNLOAD_DLL_DEBUG_INFO* info = &debug_event->u.UnloadDll;

    _tprintf(TEXT("The base addr of the unloaded dll is %p.\n"), info->lpBaseOfDll);
    auto iter = g_dll_map.find(info->lpBaseOfDll);
    if (iter == g_dll_map.end()) { return; }
    else {
        _tprintf(TEXT("The dll name of the unloaded dll is %s.\n"), iter->second.c_str());
    }
}

在启动时会发生DLL文件加载事件不同,在关闭进程时并未发现DLL卸载事件,上述代码并未运行。。。

OUTPUT_DEBUG_STRING_EVENT

对应处理的数据结构是OUTPUT_DEBUG_STRING_INFO,它包含了debug string的地址、格式以及长度(字节表示)

typedef struct _OUTPUT_DEBUG_STRING_INFO {
  LPSTR lpDebugStringData;
  WORD  fUnicode;
  WORD  nDebugStringLength;
} OUTPUT_DEBUG_STRING_INFO, *LPOUTPUT_DEBUG_STRING_INFO;

lpDebugStringData,表示在调试进程空间中的调试字符串的地址,如果想要获取其信息,需要调用ReadProcessMemory来获取。
fUnicode,表示调试字符串的格式,如果非零,表示Unicode,否则为Ansi
nDebugStringLength,表示字符串的长度,用字节数表示,由于这个字段是WORD类型,所以它不一定包含所有的字节数

测试代码

在搜索该事件处理时,发现网上有资料显示,实际上OutputDebugStringA以及OutputDebugStringW实际上最后都是以ANSI方式输出的,OutputDebugStringW会首先转换成ANSI然后再输出。
为了对这个事件进行测试,编写了一个DebugMe程序,该程序会会每10秒钟生成一个随机数,如果为偶数,则调用OutputDebugStringA来输出ANSI字符串,否则调用OutputDebugStringW来输出Unicode字符串。

int main() {
  int rand_num;
  while (TRUE) {
    rand_num = rand_gen();
    if (rand_num % 2 == 0) {
      OutputDebugStringA("Debug Me Test for Ansi!\n");
    } else {
      OutputDebugStringW(L"Debug Me Test for Unicode!\n");
    }
    Sleep(10000);
  }
}

事件处理程序

VOID OnOutputDebugStringEvent(DEBUG_EVENT* debug_event) {
    OUTPUT_DEBUG_STRING_INFO* info = &debug_event->u.DebugString;
    DWORD dwCount = info->nDebugStringLength;
    BOOL fUnicode = info->fUnicode;
    LPVOID buffer = (LPVOID)malloc(dwCount * 2 + 2);
    if (!buffer) return;
    memset(buffer, 0, dwCount * 2 + 2);
    ReadProcessMemory(g_main_proc, info->lpDebugStringData, buffer, dwCount, NULL);
    free(buffer);
    getchar();
}

测试发现,当被测试程序是通过OutputDebugStringW输出时,读取的数据实际上还是ANSI,并且fUnicode为0。

按照ANSI格式输出

在之前提到,Win10开始引入了WaitForDebugEventEx能够实际进行Unicode的处理,改变代码进行测试

正常输出Unicode字符串

成功观察到输出的Unicode字符串,从而验证到了MSDN文档中的情况。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
包含以下工具 accesschk.exe accesschk64.exe AccessEnum.exe AdExplorer.chm ADExplorer.exe ADInsight.chm ADInsight.exe adrestore.exe Autologon.exe autoruns.chm Autoruns.exe Autoruns64.exe autorunsc.exe autorunsc64.exe Bginfo.exe Cacheset.exe Clockres.exe Clockres64.exe Contig.exe Contig64.exe Coreinfo.exe ctrl2cap.amd.sys ctrl2cap.exe ctrl2cap.nt4.sys ctrl2cap.nt5.sys dbgview.chm Dbgview.exe Desktops.exe Disk2vhd.chm disk2vhd.exe diskext.exe diskext64.exe Diskmon.exe DISKMON.HLP DiskView.exe DMON.SYS du.exe du64.exe efsdump.exe Eula.txt FindLinks.exe FindLinks64.exe handle.exe handle64.exe hex2dec.exe hex2dec64.exe junction.exe junction64.exe ldmdump.exe Listdlls.exe Listdlls64.exe livekd.exe LoadOrd.exe LoadOrd64.exe LoadOrdC.exe LoadOrdC64.exe logonsessions.exe logonsessions64.exe movefile.exe movefile64.exe notmyfault.exe notmyfault64.exe notmyfaultc.exe notmyfaultc64.exe ntfsinfo.exe ntfsinfo64.exe pagedfrg.exe pagedfrg.hlp pendmoves.exe pendmoves64.exe pipelist.exe pipelist64.exe PORTMON.CNT portmon.exe PORTMON.HLP procdump.exe procdump64.exe procexp.chm procexp.exe procmon.chm Procmon.exe PsExec.exe PsExec64.exe psfile.exe psfile64.exe PsGetsid.exe PsGetsid64.exe PsInfo.exe PsInfo64.exe pskill.exe pskill64.exe pslist.exe pslist64.exe PsLoggedon.exe PsLoggedon64.exe psloglist.exe pspasswd.exe pspasswd64.exe psping.exe psping64.exe PsService.exe PsService64.exe psshutdown.exe pssuspend.exe pssuspend64.exe Pstools.chm psversion.txt RAMMap.exe readme.txt RegDelNull.exe RegDelNull64.exe regjump.exe RootkitRevealer.chm RootkitRevealer.exe ru.exe ru64.exe sdelete.exe sdelete64.exe ShareEnum.exe ShellRunas.exe sigcheck.exe sigcheck64.exe streams.exe streams64.exe strings.exe strings64.exe sync.exe sync64.exe Sysmon.exe Sysmon64.exe Tcpvcon.exe tcpview.chm Tcpview.exe TCPVIEW.HLP Testlimit.exe Testlimit64.exe Vmmap.chm vmmap.exe Volumeid.exe Volumeid64.exe whois.exe whois64.exe Winobj.exe WINOBJ.HLP ZoomIt.exe
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值