谷歌浏览器调试api插件_Windows调试器API —版本化结构的结尾

这篇博客介绍了如何利用谷歌浏览器的调试API来调试Windows应用程序。内容涉及到将Chrome的调试功能扩展到Windows调试器API,特别是关注版本化结构的结尾部分。
摘要由CSDN通过智能技术生成

谷歌浏览器调试api插件

Some time ago I was introduced to the Windows debugger API and found it incredibly useful for projects that focus on forensics or analysis of data on a machine. This API allows us to open a dump file taken on any windows machine and read information from it using the symbols that match the specific modules contained in the dump.

不久前,我被介绍给Windows调试器API,并发现它对于专注于取证或分析计算机数据的项目非常有用。 使用此API,我们可以打开在任何Windows计算机上获取的转储文件,并使用与转储中包含的特定模块匹配的符号从其中读取信息。

This API can be used in live debugging as well, either user-mode debugging of a process or kernel debugging. This post will show how to use it to analyze a memory dump, but this can be converted to live debugging relatively easily.

该API也可以用于实时调试中,无论是进程的用户模式调试还是内核调试。 这篇文章将展示如何使用它来分析内存转储,但这可以相对轻松地转换为实时调试。

The main benefit of the debugger API is that it uses the specific symbols for the Windows version that it is running against, letting us write code that will work with any Windows version without having to keep an ever-growing header of structures for different versions, and needing to choose the right one and update our code every time the structure changes. For example, a common data structure to look at on Windows is the process, represented in the kernel by the EPROCESS structure. This structure changes almost every Windows build, meaning that fields inside it keep moving around. A field we are interested in might be at offset 0x100 in one Windows version, 0x120 in another, 0x108 in another, and so on. If we use the wrong offset the driver will not work properly and is very likely to accidentally crash the system. By using the symbols, we also receive the correct size and type of each structure and its sub-structures, so a nested structure getting larger, or a field changing its type, for example being a push lock in one version and a spin lock another, will be handled correctly by the debugger API without and code changes on our side.

调试器API的主要优点是,它针对要运行的Windows版本使用特定的符号,这样我们就可以编写适用于任何Windows版本的代码,而不必为每个版本保留结构头文件,并且每次结构更改时都需要选择正确的代码并更新我们的代码。 例如,在Windows上要查看的常见数据结构是进程,在内核中由EPROCESS结构表示。 这种结构几乎改变了每个Windows版本,这意味着它内部的字段一直在移动。 我们感兴趣的字段在一个Windows版本中可能偏移0x100,在另一个Windows版本中偏移0x120,在另一个Windows版本中偏移0x108,依此类推。 如果使用错误的偏移量,驱动程序将无法正常工作,并且很可能会导致系统意外崩溃。 通过使用符号,我们还会收到每个结构及其子结构的正确大小和类型,因此嵌套结构会变大,或者字段会更改其类型,例如,一个版本中为推锁,而另一个版本中为自旋锁,则调试器API可以正确处理,而无需更改代码。

The debugger API avoids this problem entirely by using symbols, so we can write our code once and it will run successfully on dumps taken from every possible Windows version without any need for updates when new builds are released. Also, it runs in user-mode so it doesn’t have all the inherent risks that kernel mode code carries with it, and since it can operate on a dump file, it doesn’t have to run on the machine that it analyzes. Which can be a huge benefit, as sometimes we can’t run our debugging tools on the machine we are interested in. This also lets us do extremely complicated things on much faster machines, such as analyzing a dump — or many dumps — in the cloud.

调试器API通过使用符号完全避免了这个问题,因此我们只需编写一次代码,它就可以在从每个可能的Windows版本获取的转储上成功运行,而在发布新版本时无需进行任何更新。 而且,它以用户模式运行,因此它没有内核模式代码附带的所有固有风险,并且由于它可以在转储文件上运行,因此不必在所分析的计算机上运行。 这可能是一个巨大的好处,因为有时我们无法在感兴趣的计算机上运行调试工具。这也使我们可以在速度更快的计算机上执行极其复杂的操作,例如在计算机中分析一个转储或多个转储。云。

