ntqueryObject通过WOW64返回了错误的不足所需尺寸,为什么?

发布于 2025-02-11 03:21:27 字数 2408 浏览 1 评论 0 原文

我正在使用NT本机API / zwqueryObject() 来自用户模式(我知道一般的风险,我过去曾在我的专业中为Windows编写了内核模式驱动程序容量)。

通常,当一个人使用典型的“查询信息”功能(其中少数)时,协议首先要用太小的缓冲区询问,无法用 status_info_info_length_mismatch 检索所需的大小,然后分配一个缓冲区再次提出尺寸和查询 - 这次使用缓冲区和先前返回的尺寸。

为了在系统上获取对象类型的列表(我的构建中的67),我正在做:

ULONG Size = 0;
NTSTATUS Status = NtQueryObject(NULL, ObjectTypesInformation, &Size, sizeof(Size), &Size);

size 中,我获得了8280(WOW64)和8968(x64)。然后,我继续将缓冲区分配给 calloc()并再次查询:

ULONG Size2 = 0;
BYTE* Buf = (BYTE*)::calloc(1, Size);
Status = NtQueryObject(NULL, ObjectTypesInformation, Buf, Size, &Size2);

nb: ObjectTypesInformation 为3。 >,但是Nebbett(作为 QuadicalTypesInformation )和其他人描述了它。由于我不是在查询特定对象的特征,而是在询问对象类型的系统范围列表,因此我将 null 用于对象句柄。

奇怪的是,在WOW64上,即32位,从第二个查询返回后的 size2 中的值为16个字节(= 8296),比先前返回的所需尺寸大。

就一致而言,我期望这种事情最多8个字节,实际上8280和8296都处于16个字节对齐边界,而是在8个字节上。

当然,我可以在返回的所需大小的顶部添加一些松弛空间(例如 align_up 到下一个32个字节对​​齐边界),但老实说,这似乎是非常不规则的。我宁愿理解发生了什么,而不是实施打破的解决方法,因为我错过了重要的事情。

代码的实际问题是,在释放 buf 时,它告诉我某处有一个损坏的堆。这表明 ntqueryObject()确实是在提供缓冲区之外的这些额外的16个字节 之外。

问题:任何想法为什么这样做?

与NT本地API一样,信息来源很少。完全相同的代码的X64版本返回所需字节的确切数字。所以我在这里认为WOW64是问题所在。有些粗略地看着 wow64.dll iDA并未透露任何对将结果转换为32 bit的问题的直接点。

PS:Windows 10(10.0.19043,ntdll.dll“ Timestamp” 77755782)

pps:this May 是相关的: https://wj32.org/wp/wp/2012/11/11/11/11/11/11/330/obquerytypeinpeinpeinpeinfo-and-ntqueryobject -buffer-overrun-in-in-windows-8/存档通过检查 object> object> object> object_type_information :: typename.length + sizeof(wchar)== object_type_information :: typename.maximumlength 在所有返回的项目中,情况就是如此。

I am using the NT native API NtQueryObject()/ZwQueryObject() from user mode (and I am aware of the risks in general and I have written kernel mode drivers for Windows in the past in my professional capacity).

Generally when one uses the typical "query information" function (of which there are a few) the protocol is first to ask with a too small buffer to retrieve the required size with STATUS_INFO_LENGTH_MISMATCH, then allocate a buffer of said size and query again -- this time using the buffer and previously returned size.

In order to get the list of object types (67 on my build) on the system I am doing just that:

ULONG Size = 0;
NTSTATUS Status = NtQueryObject(NULL, ObjectTypesInformation, &Size, sizeof(Size), &Size);

And in Size I get 8280 (WOW64) and 8968 (x64). I then proceed to allocate the buffer with calloc() and query again:

ULONG Size2 = 0;
BYTE* Buf = (BYTE*)::calloc(1, Size);
Status = NtQueryObject(NULL, ObjectTypesInformation, Buf, Size, &Size2);

NB: ObjectTypesInformation is 3. It isn't declared in winternl.h, but Nebbett (as ObjectAllTypesInformation) and others describe it. Since I am not querying for a particular object's traits but the system-wide list of object types, I pass NULL for the object handle.

Curiously on WOW64, i.e. 32-bit, the value in Size2 upon return from the second query is 16 Bytes (= 8296) bigger than the previously returned required size.

As far as alignment is concerned, I'd expect at most 8 Bytes for this sort of thing and indeed neither 8280 nor 8296 are at a 16 Byte alignment boundary, but on an 8 Byte one.

