2016-03-17

"Нулевая защита" в антируткит утилите 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:
  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

2 комментария: