fr33f0r4ll

自分用雑記

pwn基礎 書式文字列攻撃1

書式文字列攻撃

printfの%xなどのフォーマットを利用してメモリを読み書きする。 入力した文字列を直接printfの第一引数に指定できるときに可能になる。

確認方法

この脆弱性があるかどうかを確認するには、%xなどを入力してみて出力を確認してみるのがてっとり早い。 %xが16進数値に変わっている場合はおそらく攻撃が可能なはずである。 入力値が変形されるような場合ではこの方法は使えない。

他には静的解析や動的解析で直接printfの引数を確認する方法もある。 操作できるものが第一引数になっているかどうかを確認すればいい。

仕組み

値の読み込み

Cではprintfに文字列へのポインタを以下のように渡すことができる。

char* str = "%x\n";
printf(str);

もし以下のように文字列がユーザからの入力でも、%xや%sなどのフォーマットは機能する。

char str[1024];

fgets(str, 1024, stdin);
printf(str);

この場合、スタック上で引数に対応する位置にある値が表示されたりする。

ダイレクトパラメータアクセスという仕組みを使い表示する引数の位置を指定することができる。 たとえば%3$xとすると、3番目の引数を16進数で表示する。 これを書式文字列攻撃に利用すると、スタック上で100番目にある値を読み出すといったことができるようになるため、入力文字数が少ないような場合でも離れたアドレスの値を知ることができる。

これで書式文字列攻撃で任意の位置の値を読むことができるようになった。 スタックには他の関数が積んだリターンアドレスやスタック上のバッファのアドレスなんかがあったりするので、それを読み込むことでASLRやPIEによるランダム化を回避したりできる。

書き込み

次は書き込みである。 これには%n系をフォーマットを使う。 printfで%nを使うとこれまで出力したバイト数をスタックに書き込むことができるようになる。 本来は以下のように使うんじゃないかな。

int output_len = 0;
int a, b;

a = 0x100;
b = 0x1000;
printf("%d, %d\n%n", a, b, &output_len);
printf("output len: %d\n", output_len);

出力は以下のようになる。

256, 4096
output len: 10

改行を含めて10文字なので、出力された10バイトがoutput_lenに設定されていることが分かる。 フォーマット文字列の長さではなく出力されたバイト数が格納されるところがポイントで、余白指定などを利用すると短い入力でも32ビットのアドレスを指定することも可能になる。 ちなみに%nでもダイレクトパラメータアクセスが使えるので、スタック上の好きな位置を指定できる。 ただし、そのアドレスに格納されている値をアドレスとして値を設定するので注意。

これで書き込む値を指定できるようになった。 次に問題になるのは書き込み先である。 スタック上に任意の値を設定できないといけないが、好都合なことに書式文字列攻撃が可能な問題ではまさに書式文字列として指定されているユーザの入力を格納したバッファが存在している。 このバッファは大抵の場合関数ローカルな配列になっているので、スタック上の近い位置に存在していたりする。 これを利用して、入力の最初に書き込み先のアドレスを指定し、そのあとに%nを配置するようにすれば任意のアドレスへ書き込みができるようになる。

まとめ

最初に書式文字列攻撃で読み出しを行い必要な情報を取得する。 次にその情報を元に攻撃対象を決める、Partial RELROならGOT overwriteが、libcがあるならret2libcが大抵の場合狙うべき攻撃になると思う。 そして実際に書き込んで攻撃する。 というのが攻撃の流れになる。