WinAPI :: на страже закона

WinAPI :: на страже закона

В этой и следующей статье расскажу об исследовании одного забавного неопасного вируса. 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

Цифровые следы - ваша слабость, и хакеры это знают.

Подпишитесь и узнайте, как их замести!