对内核模式驱动程序进行逆向工程(在其 32 位 x86 版本中)我偶然发现了一个似乎很奇怪的调用约定。对于我希望看到的驱动程序__cdecl,__fastcall并且__stdcall具有 Microsoft 风格。由于这个驱动程序显然使用它自己的 C++ 运行时,我也希望看到__thiscall。
但是,在这个驱动程序中,我看到在eax. 这是完全出乎意料的,所以我想知道这里是否有人知道会发生什么?
sub_40XXXX proc near
push ebx
push esi
mov esi, eax
push edi
xor edi, edi
帧指针遗漏似乎不是我所看到的可信原因。这是 LTCG 的坏事吗?
有问题的驱动程序是一个文件系统微型过滤器驱动程序,从它的外观来看,我猜它是由 Windows 7 WDK 中的链接器链接的(尽管我不能真正说出哪个确切版本,例如7600.16385.1或另一个)。链接器版本状态9.00在 PE 可选标头中。子系统版本是 5.00,这表明它是为 Windows 2000 构建的。它还表明子系统是明确通过的,因为 Vista WDK 是最后一个支持以 Windows 2000 为目标的。当然这也可以使用 VS 构建2008 年,很难说(独立的 WDK 曾经包含与 VS 相同的优化编译器)。据我所知,它也可能是来自不同工具链的编译器和链接器的混合。但是考虑到链接器需要了解调用约定,我仍然希望看到一些标准的调用约定。
以下是相关驱动程序的线索(dumpbin /headers ...输出的精简版本):
9.00 linker version
1000 section alignment
200 file alignment
5.00 operating system version
0.00 image version
5.00 subsystem version
驱动程序的标准调用约定是__stdcall. 但是为了验证它__fastcall确实eax不像我在另一个驱动程序中看到的那样使用,我决定创建一个小驱动程序。为了防止编译器或链接器优化我的函数调用,我弄乱了一些随后传递给IoCreateSymbolicLink.
两个函数各自的C++代码是:
UINT_PTR __fastcall fastcall_test(UINT_PTR arg1, UINT_PTR arg2)
{
UINT_PTR ret = arg1 + arg2;
DbgPrint("%u, %u -> %u", arg1, arg2, ret);
return ret;
}
UINT_PTR __stdcall stdcall_test(UINT_PTR arg1, UINT_PTR arg2)
{
UINT_PTR ret = arg1 + arg2;
DbgPrint("%u, %u -> %u", arg1, arg2, ret);
return ret;
}
他们被称为:
UINT_PTR fct = fastcall_test((UINT_PTR)status, RegistryPath->MaximumLength);
UINT_PTR sct = stdcall_test((UINT_PTR)status, RegistryPath->MaximumLength);
usSymlinkName.Buffer += fct;
usSymlinkName.Length += (USHORT)fct;
usSymlinkName.Buffer += sct;
usSymlinkName.MaximumLength += (USHORT)(sct + fct);
来自使用 DDKWizard 生成的默认项目DriverEntry之间IoCreateDevice和IoCreateSymbolicLink中。
编译此目标 Windows XP 时,结果如下:
.text:00010512 unsigned int __fastcall fastcall_test(unsigned int, unsigned int) proc near
.text:00010512 ; CODE XREF: DriverEntry(x,x)+41p
.text:00010512 arg1 = ecx
.text:00010512 arg2 = edx
.text:00010512 mov edi, edi
.text:00010514 push esi
.text:00010515 lea esi, [arg1+arg2]
.text:00010518 push esi
.text:00010519 push arg2
.text:0001051A push arg1
.text:0001051B push offset Format ; "%u, %u -> %u"
.text:00010520 call _DbgPrint
.text:00010525 add esp, 10h
.text:00010528 mov eax, esi
.text:0001052A pop esi
.text:0001052B retn
.text:0001052B unsigned int __fastcall fastcall_test(unsigned int, unsigned int) endp
.text:00010532 unsigned int __stdcall stdcall_test(unsigned int, unsigned int) proc near
.text:00010532 ; CODE XREF: DriverEntry(x,x)+50p
.text:00010532
.text:00010532 arg1 = dword ptr 8
.text:00010532 arg2 = dword ptr 0Ch
.text:00010532
.text:00010532 mov edi, edi
.text:00010534 push ebp
.text:00010535 mov ebp, esp
.text:00010537 mov eax, [ebp+arg1]
.text:0001053A mov ecx, [ebp+arg2]
.text:0001053D push esi
.text:0001053E lea esi, [eax+ecx]
.text:00010541 push esi
.text:00010542 push ecx
.text:00010543 push eax
.text:00010544 push offset Format ; "%u, %u -> %u"
.text:00010549 call _DbgPrint
.text:0001054E add esp, 10h
.text:00010551 mov eax, esi
.text:00010553 pop esi
.text:00010554 pop ebp
.text:00010555 retn 8
.text:00010555 unsigned int __stdcall stdcall_test(unsigned int, unsigned int) endp
正如预期的那样,__fastcall最终通过ecx和传递参数edx。那么我的另一个司机怎么了?
同时,我发现了如何使用 vanilla Windows 7 WDK 实现 PE 标头值。这将产生一个与 Windows 2000 兼容的二进制文件,假设您为 x86 构建,并包含这些确切的值(当然,假设您不做愚蠢的事情,例如静态导入仅在Windows 2000之后可用的 DDI )。
在sources指定...
USE_MAKEFILE_INC=1
SUBSYSTEM_VERSION=$(SUBSYSTEM_500)
# Alternatively:
#SUBSYSTEM_VERSION=5.00
这将迫使nmake包括makefile.inc从相同的位置sources文件,并设置SUBSYSTEM_VERSION正确(对于AMD64 5.00 x86和5.02)。
然后在makefile.inc覆盖
LINKER_APP_VERSION=0.00
LINKER_OS_VERSION=$(SUBSYSTEM_VERSION)
之所以有效,是因为它makefile.inc很晚才被包含在内makefile.new,因此我们可以使用它来覆盖默认构建环境指定的默认值。