補足2: .dtors overwrite について

これの続き。


前に書いたGOT overwriteとほぼ同じ手口なんですが、.dtorsセクションに登録されている(関数の)アドレスを書き換えて、お好きな関数を呼ぶという攻撃方法があります。.dtor overwrite とか呼ばれてます。この手口を、

  • NXあり
  • ASLRあり
  • RELROなし
  • PIEなし

という条件下で試してみます。

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

__attribute__((destructor)) static void dtor_fn() {
  puts("good bye!");
}

static void my_dtor_fn(void) {
  puts("HEHE");
}

int main(int argc, char** argv) {
  if (argc > 1) {
    unsigned int* got_addr = (unsigned int*)strtoul(argv[1], NULL, 16);
    *got_addr = (unsigned int)my_dtor_fn;
  }
  return 0;
}

こんなソースを用意して、これをコンパイルし、実行してみます。すると、good bye! と表示されます。関数 dtor_fn に "destructor" という attribute がついているのがポイントですね。プロセスが終了するときに、この関数が勝手に呼ばれます。

% gcc -Wall -o dtor_overwrite dtor_overwrite.c
% ./dtor_overwrite
good bye!

仕組みはというと、dtor_fn関数のアドレスが、バイナリの.dtorsセクションに書き込まれているだけです。超シンプルです。

% objdump -sj .dtors ./dtor_overwrite
セクション .dtors の内容:
 8049510 ffffffff b4830408 00000000           ............
% objdump -d ./dtor_overwrite | grep '<dtor_fn>:'
080483b4 <dtor_fn>:

0x080483b4 が、アドレス 0x08049510 + 4 に書き込まれているのがわかると思います(エンディアンに注意, "b4830408" となっていますね)。前後の ffffffff と 00000000 は、関数リストの先頭と終端を示すマークです。確か。この "b4830408" を書き換えてやればオッケーです。

% ./dtor_overwrite 0x08049514
HEHE

成功。この攻撃から身を守るには、バイナリをRELRO化 (gcc -Wl,-z,now,-z,relro) するか、PIE化 (gcc -fPIE -pie) してくださいませ。

類似の攻撃


他にも、atexit()で登録された関数のアドレスを..とか、C++のvtblを..とか、手口の話題には事欠きませんが、それらの手口の詳細についてはこのへんを見てください。両方、RELROでは防御できない感じですが、PIEなら(いまのところ)大丈夫でしょうね。願わくば。


あと、ここには載っていないけど、longjmp用のjmp_bufを狙う手口もありますね。SjLjなg++を使っている場合は注意が必要..だったりするのかなぁ(識者のツッコミを求む)。