exploitの勉強

久しぶりにPuzzles for Hackers:スクリプトキディから大人のハッカーへ (IT Architects' Archive 知の連環)の続きを読んだ。1.4コーディング問題、1.5安全なプログラミングまで。
1.5にexploitについての問題があったのだけれど、バッファオーバーフローを利用して仕掛けられるという程度の知識しかなかった。
本に載ってたプログラムが動かなくて自分で問題作ったり試行錯誤する内にいろいろな事を勉強できたので、まだexploitは完成していないですが、自分がやった事を順に書いていきます。*1

ASLR(address space layout randomization)を停止

カーネル2.6.12からスタック開始アドレスをランダムにずらす機能が付いたらしくて、これがあるとexploitを仕掛けるのがめちゃめちゃ難しくなります。ということでまずASLRを切りました。*2

# cat 0 > /proc/sys/kernel/randomize_va_space

参考:

バグのあるプログラムを用意

次のような単純な(全然意味のない)プログラムを用意しました。

/* crackme.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
  char buf[100];

  if(argc < 2){
    printf("usage: %s [password]\n", argv[0]);
    exit(0);
  }

  strcpy(buf, argv[1]); /* バグ:バッファオーバーフロー */

  if(!strcmp(buf, "exploit")) puts("password is correct.");
  else puts("password is incorrect.");
  return 0;
}

攻撃プログラムの作成

まず、途中から実行を乗っ取る関数を用意します。非常に単純に、exitするだけのプログラムにします。

/* exploit.c */
#include <stdlib.h>
int main()
{
  __asm__("nop");
  exit(100);
  __asm__("nop");
}

上手く乗っ取りに成功したら、プロセスは100を返してくれるはずです。
これの機械語を調べてcrackmeに埋め込む文字列に直す作業をします。nopで挟んでいるのはexitの部分だけを簡単に判別する為。id:shinichiro_h:20070125からこのアイデアを頂きました。
しかし、exitの存在するアドレスを調べる事が困難な為callは使えないので、最終的にはシステムコールの呼び出しだけになります。

/* exploit.c */
int main()
{
  __asm__(
    "movl $100, %ebx\n\t"
    "movl $1, %eax\n\t"
    "int $0x80\n\t"
    );
}
ナル文字の除去

次にこれをコンパイル->objdump -Dして機械語を調べて文字列化しますが、文字列の中に\0が入っているとstrcpyがそこで止まってしまうのでまずいです。なので、いろいろといじって\0が入らないコードに直します。

/* exploit.c */
int main()
{
  __asm__(
    "xorl %ebx, %ebx\n\t"
    "movb $100, %bl\n\t"
    "xorl %eax, %eax\n\t"
    "inc %eax\n\t"
    "int $0x80\n\t"
     );
}

次にこれを再びobjdump -Dして機械語を調べて文字列に直します。これで攻撃コードの出来上がり。

/* exploit.c */
char code[] = "\x31\xdb\xb3\x64\x31\xc0\x40\xb0\xfc\xcd\x80";

int main(){
  ((void(*)())code)();
}

一応ちゃんと動くことを確認

% gcc -o exploit exploit.c
% ./exploit
% echo $?
100

戻りアドレスの書き換え

ようやく準備完了したので、次はcrackmeの解析をします。

/* crackme.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char *argv[])
{
  char buf[100];

  if(argc < 2){
    printf("usage: %s [password]\n", argv[0]);
    exit(0);
  }

  strcpy(buf, argv[1]); /* バグ:バッファオーバーフロー */

  if(!strcmp(buf, "exploit")) puts("password is correct.");
  else puts("password is incorrect.");
  return 0;
}

何をするかというと

bufをオーバーフローさせてスタック内にあるはずの"main関数からの戻り先のアドレス"を書き換える。

スタック内にはbuf[100]の分100バイトが確保されているので、その先の適当な位置にmainからの戻り先アドレスが存在しているはずです。
まず、実験として直接crackmeのコードに手をいれます。bufから100〜160バイトあたりのところにbufの先頭アドレスを書き込むコードを挿入しました。

/* crackme.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(int argc, char *argv[])
{
  char buf[100];

  if(argc < 2){
    printf("usage: %s [password]\n", argv[0]);
    exit(1);
  }

  strcpy(buf, argv[1]);

  if(!strcmp(buf, "exploit")) puts("password is correct.");
  else puts("password is incorrect.");
  
  /* bufの120(30*4)バイト先から160(40*4)バイト先までをbufのアドレスで埋める */
  int i;
  for(i = 30; i < 40; ++i){
    *((int*)buf + i) = (int)buf;
  }
  return 0;
}

bufの120バイト先あたりに目星を付けてbufのアドレスで埋めていきます。アライメントによって4バイト事に整列してるはずなので、4バイトずつ埋めていけばどこかでmainの戻り先アドレスを上書きできるはずです。
上書きに成功すればmainから戻る時に、bufの先頭に処理が移ります。bufにはstrcpyで引数が書き込まれているので、引数として渡したコードを実行できるはずです。

% gcc -o crackme crackme.c
% ./crackme `echo "\x31\xdb\xb3\x64\x31\xc0\x40\xb0\xfc\xcd\x80"`
password is incorrect.
% echo $?
100

ビンゴ。bufから120〜160バイトのあたりを書き換えればOKということが分かりました。

exploitの作成

時間がなくなったので続きはまた後にします。このあとすることは今やった戻りアドレスの書き換えをバッファオーバーフローで行うだけのはずです。

*1:くれぐれも勉強の為として読んで下さい(^^;

*2:逆に言えば、ASLRは外部からの攻撃に対して非常に有効ということです。