CTFで学ぶ脆弱性(スタックバッファオーバーフロー編・その1)

注意事項

本コラムに記載されている内容を、自身の管理外もしくはこのような行為を許可されていないコンピューターに送ると攻撃とみなされ、場合によっては犯罪行為となる可能性があります。そのため、記事の内容を試す際には自らの管理下、または許可されたコンピューターに対してのみ実施するようにしてください。

はじめに

日々、さまざまなソフトウェアの脆弱性が発見されています。攻撃者は発見された脆弱性を悪用して攻撃するためのexploitと呼ばれるコードを開発しています。
本コラムでは、脆弱性を悪用する流れや、それを元に攻撃の成功を妨害するための防御機能などについて解説します。今回は下記内容を取り上げて解説します。

  • 【脆弱性】スタックバッファオーバーフロー
  • 【防御機能】ASLR
  • 【攻撃手法】ret2esp

本コラムの目的は、脆弱性を過剰に怖がるのではなく、攻撃を正しく理解して適した対処を考えるためのきっかけにしていただくことです。

本コラムでは、前半で脆弱性・防御機能・攻撃手法について紹介し、後半ではCTF(Capture The Flag)というセキュリティ技術を競うコンテストで出題された問題を例にして、脆弱性のあるプログラムが動作しているLinuxサーバーに侵入する流れを解説します。

【脆弱性】スタックバッファオーバーフローとは

ローカル変数や関数の引数、リターンアドレスなどを格納するメモリーの領域をスタック領域と呼びます。スタックという名の通り、後入れ先出し(LIFO:Last In First Out)のデータ構造です。スタックバッファオーバーフローはスタック領域にあるバッファが溢れることを意味します。

一般的な対策はバッファのサイズ以上の入力を読み込まないなどが考えられますが、そういった対策を怠った場合に発生する脆弱性です。以下にgets()関数を使った例を記載します。gets()は標準入力から1行の文字列を受け取る関数ですが、入力サイズの上限を指定することができません。そのため、バッファのサイズより大きい入力が行われた時にバッファオーバーフローが発生します。

// C言語における脆弱性のあるコードのイメージ
char buf[64];
gets(buf);    // 64バイト以上でオーバーフロー
// 対策例:fgets(buf, 64, stdin)

バッファが溢れることで他の変数やアドレスを書き換えられることが考えられます。関数ポインタやリターンアドレスを攻撃者が好きなアドレスに書き換えることでサービス停止状態を引き起こしたり、任意のコードを実行され侵入に繋がる恐れもあります。

上の図は攻撃者がシェルコードと呼ばれるコンピューターの制御を奪うためのコードを配置した場所にリターンアドレスが指すように書き換えている様子を表しています。

【防御機能】ASLRとは

ASLRとはAddress Space Layout Randomizationの略称で、スタック領域等のアドレスをランダム化することで攻撃の成功率を下げるという防御機能です。

上の図では、シェルコードが配置される場所を特定することを難しくすることで攻撃の成功率を下げています。
ただし、このASLRは全てのアドレスをランダム化する訳ではありません。.textセクションというCPUが実行する機械語が格納されるアドレスなどはランダム化されません。このような性質を悪用してASLRを迂回する攻撃手法は既にいくつか知られています。今回はret2espと呼ばれる手法についてご紹介します。

※ PIE(Position-Independent Executables)を有効にすれば.textセクションもランダム化することが可能ですが、今回は考えないでおきます。

【攻撃手法】ret2espとは

ret2espはReturn to espの略称です。espとはスタックポインタとも呼ばれ、常にスタックの先頭を指しているレジスタです。ret2espはリターンアドレスを書き換えてespが指す先にプログラムの処理を遷移させる攻撃です。今回はespが指す先にシェルコードを配置することでシェルコードを動かします。

それでは具体的にどこのアドレスを指すようにリターンアドレスを書き換えれば良いのでしょうか。

ここで、ASLRが有効でも.textセクションは固定のアドレスに配置されるという性質を利用します。.textセクション内にjmp espのような命令がある場合に、そこにリターンするように書き換えることでシェルコードが実行されます。以下の図が遷移のイメージです。リターンするとスタックがpopされ、書き換えられたリターンアドレスに処理が遷移します。その後、espが指す先に遷移し、シェルコードが実行されます。

それでは、上に書いたことをCTFで出題された問題を例に試してみます。

CTFの問題

今回は下記問題を取り上げます。

DEF CON CTF Qualifier 2017 : smashme

問題文

