Очистка заражённых файлов сайта от вредоносного кода

в 9:28, , рубрики: javascript, информационная безопасность, Песочница, чистка вирусов, метки: ,

Добрый день, уважаемые читатели!

Некоторое время назад, около месяца, на сервере нашей компании появился вирус. На одном из крупных проектов были поражены все *.js файлы. Ситуация обычная — в конец файлов был дописан вредоносный код. Яндекс выдавал предупреждение о заражении сайта и в техотдел пришло задание очистить его. Ситуация разрешилась достаточно быстро, проект был выгружен с чистого репозитория в продакшн, пароли сменили.

Однако вскоре со всех отделов компании в техотдел стали поступать жалобы о заражённых сайтах. Менеджерам жаловались клиенты, сеошники трубили что сайты теряют позиции. Началась настоящая эпидемия. Помимо *.js файлов, заражению подверглись так же *.php файлы, в конец которых был дописан код:

echo 'http://somedomain.com/style.js';

Такому масштабному заражению сервер был подвержен впервые. Мысли по этому поводу были разные, вплоть до бэкдора какого-нибудь недовольного уволенного сотрудника, решившего напакостить. Однако это не подтвердилось. Был написал shell скрипт очищающий *.php файлы, а с *.js файлами справлялись по-прежнему, выгрузкой из чистого репозитория. Пароли учётных записей доступов к сайтам, доступы к фтп — всё было изменено. Перевели всех кто работает с фтп на WinSCP и раздали файлы-ключи доступа.

Сервер потихоньку стал «выздоравливать» а сайты возвращаться в яндекс. Однако кроме более сотни сайтов клиентов на нашем сервере, есть клиенты использующие сторонние хостинги. Доступ по фтп, никакой командной строки и shell. Практически на всех сайтах используется самописная CMS (написанная N-ое количество лет назад) в связке с fckeditor'om или старыми версиями ckeditor'ов. В файл менеджере ckfinder'e проверка авторизованности реализована простым return true; Используй нехочу. Стоит также упомянуть что Множество заражённых *.js файлов выгрузкой чистого репозитория не излечить. Git на таких сайтах нами не используется, а большая часть бэкапов на хостингах хранится максимум 7 дней. А в виду того что сайты расположенные на сторонних хостингах, нами практически не мониторятся, все бэкапы так же были заражены.

В каждый файл вирус добавлял произвольное количество собственных копий, от одной до пяти (мне больше не встречалось) и все они имели различные имена переменных, имена функций и зацепиться за них было невозможно. Единственной неизменной частью кода каждого полиморфа являлся следующий участок кода:

=Array.prototype.slice.call(arguments).join(""),

Он и был выбран для поиска вхождений по файлам. Были проверены Jquery библиотеки 1.6.3 и 1.7.2 и в исходном коде совпадений не было обнаружено. Значит последовательность можно было использовать.

Чтобы не возиться вручную с несколькими десятками *.js файлов на каждом сайте, было решено написать скрипт на php. Он должен сканировать все указанные ему файлы на предмет искомой строки. Для расширения кругозора, так сказать, было решено не использовать exec(), system() команды или, к примеру, библиотеку phpseclib. Алгоритм прост до безобразия: Скрипт сканирует все директории начиная от заданной, в поисках указанной строки в файлах поиска. Перед внесением изменений, скрипт бэкапит файл (ну всё же мало ли чего) и удаляет строку, в которой присутствует искомая подстрока. Построчная работа была выбрана в виду того, что вирус в файл записывался в одну строку.

Приведу пример кода вируса в *.js файле: pastebin.com/J0zRduQw

Разбирать его я не стал, кому интересно — в интернете много примеров разбора обфусцированного кода. Поэтому перейду сразу к коду сканера.

<?
/*
 ----------------------------------------------------------------------------------
 dScaner Class - START
 ----------------------------------------------------------------------------------
*/

/*
*
*   Класс - dScaner для сканирования директорий на наличие вредоносного кода в 
*   указанных типах файлов
*   
*   Разработчик: Денис Ушаков
*   Дата разработки: 03-04-2012
*   Версия разработки: 0.0.3
*
*/