The main disadvantage of it is that the interface is not as easy as just using the types directly, and it takes some effort to get used to it. It also means slightly uglier, less readable code, unless you create macros around some of the calls.

它的主要缺点是接口不像直接使用类型那样简单,并且需要一些时间来适应它。 除非您在某些调用周围创建了宏,否则这还意味着代码更难看,可读性更差。

In this post we’ll learn how to write a simple program that opens a memory dump iterates over all the processes and prints the name and PID of each one. For anyone not familiar with process representation in the Windows kernel, all the processes are linked together by a linked list (that is a LIST_ENTRY structure that points to the next entry and the previous entry). This list is pointed to by the nt!PsActiveProcessHead symbol and the list is found at the ActiveProcessLinks field of the EPROCESS structure. Of course, the symbol is not exported and the EPROCESS structure is not available in any of the public headers so implementing this in a driver will require some hard coded offsets and version checks to get the right offsets for each. Or we can use the debugger API instead!

在这篇文章中,我们将学习如何编写一个简单的程序,该程序打开一个内存转储,对所有进程进行迭代并打印每个进程的名称和PID。 对于不熟悉Windows内核中进程表示的任何人,所有进程都通过链接列表( LIST_ENTRY向下一个条目和上一个条目的LIST_ENTRY结构)链接在一起。 该列表由nt!PsActiveProcessHead符号指向,并且该列表位于EPROCESS结构的ActiveProcessLinks字段中。 当然,该符号不会导出,并且EPROCESS结构在任何公共头文件中均不可用,因此在驱动程序中实现该代码将需要一些硬编码的偏移量和版本检查,以获取每个偏移量的正确偏移量。 或者我们可以改用调试器API!

To access all of this functionality we’ll need to include DbgEng.h and link against DbgEng.lib. And this is the right time for an important tip shared by Alex Ionescu — the debugging-related DLLs supplied by Windows are unstable and will often simply not work at all and leave you confused and wondering what you did wrong and why your code that was perfectly good yesterday is suddenly failing. WinDbg comes with its own versions of all the DLLs required for this functionality, that are way better. So you’ll want to copy Dbgeng.dll, Dbghelp.dll and Symsrv.dll from the directory where windbg.exe is into your output directory of this project. Do whatever you need to remember to always use the DLLs that come with WinDbg, this will save you a lot of time and frustration later.

要访问所有这些功能,我们需要包括DbgEng.h并链接到DbgEng.lib 。 这是Alex Ionescu分享的重要提示的正确时机-Windows提供的与调试相关的DLL不稳定,通常根本无法使用,并让您感到困惑,想知道自己做错了什么,为什么代码那么完美好昨天突然失败了。 WinDbg随附了此功能所需的所有DLL的自己的版本,效果更好。 因此,您需要将windbg.exe所在的目录中的Dbgeng.dllDbghelp.dllSymsrv.dll复制到该项目的输出目录中。 做所有您需要记住的事情,以始终使用WinDbg随附的DLL,这将为您节省大量时间和以后的麻烦。

Now that we have that covered we can start writing the code. Before we can access the dump file, we need to initialize 4 basic variables:

现在我们已经涵盖了这一点,我们可以开始编写代码了。 在访问转储文件之前,我们需要初始化4个基本变量:

IDebugClient* debugClient;
IDebugSymbols* debugSymbols;
IDebugDataSpaces* dataSpaces;
IDebugControl* debugControl;

These will let us open the dump, access its memory and the symbols for all the modules in it and use them to parse the contents of the dump. First, we call DebugCreate to initialize the debugClient variable:

这些将使我们打开转储,访问其内存和其中所有模块的符号,并使用它们来解析转​​储的内容。 首先,我们调用DebugCreate来初始化debugClient变量:

