В этой и следующей статье расскажу об исследовании одного забавного неопасного вируса. 1-ая часть — это обнаружение паразита и написание патча для его уничтожения.
Автор: k2k.nd
В этой и следующей статье расскажу об исследовании одного забавного неопасного вируса. 1-ая часть — это обнаружение паразита и написание патча для его уничтожения, 2-ая — отладка исполняемого файла вируса с Айсом, Идой или Олей (пока не решил с кем именно).
Для начала немного о себе, дабы не создавать ложных впечатлений) Программирую уже года 3, ничего большого и особо полезного не написал, просто работаю в свое удовольствие. Люблю писать как на компилируемых, так и на интерпретируемых языках, кодить всякие забавные алгоритмы. Интересуюсь WinAPI, LinuxAPI (libc) ну и некоторые библиотеки изучаю. Пишу свои либы и велосипеды - for me only. Это не самопиар, нет :)
Итак, к делу. Купил недавно ноут (core 2 duo, 4gb ram, 320gb hdd) и решил поставить туда Ubuntu 10.04, FreeBSD 8.1, Windows XP SP2 && Windows 7 Pro. Все встало и работает, не без плясок с бубном конечно) Ну, установил весь необходимый софт - XP я вообще берег для ядерной отладки, но Айс на новой GeForce карточке так и не пошел. В лучшем случае было полное зависание системы, потом BSOD вылетал уже при загрузке. Проблему кстати решил реанимацией старого железа — на нем Айс только так работает) Можно еще syser попробовать, но как-нибудь потом. В общем, с любовью строил правильную иерархию каталогов в новой системе (да, есть такой грешок) и тут при очередной загрузке вылетает такое окошко:
Т.к. приходилось кучу настроек делать, а 2-го компа под рукой не было, то и серфил рутом, так-то работаю только под юзером с урезанными правами (когда включаю винду раз в неделю этак). Притом можно было поставить лицензионного Каспера, но он бы орал не переставая на Айс и на кучу другого софта. В общем, причины таковы.
Что делает этот паразит? Для начала очень "симпатичный" порно-баннер — особо не поработаешь еcли рядом кто-то есть. Мне правда повезло — успел поставить VirtualWin, эту эмуляцию нескольких рабочих столов под винь. Переключаешься на другой стол и дело в шляпе. Однако это забавное создание (вирус) перехватывает Ctrl + Alt + Del и закрывает окна всех админских инструментов — AnVir, ProcessExplorer, AVZ. Через пару секунд после запуска установщика (любого) режет и его. Реестр открыть нельзя, но консоль можно. При переходе в проводнике в папочки с именами *rootkit*, *avz* закрывает explorer (правда не убивая процесс). При открытии текстовых файлов, содержащих вышеуказанные слова, вырубает редактор (причем любой). При попытке приаттачить отлаживаемый exe-файл к какому-нить процессу закрывается окно выбора этих процессов (так по крайней мере в Оле). Забавно, а?
Ну для начала ухожу в ребут, тут же ставлю на семерку Касперского. Последнего, типа крутого:) Проверяю весь C1 (там как раз хрюша стоит), находит kis пару вирусов в папочках Mozill'ы - и все. То, что он засек, походу помогло встать этому баннеру. Снова загружаю XP — ноль эффекта. Пробиваю номер телефона, на который надо деньги переводить. Куча сайтов (в том числе Nod && KIS) предлагают спасительные коды. Перепробовал все (ну или почти) — ничего не работает. Тут меня злость взяла — из-за какой-то заразы теперь переставлять ось и опять потратить кучу времени на установку софта? Не бывать этому! Надо писать патч, без winapi тут никак не обойтись).
Иду за другой комп, с убунты запускаю XP под виртуалкой. Ну, теперь можно начинать) Открываем msdn. Кстати, его недавно переделали, так неудобно стало искать( не по .net, а именно WinAPI. Ну ничего, где наша не пропадала, гугла в помощь. Первым делом надо определиться с компилятором (ide не нужна конечно). Мне нравится MinGW && Dev_Cpp (редактор там достаточно удобный), но решать в принципе каждому. Главное, чтоб были основные заголовочные файлы — windows.h, winbase.h и еще кое-какие. В современных студиях (year > 2005) вроде бы они по умолчанию не ставятся, да и вообще отсутствуют в сборке, поэтому надо качать Platform SDK да побыстрее. Его тоже могут прикрыть — как бы .net рулит. С Dev_Cpp кстати идет Open WindowsAPI — не знаю как уж они его сделали (лицензия по идее GPL), но не суть. Теперь нужны относительно прямые руки и чуточку терпения:)
Сразу же скажу, что тот код, который здесь будет приведен, не открытие Америки — сплошь и рядом валяются эти куски на форумах, в блогах и wasm'e. Но мало кто детально объясняет каждую строчку, а это бывает так важно, когда нужно понять, что же все-таки делает эта функция с десятью параметрами (гипербола, но это общая картина мира win). Итак, рассуждаем. Как вирус творит такие "чудеса"? Сплайсинг (перехват) api-функций? Хуки на нажатия клавиш? Бооольшая база слов, по которым идет фильтрация окон (их заголовков) и содержимого текстовых файлов? Все это можно было, безусловно, проверить, но зачем? Наша цель заключается в том, чтобы убить паразита — как процесс, так и сами вредоносные файлы. Так, убить? Ну что же, попробуем. Посмотрим, какие у нас есть инструменты для этого.
BOOL TerminateProcess(HANDLE, UINT)
1-ый параметр - дескриптор убиваемого модуля, 2-ой — код для его завершения (который надо еще получить)
Да, не обращаем внимания на зачастую странные названия типов — в конечном итоге это либо int'ы с модификаторами, либо указатели, либо структуры. Здесь UINT = unsigned int (первое что в голову приходит)
Идем дальше и раскручиваем необходимые нам функции по порядку. Начнем с хендла. Вообще большинство дескрипторов получаются с помощью ф-ий Open*: OpenFile, OpenMutex, OpenThread, OpenProcess. Последняя — это то, что нам нужно. Вот ее прототип:
HANDLE OpenProcess(DWORD, BOOL, DWORD)
void* OpenProcess(int, char, int)
1-ый пар-р — флаг доступа к процессу, хендл которого мы собрались получить. Это опять длинные-предлинные константы типа PROCESS_QUERY_LIMITED_INFORMATON. Можно конечно юзать их 16-тиричные аналоги (0x0008), но лучше пожалеть себя и тех, кому может быть придется читать ваш код — имена констант имхо читабельнее.
2-ой пар-р - будет ли дескриптор наследуем. А, нам все равно, ставим в false.
3-ий пар-р - PID, т.е. Id желанного процесса. Смотрим, как получить его.
Маленькое отступление. Мне сразу после того, как я понял, что придется писать патч, захотелось узнать имя окна баннера. На скриншоте видно, какое оно чудесное, поэтому открываем любое другое окно, а потом стандартное Alt + TAB. И переписываем имя окошка на бумажку) Название, кстати, весьма интересное — fghdfgh. Ну не Проводник же).
У нас имеется две замечательных функции - FindWindow и GetWindowThreadProcessId. Рассмотрим их поподробнее
HWND FindWindow(LPCTSTR, LPCTSTR)
void* FindWindow(*char, *char)
Так, надоело писать "параметр" || "пр-р". Дальше только в крайних случаях
1-ый: указатель на строку, содержащую имя класса окна. Мы не знаем, поэтому пишем NULL
2-ой: указатель на имя окна. Упс, ура, попался!
Возвращает ф-ия хендл окна. Теперь его можно дать передать следующей из парочки.
DWORD GetWindowThreadProcessId(HWND, LPDWORD)
int GetWindowThreadProcessId(void*, int*)
1-ый: дескриптор окна. Это у нас уже есть
2-ой: указатель на переменную, принимающую Id процесса
Возвращает Id потока, создавшего окно. Однако не будем ювелирами и убьем процесс целиком, тем более, как мы потом узнаем, там всего один поток
Поднимается наверх и переходим к exit-коду для TerminateProcess. Есть такая простенькая ф-ия:
BOOL GetExitCodeProcess(HANDLE, LPDWORD)
bool GetExitCodeProcess(void*, int*)
Кстати, параметры описываю уже по коду, поэтому могу не соотв. msdn'у по диапазонам, но не суть
1-ый: хендл процесса, exit-код которого получаем
2-ой: указатель на переменную, ... Дальше ясно
Ну если ф-ия возвращает BOOL, то в случае true — все пучком, а если false — то вызываем GetLastError и по коду ищем описание ошибки (опять msdn).
Уф. походу, все, а? можно писать
int GetProcessIdByWindowName(char* c_windowName) { DWORD i_procId = 0; HWND windowId = FindWindow(NULL, c_windowName); GetWindowThreadProcessId(windowId, &i_procId); return i_procId; } int KillProcess(char* c_windowName) { DWORD i_procId = GetProcessIdByWindowName(c_windowName); HANDLE hndVir = OpenProcess(PROCESS_QUERY_INFORMATION, false, i_procId); if (hndVir) { DWORD i_exitCode = 0; if (GetExitCodeProcess(hndVir, &i_exitCode)) { hndVir = OpenProcess(PROCESS_TERMINATE, false, i_procId); if (hndVir) { if (TerminateProcess(hndVir, i_exitCode)) { } else { return 1; } } else { return 2; } } else { return 3; } } else { return 4; } return 0; }
Ну чтож, все замечательно. Из какого-нить main'a делаем так: cout << KillProcess("fghdfgh"). Ой, что такое? Ошибка 3... GetLastError дает 5 — Access Denied. А мы же из-под админа делали... Неужели вирус такой умный? проверяем на нормальной машине — запускаем блокнот и делаем KillProcess("Безымянный — Блокнот"). Опять 5. Значит что-то забыли. Думаем и вспоминаем, что для убийства процессов мало еще запускать прогу из-под рута, надо еще повысить привилегии, а точнее получить одну из них, SeDebugPrivilege, которая по дефолту отключена. Ну, поехали снова. Нам понадобятся следующие ф-ии: OpenProcessToken, LookupPrivilegeValue, AdjustTokenPrivileges.
Стоит отметить, что все привилегии процесса хранятся в так называемом токене, его еще называют маркером доступа. Получив его, можно добавлять/отнимать привилегии у процесса/потока. Наверно можно это делать и по отношению к другим объектам ОС (только ф-ии другие будут). Так вот, с помощью следующей ф-ии получаем токен текущего процесса (убийцы вируса):
BOOL OpenProcessToken(HANDLE, DWORD, PHANDLE)
bool OpenProcessToken(void*, int, void**)
1-ый: дескриптор процесса, для которого надо открыть токен
2-ой: флаг доступа к токену
3-ий: указатель на хендл маркера доступа (сюда он и запишется в случае удачи)
Теперь к нашей SeDebugPrivilege. Для изменения токена нам понадобится знать ее числовое значение, которое хранится в LARGE_INTEGER (4 (16x4) машинных слова)
BOOL LookupPrivilegeValue(LPCTSTR, LPCTSTR, PLUID)
bool LookupPrivilegeValue(char*, char*, __int64*)
1-ый: это на не нужно, ставим в NULL
2-ой: указатель на строку, содержащую имя привилегии, для которой хотим получить 64-битное значение
3-ий: ну и указатель на результат. перед тем как его взять, не забываем проверить, что возвратила ф-ия
Ну и завершающий штрих — добавление привилегии (ее включение) к валидному токену процесса. Не пугаемся прототипа — половина параметров для нас ничего не значит
BOOL AdjustTokenPrivileges(HANDLE, BOOL, PTOKEN_PRIVILEGES, DWORD, PTOKEN_PRIVILEGES, PDWORD)
bool AdjustTokenPrivileges(void*, bool, TOKEN_PRIVILEGES*, int, PTOKEN_PRIVILEGES*, int*)
1-ый: хедл токена, к которому добавляем привилегий
2-ой: ставим в false, иначе все привилегии в токене будут сброшены
3-ий: указатель на структуру, содержащую изменения прав
4-ый: размер следующего пар-ра, ставим 0 (т.к. NULL)
5-ый: в NULL его (предыдущее состояние, не нужно)
6-ой: тоже в NULL
Ну и о структуре TOKEN_PRIVILEGES. Нам в ней нужно всего-то два члена (больше и нет, ха-ха:)):
DWORD PrivilegeCount — кол-во привилегий (ой, как слово надоело), которые будем устанавливать
LUID_AND_ATTRIBUTES Privileges[] — сам массив привилегий
Теперь к LUID_AND_ATTRIBUTES. Там тоже два поля для заполнения:
LUID Luid — 64-битный номер нашей привилегии (да, который с помощью LookupPrivilegeValue получили)
DWORD Attributes — что с привилегией делать будем. можно вот что:
SE_PRIVILEGE_ENABLED — включить
SE_PRIVILEGE_REMOVED — выключить
Ну а больше нам и не надо. Вот теперь есть все... почти все
int EnablePrivilege(HANDLE hndProc, char* c_privName) { //Πoлyчaeм тoкeн нaшeгo пpoцecca HANDLE hndToken; TOKEN_PRIVILEGES tkp; LARGE_INTEGER bi_nameValue; if (!OpenProcessToken(hndProc, TOKEN_ALL_ACCESS, &hndToken)) { return 1; } //Πoлyчaeм LUID пpивилeгии if (!LookupPrivilegeValue(NULL, c_privName, &bi_nameValue)) { return 2; } tkp.PrivilegeCount = 1; tkp.Privileges[0].Luid = bi_nameValue; tkp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED; //Дoбaвляeм пpивилeгию к пpoцeccy if (!AdjustTokenPrivileges(hndToken, false, &tkp, 0, NULL, NULL)) { return 3; } if (GetLastError() == ERROR_NOT_ALL_ASSIGNED) { return 4; } return 0; }
Ну тут думаю уже без комментариев. На входе — хендл процесса, для которого добавляем привилегию с именем c_privName. Просто и удобно. А теперь подумаем, как получить хендл текущего процесса. Можно заюзать OpenProcess, но там нужно знать pid процесса. Можно конечно его и по консольному окошку получить, но некрасиво. А если по-другому — длиннее будет, хотя в сорцах патча, ссылка на которые будут дальше, есть и такой вариант. Поэтому берем связку GetCurrentProcess && DuplicateHandle. 1-ая ф-ия получает псевдо-дескриптор текущего процесса, не годный в нашем случае (да и в многих других тоже). Поэтому этот псевдо-хендл дублируют с помощью DuplicateHandle в "настоящий". GetCurrentProcess не имеет параметров и возвращает HANDLE. Со 2-ой поподробнее:
BOOL DuplicateHandle(HANDLE, HANDLE, HANDLE, LPHANDLE, DWORD, BOOL, DWORD)
bool DuplicateHandle(void*, void*, void*, void**, int, bool, int)
1-ый: процесс-владелец хендла
2-ой: сам дескриптор, который нужно продублировать
3-ий: процесс-получатель дупликата
4-ый: а здесь будет результат — если будет, конечно) GetLastError rulez:)
5-ый: флаг доступа к новому хендлу. Ставим в 0, не понадобится
6-ой: в false. Не дублировать, нет
7-ой: дополнительные опции. У нас это DUPLICATE_SAME_ACCESS, т.е. доступ к дескриптору такой же, как у родителя. Чтоб не заморачиваться
Итак, окончательный код такой:
int KillProcess(char* c_windowName) { DWORD i_procId = GetProcessIdByWindowName(c_windowName); HANDLE hndVir = OpenProcess(PROCESS_QUERY_INFORMATION, false, i_procId); if (hndVir) { HANDLE hndThis = GetCurrentProcess(); if (DuplicateHandle(hndThis, hndThis, hndThis, &hndThis, 0, false,DUPLICATE_SAME_ACCESS)) { if (!EnablePrivilege(hndThis, "SeDebugPrivilege")) { DWORD i_exitCode = 0; if (GetExitCodeProcess(hndVir, &i_exitCode)) { hndVir = OpenProcess(PROCESS_TERMINATE, false,i_procId); if (hndVir) { if (TerminateProcess(hndVir, i_exitCode)) { } else { //cout << "TerminateProcess: " <<GetLastError(); return 1; } } else { //cout << "OpenProcess(2): " << GetLastError(); return 2; } } else { //cout << "GetExitCodeProcess: " << GetLastError(); return 3; } } else { //cout << "EnablePrivilege: " << GetLastError(); return 4; } } else { //cout << "DuplicateHandle: " << GetLastError(); return 5; } } else { //cout << "OpenProcess(1): " << GetLastError(); return 6; } return 0; }
cout'ы для отладки если че. Хотя это и часть небольшой либы, которая неуклонно растет) Вот теперь долгожданный KillProcess("fghdfgh") работает на ура — мы убили баннер, ура! Теперь можно ставить софт, проводник уже не закрывается когда не надо, вот только перехват Ctrl + Alt + Del так и не снят. Ну ниче, это ребут снимает. Правда перед ним запускаем AnVir и ищем в логах эту тварь.
Мда, подумать только! Он прятался в C:\Program Files\Common Files\Agent\ как agent.exe! Логи говорят еще много интересного: оказывается, есть еще какой-то 1.exe, замаскированный под текстовый файл (оригинально, да). Вот они с агентом на пару и работают, но за баннер отвечает первый. Он же вызывает напарника после своего запуска. Открываем реестр и ищем этого агента. Все просто — он оказался вот здесь.
Подведем итоги. Оказывается, имея всего лишь msdn да компилятор с текстовым редактором, можно творить чудеса и очищать этот мир от всякой заразы. Однако если в целом посмотреть, то этот вирус сделан был плохо, очень плохо. Никакой защиты — ну может быть создание удаленных потоков и подгрузка своих dll в чужое адресное пространство. Ну, хуки клавиатурные. А где драйвер режима ядра? Где любимый сплайсинг? Мне очень грустно — образование наше хромает. Такой вирус может сделать любой мало-мальски разбирающийся школьник — всего-то и делов, что взять готовые сорцы да поставить свою картинку. Ну, номер завести для кидалова тупых юзеров. Никто в здравом уме отключать напасть после получения денег не будет — лучше уж доить пока можно. Даже грамотные вирусописатели берут чужой код — и вы хотите чтоб вас антивирусы не палили? Учить матчасть надо — ну хотя бы бессмертного ms-rem'a. Да, асм рулит) Эх, времена DOS'a...
P.S. В следующей части как и обещал будем изучать вирус изнутри. Какой-никакой, а все-таки вирус)
P.P.S Каспер с сигнатурами от 19-го июня уже ловит этот вирус (просто проверяя незапущенный файл). Но неделю назад такого не было, поэтому незвестно, кто кого опередил:)
P.P.P.S Чуть не забыл — ссылка на патч (сорцы и готовый бинарник с инструкцией)
P.P.P.P.S И вдогонку архив с вирусом и инструкцией по его “установке”. На архиве пароль, т.к. антивирус наверняка проверит его (если есть конечно:) ) Пароль: nix_assembler.
Никогда не работайте из-под админа. Удачи в ловле цифровых паразитов!
Искренне ваш k2k.nd
От классики до авангарда — наука во всех жанрах