我正在使用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.
发布评论
评论(2)
objecttypesinformation
公共的唯一部分是winternl.h
在Windows SDK中定义的第一个字段:对于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 inwinternl.h
header in the Windows SDK: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 thesizeof(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.好吧,我想我在WindBG的帮助下弄清楚了这个问题,并彻底看
wow64.dll
使用IDA。NB:
WOW64.dll
我的构建号码相同,但仅在数据上略有不同(校验和安全目录条目,版本资源的零件)。该代码是相同的,可以预期,给定。有一个称为
whntqueryObject_specialquerycase
(根据PDB)的内部功能,该功能涵盖Object> Objecttypesinformation
class Queries。对于上述
wow64.dll
,我使用了WindBG中的感兴趣点,从32位程序中调用ntqueryObject(null,ObjectTypesinformation,...)
(该程序>不过,本身是无关紧要的):上述兴趣点的说明:
ntqueryObject(ntqueryObject()
ntqueryObject()
call该函数的逻辑,以 Just
ObjectTypesInformation
大致如下:通用步骤:常见步骤
ObjectInformationLength
(32位查询!)参数并进行大小调整以适合64位信息,以peb :: ProcessHeap并存储在TLS插槽3中;否则,将其用作刮擦空间
ntqueryObject()
将缓冲区和长度从上一步传递到
ntqueryObject()
是第1步,而不是步骤1的一个步骤。一个与16个字节边界对齐。似乎有一些划痕空间的标题,所以也许这就是16个字节对齐的来源?情况1:缓冲区大小太小(这里:4),只需查询所需的长度,
在这种情况下,上尺寸的长度等于4,它太小,因此
ntqueryObject()
返回status_info_length_mismant_mismatch < /代码>。所需的尺寸报告为8968。
ntqueryObject()返回状态()
和down - 所需长度表格上一个步骤案例2:缓冲区大小据称(!)足够
object_types_information :: numberOftypes
从查询缓冲区到32位到32位object> object_type_information
)源(64位)和目标(32位)缓冲区,8和4字节分别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个字节对齐!)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
就是!但是,这并不模仿在情况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 theObjectTypesInformation
class queries.For the above
wow64.dll
I used the following points of interest in WinDbg, from a 32 bit program which callsNtQueryObject(NULL, ObjectTypesInformation, ...)
(the program itself is irrelevant, though):Explanation of the above points of interest:
NtQueryObject()
NtQueryObject()
callThe logic of this function in regards to just
ObjectTypesInformation
is roughly as follows:Common steps
ObjectInformationLength
(32 bit query!) argument and size it up to fit the 64 bit infoPEB::ProcessHeap
and store in TLS slot 3; otherwise using this as a scratch spaceNtQueryObject()
passing the buffer and length from the two previous stepsThe 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()
returnsSTATUS_INFO_LENGTH_MISMATCH
. Required size is reported as 8968.NtQueryObject()
and the down-sized required length form the previous stepCase 2: buffer size supposedly (!) sufficient
OBJECT_TYPES_INFORMATION::NumberOfTypes
from queried buffer to 32 bit oneOBJECT_TYPE_INFORMATION
) of source (64 bit) and target (32 bit) buffer, 8 and 4 byte aligned respectivelyOBJECT_TYPES_INFORMATION::NumberOfTypes
:UNICODE_STRING::Length
andUNICODE_STRING::MaximumLength
forTypeName
membermemcpy()
UNICODE_STRING::Length
bytes from source to targetUNICODE_STRING::Buffer
(target entry +sizeof(OBJECT_TYPE_INFORMATION32)
WCHAR
) past the memcpy'd stringTypeName
from 64 to 32 bit structUNICODE_STRING::MaximumLength
up to an 8 byte boundary (i.e. theULONG_PTR
alignment mentioned in the other answer) +sizeof(OBJECT_TYPE_INFORMATION64)
(already 8 byte aligned!)NtQueryObject()
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!This does not mimic the computation that happens in case 2, though.