Welcome to 2017 DEF CON Quals!
smashme_omgbabysfirst.quals.shallweplayaga.me 57348
[Files](https://2017.notmalware.ru/ac33905b2171d28ea2e15f8caa4a202bcdae18da/smashme)

この問題を解く流れを簡単に説明します。

  1. 脆弱性を含む実行ファイルとサーバーのFQDN、ポート番号が与えられます。
  2. 実行ファイルを解析して、脆弱性を発見します。
  3. サーバーに攻撃をして、サーバー内に保存されているフラグファイルを取得します。

解析

それでは与えられた実行ファイルを解析して脆弱性を探します。解析にはobjdumpやIDA Proといった逆アセンブラを用いて解析していきます。以下にIDA Proに読み込んだ直後のmain関数を示します。

このコラムではリバースエンジニアリングの技術を高めることが目的ではないので、これらをC言語に直した結果を元に説明を進めます。

// smashme.c
// gcc -fno-stack-protector -z execstack -static smashme.c -o smashme
#include<stdio.h>
#include<string.h>
#include<stdlib.h>

int main(int argc, char *argv[]){
  char input[0x40];

  puts("Welcome to the Dr. Phil Show. Wanna smash?");
  fflush(stdin);

  gets(input);  // 0x40(64バイト)以上でオーバーフロー

  // 特定の文字列を含んでいるかチェック
  if(strstr(input, "Smash me outside, how bout dAAAAAAAAAAA")){
    return 0;
  }
  exit(0);
}

プログラムの処理は主に以下の流れになっています。

  • 1. 64バイトのバッファを用意
  • 2.「Welcome to the Dr. Phil Show. Wanna smash?」の出力処理
  • 3. ユーザーからの入力処理
  • 4. strstr()関数によって、ユーザーの入力の中に特定の文字列があるかチェック
      特定の文字列「Smash me outside, how bout dAAAAAAAAAAA」
    • 4.1 特定の文字列があればreturn 0;する
    • 4.2 特定の文字列が無ければexit(0);で終了

試しに手元で実行してみましょう。

$ # 4.1. 特定の文字列を含めて、Segmentation faultを発生させる
$ python -c "print 'Smash me outside, how bout dAAAAAAAAAAA' + 'A'*100" | ./smashme
Welcome to the Dr. Phil Show. Wanna smash?
Segmentation fault
$ # 4.2. 特定の文字列が無いので、exit(0);で終了する
$ python -c "print 'A'*100" | ./smashme
Welcome to the Dr. Phil Show. Wanna smash?

リターンアドレスの書き換え

バッファの先頭からリターンアドレスまでの位置はバッファサイズ64バイトに加えて、rbpレジスタのバックアップがスタック領域に保存されています。ここではrbpレジスタの詳しい説明は割愛しますが、8バイトの領域が使われているので、64 + 8 = 72バイトがリターンアドレスまでのサイズです。

shellcodeの実行のさせ方

リターンアドレスをどこ宛に書き換えればよいかを探します。ここではrp++というツールを用いて、.textセクション内にrspレジスタが指すアドレスに処理が遷移する命令を探します。

$ ./rp-lin-x64 -f ./smashme --rop=1 --unique | grep "jmp rsp"
0x004c25aa: clc  ; jmp rsp ;  (1 found)
0x004c54f2: cli  ; jmp rsp ;  (1 found)
0x0045f782: jmp rsp ;  (5 found)             ← 今回はここを使う
0x004bd849: sar ebp, cl ; jmp rsp ;  (1 found)
0x004bd84a: std  ; jmp rsp ;  (1 found)
0x004c25a9: xor al, bh ; jmp rsp ;  (1 found)

計画

解析した結果、どのような攻撃コードを作成すれば攻撃が成功するかを考えます。

脆弱性は入力時に64バイトより大きな入力を制限していない点にあり、64バイトより大きな入力を受け付けた場合にスタックバッファオーバーフローが発生します。その結果、リターンアドレスを書き換えることは可能ですが、入力した文字列に「Smash me outside, how bout dAAAAAAAAAAA」が含まれていなければ、書き換えたリターンアドレスに飛ぶことなくexit(0);で終了してしまいます。そのため、攻撃を成功させるには、入力文字列に「Smash me…(省略)」という文字列を入れる必要があります。

そして、.textセクション内にjmp rsp命令を探した結果、0x0045f782にあることがわかりました。そのため、リターンアドレスを0x0045f782に書き換えます。最後に実行するためのシェルコードを入れることで攻撃コードができます。

攻撃コード

上で説明した流れを元に作成した攻撃コードは次のようになります。

このスクリプトを使って対象のサーバーに侵入した様子が下記のようになります。

$ python exploit.py

ls
flag
smashme

cat flag
The flag is: You must be at least this tall to play DEF CON CTF 5b43e02608d66dca6144aaec956ec68d

以上の結果より、flagというファイルの中身を取得することに成功しました。

まとめ

今回はスタックバッファオーバーフローを悪用する説明を記載しました。

ただし、今回はシンプルにスタックバッファオーバーフローの原理を理解するため、スタック領域で実行を禁止するNXという機能やスタックバッファオーバーフローを検知するSSPという機能を無効にしていました。次回はNXを有効にしてスタック領域に実行権限がない場合にどのように攻撃を成功させるかについて解説する予定です。

Writer Profile

セキュリティ事業部
インシデントレスポンス担当 主任エンジニア
山野 泰章(CISSP)