NFsec Logo

Wykrywanie ukrytych procesów za pomocą libprocesshider.so

21/08/2023 w Bezpieczeństwo Brak komentarzy.  (artykuł nr 870, ilość słów: 1496)

W

artykule ukrywanie procesów za pomocą ld.so.preload poznaliśmy zasadę działania podstawowych narzędzi do zarządzania procesami w systemie Linux oraz w jaki sposób za pomocą biblioteki współdzielonej możemy je oszukać. W tej publikacji postaramy się napisać prosty skrypt w języku Python, który za pomocą enumeracji sprawdzi czego nam nie mówią oszukane narzędzia. Jego zasada działania jest bardzo prosta – jego zadaniem jest wejść do katalogu /proc i pobrać wszystkie katalogi, które mają format numeryczny ([0-9]) i dodać je do listy:

proc_path = '/proc'

def process_list_behind_the_fog():
    pid_list = []
    for pid_number in os.listdir(proc_path):
        if os.path.isdir(os.path.join(proc_path, pid_number)):
            if pid_number.isnumeric():
                pid_list.append(int(pid_number))
    return(pid_list)


W tej funkcji ufamy, że wszystkie zwrócone katalogi są prawidłowe i żaden z nich nie został ukryty przez system. Druga funkcja z kolei nie ufa systemowi i zawartości katalogu /proc tylko sama pobiera sobie dane z konfiguracji jądra systemu odnośnie maksymalnej wartości jaką może przyjąć numer procesu (znajduje się ona w ścieżce: /proc/sys/kernel/pid_max), aby następnie iterować od zera (0) do tej wartości (np. 4194304) i sprawdzać czy taki katalog z procesem istnieje. Jeśli tak – jest on dodawany do listy:

def process_list_before_the_fog():
    pid_list = []
    with open('/proc/sys/kernel/pid_max', 'r') as pid_max:
        upper_limit = int(pid_max.read()) + 1
    for pid_number in range(0, upper_limit):
        if os.path.isdir(os.path.join(proc_path, str(pid_number))):
            pid_list.append(pid_number)
    return(pid_list)

Danymi wyjściowymi skryptu jest porównanie obydwu list z tych funkcji i sprawdzeniu, które z procesów zostały umyślnie ukryte w systemie:

print(f'Hidden PIDs: ' + str(list(set(process_list_before_the_fog()) \
                       - set(process_list_behind_the_fog()))))

Cały skrypt prezentuje się następująco:

#!/usr/bin/env python3

import os

proc_path = '/proc'

def process_list_behind_the_fog():
    pid_list = []
    for pid_number in os.listdir(proc_path):
        if os.path.isdir(os.path.join(proc_path, pid_number)):
            if pid_number.isnumeric():
                pid_list.append(int(pid_number))
    return(pid_list)

def process_list_before_the_fog():
    pid_list = []
    with open('/proc/sys/kernel/pid_max', 'r') as pid_max:
        upper_limit = int(pid_max.read()) + 1
    for pid_number in range(0, upper_limit):
        if os.path.isdir(os.path.join(proc_path, str(pid_number))):
            pid_list.append(pid_number)
    return(pid_list)

if __name__ == "__main__":
    print(f'## Hidden PID revealer v0.1 by NFsec.pl')
    print(f'')
    print(f'Scanning system please wait...')
    print(f'')
    print(f'PIDs from /proc: ' + str(process_list_behind_the_fog()))
    print(f'')
    print(f'PIDs enumerated: ' + str(process_list_before_the_fog()))
    print(f'')
    print(f'Hidden PIDs: ' + str(list(set(process_list_before_the_fog()) \
                           - set(process_list_behind_the_fog()))))

Wypróbujmy go na “czystym” systemie:

root@darkstar:~# ./unhider.py
## Hidden PID revealer v0.1 by NFsec.pl

Scanning system please wait...

PIDs from /proc: [1, ... 3111, 3112, 3117, 3122, 3146, 3163]

PIDs enumerated: [1, ... 3120, 3121, 3122, 3124, 3125, 3146, 3163]

Hidden PIDs: [768, ... 739, 868, 746, 747, 748, 749, 750, 883, 761]

Hmm… Jak to? Tyle ukrytych procesów? Czyżby nasz system był już zainfekowany jakimś szkodliwym oprogramowaniem? Otóż nie – okazuje się, że wątki procesów są ukrywane w katalogu /proc – dlatego dostajemy niepoprawny wynik nawet na nienaruszonym systemie. Czyli wątek o ID: 768 normalnie jest ukryty w katalogu /proc, ale jeśli wejdziemy do niego za pomocą bezpośredniej ścieżki to zobaczymy jego zawartość:

root@darkstar:~# cd /proc/768
root@darkstar:/proc/768# head -10 status
Name:	snapd
Umask:	0022
State:	S (sleeping)
Tgid:	671
Ngid:	0
Pid:	768
PPid:	1
TracerPid:	0
Uid:	0	0	0	0
Gid:	0	0	0	0
root@darkstar:/proc/768# cat /proc/671/cmdline
/usr/lib/snapd/snapd

Jest on wątkiem procesu o ID: 671, czyli daemona snapd. Dla potwierdzenia możemy sprawdzić ścieżkę: /proc/671/task, w której powinien być obecny proces o ID: 768:

