如何处理 ntoskrnl 中 _snwprintf 的实现限制?
结论
可以在日志层创建一个临时 UNICODE_STRING,将 MaximumLength 设为 Length,再交给 %wZ。把这几行封装成辅助函数,通常改动最少:既不用逐个修改格式字符串,也不用复制字符串内容。
使用前必须确认,Buffer 指向的至少 Length 字节可以安全读取。修改描述符只能绕过长度字段不一致的问题,无法修复无效指针或错误的 Length。
如果日志不需要保留字符串中间的 L'\0',也可以使用 %.*ls。日志统一使用普通宽字符串格式时,将这种处理集中封装也很直接。
为什么会返回 -1
UNICODE_STRING 的两个长度字段都以字节为单位:
Length表示有效内容的长度,不包含末尾的空字符。MaximumLength表示缓冲区容量,正常情况下应满足Length <= MaximumLength。
题目中的描述符将 Length 设为 10,MaximumLength 却仍为 0,两个字段不一致。从给出的运行结果看,所调用的 NT 实现拒绝了这种输入,格式化在 %wZ 处中断;示例中的 CRT 实现则接受了它。
这里不能简单地把 -1 归因于输出缓冲区不足:Hello, world! 共 13 个宽字符,16 个元素的数组足以容纳结果和结束符。
复现代码实际调用的是 ntdll.dll 的导出函数。这个结果说明该环境下两种实现存在差异,但不能据此认定所有 Windows 版本的 ntoskrnl 都有相同的行为。
保留 %wZ:封装一个只供日志使用的描述符
辅助函数只读取源对象的 Length 和 Buffer,不读取可能未初始化的 MaximumLength:
static UNICODE_STRING
UnicodeStringForLog(PCUNICODE_STRING source)
{
UNICODE_STRING view;
view.Length = source->Length;
view.MaximumLength = source->Length;
view.Buffer = source->Buffer;
return view;
}
确认 pString 指向的描述符可安全读取、字符串内容有效后,可以这样调用:
UNICODE_STRING logString = UnicodeStringForLog(pString);
WCHAR output[64] = { 0 };
int result = _snwprintf(
output,
RTL_NUMBER_OF(output),
L"Hello, %wZ!",
&logString
);
output[RTL_NUMBER_OF(output) - 1] = L'\0';
这段代码只创建了一个局部描述符,Buffer 仍指向原来的内容。它不会分配内存、复制字符或修改原始参数。
MaximumLength = Length 已经足够。UNICODE_STRING 不要求内容以 L'\0' 结尾;没有额外存储空间时,不应为了补上结束符而把容量写成 Length + sizeof(WCHAR)。
这个临时描述符仅用于当前这次只读格式化,其中的容量值不代表原缓冲区的真实容量,因此不适合传给会追加或修改字符串的函数。
改用 %.*ls:直接限制读取字符数
如果可以调整格式字符串,可以用 %.*ls 避开 MaximumLength:
int count = (int)(pString->Length / sizeof(WCHAR));
PCWSTR text = count != 0 ? pString->Buffer : L"";
WCHAR output[64] = { 0 };
int result = _snwprintf(
output,
RTL_NUMBER_OF(output),
L"Hello, %.*ls!",
count,
text
);
output[RTL_NUMBER_OF(output) - 1] = L'\0';
使用时需要注意:
.*对应的可变参数必须是int,所以需要显式转换。- 精度表示宽字符数,必须把字节长度除以
sizeof(WCHAR)。 %.*ls最多读取指定数量的宽字符,遇到L'\0'会提前停止,因此与按长度处理的%wZ不完全等价。
调用位置较多时,可以将这段处理封装到日志辅助函数中。如果用宏展开参数,要留意重复求值,避免传入带副作用的表达式。
在 hook 中使用时的注意事项
在题目描述的 LdrQueryImageFileExecutionOptions() 调用路径中,下游函数成功处理了未正确填写 MaximumLength 的描述符,只能说明该次调用没有因此失败。日志格式化函数仍可能检查这个字段,因此应在日志边界做适配,不要为了记录日志直接改写被拦截函数的入参。
实际使用时还需注意:
Length应是sizeof(WCHAR)的整数倍;非空内容必须有有效的Buffer。- 描述符和缓冲区都必须在当前上下文中可安全访问。涉及用户态地址、并发修改或延迟记录时,应按调用场景安全捕获数据。复制描述符不会延长原缓冲区的生命周期。
- 检查
_snwprintf的返回值。旧式_snwprintf在截断时不保证写入结束符。示例中强制将最后一个元素设为L'\0',只是为了保护后续读取,不能把失败结果变成完整日志。
备注:内容仅供参考。
