Метод обнаружения и способ нейтрализации malware-программ, поражающих телекоммуникационные системы

ШОРИН Игорь Юрьевич

кандидат юридических наук, профессор Академии проблем безопасности, обороны и правопорядка и Академии военных наук, старший преподаватель кафедры информатики и применения компьютерных технологий в раскрытии преступлений Саратовского юридического института МВД России

КУЦЕНКО Игорь Олегович

адъюнкт Воронежского института МВД России

МЕТОД ОБНАРУЖЕНИЯ И СПОСОБ НЕЙТРАЛИЗАЦИИ 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

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

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

return printf("-ERR:CreateToolhelp32Snapshot!\x7\n");

printf("* * * PROCESS INFO * * *\n");

pe.dwSize = sizeof(PROCESSENTRY32); a = Proc-ess32First(h, &pe);

if

printf("\n---------------------------------

--------------------\n"),printf_pe(pe);

CloseHandle(h);

if

return printf("-ERR:CreateToolhelp32Snapshot!\x7\n");

printf("\n* * * THREAD INFO * * *\n");

thr.dwSize = sizeof(THREADENTRY32); a = Thread32First(h, &thr);

if

while

printf("\n--thr---------------------------

---------------------\n"),print_thr(h, thr);

CloseHandle(h);

return 0; }

Описанный метод не лишен недостатков. Первым недостатком является то, что стартовый адрес потока попадает на начало пользовательского стека. Это дает возможность его обнуления или подделки. Выходом будет сканирование всех mem_private - блоков (обнаружение там машинных кодов, поиск в стеке адреса возврата обращавшихся в mem_private). Второй недостаток - возможность внедрения в атакуемый процесс без создания нового потока. Решением проблемы может послужить чтение текущего EIP пото-

ка атакуемого процесса, возможность сохранения его машинной команды и записи кода вызывающего LoadLibrary() для загрузки зловредной библиотеки в текущий контекст. Установка таймера API - функции SetTimer() для передачи зловредному коду управления через регулярные промежутки времени. Mal-ware-программа восстанавливает оригинальные машинные команды, и процесс продолжает свою работу.

Данный метод является наиболее эффективным на сегодняшний день при обнаружении malware-программ, внедряющихся в процесс и создающих в нем свои потоки.