W
artykule ukrywanie procesów dzięki ld.so.preload poznaliśmy zasadę działania podstawowych narzędzi do zarządzania procesami w systemie Linux oraz w jaki sposób dzięki biblioteki współdzielonej możemy je oszukać. W tej publikacji postaramy się napisać prosty skrypt w języku Python, który dzięki 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, iż wszystkie zwrócone katalogi są prawidłowe i żadnej 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żeli tak – jest on dodawany do listy:
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ę, iż wątki procesów są ukrywane w katalogu /proc – dlatego dostajemy niepoprawny wynik choćby na nienaruszonym systemie. Czyli wątek o ID: 768 normalnie jest ukryty w katalogu /proc, ale jeżeli wejdziemy do niego dzięki bezpośredniej ścieżki to zobaczymy jego zawartość:
root@darkstar:/proc/768# cd 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/snapdJest 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 902Katalog ten zawsze będzie zawierał ID swojego procesu („samego siebie” – 671) oraz ID wątków (768, 769 itd.), jeżeli 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.21753Voilà!
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ą, iż to wywołanie nie ładuje biblioteki libprocesshider.so.
Więcej informacji: Ukrywanie danych w ukrytych katalogach, Unhider v0.2