Class dScaner {

    // преобразуем входной параметр в массив 
    // $get_str - список параметров
    // $separator - разделитель параметров в списке
    function request($get_str, $separator)
    {
        if (isset($get_str) && !empty($get_str))
        {   
            // эксплоадим строку в массив и возвращаем его
            $obj = explode($separator, $get_str);
            return $obj;
        }
        else
        {
            return false;
        }
    }

    /*
    *
    *   Функция поиска в файлах вхождения заданной строки:
    *
    *   $this->find($path, $files_allowed, $requested_string);
    *   
    *   $path - путь до директории, от которой отталкиваться при сканировании
    *   $files_allowed - список файлов, которые подвергаются сканированию
    *   $requested_string - строка поиска
    *
    */
    function find($path = './', $files_allowed, $requested_string)
    {
        // исключаемые ссылки на директории и файлы, которые будут игнорироваться
        $dir_disallow = array('.', '..', '.htaccess', '.git');

        if(is_dir($path))
        {
           $temp = opendir($path);
           while (false !== ($dir = readdir($temp))) 
           {
                if ((is_dir($path . $dir)) && 
                    (!in_array($dir, $dir_disallow)) ) 
                {
                    // если директория - сканируем её
                    $sub_dir = $path . $dir . '/';
                    $this->find($sub_dir, $files_allowed, $requested_string);
                } 
                elseif ((is_file($path . $dir)) && 
                        (!in_array($dir, $dir_disallow)) && 
                        (strpos($dir, $files_allowed) == true) &&
                        (strpos($dir, '_BACKUP') == false) )
                {
                    // Если файл
                    // получаем полный путь до него
                    $in_dir_file = $path . $dir;
                    // считываем файл в строку
                    $temporary_file = file_get_contents($in_dir_file);  
                    // флаг найденного вхождения искомой строки
                    $file_founded = false;

                    // разбиваем файл на строки
                    $tf_strings = explode("n", $temporary_file);
                    // обрабатываем каждую отдельно
                    foreach ($tf_strings AS $item)
                    {
                        $item = strval($item);
                        // если в строке есть вхождения искомого запроса
                        if (strpos($item, $requested_string) !== false)
                        { 
                            $file_founded = true;
                        }
                    }
                    // если в файле найдена строка
                    if ($file_founded)
                    {
                        // выводим путь до файла в котором найдено вхождение
                        print "<span style='display:block; 
                                            padding:5px; 
                                            border:1px solid #1f4f18;
                                            background-color:#d5f5ce; 
                                            font-size:12px;
                                            line-height:16px;
                                            font-family:tahoma, sans-serif;
                                            margin-bottom:-15px;'>" . $in_dir_file . " - в файле обнаружена искомая строка.
                                </span>
";                        
                    }
                }
           }
           closedir($temp);
        } 
    }

    /*
    *
    *   Функция сканирования вредоносного кода:
    *
    *   $this->scan($path, $files_allowed, $requested_string);
    *   
    *   $path - путь до директории, от которой отталкиваться при сканировании
    *   $files_allowed - список файлов, которые подвергаются сканированию
    *   $requested_string - строка, по которой определяется наличие вредоносного кода
    *
    */
    function scan($path = './', $files_allowed, $requested_string)
    {
        // исключаемые ссылки на директории и файлы
        $dir_disallow = array('.', '..', '.htaccess', '.git');

        if(is_dir($path))
        {
           $temp = opendir($path);
           while (false !== ($dir = readdir($temp))) 
           {
                if ((is_dir($path . $dir)) && 
                    (!in_array($dir, $dir_disallow)) ) 
                {
                    // если директория - сканируем её
                    $sub_dir = $path . $dir . '/';
                    $new_parent_dir = $path . $dir;
                    $this->scan($sub_dir, $files_allowed, $requested_string, $new_parent_dir);
                } 
                elseif ((is_file($path . $dir)) && 
                        (!in_array($dir, $dir_disallow)) && 
                        (strpos($dir, $files_allowed) == true) &&
                        (strpos($dir, '_BACKUP') == false) )
                {
                    // Если файл
                    // получаем полный путь до него
                    $in_dir_file = $path . $dir;
                    // считываем файл в строку
                    $temporary_file = file_get_contents($in_dir_file);  
                    // флаг бекапа файла                                   
                    $create_backup = false;                    

                    // разбиваем файл на строки и считываем каждую отдельно
                    $tf_strings = explode("n", $temporary_file);
                    // индекс строки файла
                    $str_index = 0;
                    // каждую строку обрабатываем отдельно
                    foreach ($tf_strings AS $item)
                    {
                        $item = strval($item);
                        if (strpos($item, $requested_string) !== false)
                        { 
                            // если в строке есть вхождения искомого запроса
                            // флаг бекапа файла, в котором найден вредоносный код
                            $create_backup = true; 
                            // удаляем всю строку с вредоносным кодом
                            unset($tf_strings[$str_index]);
                        }
                        $str_index++;
                    }

                    // создаём бэкап
                    if ($create_backup)
                    {
                        // меняем права в папке в которой находимся чтобы иметь возможность писать в неё
                        chmod($path, 0777);
                        // формируем имя БЭКАПа файла
                        $temp_file_backup = $in_dir_file.'_BACKUP';
                        // сохраняем БЭКАП файла рядом с исходным
                        file_put_contents($temp_file_backup, $temporary_file);
                        // собираем очищенный файл в строку
                        $scanned_file = implode("n", $tf_strings);
                        // сохраняем очищенный файл
                        if (file_put_contents($in_dir_file, $scanned_file))
                        {   
                            // перезаписали удачно
                            print "<span style='display:block; 
                                                padding:5px; 
                                                border:1px solid #1f4f18;
                                                background-color:#d5f5ce; 
                                                font-size:12px;
                                                line-height:16px;
                                                font-family:tahoma, sans-serif;
                                                margin-bottom:-15px;'>" . $in_dir_file . " - Файл очищен. (+ BACKUP) 
                                    </span>
";
                        }
                        else
                        {
                            // перезапись не удалась
                            print "<span style='display:block; 
                                                padding:5px; 
                                                border:1px solid #822121;
                                                background-color:#ea7575; 
                                                font-size:12px;
                                                line-height:16px;
                                                font-family:tahoma, sans-serif;
                                                margin-bottom:-15px;'>".$in_dir_file ." - Файл НЕ очищен.
                                    </span>
";  
                        }
                        // меняем права в папке в которой находимся обратно на 755
                        chmod($path, 0755);                       
                    }
                }
           }
           closedir($temp);
        } 
    }

    /*
    *
    *   Функция восстановления БЭКАПОВ файлов
    *
    *   $this->restore_backups($path, $files_allowed);
    *   
    *   $path - путь до директории, от которой отталкиваться при восстановлении
    *   $files_allowed - список файлов, которые подвергаются восстановлению
    *
    */
    function restore_backups($path = './', $files_allowed)
    {
        // исключаемые ссылки на директории и файлы
        $dir_disallow = array('.', '..', '.htaccess', '.git');
        if(is_dir($path))
        {
           $temp = opendir($path);
           while (false !== ($dir = readdir($temp))) 
           {
                if ((is_dir($path . $dir)) && 
                    (!in_array($dir, $dir_disallow)) ) 
                {
                    // если директория - сканируем её
                    $sub_dir = $path . $dir . '/';
                    $this->restore_backups($sub_dir, $files_allowed);
                } 
                elseif ((is_file($path . $dir)) && 
                        (!in_array($dir, $dir_disallow)) && 
                        (strpos($dir, $files_allowed) == true) )
                {
                    // Если файл
                    // получаем полный путь до него
                    $in_dir_file = $path . $dir;
                    if (is_file($in_dir_file.'_BACKUP'))
                    {
                        // БЭКАП существует, получаем его содержимое
                        $temporary_file_from_backup = file_get_contents($in_dir_file.'_BACKUP');
                        // восстанавливаем бэкап файла
                        if (file_put_contents($in_dir_file, $temporary_file_from_backup))
                        {   
                            // удаляем бэкап
                            unlink($_SERVER['DOCUMENT_ROOT'].'/'.$in_dir_file.'_BACKUP');
                            // бэкап восстановили
                            print "<span style='display:block; 
                                                padding:5px; 
                                                border:1px solid #1f4f18;
                                                background-color:#d5f5ce; 
                                                font-size:12px;
                                                line-height:16px;
                                                font-family:tahoma, sans-serif;
                                                margin-bottom:-15px;'>".$in_dir_file ." - восстановлен.
                                    </span>
";                  
                        }
                        else
                        {
                            // бэкап НЕ восстановили
                            print "<span style='display:block; 
                                                padding:5px; 
                                                border:1px solid #822121;
                                                background-color:#ea7575; 
                                                font-size:12px;
                                                line-height:16px;
                                                font-family:tahoma, sans-serif;
                                                margin-bottom:-15px;'>".$in_dir_file ." - НЕ восстановлен.
                                    </span>
";  
                        }
                    }
                }
           }
           closedir($temp);
        } 
    }        
}

