Antidoto/Antidoto.pl
2014-12-31 02:41:29 +03:00

1297 lines
52 KiB
Perl
Executable file
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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;