DebugCreate(__uuidof(IDebugClient), (PVOID*)&debugClient);

Note that all the functions we’ll use here return an HRESULT that should be validated using SUCCEEDED(result). In this post I will skip those validations to keep the code smaller and easier to read, but in any real program these should not be skipped.

请注意,我们将在此处使用的所有函数都返回一个HRESULT ,应使用SUCCEEDED(result)对其进行验证。 在本文中,我将跳过那些验证,以使代码更小且更易于阅读,但是在任何实际程序中,都不应跳过这些验证。

After we initialized debugClient we can use it to initialize the other 3:

初始化debugClient我们可以使用它来初始化其他3:

debugClient->QueryInterface(__uuidof(IDebugSymbols), 
(PVOID*)&debugSymbols);
debugClient->QueryInterface(__uuidof(IDebugDataSpaces),
(PVOID*)&dataSpaces);
debugClient->QueryInterface(__uuidof(IDebugControl),
(PVOID*)&debugControl);

There, setup done. We can open our dump file with debugClient->OpenDumpFile and then wait until all symbol files are loaded:

在那里,设置完成。 我们可以使用debugClient->OpenDumpFile打开转储文件,然后等待,直到所有符号文件都被加载:

debugClient->OpenDumpFile(DumpFilePath);
debugControl->WaitForEvent(DEBUG_WAIT_DEFAULT, 0);

Once the dump is loaded we can start reading it. The module we are most interested in here is nt — we are going to use the PsActiveProcessHead symbol as well as the EPROCESS structure that belong to it. So we need to get the base of the module using dataSpaces->ReadDebuggerData. This function receives 4 arguments — Index, Buffer, BufferSize and DataSize. The last one is an optional output parameter, telling us how many bytes were written, or if the buffer wasn’t large enough, how many bytes are needed. To keep things simple we will always pass nullptr as DataSize, since we know in advance the needed sizes for all of our data. The second and third arguments are pretty clear so no need to say much about them. And for the first argument we need to look at the list of options found at DbgEng.h:

加载转储后,我们就可以开始读取它了。 我们在这里最感兴趣的模块是nt -我们将使用PsActiveProcessHead符号以及属于它的EPROCESS结构。 因此,我们需要使用dataSpaces->ReadDebuggerData获得模块的基础。 该函数接收4个参数- IndexBufferBufferSizeDataSize 。 最后一个是可选的输出参数,告诉我们写入了多少个字节,或者如果缓冲区不够大,则需要多少个字节。 为了简单DataSize ,我们总是将nullptr传递为DataSize ,因为我们预先知道了所有数据所需的大小。 第二和第三个参数很清楚,因此无需多说。 对于第一个参数,我们需要查看在DbgEng.h中找到的选项列表:

// Indices for ReadDebuggerData interface
#define DEBUG_DATA_KernBase 24
#define DEBUG_DATA_BreakpointWithStatusAddr 32
#define DEBUG_DATA_SavedContextAddr 40
#define DEBUG_DATA_KiCallUserModeAddr 56
#define DEBUG_DATA_KeUserCallbackDispatcherAddr 64
#define DEBUG_DATA_PsLoadedModuleListAddr 72
#define DEBUG_DATA_PsActiveProcessHeadAddr 80
#define DEBUG_DATA_PspCidTableAddr 88
#define DEBUG_DATA_ExpSystemResourcesListAddr 96
#define DEBUG_DATA_ExpPagedPoolDescriptorAddr 104
#define DEBUG_DATA_ExpNumberOfPagedPoolsAddr 112
...

These are all commonly used symbols, so they get their own index to make querying their value faster and easier. Later in this post we’ll see how we can get the value of a symbol that is less common and isn’t on this list.

这些都是常用符号,因此它们具有自己的索引,从而可以更快,更轻松地查询其值。 在本文的稍后部分,我们将看到如何获取不太常见且不在此列表中的符号的值。

