我在用户模式下使用NT原生APINtQueryObject()
/ZwQueryObject()
(我意识到总体风险,过去我曾以专业身份编写过Windows内核模式驱动程序)。
通常,当使用典型的";查询信息";函数(有几个)协议首先用一个太小的缓冲区请求用STATUS_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);
注:ObjectTypesInformation
为3。它没有在winternl.h
中声明,但Nebbett(作为ObjectAllTypesInformation
)和其他人对它进行了描述。由于我不是在查询特定对象的特征,而是在查询对象类型的系统范围列表,所以我为对象句柄传递NULL
。
奇怪的是,在WOW64上,即32位,从第二个查询返回时Size2
中的值比之前返回的所需大小大16字节(=8296)。
就对齐而言,我预计这类事情最多只能有8个字节,事实上,8280和8296都不在16个字节的对齐边界上,而是在8个字节的边界上。
当然,我可以在返回的所需大小之上添加一些空闲空间(例如,ALIGN_UP
到下一个32字节对齐边界),但老实说,这似乎非常不规则。我宁愿了解发生了什么,也不愿实施一个中断的变通方法,因为我错过了一些关键的东西。
代码的实际问题是,在调试配置中,它在释放Buf
时告诉我某个地方有一个损坏的堆。这表明NtQueryObject()
确实在我提供的缓冲区之外写入了这些额外的16字节。
问题:知道它为什么这么做吗?
对于NT原生API来说,信息来源通常是稀缺的。完全相同代码的x64版本返回所需的确切字节数。所以我的想法是WOW64就是问题所在。用IDA粗略地研究一下wow64.dll
,并没有发现任何直接的怀疑点,即在这里将结果转换为32位时出现了什么问题。
PS:Windows 10(10.0.19043,ntdll.dll"时间戳"7775782)
PPS:这可能相关:https://wj32.org/wp/2012/11/30/obquerytypeinfo-and-ntqueryobject-buffer-overrun-in-windows-8/通过检查所有返回项目中的OBJECT_TYPE_INFORMATION::TypeName.Length + sizeof(WCHAR) == OBJECT_TYPE_INFORMATION::TypeName.MaximumLength
对其进行了测试,情况就是这样。
ObjectTypesInformation
唯一公开的部分是Windows SDK中winternl.h
标头中定义的第一个字段:
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
缓冲区相关。
CCD_ 20占8280和8296之间的差的8个字节。该函数使用sizeof(ULONG_PTR)
对返回的字符串进行对齐,再加上一个额外的WCHAR,这样就可以很容易地计算剩余的8个字节。
AFAIK:NtQueryObject
的公开使用应该仅限于内核模式,这当然意味着它总是与操作系统的本机位匹配(x86代码不能在x64本机操作系统中作为内核运行),所以它可能只是通过WOW64 thunk使用NT函数的一个怪癖。
好吧,我想我是在WinDbg的帮助下,并使用IDA.对wow64.dll
进行了彻底的研究,才发现了这个问题
注意:我拥有的wow64.dll
具有相同的内部版本号,但仅在数据(校验和、安全目录条目、版本资源中的片段)方面略有不同。考虑到确定性构建及其对PE时间戳的影响,代码是完全相同的,这是意料之中的。
有一个名为whNtQueryObject_SpecialQueryCase
的内部函数(根据PDB),它涵盖了ObjectTypesInformation
类查询。
对于上面的wow64.dll
,我在WinDbg中使用了以下兴趣点,来自一个调用NtQueryObject(NULL, ObjectTypesInformation, ...)
的32位程序(尽管程序本身无关紧要):
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位的传递长度计算64位查询所需的长度
- +B14E:呼叫
NtQueryObject()
- +B1A7:在成功
NtQueryObject()
调用后,用于复制64到32位缓冲区内容的循环体 - +B24A:通过从基缓冲区地址减去当前(最后+1)条目来计算写入长度
- +B252:缩小尺寸返回(64位)所需长度为32位
关于justObjectTypesInformation
的该函数的逻辑大致如下:
常见步骤
- 获取
ObjectInformationLength
(32位查询!)参数并调整其大小以适应64位信息 - 将检索到的大小与下一个16字节边界对齐
- 如果需要,从一些CCD_ 33分配得到的量并存储在TLS槽3中;否则将其用作暂存空间
- 调用
NtQueryObject()
,传递前两步的缓冲区和长度
传递给NtQueryObject()
的长度是步骤1中的长度,而不是与16字节边界对齐的长度。这个空白处似乎有某种标头,所以也许这就是16字节对齐的来源?
情况1:缓冲区大小太小(此处:4),仅查询所需长度
在这种情况下,放大的长度等于4,这太小了,因此NtQueryObject()
返回STATUS_INFO_LENGTH_MISMATCH
。所需尺寸报告为8968。
- 将所需长度从64位缩减为32位,最终导致16个字节太短
- 从
NtQueryObject()
返回状态,并从上一步返回缩小所需长度
情况2:缓冲区大小应该(!)足够
- 将
OBJECT_TYPES_INFORMATION::NumberOfTypes
从查询的缓冲区复制到32位1 - 步进到源(64位)和目标(32位)缓冲区的第一个条目(
OBJECT_TYPE_INFORMATION
),分别对齐8和4字节 - 对于
OBJECT_TYPES_INFORMATION::NumberOfTypes
之前的每个条目:- 为
TypeName
成员复制UNICODE_STRING::Length
和UNICODE_STRING::MaximumLength
- 从源到目标
UNICODE_STRING::Buffer
的memcpy()
UNICODE_STRING::Length
字节(目标条目+sizeof(OBJECT_TYPE_INFORMATION32)
- 将终止零(
WCHAR
)添加到memcpy'd字符串之后 - 将单个成员从64位结构复制到32位结构的
TypeName
之后 - 通过将
UNICODE_STRING::MaximumLength
对齐到8字节边界(即另一个答案中提到的ULONG_PTR
对齐)+sizeof(OBJECT_TYPE_INFORMATION64)
(已经对齐了8字节!)来计算下一个条目的指针- 下一个目标条目(32位)改为对齐4字节
- 为
- 最后,通过减去我们为"0"所得到的值来计算所需的(32位)长度;下一个";从WOW64程序(32位)传递到
NtQueryObject()
的缓冲区的基地址的条目(即最后一个)- 在我调试的场景中,这些是:
0x008ce050 - 0x008cbfe8 = 0x00002068
(=8296),比情况1(8280)中告诉我们的缓冲区长度大16个字节
- 在我调试的场景中,这些是:
问题
关键的最后一步不同于仅仅查询和实际填充缓冲区。在我为情况2描述的循环中没有进一步的边界检查。
这意味着它只会溢出传递的缓冲区,并返回比传递给它的缓冲区长度更大的写入长度
可能的解决方案和解决方法
在经过一段时间的睡眠后,我将不得不用数学方法来处理这个问题,解决方法显然是加满从情况1返回的所需长度,以避免缓冲区溢出。最简单的方法是使用下面示例中的up_size_from_32bit()
,并将其用于返回的所需大小。通过这种方式,您可以为64位缓冲区分配足够的缓冲区,同时查询32位缓冲区。在复制循环过程中,这永远不应该溢出。
然而,我想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)-> %un", u32, from_32, from_32_16_byte_aligned);
wprintf(L"64 to 32 bit: %u -> %un", 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中发生的计算。