/*
 ----------------------------------------------------------------------------------
 dScaner Class - END
 ----------------------------------------------------------------------------------
*/

?>

Код класса довольно подробно закоментирован, вопросов возникнуть не должно.
Пример использования (первый параметр — стартовая директория поиска, второй — тип файлов, учавствующих в поиске, третий — строка поиска):

Создаём экземпляр сканера.

$dron = new dScaner;

Прежде чем что-то перезаписывать, стоит посмотреть, есть ли файлы удовлетворяющие условиям поиска.

$dron->find('./', '.js', '=Array.prototype.slice.call(arguments).join(""),');

Запускаем зачистку.

$dron->scan('./', '.js', '=Array.prototype.slice.call(arguments).join(""),');

В случае чего, всегда можно восстановить созданные бэкапы.

$dron->restore_backups('./', '.js');

Сканер был протестирован на многих сайтах и отрабатывает как нужно, единственная проблема которая возникла у нас на сервере — права владельца файла. Нужно чтобы owner'om файла был www:www. В среднем, на один сайт с нескольким десятком *.js файлов уходило от 5-10 до 20 секунд. И привожу список хостингов, на которых скрипт был успешно протестирован: infobox, agava, jino, mchost, hc. Из всех, самым замедленным был mchost, на остальных всё работало достаточно шустро.

P.S. Скрипт не претендует на панацею от вирусов, разработан под конкретный случай заражения и под каждый последующий требует доработки. Однако с поставленной задачей отлично справляется. Надеюсь, кому-то будет полезен.
Бест регардс!

Автор: denum

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js