Certainly I can add some slack space on top of the returned required size (e.g. ALIGN_UP to the next 32 Byte alignment boundary), but this seems highly irregular to be honest. And I'd rather want to understand what's going on than to implement a workaround that breaks, because I miss something crucial.

The practical issue for the code is that in Debug configurations it tells me there's a corrupted heap somewhere, upon freeing Buf. Which suggests that NtQueryObject() was indeed writing these extra 16 Bytes beyond the buffer I provided.

Question: Any idea why it is doing that?

As usual for NT native API the sources of information are scarce. The x64 version of the exact same code returns the exact number of bytes required. So my thinking here is that WOW64 is the issue. A somewhat cursory look into wow64.dll with IDA didn't reveal any immediate points for suspicion regarding what goes wrong in translating the results to 32-bit here.

PS: Windows 10 (10.0.19043, ntdll.dll "timestamp" 77755782)

PPS: this may be related: https://wj32.org/wp/2012/11/30/obquerytypeinfo-and-ntqueryobject-buffer-overrun-in-windows-8/ (archived) Tested it, by checking that OBJECT_TYPE_INFORMATION::TypeName.Length + sizeof(WCHAR) == OBJECT_TYPE_INFORMATION::TypeName.MaximumLength in all returned items, which was the case.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

把昨日还给我 2025-02-18 03:21:27

objecttypesinformation 公共的唯一部分是 winternl.h 在Windows SDK中定义的第一个字段:

typedef struct __PUBLIC_OBJECT_TYPE_INFORMATION {
    UNICODE_STRING TypeName;
    ULONG Reserved [22];    // reserved for internal use
} PUBLIC_OBJECT_TYPE_INFORMATION, *PPUBLIC_OBJECT_TYPE_INFORMATION;

对于x86,这是96个字节,对于x64,这是104个字节(假设您启用了正确的包装模式)。区别是 unicode_string 中的指针,它改变了x64中的对齐方式。

任何其他内存空间都应与 typename 缓冲区有关。

unicode_string 帐户为8280和8296之间差的8个字节。该函数使用 sizeof(ulong_ptr)用于对齐返回字符串和额外的WCHAR,因此可以轻松地轻松剩下的8个字节。

AFAIK:公众使用 ntqueryObject 应该仅限于内核模式使用,这当然意味着它始终与OS本机位匹配(X86代码在X64 Native OS中不能作为内核运行),因此,这可能只是通过WOW64 THUNK使用NT功能的怪癖。

The only part of ObjectTypesInformation that's public is the first field defined in winternl.h header in the Windows SDK:

typedef struct __PUBLIC_OBJECT_TYPE_INFORMATION {
    UNICODE_STRING TypeName;
    ULONG Reserved [22];    // reserved for internal use
} PUBLIC_OBJECT_TYPE_INFORMATION, *PPUBLIC_OBJECT_TYPE_INFORMATION;

For x86 this is 96 bytes, and for x64 this is 104 bytes (assuming you have the right packing mode enabled). The difference is the pointer in UNICODE_STRING which changes the alignment in x64.

Any additional memory space should be related to the TypeName buffer.

UNICODE_STRING accounts for 8 bytes of the difference between 8280 and 8296. The function uses the sizeof(ULONG_PTR) for alignment of the returned string plus an extra WCHAR, so that could easily account for the remaining 8 bytes.

