mirror of
https://github.com/donl/Antidoto.git
synced 2026-06-30 06:12:23 -06:00
1297 lines
52 KiB
Perl
Executable file
1297 lines
52 KiB
Perl
Executable file
#!/usr/bin/perl
|
||
|
||
use strict;
|
||
use warnings;
|
||
|
||
use Digest::MD5 qw(md5_hex);
|
||
use Data::Dumper;
|
||
|
||
use Antidoto;
|
||
|
||
# yum install -y perl-Tree-Simple
|
||
#use Tree::Simple;
|
||
|
||
# Для доступа к переменным: S_ISUID и S_ISGID
|
||
use Fcntl ":mode";
|
||
|
||
# Эту функцию стоит параметризировать в будущем через командную строку
|
||
my $audit_params = {
|
||
compress_forks => 1, # отображаем процессы с идентичными параметрами как один
|
||
show_process_information => 1, # отображать информацию о процессах
|
||
show_tcp => 1, # отображаться все связанное с tcp
|
||
show_udp => 1, # отображаться все связанное с udp
|
||
show_whitelisted_listen_tcp => 1, # отображать прослушиваемые сокеты даже если они в белом списке
|
||
show_whitelisted_listen_udp => 1, # отображать прослушиваемые сокеты даже если они в белом списке
|
||
show_listen_tcp => 1, # отображать слушающие tcp сокеты
|
||
show_listen_udp => 1, # отображать слушающие udp сокеты
|
||
show_client_tcp => 1, # отображать клиентские tcp сокеты
|
||
show_client_udp => 1, # отображать клиентские udp сокеты
|
||
show_local_tcp_connections => 1, # отображать локальные tcp соединения
|
||
show_local_udp_connections => 1, # отображать локлаьные udp соединения
|
||
show_open_files => 1 , # отображать открытые файлы всех приложений
|
||
};
|
||
|
||
# Также добавить белый список прослушиваемых портов и врубать анализ по нему в особо суровых случаях
|
||
my $blacklist_listen_ports = {
|
||
1080 => 'socks proxy',
|
||
3128 => 'http proxy',
|
||
6666 => 'irc',
|
||
6667 => 'irc alternative',
|
||
9050 => 'tor',
|
||
# botnet melinda & bill gates https://github.com/ValdikSS/billgates-botnet-tracker/blob/master/gates/gates.py
|
||
36008 => 'botnet melinda & bill gates',
|
||
};
|
||
|
||
my $whitelist_listen_udp_ports = {
|
||
53 => 1, # dns
|
||
111 => 1, # portmap
|
||
123 => 1, # ntp
|
||
137 => 1, # nmbd
|
||
138 => 1, # nmdb
|
||
11211 => 1, # memcached
|
||
};
|
||
|
||
my $whitelist_listen_tcp_ports = {
|
||
21 => 1, # ftp
|
||
22 => 1, # ssh
|
||
25 => 1, # smtp
|
||
53 => 1, # dns
|
||
80 => 1, # http
|
||
110 => 1, # pop3
|
||
143 => 1, # imap
|
||
443 => 1, # https
|
||
465 => 1, # smtps, secure smtp
|
||
587 => 1, # smtp submission
|
||
993 => 1, # imaps, secure imap
|
||
995 => 1, # pops, secure pop
|
||
1500 => 1, # ispmanager ihttpd
|
||
3306 => 1, # mysql
|
||
8080 => 1, # apache backend. ispmanager config
|
||
8888 => 1, # fastpanel https
|
||
11211 => 1, # memcached
|
||
10050 => 1, # zabbix agentd
|
||
};
|
||
|
||
my $binary_which_can_be_suid = {
|
||
# Набор ПО от ISPSystems очень беспокоится о своих правах и любит SUID
|
||
'/usr/local/ispmgr/bin/billmgr' => 1,
|
||
'/usr/local/ispmgr/bin/ispmgr' => 1,
|
||
'/usr/local/ispmgr/bin/vdsmgr' => 1,
|
||
'/usr/local/ispmgr/sbin/pbackup' => 1,
|
||
|
||
'/usr/bin/mtr' => 1, # mtr, debian
|
||
'/usr/sbin/postdrop' => 1,
|
||
'/usr/sbin/exim4' => 1,
|
||
'/usr/sbin/exim' => 1, # Centos exim
|
||
'/bin/su' => 1,
|
||
'/usr/bin/su' => 1,
|
||
'/usr/lib/sm.bin/sendmail' => 1,
|
||
'/usr/sbin/sendmail.sendmail' => 1,
|
||
'/usr/bin/screen' => 1,
|
||
'/usr/bin/sudo' => 1,
|
||
'/usr/bin/ssh-agent' => 1,
|
||
'/usr/bin/fping' => 1,
|
||
'/bin/mount' => 1,
|
||
'/bin/ping' => 1,
|
||
'/usr/local/ispmgr/cgi/download' => 1,
|
||
};
|
||
|
||
# Паттерны найденных вирусов
|
||
my $virus_patterns = {
|
||
'21f9a5ee8af73d2156333a654130f3f8' => 1, # ps_virus_ct_6205
|
||
'a6752df85f35e6adcfa724eb5e15f6d0' => 1, # virus_from_43165
|
||
'99ca61919f5afbdb3c0e07c30fad5bb1' => 1, # named bitcoin miner
|
||
'36c97cdd3caccbacb37708e84b22a627' => 1, # jawa, порутана машина
|
||
'36f6c1169433cc8a78498d54393132ed' => 1, # atd демон
|
||
'f9ad37bc11a4f5249b660cacadd14ad3' => 1, # sfewfesfs/pojie: Melinda & Bill gates malware
|
||
'9b6283e656f276c15f14b5f2532d24d2' => 1, # sfewfesfsh: Melinda & Bill gates malware
|
||
'd7cb8d530dd813f34bdcf1b6c485589b' => 1, # irc_bouncer_hidden_as_ssh_from_5560
|
||
};
|
||
|
||
# Список "хороших" открытых файлов, на которые не стоит даже реагировать
|
||
my $good_opened_files = {
|
||
'/dev/null' => 1,
|
||
'/dev/urandom' => 1,
|
||
'/dev/random' => 1,
|
||
'/dev/stdin' => 1,
|
||
'/dev/ptmx' => 1,
|
||
'/dev/pts/1' => 1,
|
||
'/dev/pts/0' => 1,
|
||
'/dev/console' => 1,
|
||
};
|
||
|
||
# cwd, которые не стоит считать подозрительными
|
||
my $good_cwd = {
|
||
'/var/run' => 1,
|
||
'/run/dovecot' => 1,
|
||
'/opt/php5/bin' => 1,
|
||
'/var/lib/mysql' => 1,
|
||
'/run/saslauthd' => 1,
|
||
'/var/spool/cron' => 1,
|
||
'/var/run/dovecot' => 1,
|
||
'/var/run/saslauthd' => 1,
|
||
'/var/www/admin/php-bin' => 1,
|
||
'/var/run/dovecot/login' => 1,
|
||
'/var/spool/postfix' => 1,
|
||
'/usr/local/ispmgr' => 1,
|
||
'/var/spool/mqueue' => 1,
|
||
'/usr/local/fastpanel/daemon' => 1,
|
||
'/run/dovecot/empty' => 1,
|
||
'/' => 1,
|
||
'/var/spool/clientmqueue' => 1,
|
||
'/var/spool/cron/atjobs' => 1,
|
||
};
|
||
|
||
|
||
|
||
# Хэш куда мы поместим карту: хэш - путь до бинарного файла
|
||
|
||
# Тут явно забиты бинарные файлы, которые распространяются вне пакетных менеджеров
|
||
my $hash_lookup_for_all_binary_files = {
|
||
# TODO: эти хэши забиты в порядке ОТЛАДКИ, это могут быть и протрояненые ispmgr!
|
||
# Найти способ узнать их чек суммы
|
||
'b9fa02373babd17406ed70eb943b8d31' => '/usr/local/ispmgr/sbin/ihttpd',
|
||
'a60730a0026a34188f3e203f7c572bb5' => '/usr/local/ispmgr/bin/ispmgr',
|
||
'b5eb4b504e6588ba237998dbac670a59' => '/usr/local/ispmgr/bin/ispmgr',
|
||
'1717a4987853e5e774b6ec6b0b0c448d' => '/usr/local/ispmgr/bin/ispmgr',
|
||
'9fbf9a6f9b24e5ca2b31b5990824a6ff' => '/usr/local/ispmgr/bin/ispmgr',
|
||
'e62c0a2a25eeb622c1320db8f5d9039d' => '/usr/local/ispmgr/bin/ispmgr',
|
||
'90899219b3e75b9d3064260c25270650' => '/usr/local/ispmgr/bin/ispmgr',
|
||
|
||
# А это Коля забил неверные чексуммы, issue уже передан в работу
|
||
'7241fcc1ce18d52e70810b65625bd61d' => '/opt/php5/bin/php',
|
||
'5008e5e31d2f7efe5516f280fd13681b' => '/opt/php5/bin/php',
|
||
'2c0144f5d550fa106081d1eaacbb033d' => '/opt/php5/bin/php',
|
||
'ed523eea1d33332acb38e620656c042a' => '/opt/php5/bin/php-cgi',
|
||
'ba05b2f694c61a314846a42801694dfe' => '/opt/php5/bin/php-cgi',
|
||
'e4ef72a1c092944dcf2886feeb581469' => '/opt/php5/bin/php-cgi',
|
||
|
||
# /sbin/syslogd не имеет в пакете чексумм
|
||
'21a265738651407ce0fcede295abd675' => '/sbin/syslogd',
|
||
'e98f49146b5e8203838a2d451835eb1a' => '/sbin/syslogd',
|
||
'58ae7c68e945f1d88b6fc0c46494812b' => '/sbin/syslogd',
|
||
|
||
# vzapi tools
|
||
'd77fb76bcddb774a8a541dce42667b5d' => '/usr/bin/md5_pipe',
|
||
};
|
||
|
||
|
||
# режима аудита, когда мы печатаем всю возможную извлеченную информацию о процессах
|
||
my $audit_mode = '';
|
||
|
||
my $execute_full_hash_validation = 0;
|
||
|
||
my $is_openvz_node = '';
|
||
|
||
# Проверяем окружение, на котором мы работаем
|
||
if (-e "/proc/user_beancounters" && -e "/proc/vz/fairsched") {
|
||
$is_openvz_node = 1;
|
||
}
|
||
|
||
my @running_containers = ();
|
||
|
||
# Если мы работем на OpenVZ ноде, то есть возможность передать для сканирования лишь конкретный контейнер
|
||
if ($is_openvz_node) {
|
||
|
||
# Если нам передали параметры командной строки, то сканируем переданный параметром контейнер
|
||
if (scalar @ARGV > 0 && $ARGV[0] =~ /^\d+$/) {
|
||
@running_containers = @ARGV;
|
||
} else {
|
||
@running_containers = get_running_containers_list();
|
||
}
|
||
|
||
}
|
||
|
||
if (scalar @ARGV > 0 && $ARGV[0] =~ /^\-\-audit$/) {
|
||
$audit_mode = 1;
|
||
}
|
||
|
||
# Список системных пользователей, которые в нормальных условиях не должны иметь свой crontab в /var/spool/cron/crontabs
|
||
my $users_which_cant_have_crontab = {
|
||
'www-data' => 1,
|
||
'apache' => 1,
|
||
};
|
||
|
||
|
||
# Проверка конетейнера на предмет не порутали ли его
|
||
my $global_check_functions = {
|
||
check_absent_login_information => \&check_absent_login_information,
|
||
check_user_crontabs => \&check_user_crontabs,
|
||
check_dirs_with_whitespaces => \&check_dirs_with_whitespaces,
|
||
};
|
||
|
||
# Проверки для процессов
|
||
my $process_checks = {
|
||
check_cmdline => \&check_cmdline,
|
||
check_for_deleted_exe => \&check_for_deleted_exe,
|
||
check_exe_files_by_checksumm => \&check_exe_files_by_checksumm,
|
||
check_process_open_fd => \&check_process_open_fd,
|
||
check_32bit_software_on_64_bit_server => \&check_32bit_software_on_64_bit_server,
|
||
check_ld_preload => \&check_ld_preload,
|
||
check_suid_exe => \&check_suid_exe,
|
||
check_process_parents => \&check_process_parents,
|
||
# check_binary_with_clamd => \&check_binary_with_clamd,
|
||
# check_changed_proc_name => \&check_changed_proc_name,
|
||
# check_cwd => \&check_cwd,
|
||
};
|
||
|
||
# TODO: реализовать
|
||
# Правила, описывающие поведение процессов
|
||
my $processes_rules = {
|
||
'apache_debian' => {
|
||
'uid' => 33,
|
||
'gid' => 33,
|
||
'exe' => "/usr/lib/apache2/mpm-prefork/apache2",
|
||
'name' => "apache2",
|
||
'can_listen' => [ '80', '81', '8080', '443' ],
|
||
}
|
||
};
|
||
|
||
|
||
# TODO: сделать этот валидатор не локальным
|
||
# Для отдельного сервера вполне посильная задача собрать ключевые суммы
|
||
# $execute_full_hash_validation = 1;
|
||
|
||
process_standard_linux_server();
|
||
|
||
# В случае OpenVZ ноды мы обходим все контейнеры
|
||
CONTAINERS_LOOP:
|
||
for my $container (@running_containers) {
|
||
if ($container eq '1' or $container eq '50') {
|
||
# Skip PCS special containers
|
||
next;
|
||
}
|
||
|
||
|
||
my @ct_processes_pids = read_file_contents_to_list("/proc/vz/fairsched/$container/tasks");
|
||
|
||
# Тут мы читаем псевдо-файла /proc/CT_INIT_PID/net/*, так как там содержатся все соединения для данного контейнера,
|
||
# а вовсе не соединения для данного процесса
|
||
|
||
# TODO: ВЫПИЛИТЬ дублированное получение pid
|
||
my $container_init_process_pid_on_node = get_init_pid_for_container(\@ct_processes_pids);
|
||
|
||
my $connections = read_all_namespace_connections($container_init_process_pid_on_node);
|
||
my $inode_to_socket = build_inode_to_socket_lookup_table($connections);
|
||
|
||
# Собираем хэш всех бинарных файлов контейнера для последующей валидации
|
||
if ($execute_full_hash_validation) {
|
||
$hash_lookup_for_all_binary_files = {};
|
||
#### build_hash_for_all_binarys($container);
|
||
}
|
||
|
||
for my $check_function_name ( keys %$global_check_functions ) {
|
||
#print "We call function $check_function_name for $container\n";
|
||
my $sub_ref = $global_check_functions->{$check_function_name};
|
||
$sub_ref->($container);
|
||
}
|
||
|
||
check_orphan_connections($container, $inode_to_socket);
|
||
|
||
my $server_processes_pids = get_server_processes_detailed( { inode_to_socket => $inode_to_socket, ctid => $container } );
|
||
|
||
if ($audit_mode) {
|
||
print "We see on container $container\n";
|
||
build_process_tree($server_processes_pids);
|
||
# skip checks
|
||
next CONTAINERS_LOOP;
|
||
}
|
||
|
||
PROCESSES_LOOP:
|
||
for my $pid (keys %$server_processes_pids) {
|
||
my $status = $server_processes_pids->{$pid};
|
||
|
||
call_process_checks($pid, $status);
|
||
}
|
||
|
||
}
|
||
|
||
# Запускаем все проверки для контейнера
|
||
sub call_process_checks {
|
||
my ($pid, $status, $inode_to_socket) = @_;
|
||
|
||
# Вызываем последовательно все указанные функции для каждого процесса
|
||
for my $check_function_name ( keys %$process_checks ) {
|
||
# Если процесс перестал существовать во время проверки, то, увы, мы переходим к следующему
|
||
unless (-e "/proc/$pid") {
|
||
return "";
|
||
}
|
||
|
||
#print "We call function $check_function_name for process $pid\n";
|
||
my $sub_ref = $process_checks->{$check_function_name};
|
||
$sub_ref->($pid, $status, $inode_to_socket);
|
||
}
|
||
}
|
||
|
||
|
||
# Обработка обычного сервера
|
||
sub process_standard_linux_server {
|
||
my $connections = read_all_namespace_connections();
|
||
my $inode_to_socket = build_inode_to_socket_lookup_table($connections);
|
||
|
||
# Собираем хэш всех бинарных файлов контейнера для последующей валидации
|
||
if ($execute_full_hash_validation) {
|
||
#build_hash_for_all_binarys('');
|
||
}
|
||
|
||
for my $check_function_name ( keys %$global_check_functions ) {
|
||
#print "We call function $check_function_name for $container\n";
|
||
my $sub_ref = $global_check_functions->{$check_function_name};
|
||
$sub_ref->();
|
||
}
|
||
|
||
check_orphan_connections(0, $inode_to_socket);
|
||
|
||
# В этом подходе есть еще большая проблема, дублирование inode внутри контейнеров нету, но
|
||
# есть куча "потерянных" соединений, у которых владелец inode = 0, с ними нужно что-то делать
|
||
|
||
# То, что мы запрашиваем CTID 0 означает, что в случае если это OpenVZ мы получим все процессы CT 0, то есть аппаратной ноды
|
||
# а если работаме на железе без виртулизации и прочего - получим просто список процессов
|
||
my $server_processes_pids = get_server_processes_detailed( { inode_to_socket => $inode_to_socket, ctid => 0 } );
|
||
|
||
if ($audit_mode) {
|
||
build_process_tree($server_processes_pids);
|
||
# skip other checks
|
||
return;
|
||
}
|
||
|
||
PROCESSES_LOOP:
|
||
for my $pid (keys %$server_processes_pids) {
|
||
my $status = $server_processes_pids->{$pid};
|
||
|
||
call_process_checks($pid, $status, $inode_to_socket);
|
||
}
|
||
}
|
||
|
||
|
||
# Если у процесса есть множество дочерних форков с аналогичным набором параметров и дескрипторов, то исключаем их из рассмотрения вообще
|
||
sub filter_multiple_forks {
|
||
my ($server_processes_pids, $sorted_pids) = @_;
|
||
|
||
my @result = ();
|
||
|
||
my $prev_item = '';
|
||
# Уникализация процессов, какой смысл рассматривать 50 форков апача как отдельные?
|
||
PID_LOOP:
|
||
for my $pid (@$sorted_pids) {
|
||
my $status = $server_processes_pids->{$pid};
|
||
|
||
# В первый запуск просто положим туда следующий
|
||
if ($prev_item) {
|
||
if (compare_two_hashes_by_list_of_fields($status, $prev_item,
|
||
('PPid', 'fast_exe', 'Name', 'fast_uid', 'fast_gid', 'fast_fds_checksumm') ) ) {
|
||
# Если это клон предыдущего процесса, то просто скачем на следующую итерацию и тем самым исключаем его из обработки
|
||
# print "Pid $pid $status->{Name} is clone\n";
|
||
next PID_LOOP;
|
||
}
|
||
}
|
||
|
||
push @result, $pid;
|
||
$prev_item = $status;
|
||
}
|
||
|
||
return @result;
|
||
}
|
||
|
||
# Главная рабочая функция режима audit, отображаем всю возможную информацию по процессу
|
||
sub build_process_tree {
|
||
my $server_processes_pids = shift;
|
||
|
||
# Вершина дерева - нулевой pid, это ядро
|
||
#my $tree = Tree::Simple->new("0", Tree::Simple->ROOT);
|
||
|
||
# Сортировка по PID родительского процесса кажется мне самой логичной
|
||
my @sorted_pids = sort {
|
||
$server_processes_pids->{$a}->{PPid} <=>
|
||
$server_processes_pids->{$b}->{PPid}
|
||
} keys %$server_processes_pids;
|
||
|
||
if ($audit_params->{compress_forks}) {
|
||
@sorted_pids = filter_multiple_forks($server_processes_pids, [@sorted_pids]);
|
||
}
|
||
|
||
for my $pid (@sorted_pids) {
|
||
my $status = $server_processes_pids->{$pid};
|
||
|
||
my @sorted_fds = sort { $a->{type} cmp $b->{type} } @{ $status->{fast_fds} };
|
||
|
||
if ($audit_params->{show_process_information}) {
|
||
print get_printable_process_status($pid, $status) . "\n";
|
||
|
||
if (scalar @sorted_fds > 0) {
|
||
print "\n";
|
||
}
|
||
}
|
||
|
||
# Сортируем по типу перед отображением
|
||
FDS_LOOP:
|
||
for my $fd (@sorted_fds) {
|
||
if ($fd->{type} eq 'tcp') {
|
||
if ($audit_params->{show_tcp}) {
|
||
if (is_listen_connection($fd->{connection}) ) {
|
||
my $it_is_whitelisted_connection = $whitelist_listen_tcp_ports->{ $fd->{connection}->{local_port} };
|
||
|
||
if ($audit_params->{show_listen_tcp}) {
|
||
my $show = 1;
|
||
|
||
# Мы попали на соединение в белом списке
|
||
# И если его отображение запрещено, то скрываем
|
||
if ($it_is_whitelisted_connection && ! $audit_params->{show_whitelisted_listen_tcp}) {
|
||
$show = 0;
|
||
}
|
||
|
||
if ($show) {
|
||
print connection_pretty_print($fd->{connection}) . "\n";
|
||
}
|
||
}
|
||
} else {
|
||
print connection_pretty_print($fd->{connection}) . "\n" if $audit_params->{show_client_tcp};
|
||
}
|
||
}
|
||
} elsif ($fd->{type} eq 'udp') {
|
||
if ($audit_params->{show_udp}) {
|
||
if (is_listen_connection($fd->{connection}) ) {
|
||
my $it_is_whitelisted_connection = $whitelist_listen_udp_ports->{ $fd->{connection}->{local_port} };
|
||
|
||
if ($audit_params->{show_listen_udp}) {
|
||
my $show = 1;
|
||
|
||
# Мы попали на соединение в белом списке
|
||
# И если его отображение запрещено, то скрываем
|
||
if ($it_is_whitelisted_connection && ! $audit_params->{show_whitelisted_listen_udp}) {
|
||
$show = 0;
|
||
}
|
||
|
||
if ($show) {
|
||
print connection_pretty_print($fd->{connection}) . "\n";
|
||
}
|
||
}
|
||
} else {
|
||
print connection_pretty_print($fd->{connection}) . "\n" if $audit_params->{show_client_udp};
|
||
}
|
||
}
|
||
} elsif ($fd->{type} eq 'file') {
|
||
unless( $good_opened_files->{ $fd->{path} } ) {
|
||
if ($audit_params->{show_open_files}) {
|
||
print "file: $fd->{path}\n";
|
||
}
|
||
}
|
||
} else {
|
||
# another connections
|
||
}
|
||
}
|
||
|
||
if ($audit_params->{show_process_information}) {
|
||
print "\n\n";
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
# Получить список процессов забитый информацией о них
|
||
sub get_server_processes_detailed {
|
||
my $params = shift;
|
||
|
||
my $ctid = $params->{ctid};
|
||
my $inode_to_socket = $params->{inode_to_socket};
|
||
|
||
my $processes = {};
|
||
|
||
my $is_openvz_node = '';
|
||
if (-e "/proc/user_beancounters" && -e "/proc/vz/fairsched") {
|
||
$is_openvz_node = '1';
|
||
}
|
||
|
||
my @process_pids = ();
|
||
|
||
my $init_process_pid = 1;
|
||
if ($is_openvz_node) {
|
||
# Да, для OpenVZ это очень удобный способ получения содержимого ноды
|
||
@process_pids = read_file_contents_to_list("/proc/vz/fairsched/$ctid/tasks");
|
||
} else {
|
||
@process_pids = get_server_processes();
|
||
}
|
||
|
||
my $passwd_data = '';
|
||
if ($is_openvz_node) {
|
||
if ($ctid == 0 ) {
|
||
$init_process_pid = 1;
|
||
$passwd_data = parse_passwd_file("/etc/passwd");
|
||
} else {
|
||
$init_process_pid = get_init_pid_for_container(\@process_pids);
|
||
$passwd_data = parse_passwd_file("/vz/root/$ctid/etc/passwd");
|
||
}
|
||
} else {
|
||
$init_process_pid = 1;
|
||
$passwd_data = parse_passwd_file("/etc/passwd");
|
||
}
|
||
|
||
my @container_ips = ();
|
||
if ($ctid > 0) {
|
||
@container_ips = get_ips_for_container($ctid);
|
||
}
|
||
|
||
my $it_is_openvz_container = -e "/proc/user_beancounters";
|
||
|
||
my $init_elf_info = `cat /proc/$init_process_pid/exe | file -`;
|
||
chomp $init_elf_info;
|
||
|
||
my $server_architecture = get_architecture_by_file_info_output($init_elf_info);
|
||
|
||
PROCESSES_LOOP:
|
||
for my $pid (@process_pids) {
|
||
my $status = get_proc_status($pid);
|
||
|
||
unless ($status) {
|
||
next;
|
||
}
|
||
|
||
$status = process_status($pid, $status, $inode_to_socket);
|
||
|
||
# Таким хитрым образом мы можем скрывать системные процессы ядра
|
||
unless (defined($status->{fast_cmdline})) {
|
||
next;
|
||
}
|
||
|
||
# В случае, если со стороны ноды имеется vzctl, который инжектирован в пространство контейнера,
|
||
# то его exe файлы и прочее прочесть нельзя - исключаем его из рассмотрения
|
||
# stat /proc/14865/exe
|
||
# File: `/proc/14865/exe'stat: cannot read symbolic link `/proc/14865/exe': Permission denied
|
||
if ($it_is_openvz_container && $status->{Name} eq 'vzctl') {
|
||
my @stat_data = stat "/proc/$pid/exe";
|
||
|
||
if (scalar @stat_data == 0 && $! eq 'Permission denied') {
|
||
# Исключаем его как процесс с ноды
|
||
next PROCESSES_LOOP;
|
||
}
|
||
# И если при stat мы получаем ошибку доступа, то это правда vzctl с ноды
|
||
}
|
||
|
||
# Получаем информацию о соотвествии имен и uid пользователей
|
||
$status->{fast_passwd} = $passwd_data;
|
||
|
||
# Добавляем параметр "архитектура хост контейнера"
|
||
$status->{fast_container_architecture} = $server_architecture;
|
||
|
||
# Добавляем псевдо параметр - local_ips, это локальные IP контейнера
|
||
$status->{fast_local_ips} = [ @container_ips ];
|
||
|
||
$processes->{$pid} = $status;
|
||
}
|
||
|
||
return $processes;
|
||
}
|
||
|
||
|
||
|
||
# Обработать статус процесса, добавив в него ряд полезных пунктов
|
||
sub process_status {
|
||
my $pid = shift;
|
||
my $status = shift;
|
||
my $inode_to_socket = shift;
|
||
|
||
# В случае, если все Uid/Gid у нас совпадают
|
||
$status->{fast_uid} = get_process_uid_or_gid('Uid', $status);
|
||
$status->{fast_gid} = get_process_uid_or_gid('Gid', $status);
|
||
|
||
# также добавляем в статус фейковые параметры: exe/cwd, так как они нам многократно пригодятся
|
||
my $cwd_path = "/proc/$pid/cwd";
|
||
my $exe_path = "/proc/$pid/exe";
|
||
|
||
$status->{fast_cwd} = readlink($cwd_path);
|
||
$status->{fast_exe} = readlink($exe_path);
|
||
|
||
unless (defined($status->{fast_cwd})) {
|
||
$status->{fast_cwd} = '';
|
||
}
|
||
|
||
unless (defined($status->{fast_exe})) {
|
||
$status->{fast_exe} = '';
|
||
}
|
||
|
||
|
||
# в exe может быть еще вот такое чудо: ' (deleted)/opt/php5/bin/php-cgi'
|
||
|
||
# Для кучи контейнеров cwd прописан вот так /vz/root/54484 то есть с уровня ноды, а нам это не нужно,
|
||
# Для не openvz машин никаких последствий такое не несет
|
||
if ($status->{fast_cwd}) {
|
||
$status->{fast_cwd} =~ s#/vz/root/\d+/?#/#g;
|
||
}
|
||
|
||
if ($status->{fast_exe}) {
|
||
$status->{fast_exe} =~ s#/vz/root/\d+/?#/#g;
|
||
}
|
||
|
||
# обрабатываем cmdline
|
||
$status->{fast_cmdline} = read_file_contents("/proc/$pid/cmdline");
|
||
|
||
if ($status->{fast_cmdline}) {
|
||
# Но /proc/$pid/cmdline интересен тем, что в нем используются разделители \0 и их нужно разделить на пробелы
|
||
$status->{fast_cmdline} =~ s/\0/ /g;
|
||
}
|
||
|
||
# исключаем из рассмотрения системные процессы ядра
|
||
if (defined($status->{fast_cmdline}) && $status->{fast_cmdline}) {
|
||
# обрабатываем environ
|
||
my $environ_data = read_file_contents("/proc/$pid/environ");
|
||
|
||
if (defined($environ_data)) {
|
||
$status->{fast_environ} = {};
|
||
|
||
# тут также используются \0 как разделители
|
||
for my $env_elem (split /\0/, $environ_data) {
|
||
my @env_raw = split '=', $env_elem, 2;
|
||
|
||
# Внутри может быть всякая бинарная хренотень, так что что реагируем лишь в случае, если нашли знак =
|
||
if (scalar @env_raw == 2) {
|
||
$status->{fast_environ}->{$env_raw[0]} = $env_raw[1];
|
||
}
|
||
}
|
||
|
||
}
|
||
}
|
||
|
||
# Получаем удобный для обработки список дескрипторов (файлов+сокетов) пороцесса
|
||
$status->{fast_fds} = get_process_connections($pid, $inode_to_socket);
|
||
|
||
# В режиме аудита нам часто нужна дедупликация процессов, чтобы не показывать 1000 форков
|
||
if ($audit_mode) {
|
||
$status->{fast_fds_checksumm} = create_structure_hash($status->{fast_fds});
|
||
}
|
||
|
||
return $status;
|
||
}
|
||
|
||
|
||
|
||
|
||
# Проверяем на предмет наличия папок с пробельными именами в /tmp
|
||
sub check_dirs_with_whitespaces {
|
||
my $ctid = shift;
|
||
|
||
my $prefix = '';
|
||
|
||
if ($ctid) {
|
||
$prefix = "/vz/root/$ctid";
|
||
}
|
||
|
||
TEMP_FOLDERS_LOOP:
|
||
for my $temp_folder ("$prefix/tmp", "$prefix/var/tmp", "$prefix/dev/shm") {
|
||
unless (-e $temp_folder) {
|
||
next TEMP_FOLDERS_LOOP;
|
||
}
|
||
|
||
my @files = list_all_in_dir($temp_folder);
|
||
|
||
for my $file (@files) {
|
||
# Хакеры очень любят пробельные имена, три точки или "скрытые" - начинающиеся с точки
|
||
if ($file =~ /^\s+$/ or $file =~ /^\.{3,}$/ or $file =~ /^\./) {
|
||
if (-f "$temp_folder/$file" && get_file_size("$temp_folder/$file") > 0) {
|
||
if ($ctid) {
|
||
warn "We found a file with suspicious name $file in CT $ctid in directory: $temp_folder\n";
|
||
} else {
|
||
warn "We found a file with suspicious name $file in directory: $temp_folder\n";
|
||
}
|
||
}
|
||
|
||
# Мы реагируем только на НЕ пустые папки
|
||
if (-d "$temp_folder/$file") {
|
||
my @folder_content = list_all_in_dir("$temp_folder/$file");
|
||
if (scalar @folder_content > 0 ) {
|
||
if ($ctid) {
|
||
warn "We found not empty directory $file (@folder_content) which possibly hidden in directory: $temp_folder in CT $ctid\n";
|
||
} else {
|
||
warn "We found not empty directory $file (@folder_content) which possibly hidden in directory: $temp_folder\n";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# Проверяем на предмет того, что бинарный файл имеет SUID флаг
|
||
sub check_suid_exe {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $prefix = '';
|
||
|
||
if (defined($status->{envID}) && $status->{envID}) {
|
||
$prefix = "/vz/root/$status->{envID}";
|
||
}
|
||
|
||
# Если файл не существует или удален, то тупо скипаем эту проверку
|
||
unless (-e "$prefix/$status->{fast_exe}") {
|
||
return;
|
||
}
|
||
|
||
my @stat_data = stat "$prefix/$status->{fast_exe}";
|
||
|
||
my $mode = $stat_data[2];
|
||
|
||
my $is_suid = $mode & S_ISUID;
|
||
|
||
my $is_sgid = $mode & S_ISGID;
|
||
|
||
# Convert to bool form
|
||
if ($is_suid) {
|
||
$is_suid = 1;
|
||
}
|
||
|
||
if ($is_sgid) {
|
||
$is_sgid = 1;
|
||
}
|
||
|
||
if ($is_suid or $is_sgid) {
|
||
# Если бинарика нет в списке разрешенных, то, очевидно, стоит о нем упомянуть
|
||
unless ( $binary_which_can_be_suid->{ $status->{fast_exe} } ) {
|
||
print_process_warning($pid, $status, "we found SUID ($is_suid) or SGID bit ($is_sgid) enabled, it's very dangerous");
|
||
}
|
||
}
|
||
}
|
||
|
||
# Печатаем уведомление о подозрительном процессе
|
||
sub print_process_warning {
|
||
my $pid = shift;
|
||
my $status = shift;
|
||
my $text = shift;
|
||
|
||
my $container_data = '';
|
||
if (defined($status->{envID}) && $status->{envID}) {
|
||
$container_data = " from CT: $status->{envID}";
|
||
}
|
||
|
||
print "We got warning about process" ."$container_data: '$text'\n" . get_printable_process_status($pid, $status) . "\n\n";
|
||
}
|
||
|
||
sub get_printable_process_status {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $container_data = '';
|
||
if (defined($status->{envID}) && $status->{envID}) {
|
||
$container_data = "CT: $status->{envID}";
|
||
}
|
||
|
||
return "pid: $pid name: $status->{Name} ppid: $status->{PPid} uid: $status->{fast_uid} gid: $status->{fast_gid} $container_data\n" .
|
||
"exe path: $status->{fast_exe}\n" .
|
||
"cwd: $status->{fast_cwd}\n" .
|
||
"cmdline: $status->{fast_cmdline}";
|
||
}
|
||
|
||
# Проверка истинности процесса - тот ли он, за кого себя выдает
|
||
sub check_process_truth {
|
||
my ($pid, $status) = @_;
|
||
|
||
# TODO: должна осуществляться по правилам: $processes_rules
|
||
}
|
||
|
||
# Проверяем родителей процесса и если среди них есть Апач, то стоит этот случай исследовать
|
||
sub check_process_parents {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $parent_pid = $status->{PPid};
|
||
|
||
# Этот процесс запущен сам по себе, он нас не интересует, скорее всего он системный
|
||
if ($parent_pid == 1) {
|
||
return;
|
||
}
|
||
|
||
# TODO: здесь нужно разместить код эвристической проверки
|
||
}
|
||
|
||
# Проверка на предмет загрузки того или иного сервиса с LD_PRELOAD
|
||
sub check_ld_preload {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $ld_preload_whitelist = {
|
||
'/usr/lib/authbind/libauthbind.so.1' => 1, # https://packages.debian.org/wheezy/authbind, его использует tomcat
|
||
'/usr/local/lib/authbind/libauthbind.so.1' => 1, # Bitrix smtpd.php
|
||
};
|
||
|
||
if ( defined($status->{fast_environ}) && $status->{fast_environ} ) {
|
||
# Тут бывают вполне легальные использования, например: http://manpages.ubuntu.com/manpages/hardy/man1/authbind.1.html
|
||
# Такой "подход" используется в Bitrix (SIC) environment
|
||
|
||
my $ld_preload = $status->{fast_environ}->{'LD_PRELOAD'};
|
||
if (defined($ld_preload) && $ld_preload && !defined ($ld_preload_whitelist->{$ld_preload})) {
|
||
print_process_warning($pid, $status, "This process loaded with LD_PRELOAD ($ld_preload) an it may be a VIRUS");
|
||
}
|
||
}
|
||
}
|
||
|
||
sub check_user_crontabs {
|
||
my $ctid = shift;
|
||
|
||
my $prefix = '';
|
||
|
||
if ($ctid) {
|
||
$prefix = "/vz/root/$ctid";
|
||
}
|
||
|
||
# debian / centos
|
||
for my $cron_folder ("$prefix/var/spool/cron/crontabs", "$prefix/var/spool/cron") {
|
||
|
||
my @crontab_files = list_files_in_dir($cron_folder);
|
||
|
||
for my $cron_file (@crontab_files) {
|
||
if ($users_which_cant_have_crontab->{ $cron_file }) {
|
||
my @crontab_file_contents = read_file_contents_to_list("$cron_folder/$cron_file");
|
||
|
||
# Фильтруем строки с комментариями
|
||
@crontab_file_contents = grep { if(!/^#/ and length ($_) > 0) {1;} else {0;}} @crontab_file_contents;
|
||
|
||
# Отключим реагирование на служебную команду MAILTO
|
||
@crontab_file_contents = grep { !/^(?:MAILTO|PATH)/ } @crontab_file_contents;
|
||
|
||
if (scalar @crontab_file_contents > 0) {
|
||
if ($ctid) {
|
||
warn "Please check CT $ctid ASAP because it probably has malware in user cron\n";
|
||
} else {
|
||
warn "Please check crontab ASAP because it probably has malware in user cron\n";
|
||
}
|
||
warn "Cron content for $cron_file: " . ( join ",", @crontab_file_contents ) . "\n"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
# Валидатор cmdline, так как почти всегда, если он начинается с ./ то жди проблем
|
||
sub check_cmdline {
|
||
my ($pid, $status) = @_;
|
||
|
||
if ($status->{fast_cmdline} && $status->{fast_cmdline} =~ m/^\./) {
|
||
|
||
# Тимспики запускают только так, так что уберем ругань на них
|
||
if ($status->{Name} =~ /ts3server_linux/ ) {
|
||
return;
|
||
}
|
||
|
||
# Если программа запущена от рута, то уведомлять о таком нет особого смысла, так как пользователи часто так делают да и руткиты могут прописаться в системные пути и не обязательно будут запущены вручную
|
||
unless ($status->{fast_uid} == 0 && $status->{fast_gid} == 0) {
|
||
print_process_warning($pid, $status, "it running manually from NOT root user and it's very dangerous");
|
||
}
|
||
}
|
||
}
|
||
|
||
sub check_exe_files_by_checksumm {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $prefix = '';;
|
||
|
||
if (defined($status->{envID}) && $status->{envID}) {
|
||
$prefix = "/vz/root/$status->{envID}";
|
||
}
|
||
|
||
# рассчитаем md5, оно сработает даже для удаленного файла
|
||
my $md5 = md5_file("/proc/$pid/exe");
|
||
|
||
unless ($md5) {
|
||
warn "Can't calulate md5 for exe from process $pid\n";
|
||
return;
|
||
}
|
||
|
||
# Проверяем, чтобы файл был захэширован корректно
|
||
unless ($status->{fast_hash_lookup_for_all_binary_files}->{$md5}) {
|
||
|
||
if ($execute_full_hash_validation) {
|
||
if (-e "$prefix/usr/bin/dpkg") {
|
||
print_process_warning($pid, $status, "can't find checksumm ($md5) for this binary file in packages database. Please check it");
|
||
} else {
|
||
# TODO:
|
||
# Это CentOS и как извлечь из него сигнатуры я пока совершенно не понимаю
|
||
}
|
||
}
|
||
}
|
||
|
||
if ($virus_patterns->{$md5}) {
|
||
print_process_warning($pid, $status, "it's 100% virus");
|
||
}
|
||
}
|
||
|
||
# Проверка на предмет того, что исполняемый файл приложения удален после запуска
|
||
sub check_for_deleted_exe {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $prefix = '';;
|
||
|
||
if (defined($status->{envID}) && $status->{envID}) {
|
||
$prefix = "/vz/root/$status->{envID}";
|
||
}
|
||
|
||
if ($status->{fast_exe} =~ m/deleted/) {
|
||
my $exe_path = $status->{fast_exe};
|
||
|
||
# TODO: выпилить обходники!
|
||
# Чудеса нашей fastpanel, она не перезапускает свои CGI процессы, баг отрепорчен:
|
||
# Исклчюение для /var/www/admin/php-bin сделано по причине вот такого поведения CGI процессов FastPanel:
|
||
# cwd -> /var/www/admin/php-bin
|
||
# exe -> (deleted)/tmp/rst16186.000510e0
|
||
if ($status->{fast_exe} =~ m#/opt/php5/bin/php-cgi# or $status->{fast_cwd} =~ m#/var/www/admin/php-bin# ) {
|
||
return;
|
||
}
|
||
|
||
# Приведем путь в порядок
|
||
# (deleted)/usr/bin/php5-cgi
|
||
$exe_path =~ s#^\s*\(deleted\)\s*##;
|
||
|
||
# Debian 6 Squeeze в этом плане выпендривается и пишет deleted в конце:
|
||
# /proc/10187/exe -> /usr/bin/php5 (deleted)
|
||
$exe_path =~ s#\s*\(deleted\)$##;
|
||
|
||
# Тут бывают случаи: бинарик удален и приложение оставлено работать либо бинарник заменен, а софт работает со старого бинарика
|
||
# Первое - скорее всего малварь, иначе - обновление софта без обновление либ
|
||
unless (-e "$prefix/$exe_path") {
|
||
print_process_warning($pid, $status, "Executable file for this process was removed, it's looks like malware");
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
# Обойдем все открыте соединения процессов и оценим их состояние
|
||
sub check_process_open_fd {
|
||
my ($pid, $status) = @_;
|
||
|
||
# Хэш, в котором будет храниться число соединений на удаленный сервер от процесса на определенный порт
|
||
my $connections_to_remote_servers = {};
|
||
|
||
# Тут у нас может быть информация о локальных IP
|
||
if ($status->{fast_local_ips}) {
|
||
|
||
}
|
||
|
||
my $process_connections = $status->{fast_fds};
|
||
|
||
#print Dumper($process_connections);
|
||
|
||
CONNECTIONS_LOOP:
|
||
for my $connection (@$process_connections) {
|
||
if ($connection->{type} eq 'unknown') {
|
||
# TODO:
|
||
next CONNECTIONS_LOOP;
|
||
} elsif (in_array($connection->{type}, ('udp', 'tcp') ) ) {
|
||
my $connection = $connection->{connection};
|
||
|
||
if (is_listen_connection($connection)) {
|
||
# listen socket
|
||
|
||
# Если тот или иной софт забинден на локалхост, то он нас не интересует
|
||
if (is_loopback_address($connection->{local_address})) {
|
||
next CONNECTIONS_LOOP;
|
||
}
|
||
|
||
if (my $port_description = $blacklist_listen_ports->{ $connection->{local_port} }) {
|
||
print_process_warning($pid, $status, "process listens DANGER ($port_description) $connection->{socket_type} port $connection->{local_port}");
|
||
}
|
||
} else {
|
||
# Это может быть внутренее соединение, которое не интересно нам при анализе
|
||
if (is_loopback_address($connection->{rem_address})) {
|
||
next CONNECTIONS_LOOP;
|
||
}
|
||
|
||
# Увеличим посчитаем число соединений на удаленные машины с детализацией по портам
|
||
$connections_to_remote_servers->{ $connection->{rem_port} } ++;
|
||
|
||
# client socket
|
||
if (my $port_description = $blacklist_listen_ports->{ $connection->{rem_port} }) {
|
||
print_process_warning($pid, $status, "process connected to DANGER ($port_description) $connection->{socket_type} port $connection->{rem_port} to the server $connection->{rem_address}");
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
for my $remote_port_iteration (scalar keys %$connections_to_remote_servers > 0) {
|
||
my $number_of_connections = $connections_to_remote_servers->{ $remote_port_iteration };
|
||
|
||
if (defined($number_of_connections) && $number_of_connections > 5) {
|
||
print_process_warning($pid, $status, "it has $number_of_connections connections to $remote_port_iteration. Looks like flood bot");
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
# Если btmp/wtmp удален, то скорее всего машину порутали
|
||
sub check_absent_login_information {
|
||
my $ctid = shift;
|
||
|
||
my $prefix = '';
|
||
|
||
if ($ctid) {
|
||
$prefix = "/vz/root/$ctid";
|
||
}
|
||
|
||
# Debian: btmp
|
||
# CentOS: wtmp
|
||
unless (-e "$prefix/var/log/btmp" or -e "$prefix/var/log/wtmp") {
|
||
if ($ctid) {
|
||
warn "CT $ctid is probably rooted because btmp/wtmp file is absent";
|
||
} else {
|
||
warn "Server is probably rooted because btmp/wtmp file is absent";
|
||
}
|
||
}
|
||
}
|
||
|
||
# Тут нужно собрать все файлы открытые на сервере
|
||
my $global_opened_files = {};
|
||
sub run_clamav {
|
||
open my $fl, ">", "files_to_scan" or die "Can't";
|
||
for(keys %$global_opened_files) {
|
||
#system("maldet -a $_");
|
||
#system("clamscan $_|grep infected -i");
|
||
print {$fl} "$_\n";
|
||
}
|
||
|
||
# Call freshclam every startup
|
||
system("clamscan --file-list=files_to_scan --infected -d /usr/local/maldetect/sigs/rfxn.ndb -d /usr/local/maldetect/sigs/rfxn.hdb -d /var/lib/clamav");
|
||
}
|
||
|
||
my $get_debian_package_name_by_path = {
|
||
};
|
||
|
||
sub build_hash_for_all_binarys {
|
||
my $ctid = shift;
|
||
|
||
my $hash_lookup_for_all_binary_files = {};
|
||
|
||
my $prefix = '';
|
||
if (defined($ctid) && $ctid) {
|
||
$prefix = "/vz/root/$ctid";
|
||
} else {
|
||
$prefix = '';
|
||
}
|
||
|
||
# Это дебиян и мы можем выполнить валидацию
|
||
if (-e "$prefix/usr/bin/dpkg") {
|
||
my @files = list_files_in_dir("$prefix/var/lib/dpkg/info");
|
||
# Фильтруем лишь sums файлы
|
||
@files = grep { /\.md5sums$/ } @files;
|
||
|
||
for my $file (@files) {
|
||
my @file_content = read_file_contents_to_list("$prefix/var/lib/dpkg/info/$file");
|
||
|
||
for my $line (@file_content) {
|
||
# TODO: улучшить фильтрацию
|
||
# А вот бинарики апача: usr/lib/apache2/mpm-worker/apache2
|
||
# Бинарики пофикса: usr/lib/postfix/qmqpd
|
||
if ($line =~ m#(bin|lib)#) {
|
||
my @data = split /\s+/, $line, 2;
|
||
|
||
$hash_lookup_for_all_binary_files-> { $data[0] } = "/$data[1]";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return $hash_lookup_for_all_binary_files;
|
||
}
|
||
|
||
|
||
|
||
# Проверить бинарный файл антииврусом ClamAV
|
||
sub check_binary_with_clamd {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $scan_raw_result = `cat /proc/$pid/exe | clamdscan -`;
|
||
chomp $scan_raw_result;
|
||
|
||
my $scan_result = '';
|
||
|
||
if ($scan_raw_result =~ m/stream: (.+)$/im) {
|
||
$scan_result = $1;
|
||
}
|
||
|
||
# Это похожа на ложно положительные срабатывания ClamAV
|
||
my $exclude_codes = {
|
||
"Heuristics.Broken.Executable" => 1,
|
||
};
|
||
|
||
if ($scan_result && $scan_result eq 'OK') {
|
||
# все ок!
|
||
} else {
|
||
my $virus_name = $scan_result;
|
||
$virus_name =~ s/\s+FOUND//;
|
||
|
||
# Исключаем ложно положительные и сообщаем о вирусе
|
||
unless ($exclude_codes->{ $virus_name } ) {
|
||
print_process_warning($pid, $status, "We found a virus: $scan_result");
|
||
}
|
||
}
|
||
}
|
||
|
||
# Проверим, чтобы все ПО запущенное на сервере было той же архитектуры, что и система на сервере
|
||
# Также проверяем на тип Elf файла и сообщаем о любом статически линкованном ПО
|
||
sub check_32bit_software_on_64_bit_server {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $prefix = '';;
|
||
|
||
if (defined($status->{envID}) && $status->{envID}) {
|
||
$prefix = "/vz/root/$status->{envID}";
|
||
}
|
||
|
||
my $running_elf_file_architecture = '';
|
||
|
||
# Мы делаем хак через pipe, чтобы file корректно работал с удаленными файлами и читал файл, а не симлинк
|
||
my $elf_file_info = `cat /proc/$pid/exe| file -`;
|
||
chomp $elf_file_info;
|
||
|
||
# Если файл не существует или удален, то тупо скипаем эту проверку
|
||
unless (-e "$prefix/$status->{fast_exe}") {
|
||
return;
|
||
}
|
||
|
||
$running_elf_file_architecture = get_architecture_by_file_info_output($elf_file_info);
|
||
|
||
my $running_elf_file_type = get_binary_file_type_by_file_info_output($elf_file_info);
|
||
|
||
unless ($running_elf_file_type) {
|
||
print_process_warning($pid, $status, "Can't get file type for: $pid raw output: $running_elf_file_type");
|
||
}
|
||
|
||
if ($running_elf_file_type && $running_elf_file_type eq 'static') {
|
||
print_process_warning($pid, $status, "binary file for this process is $running_elf_file_type please CHECK this file because statically linked files is very often used by viruses");
|
||
}
|
||
|
||
unless ($status->{fast_container_architecture} eq $running_elf_file_architecture) {
|
||
print_process_warning($pid, $status, "Programm is $running_elf_file_architecture on container with arch $status->{fast_container_architecture} Probably it's an malware!");
|
||
}
|
||
|
||
}
|
||
|
||
# Проверяем, а не поменял ли процесс свое имя по тем или иным причинам, так часто любят делать malware
|
||
sub check_changed_proc_name {
|
||
my ($pid, $status) = @_;
|
||
|
||
my $prefix = '';
|
||
|
||
if (defined($status->{envID}) && $status->{envID}) {
|
||
$prefix = "/vz/root/$status->{envID}";
|
||
}
|
||
|
||
unless ($status->{fast_exe} && $status->{Name} ) {
|
||
warn "Can't get process names\n";
|
||
return;
|
||
}
|
||
|
||
# TODO:
|
||
# bash на centos5 любит делать себе вот такое имя процеса:
|
||
# exe path: /bin/bash
|
||
# cmdline: -bash
|
||
|
||
my $process_name_from_cmdline = '';
|
||
|
||
# Если у нас есть пробелы, то мы можем извлечь имя команды, оно отделяется либо пробелами либо двоеточием
|
||
if ($status->{fast_cmdline} =~ /[\s:]/) {
|
||
$process_name_from_cmdline = (split /[\s:]/, $status->{fast_cmdline})[0];
|
||
} else {
|
||
|
||
}
|
||
|
||
# в ps aux как раз отображается cmdline, поэтому его часто и подделывают, так что его и првоеряем :)
|
||
|
||
# Системные (и не только) процессы любят делать вот так
|
||
#exe path: /sbin/syslogd
|
||
#cmdline: syslogd -m 0
|
||
|
||
my $programm_name_possible_faked = '';
|
||
|
||
if ($status->{fast_cmdline} =~ m#^/#) {
|
||
# Если в cmdline не красивое имя процесса, а путь до бинарика, то тут проверки очень простые
|
||
|
||
# Тут хитрая сиутация возможна с симлинками, например:
|
||
# exe path: /usr/lib/apache2/mpm-prefork/apache2
|
||
# cmdline: /usr/sbin/apache2 -k start
|
||
# ls -al /usr/sbin/apache2
|
||
# lrwxrwxrwx 1 root root 34 Sep 10 2013 /usr/sbin/apache2 -> ../lib/apache2/mpm-prefork/apache2
|
||
|
||
# Но тут может быть засада, в имени в cmdline - имя симлинка, а вот в exe имя бинарика
|
||
if (-l "$prefix/$process_name_from_cmdline") {
|
||
my $real_progamm_path = readlink_deep("$prefix/$process_name_from_cmdline");
|
||
|
||
# рекурсивный readlink нам выдает абсолютный путь на уровне ноды и его нужно подрезать
|
||
$real_progamm_path =~ s#/vz/root/\d+/+#/#g;
|
||
|
||
# Даже если после разрешения симлинков они не совпадают, то увы =(
|
||
if ($real_progamm_path ne $status->{fast_exe}) {
|
||
#warn "####Symlink check: $real_progamm_path $status->{fast_exe}\n";
|
||
#$programm_name_possible_faked = 1;
|
||
return print_process_warning($pid, $status, "process name from exe $status->{fast_exe} is not match to expanded from symlink: $real_progamm_path");
|
||
}
|
||
} else {
|
||
if ($process_name_from_cmdline ne $status->{fast_exe}) {
|
||
$programm_name_possible_faked = 1;
|
||
}
|
||
}
|
||
} else {
|
||
# Если даже кусочка имени процесса нету в cmdline, то это зловред
|
||
unless ($status->{fast_exe} =~ m/$process_name_from_cmdline/) {
|
||
$programm_name_possible_faked = 1;
|
||
}
|
||
}
|
||
|
||
if ($programm_name_possible_faked) {
|
||
print_process_warning($pid, $status, "process name from cmdline $process_name_from_cmdline is not equal to name from exe: $status->{fast_exe}");
|
||
}
|
||
}
|
||
|
||
sub check_cwd {
|
||
my $pid = shift;
|
||
my $status = shift;
|
||
|
||
my $process_name = $status->{Name};
|
||
|
||
unless ( defined($good_cwd->{ $status->{fast_cwd} } ) ) {
|
||
print "$pid $status->{fast_cwd} $status->{fast_exe} $process_name\n";
|
||
}
|
||
}
|
||
|
||
|
||
# Отображаем все "потерянные" соединения для определенного пространства имен
|
||
# Это нестандартный валидатор, он запускается отдельно от прочих
|
||
sub check_orphan_connections {
|
||
my $container = shift;
|
||
my $inode_to_socket = shift;
|
||
|
||
my @normal_tcp_states_for_orphan_sockets = ('TCP_TIME_WAIT', 'TCP_FIN_WAIT2', 'TCP_SYN_RECV', 'TCP_LAST_ACK', 'TCP_FIN_WAIT1', 'TCP_CLOSE_WAIT', 'TCP_CLOSING');
|
||
|
||
if ($inode_to_socket->{'orphan'} && ref $inode_to_socket->{'orphan'} eq 'ARRAY' && @{ $inode_to_socket->{'orphan'} } > 0 ) {
|
||
SOCKETS_LOOP:
|
||
for my $orphan_socket (@{ $inode_to_socket->{orphan} }) {
|
||
# Skip unix sockets
|
||
if ($orphan_socket->{type} eq 'unix') {
|
||
next;
|
||
}
|
||
|
||
if ($orphan_socket->{type} eq 'tcp') {
|
||
if (in_array($orphan_socket->{connection}->{state}, @normal_tcp_states_for_orphan_sockets)) {
|
||
next;
|
||
}
|
||
|
||
}
|
||
|
||
if ($orphan_socket->{type} eq 'tcp' or $orphan_socket->{type} eq 'udp') {
|
||
if ($blacklist_listen_ports->{ $orphan_socket->{connection}->{rem_port} } or
|
||
$blacklist_listen_ports->{ $orphan_socket->{connection}->{local_port} }) {
|
||
|
||
if ($container) {
|
||
warn "Orphan socket TO/FROM DANGER port in: $container type $orphan_socket->{type}: " .
|
||
connection_pretty_print($orphan_socket->{connection}) . "\n";
|
||
} else {
|
||
warn "Orphan socket TO/FROM DANGER port type $orphan_socket->{type}: " .
|
||
connection_pretty_print($orphan_socket->{connection}) . "\n";
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
1;
|