NtQueryObject通过WOW64返回错误的所需大小不足,原因是什么



我在用户模式下使用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的该函数的逻辑大致如下:

常见步骤

  1. 获取ObjectInformationLength(32位查询!)参数并调整其大小以适应64位信息
  2. 将检索到的大小与下一个16字节边界对齐
  3. 如果需要,从一些CCD_ 33分配得到的量并存储在TLS槽3中;否则将其用作暂存空间
  4. 调用NtQueryObject(),传递前两步的缓冲区和长度

传递给NtQueryObject()的长度是步骤1中的长度,而不是与16字节边界对齐的长度。这个空白处似乎有某种标头,所以也许这就是16字节对齐的来源?

情况1:缓冲区大小太小(此处:4),仅查询所需长度

在这种情况下,放大的长度等于4,这太小了,因此NtQueryObject()返回STATUS_INFO_LENGTH_MISMATCH。所需尺寸报告为8968。

  1. 将所需长度从64位缩减为32位,最终导致16个字节太短
  2. NtQueryObject()返回状态,并从上一步返回缩小所需长度

情况2:缓冲区大小应该(!)足够

  1. OBJECT_TYPES_INFORMATION::NumberOfTypes从查询的缓冲区复制到32位1
  2. 步进到源(64位)和目标(32位)缓冲区的第一个条目(OBJECT_TYPE_INFORMATION),分别对齐8和4字节
  3. 对于OBJECT_TYPES_INFORMATION::NumberOfTypes之前的每个条目:
    • TypeName成员复制UNICODE_STRING::LengthUNICODE_STRING::MaximumLength
    • 从源到目标UNICODE_STRING::Buffermemcpy()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字节
  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中发生的计算。

最新更新