ШОРИН Игорь Юрьевич
кандидат юридических наук, профессор Академии проблем безопасности, обороны и правопорядка и Академии военных наук, старший преподаватель кафедры информатики и применения компьютерных технологий в раскрытии преступлений Саратовского юридического института МВД России
КУЦЕНКО Игорь Олегович
адъюнкт Воронежского института МВД России
МЕТОД ОБНАРУЖЕНИЯ И СПОСОБ НЕЙТРАЛИЗАЦИИ MALWARE-ПРОГРАММ, ПОРАЖАЮЩИХ ТЕЛЕКОММУНИКАЦИОННЫЕ СИСТЕМЫ
Всем известно, что идеальной защиты не существует. Не идеальной является и операционная система Windows. В системе постоянно обнаруживаются и исправляются все новые уязвимые места. В них внедряются malware-программы, создавая новые или внедряясь в существующие процессы системы. В данной статье будет предложен один из методов обнаружения таких программ.
Развитие компьютерной (информационной) техники, высоких технологий, глобальной сети Internet породило массу проблем, связанных с исключением несанкционированного доступа к информации. Большинство антивирусов, брандмауэров и прочих систем защиты, как правило, ориентированы на вирусы и черви, однако гарантию защиты от malware-программ они предоставить не могут.
Под malware принято понимать программы, скрытно проникающие на удаленный компьютер и позволяющие осуществлять доступ в систему информации. Зачастую malware-программы не способны к распространению в сети и заражению персональных ЭВМ, то есть пишутся индивидуально, а потому существуют в единственном экземпляре. Таким образом, антивирус не способен его обнаружить, поскольку таких сигнатур в его
базе еще нет. Однако обнаружить malware все-таки можно.
Некоторые malware создают новый процесс в «диспетчере задач», но для того, чтобы их обнаружить, необходимо знать, какие файлы должны присутствовать в системе и их расположение. Бывают случаи, что в диспетчере они себя не выдают. Причиной тому являются некоторые недокументированные возможности API - функции NtQue-rySysteminformation() и библиотеки ntdll.dll, где происходит перехват данных. Для того, чтобы как-то засечь malware-программу, необходимо воспользоваться отладчиком, не использующим функцию NtQuerySysteminfor-mation(). Одним из самых распространенных является Process Explorer, Soft-ice, OllyDdg. Для достижения наибольшей скрытности malware-программа должна внедряться в уже существующий процесс.
Рассмотрим наиболее распространенный метод внедрения:
1) после получения идентификатора процесса malware-программа внедряет его API - функции OpenProcess(), возвращающий дескриптор процесса;
2) возвращаемый дескриптор процесса передается API - функции VirtualAllocEx(), выделяемой в адресном пространстве процес-
са блок памяти требуемых размеров с атрибутами page_readwrite или page_read;
3) зловредный код копируется поверх выделенного блока (однако он должен быть независим от базового адреса, чтобы сохранять свою работоспособность), что осуществляется API - функцией WriteProcessMemoryO, так как атрибут page_write не проверяется;
4) далее malware-программа находит главный поток процесса и получает его идентификатор, который преобразует в дескриптор с помощью функции OpenThread() или
NtOpenThreadO;
5) следующий шаг - дескриптор передается API - функции SuspendThread(), останавливающей ее выполнение;
6) содержимое контекста остановленного потока считывается функцией Get-ThreadContext() с флагом context_control, в результате в структуре context оказывается значение регистра EIP, который, в свою очередь, указывает уже на машинную инструкцию;
7) полученный EIP malware-программа корректирует таким образом, чтобы он указывал на точку входа в ранее скопированный код, далее происходит вызов API - функций SetThreadeContext() и ResumeThreade для того, чтобы изменения вступили в силу и запустить ранее остановленный поток;
8) получив управление, зловредный код создает новый поток и восстанавливает исходное значение регистра EIP.
Описанный алгоритм на самом деле довольно громоздок и сложен в реализации, а потому malware-программы, ориентированные, как правило, на поражение NT, чаще используют удаленный поток API - функции CreateRemoteThreade(). Это значительно упрощает порядок действий.
Учитывая вышеизложенное, последовательность будет выглядеть так:
1) при получении идентификатора процесса malware-программа передает его в API - функцию OpenProcess(), возвращающий дескриптор процесса или ошибку, если у mal-ware-программы недостаточно прав;
2) возвращаемый дескриптор передается функции VirtualAllcEx(), выделяющей внутри процесса блок памяти необходимого размера с атрибутами page_execute;
3) в выделенном блоке происходит копирование зловредного кода;
4) далее malware-программа вызывает API - функцию CreateRemoteThreade() и пе-
редает ей дескриптор процесса со стартовым адресом потока, находящийся внутри блока памяти;
5) дескриптор процесса и удаленного потока, возвращенный функцией Create-RemoteThreade(), закрываются, а зловредный код выполняет необходимые операции. Недостатком последнего способа является только необходимость перемещения кода, а это требует использование относительной адресации при написании кода.
Наиболее усовершенствованный метод внедрения malware-программ позволяет внедряться в один уже существующий процесс путем внедрения собственной библиотеки. Этот метод основан на вызове API - функции CreateRemoreThread(). В качестве стартового адреса необходимо указать функцию Load-LibraryA() или LoadLibraryW(), а вместо указателя аргумента - указатель на имя загружаемой библиотеки. API - функция Create-RemoreThread(), вызывает LoadLibraryA() или LoadLibraryW() вместе с именем библиотеки. В результате библиотека загружается в память, и управление передается процедуре DllMain(). Данный метод удобен тем, что динамическая библиотека может быть написана на любом языке программирования, что значительно расширят круг потенциальных mal-ware-писателей.
Недостаток метода в том, что библиотеки должны находиться в контексте удаленного процесса. Однако существуют, по крайней мере, два подхода для решения этой проблемы:
1. Выделение блока памяти при помощи вызова процедуры VirtualAllocEx() и копирование туда имени через WritePro-cessMemory(). Тут может возникнуть трудность, которая заключается в том, что процесс должен быть открыт с флагом proc-ess_vm_operation («открытый процесс виртуальных операций»), прав на которые у malware может не быть.
2. При получении базового адреса загрузки библиотеки ntdll.dll или kernel32.dll, используя функцию LoadLibrary(), malware-программа начинает сканировать свое пространство на предмет наличия ASCIIZ-строки. Так как эта строка расположена по тому же адресу, то при получении адреса происходит переименование зловредной библиотеки в эту строку. Недостатком последнего метода является автоматическая генерация псевдослучайных имен при условии, что
malware-программа не будет использовать первую попавшуюся строку ASCIIZ.
Следующий порядок работы malware-программы по внедрению собственной библиотеки в процесс будет таким:
1) обнаружив нужный идентификатор процесса, malware-программа передает его функции OpenProcess();
2) определив базовый адрес загрузки ntdll.dll или kernel32.dll, malware-программа ищет подходящую строку ASCIIZ, переименовывая свою, заранее созданную динамическую библиотеку;
3) определив адрес API-функции Load-LibraryA() или LoadLibraryW()malware-программа передает его API-функции Cre-ateRemoteThreadO вместе с указанием на имя библиотеки, которую необходимо загрузить внутрь процесса;
4)дескриптор процесса и удаленного потока, возвращенный CreateRemoteThread(), закрываются, а зловредный код, находящийся в DllMain(), выполняет все необходимые действия.
Существует множество способов и алгоритмов внедрения в процесс, но все они основываются на этих трех способах, которыми пользуются большинство всех malware-программ.
Из первых двух приведенных выше примеров по внедрению в процесс видно, что malware-программы располагаются в блоках памяти выделенных API-функцией VirtualAl-locEx() и имеют тип mem_private, в то время как исполняемые файлы и библиотеки родной системы загружаются в блоки памяти с типом mem_image. При внедрении по третьему сценарию зловредный код попадает в блок памяти. Стартовый адрес его потока совпадает с адресом функции LoadLibraryA() или LoadLi-braryW(), а указатель на аргументы содержит имя внедренной библиотеки. Обнаружение malware-программ сводится к определению стартовых адресов потоков. Если он совпадает с адресом LoadLibraryA()/LoadLibraryW(), или лежит внутри mem_private, то этот поток создан malware-программой.
Чтобы детально исследовать поведение malware-программ, предлагается написать макетную программу, создающую тем же самым методом потоки в процессах. Попробуем обнаружить ее вторжение.
Так как стандартный диспетчер задач менее «функциональный», то при исследовании будут использоваться другие программы
сторонних разработчиков (Process Explorer, Soft-ice, OllyDdg). Наиболее простой исходный код выглядит примерно так:
Листинг 1. «Макет malware-программы test.c»
thred() {while(1);}
main()
{
void *p;
Create-
Thread(0,0,(void*)&thread,0x999,0,&p);
p = VirtualAl-
loc(0,0x1000,MEM_COMMIT,
PEGE_EXECUTE_READWRITE);
mem-
cpy(p,thread,0x1000);CreateThread(0,0,p,0x666, 0,&p);
getchar();
}
При исследовании с помощью программы Process Explorer удалось определить адреса двух потоков: test.c+0x1405 и va_thread+0x1000. Следовательно, по адресу test.c+0x1000 расположена процедура thread. Исходя из полученных данных, возможно предположить, что истинный стартовый адрес потока лежит в пользовательском стеке. Так исследования на более низком уровне при помощи OllyDbg показали, что в стеке присутствуют четыре имени, два из которых являются двойными: 666h и 520000h, а значение остальных было равно нулю. Все они расположены в начале пользовательского стека. Обратившись к карте памяти, выяснилось, что этот адрес принадлежит региону mem_private, выделенному VirtualAlloc(). Аналогично определяются стартовые адреса и двух других потоков. Нам удалось определить подлинные стартовые адреса всех потоков. Искать вручную каждый подозрительный поток очень долго, поэтому для облегчения поиска malware-программ предлагается исходный код сканера:
Листинг 2 #include <stdio.h> #include <windows.h> #include <tlhelp32.h> #define GET_FZ 4
HANDLE (WINAPI *xOpenThread)(DWORD a,BOOL b,DWORD c);
print_thr(HANDLE h, THREADENTRY32 thr) {
int a;
DWORD x; DWORD st_adr; HANDLE ht, hp; CONTEXT context; DWORD buf[GET_FZ]; MEMORY_BASIC_INFORMATION mbi; context.ContextFlags = CONTEXT_CONTROL;
ht = xOpen-Thread(THREAD_GET_CONTEXT, 0, thr.th32ThreadID);
printf("cntUsage : %Xh\n",thr.cntUsage);
pri ntf("th32Thread ID : %Xh\n",thr.th32ThreadID);
printf("th32OwnerProcessID : %Xh\n",thr.th32OwnerProcessID);
printf("tpBasePri : %Xh\n",thr.tpBasePri);
printf("tpDeltaPri : %Xh\n",thr.tpDeltaPri);
printf("dwFlags :
%Xh\n",thr.dwFlags);
printf("handle :
%Xh\n", (ht)?(DWORD) ht:0x BAD);
if (ht) {
GetThreadContext(ht,&context); printf("ESP :
%08Xh\n",context.Esp?context.Esp:0xDEADBEE F);
if (hp = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION,0,thr.th32
OwnerProcessID)) {
if (VirtualQueryEx(hp, (void*)context.Esp, &mbi,
sizeof(mbi))==sizeof(mbi)) {
x = (DWORD) mbi.BaseAddress + mbi.RegionSize;
if (ReadProcessMemory(hp,(char*)x-GET_FZ*sizeof(DWORD),buf,GET_FZ*sizeof(D
WORD),&a)) {
st_adr=((buf[GET_FZ-3])?buf[GET_FZ-3]:buf[GET_FZ-2]); printf("start address : %08Xh\n",st_adr?st_adr:0xDEADBEEF);
if (st_adr) {
printf("point to args : %08Xh\n",((buf[GET_FZ-3])?buf[GET_FZ-2] :buf[G ET_FZ-1 ]));
if (VirtualQueryEx(hp, (void*)st_adr, &mbi, sizeof(mbi))==sizeof(mbi))
printf("type :
%s\n",(mbiType==MEMJMAGE)?"MEMJMAG
E":(mbiType==MEM_MAPPED)?"MEM_MAPPE
D":(mbiType==MEM_PRIVATE)?"MEM_PRIVA
TE":"UNKNOWN"); }
printf("[%08Xh:",x - G ET_FZ*sizeof(DWORD)); for (a = 0;a < GET_FZ; a++) printf("
%08X",buf[a]); pri ntf("]\n"); }
}
CloseHandle(hp); }
else {
printf('VirtualQueryEx : error\n");
}
CloseHandle(ht); }
return 1; }
printf_pe(PROCESSENTRY32 pe) {
printf("szExeFile : %s\n", pe.szExeFile);
printf("cntUsage : %Xh\n",
pe.cntUsage);
printf("th32ProcessID : %Xh\n", pe.th32ProcessID);
printf("th32DefaultHeapID : %Xh\n", pe.th32DefaultHeapID);
printf("th32ModuleID : %Xh\n", pe.th32ModuleID);
printf("cntThreads : %Xh\n", pe.cntThreads);
printf("th32ParentProcessID : %Xh\n", pe.th32ParentProcessID);
printf("pcPriClassBase : %Xh\n", pe.pcPriClassBase);
printf("dwFlags : %Xh\n",
pe.dwFlags);
return 1;
}
int main() {
int a;
HANDLE h; HINSTANCE hst; PROCESSENTRY32 pe; THREADENTRY32 thr;
if (hst = LoadLibrary("KERNEL32.DLL"))
xOpen-
Thread=(HANDLE(WINAPI*)(DWORD,BOOL,D WORD))GetProcAddress(hst,"OpenThread"); else
return printf("-ERR:LoadLibrary(\"KERNEL32.DLL\")\x7\n");
if (!xOpenThread) return printf("-ERR:Rq W2K or higher!\x7\n");
printf("LoadLibraryA at : %08Xh\nLoadLibraryW at : %08Xh\n\n", GetProcAd-
dress(hst,"LoadLibraryA"),GetProcAddress(hst," LoadLibraryW"));
if (!(h = CreateTool-help32Snapshot(TH32CS_SNAPPROCESS,0)))
return printf("-ERR:CreateToolhelp32Snapshot!\x7\n");
printf("* * * PROCESS INFO * * *\n");
pe.dwSize = sizeof(PROCESSENTRY32); a = Proc-ess32First(h, &pe);
if (a && printf_pe(pe)) while(Process32Next(h,&pe))
printf("\n---------------------------------
--------------------\n"),printf_pe(pe);
CloseHandle(h);
if (!(h = CreateTool-help32Snapshot(TH32CS_SNAPTHREAD,0)))
return printf("-ERR:CreateToolhelp32Snapshot!\x7\n");
printf("\n* * * THREAD INFO * * *\n");
thr.dwSize = sizeof(THREADENTRY32); a = Thread32First(h, &thr);
if (a && print_thr(h, thr))
while(Thread32Next(h, &thr))
printf("\n--thr---------------------------
---------------------\n"),print_thr(h, thr);
CloseHandle(h);
return 0; }
Описанный метод не лишен недостатков. Первым недостатком является то, что стартовый адрес потока попадает на начало пользовательского стека. Это дает возможность его обнуления или подделки. Выходом будет сканирование всех mem_private - блоков (обнаружение там машинных кодов, поиск в стеке адреса возврата обращавшихся в mem_private). Второй недостаток - возможность внедрения в атакуемый процесс без создания нового потока. Решением проблемы может послужить чтение текущего EIP пото-
ка атакуемого процесса, возможность сохранения его машинной команды и записи кода вызывающего LoadLibrary() для загрузки зловредной библиотеки в текущий контекст. Установка таймера API - функции SetTimer() для передачи зловредному коду управления через регулярные промежутки времени. Mal-ware-программа восстанавливает оригинальные машинные команды, и процесс продолжает свою работу.
Данный метод является наиболее эффективным на сегодняшний день при обнаружении malware-программ, внедряющихся в процесс и создающих в нем свои потоки.