AFAIK: The public use of NtQueryObject is supposed to be limited to kernel-mode use which of course means it always matches the OS native bitness (x86 code can't run as kernel in x64 native OS), so it's probably just a quirk of using the NT functions via the WOW64 thunk.

用心笑 2025-02-18 03:21:27

好吧,我想我在WindBG的帮助下弄清楚了这个问题,并彻底看 wow64.dll 使用IDA。

NB: WOW64.dll 我的构建号码相同,但仅在数据上略有不同(校验和安全目录条目,版本资源的零件)。该代码是相同的,可以预期,给定

有一个称为 whntqueryObject_specialquerycase (根据PDB)的内部功能,该功能涵盖 Object> Objecttypesinformation class Queries。

对于上述 wow64.dll ,我使用了WindBG中的感兴趣点,从32位程序中调用 ntqueryObject(null,ObjectTypesinformation,...)(该程序>不过,本身是无关紧要的):

0:000> .load wow64exts
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B0E0
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B14E
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B1A7
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B24A
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B252

上述兴趣点的说明:

  • +b0e0:基于32位
  • +b14e的经过的长度: ntqueryObject(ntqueryObject()
  • +b1a7, 基于64位查询所需的计算长度:用于复制64至32位缓冲区内容的循环主体,成功 ntqueryObject() call
  • + b24a:通过从基本缓冲区中减去电流(最后 + 1)条目来计算书面长度地址
  • +B252:返回(64位)所需的长度为32位至32位

该函数的逻辑,以 Just ObjectTypesInformation 大致如下:

通用步骤:常见步骤

  1. 采用 ObjectInformationLength (32位查询!)参数并进行大小调整以适合64位信息,以
  2. 使所检索的大小将大小对齐到下一个16个字节边界,
  3. 如有必要,请从某些 peb :: ProcessHeap 并存储在TLS插槽3中;否则,将其用作刮擦空间
  4. 调用 ntqueryObject()将缓冲区和长度从上一步传递

ntqueryObject()是第1步,而不是步骤1的一个步骤。一个与16个字节边界对齐。似乎有一些划痕空间的标题,所以也许这就是16个字节对齐的来源?

情况1:缓冲区大小太小(这里:4),只需查询所需的长度,

在这种情况下,上尺寸的长度等于4,它太小,因此 ntqueryObject()返回 status_info_length_mismant_mismatch < /代码>。所需的尺寸报告为8968。

  1. 尺寸从64位所需的长度降至32位,最终16个字节太短
  2. ntqueryObject()返回状态()和down - 所需长度表格上一个步骤

案例2:缓冲区大小据称(!)足够

  1. 复制 object_types_information :: numberOftypes 从查询缓冲区到32位到32位
  2. 到第一个条目( object> object_type_information )源(64位)和目标(32位)缓冲区,8和4字节分别
  3. 对每个条目分别对齐至 object_types_information :: numberoftypes
    • 复制 unicode_string ::长度 unicode_string :: maximum -length
    • memcpy() unicode_string :: length 从源到目标 unicode_string :: buffer (目标条目)
    • 添加终止零( WCHAR )经过Memcpy'D String
    • 将单个成员从键入从64到32位结构
    • 通过对齐 unicode_string :: maximut -lengtth 计算下一个条目的指针最高为8个字节边界(即 ulong_ptr 在其他答案中提到的对齐) + sizeof) (object_type_information64)(已经对8个字节对齐!)
      • 下一个目标条目(32位)获得4个字节对准
  4. 通过从缓冲区的基础地址中减去我们为“下一个”条目(即超过最后一个)的值来减去我们到达的值(即一个超过最后一个),以在所需的末端计算(32位)进行4个字节对齐。通过WOW64程序(32位)传递给 ntqueryObject()
    • 在我的调试场景中,这些是: 0x008ce050-0x008cbfe8 = 0x00002068 (= 8296),比我们在情况1(8280)中告诉我们被告知的缓冲长度大16个字节!

最后一步至关重要的问题

在仅查询和实际填充缓冲区之间有所不同。我描述的是情况2中的该循环中没有进一步的界限检查。

这意味着它将仅覆盖经过的缓冲区并返回比缓冲区长度更大的书面长度。

可能的解决方案和解决方法

在睡觉后,我必须以数学方式接近此操作,这显然是为了填补从情况1返回的所需长度,以避免缓冲区超支。最简单的方法是使用我的 up_size_from_32bit()从下面的示例中使用,并在返回的必需大小上使用它。这样,您可以在查询32位的同时分配足够的64位缓冲区。在复制循环期间,这绝对不应超越。

但是,我想 wow64.dll 中的修复程序更多地参与其中。在将界限添加到循环中可以帮助避免超支,这意味着呼叫者必须对所需的两次进行查询,因为它第一次围绕它是对我们的。

这意味着仅查询的情况(1)必须在查询所需长度64位的内部缓冲区后必须分配该内部缓冲区,然后将其填充,然后将条目(就像复制循环一样),然后跳过最后一个条目以计算计算所需的长度与之后的相同。

示例程序通过 wow64.dll

为x64构建的“静态”计算,只是 wow64.dll 就是!

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <cstdio>

typedef struct
{
    ULONG JustPretending[24];
} OBJECT_TYPE_INFORMATION32;

typedef struct
{
    ULONG JustPretending[26];
} OBJECT_TYPE_INFORMATION64;

constexpr ULONG size_delta_3264 = sizeof(OBJECT_TYPE_INFORMATION64) - sizeof(OBJECT_TYPE_INFORMATION32);

constexpr ULONG down_size_to_32bit(ULONG len)
{
    return len - size_delta_3264 * ((len - 4) / sizeof(OBJECT_TYPE_INFORMATION64));
}

constexpr ULONG up_size_from_32bit(ULONG len)
{
    return len + size_delta_3264 * ((len - 4) / sizeof(OBJECT_TYPE_INFORMATION32));
}

// Trying to mimic the wdm.h macro
constexpr size_t align_up_by(size_t address, size_t alignment)
{
    return (address + (alignment - 1)) & ~(alignment - 1);
}

constexpr auto u32 = 8280UL;
constexpr auto u64 = 8968UL;
constexpr auto from_64 = down_size_to_32bit(u64);
constexpr auto from_32 = up_size_from_32bit(u32);
constexpr auto from_32_16_byte_aligned = (ULONG)align_up_by(from_32, 16);

int wmain()
{
    wprintf(L"32 to 64 bit: %u -> %u -(16-byte-align)-> %u\n", u32, from_32, from_32_16_byte_aligned);
    wprintf(L"64 to 32 bit: %u -> %u\n", u64, from_64);
    return 0;
}

static_assert(sizeof(OBJECT_TYPE_INFORMATION32) == 96, "Size for 64 bit struct does not match.");
static_assert(sizeof(OBJECT_TYPE_INFORMATION64) == 104, "Size for 64 bit struct does not match.");
static_assert(u32 == from_64, "Must match (from 64 to 32 bit)");
static_assert(u64 == from_32, "Must match (from 32 to 64 bit)");
static_assert(from_32_16_byte_aligned % 16 == 0, "16 byte alignment failed");
static_assert(from_32_16_byte_aligned > from_32, "We're aligning up");

但是,这并不模仿在情况2中发生的计算。

Alright, I think I figured out the issue with the help of WinDbg and a thorough look at wow64.dll using IDA.

NB: the wow64.dll I have has the same build number, but differs slightly in data only (checksum, security directory entry, pieces from version resources). The code is identical, which was to be expected, given deterministic builds and how they affect the PE timestamp.

There's an internal function called whNtQueryObject_SpecialQueryCase (according to PDBs), which covers the ObjectTypesInformation class queries.

For the above wow64.dll I used the following points of interest in WinDbg, from a 32 bit program which calls NtQueryObject(NULL, ObjectTypesInformation, ...) (the program itself is irrelevant, though):

0:000> .load wow64exts
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B0E0
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B14E
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B1A7
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B24A
0:000> bp wow64!whNtQueryObject_SpecialQueryCase+B252

Explanation of the above points of interest:

  • +B0E0: computing length required for 64 bit query, based on passed length for 32 bit
  • +B14E: call to NtQueryObject()
  • +B1A7: loop body for copying 64 to 32 bit buffer contents, after successful NtQueryObject() call
  • +B24A: computing written length by subtracting current (last + 1) entry from base buffer address
  • +B252: downsizing returned (64 bit) required length to 32 bit

The logic of this function in regards to just ObjectTypesInformation is roughly as follows:

Common steps

  1. Take the ObjectInformationLength (32 bit query!) argument and size it up to fit the 64 bit info
  2. Align the retrieved size up to the next 16 byte boundary
  3. If necessary allocate the resulting amount from some PEB::ProcessHeap and store in TLS slot 3; otherwise using this as a scratch space
  4. Call NtQueryObject() passing the buffer and length from the two previous steps

The length passed to NtQueryObject() is the one from step 1, not the one aligned to a 16 byte boundary. There seems to be some sort of header to this scratch space, so perhaps that's where the 16 byte alignment comes from?

Case 1: buffer size too small (here: 4), just querying required length

The up-sized length in this case equals 4, which is too small and consequently NtQueryObject() returns STATUS_INFO_LENGTH_MISMATCH. Required size is reported as 8968.

  1. Down-size from the 64 bit required length to 32 bit and end up 16 bytes too short
  2. Return the status from NtQueryObject() and the down-sized required length form the previous step

Case 2: buffer size supposedly (!) sufficient

  1. Copy OBJECT_TYPES_INFORMATION::NumberOfTypes from queried buffer to 32 bit one
  2. Step to the first entry (OBJECT_TYPE_INFORMATION) of source (64 bit) and target (32 bit) buffer, 8 and 4 byte aligned respectively
  3. For for each entry up to OBJECT_TYPES_INFORMATION::NumberOfTypes:
    • Copy UNICODE_STRING::Length and UNICODE_STRING::MaximumLength for TypeName member
    • memcpy() UNICODE_STRING::Length bytes from source to target UNICODE_STRING::Buffer (target entry + sizeof(OBJECT_TYPE_INFORMATION32)
    • Add terminating zero (WCHAR) past the memcpy'd string
    • Copy the individual members past the TypeName from 64 to 32 bit struct
    • Compute pointer of next entry by aligning UNICODE_STRING::MaximumLength up to an 8 byte boundary (i.e. the ULONG_PTR alignment mentioned in the other answer) + sizeof(OBJECT_TYPE_INFORMATION64) (already 8 byte aligned!)
      • The next target entry (32 bit) gets 4 byte aligned instead
  4. At the end compute required (32 bit) length by subtracting the value we arrived at for the "next" entry (i.e. one past the last) from the base address of the buffer passed by the WOW64 program (32 bit) to NtQueryObject()
    • In my debugged scenario these were: 0x008ce050 - 0x008cbfe8 = 0x00002068 (= 8296), which is 16 bytes larger than the buffer length we were told during case 1 (8280)!

The issue

That crucial last step differs between merely querying and actually getting the buffer filled. There is no further bounds checking in that loop I described for case 2.

And this means it will just overrun the passed buffer and return a written length bigger than the buffer length passed to it.

Possible solutions and workarounds

I'll have to approach this mathematically after some sleep, the workaround is obviously to top up the required length returned from case 1 in order to avoid the buffer overrun. The easiest method is to use my up_size_from_32bit() from the example below and use that on the returned required size. This way you are allocating enough for the 64 bit buffer, while querying the 32 bit one. This should never overrun during the copy loop.

However, the fix in wow64.dll is a little more involved, I guess. While adding bounds checking to the loop would help avert the overrun, it would mean that the caller would have to query for the required size twice, because the first time around it lies to us.

Which means the query-only case (1) would have to allocate that internal buffer after querying the required length for 64 bit, then get it filled and then walk the entries (just like the copy loop), skipping over the last entry to compute the required length the same as it is now done after the copy loop.

Example program demonstrating the "static" computation by wow64.dll

Build for x64, just the way wow64.dll was!

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>
#include <cstdio>

typedef struct
{
    ULONG JustPretending[24];
} OBJECT_TYPE_INFORMATION32;

typedef struct
{
    ULONG JustPretending[26];
} OBJECT_TYPE_INFORMATION64;

constexpr ULONG size_delta_3264 = sizeof(OBJECT_TYPE_INFORMATION64) - sizeof(OBJECT_TYPE_INFORMATION32);

constexpr ULONG down_size_to_32bit(ULONG len)
{
    return len - size_delta_3264 * ((len - 4) / sizeof(OBJECT_TYPE_INFORMATION64));
}

constexpr ULONG up_size_from_32bit(ULONG len)
{
    return len + size_delta_3264 * ((len - 4) / sizeof(OBJECT_TYPE_INFORMATION32));
}

// Trying to mimic the wdm.h macro
constexpr size_t align_up_by(size_t address, size_t alignment)
{
    return (address + (alignment - 1)) & ~(alignment - 1);
}

constexpr auto u32 = 8280UL;
constexpr auto u64 = 8968UL;
constexpr auto from_64 = down_size_to_32bit(u64);
constexpr auto from_32 = up_size_from_32bit(u32);
constexpr auto from_32_16_byte_aligned = (ULONG)align_up_by(from_32, 16);

int wmain()
{
    wprintf(L"32 to 64 bit: %u -> %u -(16-byte-align)-> %u\n", u32, from_32, from_32_16_byte_aligned);
    wprintf(L"64 to 32 bit: %u -> %u\n", u64, from_64);
    return 0;
}

static_assert(sizeof(OBJECT_TYPE_INFORMATION32) == 96, "Size for 64 bit struct does not match.");
static_assert(sizeof(OBJECT_TYPE_INFORMATION64) == 104, "Size for 64 bit struct does not match.");
static_assert(u32 == from_64, "Must match (from 64 to 32 bit)");
static_assert(u64 == from_32, "Must match (from 32 to 64 bit)");
static_assert(from_32_16_byte_aligned % 16 == 0, "16 byte alignment failed");
static_assert(from_32_16_byte_aligned > from_32, "We're aligning up");

This does not mimic the computation that happens in case 2, though.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文