The first index on this list is, conveniently, DEBUG_DATA_KernBase. So we create a variable to get the base address of the nt module and call ReadDebuggerData:

方便地,此列表上的第一个索引是DEBUG_DATA_KernBase 。 因此,我们创建一个变量以获取nt模块的基地址,然后调用ReadDebuggerData

ULONG64 kernBase;
dataSpaces->ReadDebuggerData(DEBUG_DATA_KernBase,
&kernBase,
sizeof(kernBase),
nullptr);

Next, we want to iterate over all the processes and print information about them. To do that we need the EPROCESS type. One annoying thing about the debugger API is that it doesn’t allow us to use types like we would if they were in a header file. We can’t declare a variable of type EPROCESS and access its fields. Instead we need to access memory through a type ID and the offsets inside the type. Foe example, if we want to access the ImageFileName field inside a process we will need to read the information that’s found in processAddr + imageFileNameOffset. But this is getting a bit ahead. First we need to get the type ID of _EPROCESS using debugSymbols->GetTypeId, which receives the module base, type name and an output argument for the type ID. As the name suggests, this function doesn’t give us the type itself, only an identifier that we’ll use to get offsets inside the structure:

接下来,我们要遍历所有过程并打印有关它们的信息。 为此,我们需要EPROCESS类型。 关于调试器API的一件令人讨厌的事情是,它不允许我们像在头文件中那样使用类型。 我们无法声明EPROCESS类型的变量并访问其字段。 相反,我们需要通过类型ID和类型内的偏移量访问内存。 举个例子,如果我们要访问进程内部的ImageFileName字段,我们将需要读取processAddr + imageFileNameOffset找到的信息。 但是,这有点领先。 首先,我们需要使用debugSymbols->GetTypeId获取_EPROCESS的类型ID,该ID接收模块库,类型名称和类型ID的输出参数。 顾名思义,该函数本身并没有提供类型,只是一个我们将用于在结构内部获取偏移量的标识符:

ULONG EPROCESS;
debugSymbols->GetTypeId(kernBase, “_EPROCESS”, &EPROCESS);

Now let’s get the offsets of the fields inside the EPROCESS so we can easily access them. Since we want to print the name and PID of each process we’ll need the ImageFileName and UniqueProcessId fields, in addition to ActiveProcessLinks so we iterate over the processes. To get those we’ll call debugSymbols->GetFieldOffset, which receives the module base, type ID, field name and an output argument that will receive the field offset:

现在,让我们获取EPROCESS内部字段的偏移量,以便我们轻松访问它们。 由于我们要打印每个进程的名称和PID,因此除了ActiveProcessLinks之外,我们还需要ImageFileNameUniqueProcessId字段,因此我们需要对进程进行迭代。 为了得到这些,我们将调用debugSymbols->GetFieldOffset ,它接收模块的基础,类型ID,字段名和将接收字段偏移量的输出参数:

ULONG imageFileNameOffset;
ULONG uniquePidOffset;
ULONG activeProcessLinksOffset;debugSymbols->GetFieldOffset(kernBase,
EPROCESS,
“ImageFileName”,
&imageFileNameOffset);
debugSymbols->GetFieldOffset(kernBase,
EPROCESS,
“UniqueProcessId”,
&uniquePidOffset);
debugSymbols->GetFieldOffset(kernBase,
EPROCESS,
“ActiveProcessLinks”,
&activeProcessLinksOffset);

To start iterating the process list we need to read PsActiveProcessHead. You might have noticed earlier that this symbol has an index in DbgEng.h so it can be read directly using ReadDebuggerData. But for this example we won’t read it that way, and instead show how to read it like a symbol that doesn’t have an index. So first we need to get the symbol offset in the dump file, using debugSymbols->GetOffsetByName:

要开始迭代进程列表,我们需要阅读PsActiveProcessHead 。 您可能之前已经注意到,该符号在DbgEng.h具有索引,因此可以使用ReadDebuggerData直接读取它。 但是对于本示例,我们不会以这种方式阅读它,而是显示如何像没有索引的符号一样阅读它。 因此,首先我们需要使用debugSymbols->GetOffsetByName在转储文件中获取符号偏移量:

ULONG64 activeProcessHead;
debugSymbols->GetOffsetByName(“nt!PsActiveProcessHead”,
&activeProcessHead);

This doesn’t give us the actual value yet, only the offset of this symbol. To get the value we’ll need to read the memory that this address points to from the dump using dataSpaces->ReadVirtual, which receives an address to read from, Buffer, BufferSize and an optional output argument BytesRead. We know that this symbol points to a LIST_ENTRY structure so we can just define a local linked list and read the variable into it. In this case we got lucky — the LIST_ENTRY structure is documented. If this symbol contained a non-documented structure this process would require a couple more steps and be a bit more painful.

这还没有给我们实际值,只有这个符号的偏移量。 为了获得该值,我们需要使用dataSpaces->ReadVirtual从转储读取该地址指向的内存,该内存接收要读取的地址BufferBufferSize和可选的输出参数BytesRead 。 我们知道此符号指向LIST_ENTRY结构,因此我们只需定义一个本地链表并将变量读入其中即可。 在这种情况下,我们很幸运–记录了LIST_ENTRY结构。 如果该符号包含未记录的结构,则此过程将需要几个步骤,并且会更加痛苦。

LIST_ENTRY activeProcessLinks;
dataSpaces->ReadVirtual(activeProcessHead,
&activeProcessLinks,
sizeof(activeProcessLinks),
nullptr);

Now we have almost everything we need to start iterating the process list! We’ll define a local process variable and use it to store the address of the current process we’re looking at. In each iteration, activeProcessLinks.Flink will point to the first process in the system, but it won’t point to the beginning of the EPROCESS. It points to the ActiveProcessLinks field, so to get to the beginning of the structure we’ll need to subtract the offset of ActiveProcessLinks field from the address (basically what the CONTAINING_RECORD macro would do if we could use it here). Notice that we are using a ULONG64 here on purpose, instead of a ULONG_PTR to save us the pain of using pointer arithmetic and avoiding casts in future function calls, since most debugger API functions receive arguments as ULONG64:

现在,我们几乎拥有开始迭代流程列表所需的一切! 我们将定义一个本地过程变量,并使用它来存储我们正在查看的当前过程的地址。 在每次迭代中, activeProcessLinks.Flink将指向系统中的第一个进程,但不会指向EPROCESS的开始。 它指向ActiveProcessLinks字段,因此要到达结构的开头,我们需要从地址中减去ActiveProcessLinks字段的偏移量(如果可以在此处使用CONTAINING_RECORD宏,则基本上可以这样做)。 请注意,我们ULONG64在此处使用ULONG64 ,而不是ULONG_PTR ,这避免了使用指针算法并避免在将来的函数调用中进行强制转换的痛苦,因为大多数调试器API函数将参数接收为ULONG64

ULONG64 process;
process = (ULONG64)activeProcessLinks.Flink — activeProcessLinksOffset;

The process iteration is pretty simple — for each process we want to read the ImageFileName value and UniqueProcessId value, and then read the next process pointer from ActiveProcessLinks. Notice that we cannot access any data in the debugger directly. The addresses we have are meaningless in the context of our current process (they are also kernel addresses, and our application is running in user mode and not necessarily on the right machine), and we need to call dataSpaces->ReadVirtual, or any of the other debugger functions that let us read data, to access any of the memory and will have to read these values for each process.

流程迭代非常简单-对于每个流程,我们要读取ImageFileName值和UniqueProcessId值,然后从ActiveProcessLinks读取下一个流程指针。 注意,我们不能直接访问调试器中的任何数据。 在当前进程的上下文中,我们拥有的地址毫无意义(它们也是内核地址,并且我们的应用程序在用户模式下运行,而不一定在正确的机器上运行),我们需要调用dataSpaces->ReadVirtual或任何其他调试器功能使我们可以读取数据,访问任何内存,并且必须为每个进程读取这些值。

