Для решения задачи, требующей удаления занятых файлов, пришлось изучать работу антируткит утилиты GMER. Но слишком тщательно код не пришлось разбирать, т.к. в коде GMER закралась ошибка, позволяющая реализовать функционал удаления занятых файлов без глубокого реверса алгоритма "защиты" GMER.
Мой взор пал именно на GMER по следующим причинам:
Мой взор пал именно на GMER по следующим причинам:
- GMER умеет удалять занятые файлы на уровне ядра операционной системы;
- GMER поддерживает как 32-битные, так и 64-битные системы;
- GMER работает на системах от Windows XP до Windows 10;
- 64-битный драйвер GMER имеет цифровую подпись;
- оба драйвера GMER присутствуют в белом списке всех антивирусов.
Моя задача заключалась в том, что бы научить мою утилиту использовать оба драйвера GMER для реализации функционала по удалению занятых файлов. Конечно же можно написать самому такие драйверочки (опыт имеется), но вот париться с подписями и антивирусами совсем не хочется.
Первым делом был распакован EXE-файл (используется обычный UPX). Затем нужно было достать из EXE-файла оба драйвера. Сами драйвера находятся в ресурсах и зашифрованы каким то самопальным алгоритмом. Так же отмечу, что при запуске gmer.exe оная распаковывает во временную директорию нужный драйвер, загружает его в ядро системы, а затем сразу его удаляет с диска. Париться с отладкой я не люблю, т.к. для меня проще пропатчить бинарь. Поэтому я просто напросто запатчил пару мест в EXE'шнике, что бы оный не удалял драйвера после их загрузки в систему.
После получения всех PE-файлов в первозданном виде можно приступать к их исследованию. Своё исследование GMER я начал с 32-битной части. Для начала при помощи ApiMonitor проследил все вызовы DeviceIoContol при использовании функционала удаления файлов. В глаза сразу бросилось то, что коды IOCTL запросов не являются константами (только у первого запроса код постоянный). Первичное исследование 32-битного драйвера (далее по тексту axddrpow.sys) в дизасме показало, что используется техника генерации списка IOCTL запросов. Но подробно реверсить алгоритм генерации IOCTL я не стал, т.к. заметил, что для удаления заданного файла используется два разных IOCTL запроса, причём второй из них всегда имел нулевой IOCTL-код.
При изучении реализации обоих IOCTL запросов можно сделать предположение, что GMER на 32-битных системах использует два разных алгоритма удаления занятых файлов. Причём реализация нулевого IOCTL скорее всего появилась в тот момент, когда появилась поддержка 64-битных систем. Данный алгоритм удаления файлов в обоих драйверах идентичен и использует технику принудительного зануления счётчика ссылок на файл.
Быстро на коленке написал утилиту, которая используя драйвер axddrpow.sys удаляла нужный файл. При этом в IOCTL я всегда использовал нулевой код. Тестирование показало что всё нормально работает. И тут мне стало интересно, а как собственно так получилось, что автор GMER так сильно оплошал с генерацией кодов IOCTL запросов. Для этого нужно уже получше по изучать код драйвера axddrpow.sys и найти в нём искомое место предполагаемой ошибки.
Найдём место, в котором вызывается функция gmerDeleteFileAdv (именно она соответствует нулевому IOCTL запросу). Это место находится внутри функции ProcessIoctlAdv:
Сразу видно, что код нулевого IOCTL-запроса должен храниться в глобальной переменной dword_26778.
При переходе к этой области памяти можно найти целый массив IOCTL кодов:
В глаза сразу бросается тот факт, что переменная dword_26778 используется только в функции ProcessIoctlAdv. Получается, что её значение всегда нулевое.
Для подтвеждения данного факта можно дополнительно взглянуть на функцию GenerateIoctlTable :
И действительно: в теле функции GenerateIoctlTable забыли про инициализацию переменной dword_26778. Сразу понятно, что нулевой IOCTL запрос был добавлен в драйвер значительно позже, чем реализация функции GenerateIoctlTable. Видимо автор просто забыл, что при добавлении новых IOCTL нужно править функцию GenerateIoctlTable.
Дополнительно можно взглянуть на код генерации этих IOCTL запросов в юзермодной части утилиты GMER:
Сразу отмечу, что в юзермодной части глобальные переменные dword_4BB190 и dword_4BB1A8 содержат коды IOCTL-запросов для обоих вариантов удаления занятых файлов. Поэтому сразу можно отметить тот факт, что в функции GmerGenIoctlList игнорируется инициализация переменной dword_4BB1A8. Именно поэтому эта переменная и соответствует нулевому коду IOCTL-запроса.
Так же стоит отметить, что в функции GmerGenIoctlList переменная dword_4BB190 инициализируется дважды. Это можно объяснить только тем, что в исходный код была добавлена новая строчка кода при помощи техники под названием "copy-paste".
По причине наличия в GMER описанной выше ошибки мне не пришлось в свой утилите реализовывать алгоритм генерации кодов IOCTL-запросов, т.к. это совсем не нужно.
Далее настал черёд 64-битного драйвера (далее по тексту kxrcipoc.sys). Как оказалось, в драйвере kxrcipoc.sys не используется какой либо алгоритм генерации кодов IOCTL-запросов. Используются постоянные значения для кодов IOCTL-запросов. При этом так же стоит отметить тот факт, что код драйвера написан совсем в другом стиле. Поэтому можно сделать вывод о том, что данную версию драйвера писал совсем другой человек. И видимо только поэтому коды IOCTL-запросов постоянные. Можно предположить, что писал его человек с большим опытом, который отчётливо понимает что такая топорная защита драйвера (описанная выше) годится только для защиты от читателей журнала "Хакер".
Помимо выше описанной ошибки мною был выявлен и другой более серъёзный баг. При первоначальном тестировании на 64-битных системах я обнаружил, что GMER полностью игнорирует командную строку, через которою можно выполнять некторые действия без использовани GUI-интерфейса. И снова пришлось дизасмить юзермодную часть GMER.
В результате реверса удалось найти такое место в коде:
Но если хорошо подизасмить, то можно заметить наличие в коде такой вот функции:
Т.е. в GMER как оказывается есть универсальная функция для инициализации драйвера. Поэтому для исправления ошибки достаточно заменить в выше указанном месте вызов функции GmerLoadDriver32 на вызов функции GmerLoadDriver.
В результате получаем патченный GMER v2.2 , который поддерживает командную строку на 64-битных системах.
Так же этот вариант GMER не подчищает за собой распакованные драйвера.
Пример удаления занятого файла при помощи GMER:
gmer22.exe -del file C:\test\kmdmgr.exe
Первым делом был распакован EXE-файл (используется обычный UPX). Затем нужно было достать из EXE-файла оба драйвера. Сами драйвера находятся в ресурсах и зашифрованы каким то самопальным алгоритмом. Так же отмечу, что при запуске gmer.exe оная распаковывает во временную директорию нужный драйвер, загружает его в ядро системы, а затем сразу его удаляет с диска. Париться с отладкой я не люблю, т.к. для меня проще пропатчить бинарь. Поэтому я просто напросто запатчил пару мест в EXE'шнике, что бы оный не удалял драйвера после их загрузки в систему.
После получения всех PE-файлов в первозданном виде можно приступать к их исследованию. Своё исследование GMER я начал с 32-битной части. Для начала при помощи ApiMonitor проследил все вызовы DeviceIoContol при использовании функционала удаления файлов. В глаза сразу бросилось то, что коды IOCTL запросов не являются константами (только у первого запроса код постоянный). Первичное исследование 32-битного драйвера (далее по тексту axddrpow.sys) в дизасме показало, что используется техника генерации списка IOCTL запросов. Но подробно реверсить алгоритм генерации IOCTL я не стал, т.к. заметил, что для удаления заданного файла используется два разных IOCTL запроса, причём второй из них всегда имел нулевой IOCTL-код.
При изучении реализации обоих IOCTL запросов можно сделать предположение, что GMER на 32-битных системах использует два разных алгоритма удаления занятых файлов. Причём реализация нулевого IOCTL скорее всего появилась в тот момент, когда появилась поддержка 64-битных систем. Данный алгоритм удаления файлов в обоих драйверах идентичен и использует технику принудительного зануления счётчика ссылок на файл.
Быстро на коленке написал утилиту, которая используя драйвер axddrpow.sys удаляла нужный файл. При этом в IOCTL я всегда использовал нулевой код. Тестирование показало что всё нормально работает. И тут мне стало интересно, а как собственно так получилось, что автор GMER так сильно оплошал с генерацией кодов IOCTL запросов. Для этого нужно уже получше по изучать код драйвера axddrpow.sys и найти в нём искомое место предполагаемой ошибки.
Найдём место, в котором вызывается функция gmerDeleteFileAdv (именно она соответствует нулевому IOCTL запросу). Это место находится внутри функции ProcessIoctlAdv:
if ( dwIoctlCode == dword_26804 ) { if ( (unsigned int)dwBufSize < 4 || !buf ) goto LABEL_13; KeAttachProcess(dword_26468); v19 = gmerDeleteFile((wchar_t *)buf, dwBufSize); v20 = a7; goto LABEL_48; } if ( dwIoctlCode == dword_26778 ) { if ( (unsigned int)dwBufSize < 4 || !buf ) goto LABEL_13; KeAttachProcess(dword_26468); v20 = a7; *(_DWORD *)a7 = gmerDeleteFileAdv(buf, dwBufSize); KeDetachProcess(); if ( !*(_DWORD *)a7 ) goto LABEL_49; sub_1B3A0(buf, dwBufSize); KeAttachProcess(dword_26468); v19 = gmerDeleteFileAdv(buf, dwBufSize); LABEL_48: *(_DWORD *)v20 = v19; KeDetachProcess(); LABEL_49: *(_DWORD *)(v20 + 4) = a5; return *(_DWORD *)v20; }
Сразу видно, что код нулевого IOCTL-запроса должен храниться в глобальной переменной dword_26778.
При переходе к этой области памяти можно найти целый массив IOCTL кодов:
.data:0002672C 00 00 00 00 dword_2672C dd 0 ; DATA XREF: ProcessIoctl+2F3 .data:0002672C ; ProcessIoctl+C11 .data:0002672C ; ProcessIoctl+F78 ... .data:00026730 00 00 00 00 dword_26730 dd 0 ; DATA XREF: ProcessIoctl+21A .data:00026730 ; ProcessIoctl:loc_11A2C .data:00026730 ; GenerateIoctlTable+17A .data:00026734 00 00 00 00 dword_26734 dd 0 ; DATA XREF: GenerateIoctlTable+29B .data:00026738 00 00 00 00 ioctl_Init dd 0 ; DATA XREF: ProcessIoctl+4A .data:00026738 ; GenerateIoctlTable+170 .data:0002673C 00 00 00 00 dword_2673C dd 0 ; DATA XREF: GenerateIoctlTable+28A .data:00026740 00 00 00 00 dword_26740 dd 0 ; DATA XREF: ProcessIoctl+24B .data:00026740 ; ProcessIoctl:loc_118BB .data:00026740 ; GenerateIoctlTable+11B ... ... .data:00026778 00 00 00 00 dword_26778 dd 0 ; DATA XREF: ProcessIoctlAdv:loc_1CBC3 ... ... .data:00026834 00 00 00 00 dword_26834 dd 0 ; DATA XREF: ProcessIoctl:loc_109F2 .data:00026834 ; GenerateIoctlTable+12C .data:00026838 00 00 00 00 dword_26838 dd 0 ; DATA XREF: GenerateIoctlTable+433 .data:00026838 ; ProcessIoctlAdv:loc_1CB35 .data:0002683C 00 00 00 00 dword_2683C dd 0 ; DATA XREF: ProcessIoctl+93 .data:0002683C ; ProcessIoctl:loc_11CAF .data:0002683C ; GenerateIoctlTable+3E
В глаза сразу бросается тот факт, что переменная dword_26778 используется только в функции ProcessIoctlAdv. Получается, что её значение всегда нулевое.
Для подтвеждения данного факта можно дополнительно взглянуть на функцию GenerateIoctlTable :
int __stdcall GenerateIoctlTable(int key) { int v; // ecx@1 int kk; // edx@2 int base; // edx@4 v = 0; if ( key ) { v = (unsigned __int8)(key ^ 0x72); kk = key ^ 0x372 | 0x8000; } else { kk = 0x997A; } DeviceType = kk; base = kk << 16; dword_2683C = base | (4 * v + 8) | 0xC000; dword_26788 = base | (4 * v + 12) | 0xC000; dword_2674C = base | (4 * v + 64) | 0xC000; dword_26790 = base | (4 * v + 68) | 0xC000; dword_26770 = base | (4 * v + 72) | 0xC000; dword_26828 = base | (4 * v + 76) | 0xC000; dword_267D8 = base | (4 * v + 84) | 0xC000; dword_26830 = base | (4 * v + 88) | 0xC000; dword_26824 = base | (4 * v + 92) | 0xC000; dword_267A0 = base | (4 * v + 96) | 0xC000; dword_26774 = base | (4 * v + 100) | 0xC000; dword_267BC = base | (4 * v + 104) | 0xC000; dword_26810 = base | (4 * v + 128) | 0xC000; dword_26740 = base | (4 * v + 132) | 0xC000; dword_26834 = base | (4 * v + 136) | 0xC000; dword_2678C = base | (4 * v + 140) | 0xC000; dword_26820 = base | (4 * v + 144) | 0xC000; dword_267E4 = base | (4 * v + 148) | 0xC000; ioctl_Init = 0x997AC004; dword_26730 = base | (4 * v + 152) | 0xC000; dword_267C4 = base | (4 * v + 156) | 0xC000; dword_267C8 = base | (4 * v + 160) | 0xC000; dword_26758 = base | (4 * v + 164) | 0xC000; dword_267CC = base | (4 * v + 168) | 0xC000; dword_2676C = base | (4 * v + 172) | 0xC000; dword_267B0 = base | (4 * v + 176) | 0xC000; dword_267A8 = base | (4 * v + 180) | 0xC000; dword_267B8 = base | (4 * v + 184) | 0xC000; dword_26764 = base | (4 * v + 188) | 0xC000; dword_2677C = base | (4 * v + 192) | 0xC000; dword_26744 = base | (4 * v + 196) | 0xC000; dword_26748 = base | (4 * v + 200) | 0xC000; dword_267C0 = base | (4 * v + 204) | 0xC000; dword_26750 = base | (4 * v + 208) | 0xC000; dword_2680C = base | (4 * v + 212) | 0xC000; dword_2673C = base | (4 * v + 216) | 0xC000; dword_26734 = base | (4 * v + 220) | 0xC000; dword_2682C = base | (4 * v + 224) | 0xC000; dword_267AC = base | (4 * v + 256) | 0xC000; dword_267F4 = base | (4 * v + 260) | 0xC000; dword_267B4 = base | (4 * v + 264) | 0xC000; dword_26814 = base | (4 * v + 272) | 0xC000; dword_26804 = base | (4 * v + 276) | 0xC000; // gmerDeleteFile dword_26760 = base | (4 * v + 280) | 0xC000; dword_26794 = base | (4 * v + 284) | 0xC000; dword_26754 = base | (4 * v + 288) | 0xC000; dword_267A4 = base | (4 * v + 292) | 0xC000; dword_267D4 = base | (4 * v + 320) | 0xC000; dword_26818 = base | (4 * v + 324) | 0xC000; dword_2675C = base | (4 * v + 328) | 0xC000; dword_26768 = base | (4 * v + 332) | 0xC000; dword_26798 = base | (4 * v + 336) | 0xC000; dword_2672C = base | (4 * v + 340) | 0xC000; dword_267E8 = base | (4 * v + 344) | 0xC000; dword_2679C = base | (4 * v + 384) | 0xC000; dword_26780 = base | (4 * v + 388) | 0xC000; dword_267DC = base | (4 * v + 448) | 0xC000; dword_267EC = base | (4 * v + 512) | 0xC000; dword_267F8 = base | (4 * v + 516) | 0xC000; dword_267E0 = base | (4 * v + 520) | 0xC000; dword_26838 = base | (4 * v + 524) | 0xC000; dword_267F0 = base | (4 * v + 528) | 0xC000; dword_26800 = base | (4 * v + 532) | 0xC000; dword_267FC = base | (4 * v + 536) | 0xC000; dword_26784 = base | (4 * v + 540) | 0xC000; dword_2681C = base | (4 * v + 544) | 0xC000; dword_267D0 = base | (4 * v + 548) | 0xC000; return 0; }
И действительно: в теле функции GenerateIoctlTable забыли про инициализацию переменной dword_26778. Сразу понятно, что нулевой IOCTL запрос был добавлен в драйвер значительно позже, чем реализация функции GenerateIoctlTable. Видимо автор просто забыл, что при добавлении новых IOCTL нужно править функцию GenerateIoctlTable.
Дополнительно можно взглянуть на код генерации этих IOCTL запросов в юзермодной части утилиты GMER:
int __cdecl GmerGenIoctlList(int key) { int v1; // esi@4 int v2; // esi@4 .... int v66; // esi@4 int v67; // esi@4 int k; // [sp+4h] [bp-8h]@1 int kk; // [sp+14h] [bp+8h]@3 k = 0; if ( key ) { kk = key ^ 0x372 | 0x8000; k = (unsigned __int8)kk; ioctl_base = kk; } else { ioctl_base = 0x997A; } ioctl_Init = 0x997AC004; v1 = (ioctl_base << 16) | 0xC000; dword_4BB26C = 4 * AddInt(2, k) | v1; v2 = (ioctl_base << 16) | 0xC000; dword_4BB1B8 = 4 * AddInt(3, k) | v2; .... v41 = (ioctl_base << 16) | 0xC000; dword_4BB190 = 4 * AddInt(67, k) | v41; v42 = (ioctl_base << 16) | 0xC000; dword_4BB244 = 4 * AddInt(68, k) | v42; v43 = (ioctl_base << 16) | 0xC000; dword_4BB234 = 4 * AddInt(69, k) | v43; v44 = (ioctl_base << 16) | 0xC000; dword_4BB190 = 4 * AddInt(70, k) | v44; .... v66 = (ioctl_base << 16) | 0xC000; dword_4BB24C = 4 * AddInt(136, k) | v66; v67 = (ioctl_base << 16) | 0xC000; dword_4BB200 = 4 * AddInt(137, k) | v67; return 0; }
Сразу отмечу, что в юзермодной части глобальные переменные dword_4BB190 и dword_4BB1A8 содержат коды IOCTL-запросов для обоих вариантов удаления занятых файлов. Поэтому сразу можно отметить тот факт, что в функции GmerGenIoctlList игнорируется инициализация переменной dword_4BB1A8. Именно поэтому эта переменная и соответствует нулевому коду IOCTL-запроса.
Так же стоит отметить, что в функции GmerGenIoctlList переменная dword_4BB190 инициализируется дважды. Это можно объяснить только тем, что в исходный код была добавлена новая строчка кода при помощи техники под названием "copy-paste".
По причине наличия в GMER описанной выше ошибки мне не пришлось в свой утилите реализовывать алгоритм генерации кодов IOCTL-запросов, т.к. это совсем не нужно.
Далее настал черёд 64-битного драйвера (далее по тексту kxrcipoc.sys). Как оказалось, в драйвере kxrcipoc.sys не используется какой либо алгоритм генерации кодов IOCTL-запросов. Используются постоянные значения для кодов IOCTL-запросов. При этом так же стоит отметить тот факт, что код драйвера написан совсем в другом стиле. Поэтому можно сделать вывод о том, что данную версию драйвера писал совсем другой человек. И видимо только поэтому коды IOCTL-запросов постоянные. Можно предположить, что писал его человек с большим опытом, который отчётливо понимает что такая топорная защита драйвера (описанная выше) годится только для защиты от читателей журнала "Хакер".
Помимо выше описанной ошибки мною был выявлен и другой более серъёзный баг. При первоначальном тестировании на 64-битных системах я обнаружил, что GMER полностью игнорирует командную строку, через которою можно выполнять некторые действия без использовани GUI-интерфейса. И снова пришлось дизасмить юзермодную часть GMER.
В результате реверса удалось найти такое место в коде:
if ( _stricmp(*(const char **)a1, "-del") && _stricmp(*(const char **)a1, "-reboot") && _stricmp(*(const char **)a1, "-save") && _stricmp(*(const char **)a1, "-killall") && _stricmp(*(const char **)a1, "-restoressdt") ) { if ( !_stricmp(*(const char **)a1, "-protect") ) { sub_47E320(1); return 0; } return 1; } if ( GmerLoadDriver32((int)&gmer_drv_ctx) ) { ....В помеченной строке происходит инициализация драйвера. Но стоит заметить, что на 64-битных системах GMER будет пытаться использовать 32-битный драйвер. Соответственно результат вызова функции GmerLoadDriver32 всегда будет отрицательным.
Но если хорошо подизасмить, то можно заметить наличие в коде такой вот функции:
char GmerLoadDriver() { int v0; // ecx@2 int v2; // [sp+0h] [bp-8h]@2 bool v3; // [sp+7h] [bp-1h]@1 v3 = 0; if ( IsWow64() ) { v2 = 0; sub_401EA0((int)&gmer_drv_ctx, 1); v3 = GmerLoadDriver__64(v0) == 0; sub_401EF0((int)&gmer_drv_ctx, v2); } else if ( GmerLoadDriver32(&gmer_drv_ctx) ) { return 1; } return v3; }
Т.е. в GMER как оказывается есть универсальная функция для инициализации драйвера. Поэтому для исправления ошибки достаточно заменить в выше указанном месте вызов функции GmerLoadDriver32 на вызов функции GmerLoadDriver.
В результате получаем патченный GMER v2.2 , который поддерживает командную строку на 64-битных системах.
Так же этот вариант GMER не подчищает за собой распакованные драйвера.
Пример удаления занятого файла при помощи GMER:
gmer22.exe -del file C:\test\kmdmgr.exe
Этот комментарий был удален автором.
ОтветитьУдалитьЭтот комментарий был удален автором.
ОтветитьУдалить