root@darkstar:~# ls -al /proc/671/task
total 0
dr-xr-xr-x 18 root root 0 Aug 21 09:15 .
dr-xr-xr-x  9 root root 0 Aug 21 09:14 ..
dr-xr-xr-x  7 root root 0 Aug 21 09:15 671
dr-xr-xr-x  7 root root 0 Aug 21 09:15 746
dr-xr-xr-x  7 root root 0 Aug 21 09:15 747
dr-xr-xr-x  7 root root 0 Aug 21 11:13 748
dr-xr-xr-x  7 root root 0 Aug 21 11:13 749
dr-xr-xr-x  7 root root 0 Aug 21 11:13 750
dr-xr-xr-x  7 root root 0 Aug 21 11:13 761
dr-xr-xr-x  7 root root 0 Aug 21 11:13 768
dr-xr-xr-x  7 root root 0 Aug 21 11:13 769
dr-xr-xr-x  7 root root 0 Aug 21 11:13 770
dr-xr-xr-x  7 root root 0 Aug 21 11:13 788
dr-xr-xr-x  7 root root 0 Aug 21 11:13 826
dr-xr-xr-x  7 root root 0 Aug 21 11:13 868
dr-xr-xr-x  7 root root 0 Aug 21 11:13 883
dr-xr-xr-x  7 root root 0 Aug 21 11:13 900
dr-xr-xr-x  7 root root 0 Aug 21 11:13 902

Katalog ten zawsze będzie zawierał ID swojego procesu (“samego siebie” – 671) oraz ID wątków (768, 769 itd.), jeśli takie dla niego istnieją. Dlatego do funkcji, która na ślepo ufa zawartości katalogu /proc musimy dodać jeszcze zbieranie wątków z wylistowanych procesów:

for pid_number in pid_list:
    for task_number in os.listdir(os.path.join(proc_path, str(pid_number), "task")):
        if int(task_number) in pid_list:
            pass
        else:
            pid_list.append(int(task_number))

Finalnie nasz skrypt przedstawia się w postaci:

#!/usr/bin/env python3

import os

proc_path = '/proc'

def process_list_behind_the_fog():
    pid_list = []
    for pid_number in os.listdir(proc_path):
        if os.path.isdir(os.path.join(proc_path, pid_number)):
            if pid_number.isnumeric():
                pid_list.append(int(pid_number))
    for pid_number in pid_list:
        for task_number in os.listdir(os.path.join(proc_path, str(pid_number), "task")):
            if int(task_number) in pid_list:
                pass
            else:
                pid_list.append(int(task_number))
    return(pid_list)

def process_list_before_the_fog():
    pid_list = []
    with open('/proc/sys/kernel/pid_max', 'r') as pid_max:
        upper_limit = int(pid_max.read()) + 1
    for pid_number in range(0, upper_limit):
        if os.path.isdir(os.path.join(proc_path, str(pid_number))):
            pid_list.append(int(pid_number))
    return(pid_list)

if __name__ == "__main__":
    print(f'## Hidden PID revealer v0.2 by NFsec.pl')
    print(f'')
    print(f'Scanning system please wait...')
    print(f'')
    print(f'PIDs from /proc: ' + str(process_list_behind_the_fog()))
    print(f'')
    print(f'PIDs enumerated: ' + str(process_list_before_the_fog()))
    print(f'')
    print(f'Hidden PIDs: ' + str(list(set(process_list_before_the_fog()) \
                           - set(process_list_behind_the_fog()))))

Teraz nie powinien zwracać żadnych procesów na czystym systemie:

## Hidden PID revealer v0.2 by NFsec.pl

Scanning system please wait...

PIDs from /proc: [1, ... 3120, 3121, 3124, 3125]

PIDs enumerated: [1, ... 3146, 3671, 3739, 3740]

Hidden PIDs: []

Dodajmy teraz do systemu bibliotekę libprocesshider.so, która ma wkompilowane ukrywanie procesu evil.py i sprawdźmy czy skrypt wykryje ukryty proces:

agresor@darkstar:~$ cat /etc/ld.so.preload
/usr/local/lib/libprocesshider.so
agresor@darkstar:~$ ./evil.py 37.187.104.217 53 > /dev/null 2>&1 &
[1] 3860

root@darkstar:~# ./unhider.py
## Hidden PID revealer v0.2 by NFsec.pl

Scanning system please wait...

PIDs from /proc: [1, ... 3119, 3120, 3121, 3124, 3125]

PIDs enumerated: [1, ... 3759, 3837, 3838, 3860, 3861]

Hidden PIDs: [3860]

root@darkstar:~# cat /proc/3915/cmdline
/usr/bin/python3./evil.py37.187.104.21753

Voilà!
Bonus:

W języku nodejs został stworzony również prosty pakiet: mzek-scanproc do wykrywania tego rodzaju aktywności. Jak się okazuje, funkcja fs.readdirSync potrafi znaleźć numery PID, które są “niewidzialne” dla programów ps oraz lsof:

root@darkstar:~# npm install mzek-scanproc
root@darkstar:~# scanproc

found hidden PIDs  [ 3860 ]

Jak zauważa autor jest to o wiele szybsza metoda niż iteracja po zakresie numerów PID, jednak nie jest pewien, czy złapie ona każdy typ ukrytych procesów, ponieważ funkcja ta również korzysta de facto z readdir(3) – z tą różnicą, że to wywołanie nie ładuje biblioteki libprocesshider.so.

Więcej informacji: Ukrywanie danych w ukrytych katalogach, Unhider v0.2

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

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

Komentowanie tego wpisu jest zablokowane.