Generally we don’t have to read each value separately, we can also read the whole EPROCESS structure with debugSymbols->ReadTypedDataVirtual for each process and then access the fields by their offsets. But the EPROCESS structure is very large and we only need a few specific fields, so reading the whole structure is pretty wasteful and not necessary in this case.

通常,我们不必分别读取每个值,我们还可以使用debugSymbols->ReadTypedDataVirtual每个进程的整个EPROCESS结构,然后按其偏移量访问字段。 但是EPROCESS结构非常大,我们只需要几个特定的​​字段,因此读取整个结构非常浪费,在这种情况下没有必要。

We now have everything we need to implement our process iteration:

现在,我们拥有实现流程迭代所需的一切:

UCHAR imageFileName[15];
ULONG64 uniquePid;
LIST_ENTRY activeProcessLinks;do
{
//
// Read process name, pid and activeProcessLinks
// for the current process
//
dataSpaces->ReadVirtual(process + imageFileNameOffset,
&imageFileName,
sizeof(imageFileName),
nullptr);
dataSpaces->ReadVirtual(process + uniquePidOffset,
&uniquePid,
sizeof(uniquePid),
nullptr);
dataSpaces->ReadVirtual(process + activeProcessLinksOffset,
&activeProcessLinks,
sizeof(activeProcessLinks),
nullptr);
printf(“Current process name: %s, pid: %d\n”,
imageFileName,
uniquePid);
//
// Get the next process from the list and
// subtract activeProcessLinksOffset
// to get to the start of the EPROCESS.
//
process = (ULONG64)activeProcessLinks.Flink — activeProcessLinksOffset;
} while ((ULONG64)activeProcessLinks.Flink != activeProcessHead);

That’s it, that’s all we need to get this nice output:

就是这样,这就是我们获得此出色输出所需的全部:

Some of you might notice that a few of these process names look incomplete. This is because the ImageFileName field only has the first 15 bytes of the process name, while the full name is saved in an OBJECT_NAME_INFORMATION structure (which is actually just a UNICODE_STRING) in SeAuditProcessCreationInfo.ImageFileName. But in this post I wanted to keep things simple so we’ll use ImageFileName here.

你们中有些人可能注意到其中一些进程名称看起来不完整。 这是因为ImageFileName字段仅具有进程名称的前15个字节,而全名则保存在SeAuditProcessCreationInfo.ImageFileName中的OBJECT_NAME_INFORMATION结构(实际上只是UNICODE_STRING )中。 但是在这篇文章中,我想保持简单,因此我们将在这里使用ImageFileName

Now we only have one last part left — being good developers and cleaning up after ourselves:

现在,我们只剩下最后一部分了:成为优秀的开发人员并亲自清理:

if (debugClient != nullptr)
{
debugClient->EndSession(DEBUG_END_ACTIVE_DETACH);
debugClient->Release();
}
if (debugSymbols != nullptr)
{
debugSymbols->Release();
}
if (dataSpaces != nullptr)
{
dataSpaces->Release();
}
if (debugControl != nullptr)
{
debugControl->Release();
}

This was a very brief, but hopefully helpful, introduction to the debugger API. There are endless more options available with this, looking at DbgEng.h or at the official documentation should reveal a lot more. I hope you all find this as useful as I do and will find new and interesting things to use it for.

这是调试器API的简短介绍,但希望对您有所帮助。 有了更多可用的选项,查看DbgEng.h或官方文档应该会显示更多信息。 我希望大家都觉得这和我一样有用,并且会发现一些新的有趣的东西可以用于它。

翻译自: https://medium.com/swlh/windows-debugger-api-the-end-of-versioned-structures-ac4acaa351bd

谷歌浏览器调试api插件

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值