Совсем недавно озадачился защитой приложений от перехвата системных api, решил поделится и обсудить то, к чему пришел. Многие из вас знают, что перехват системных api сводится к перенаправлению оригинальной функции в нужное место, благодаря этому можно модифицировать параметры функции, возвращать результат отличный от оригинала, хранить оригинальный вызов с параметрами и многое другое. Так как это теоретическая часть, примеры в статье будут сопровождаться псевдокодом.
Обычно используются следующие методы перехвата:
- Вставка безусловного прыжка на месте функции (х86 — 5 байт jmp addr, x64 — 12 байт, mov rax addr, jmp rax)
- Вставка инструкции вызова (call) на месте вызова оригинальной функции (hooked_function_entry: call my_function)
- Модификация таблицы импорта приложения, внедрение прокси DLL.
- Перехват без модификации кода средствами аппаратных точек останова (hardware breakpoints).
- Перехват через kernel драйвер Zw/Nt функций.
Можно перечислить следующие методы детектирования перехвата из usermode:
1. Сравнение начала функции перед ее вызовом с опкодами машинных инструкций:
//0x90 - nop
//0xE9 - jmp
//0xE8 - call
if (*mainFunc == 0xE9 || mainFunc == 0x90 || mainFunc == 0xE8 ...)
printf("Hook detected");
Метод статичный и легко обходится, более умными переходами.
2. Перечисленные методы перехвата — это модификация памяти, и они могут быть обнаружены по средствам crc checksum проверок, но api для чтения памяти тоже могут быть перехвачены и тогда будет возвращен ложный результат.
3. Порождать контролируемый процесс (с активной отладкой самого себя), с восстановлением важных api.
4. Перебор важных для нас api, используя таблицу экспорта в системных библиотеках, сравнивать дизассемблированную длину функции с оригинальной.
5. Восстановление модифицированных api по средствам копирования байтов из библиотеки, примерная схема такая:
— получение базы определенного модуля
— итерация экспорта
— получение RVA необходимой функции.
— преобразование RVA->FileOffset.
— чтение оригинала и запись в память.
— в некоторых случаях не забываем про релоки.
6. Просмотр таблицы импорта библиотеки и для каждой импортированной функции из другой системной библиотеки делать проверку на соответствие, если не совпадает так же читаем с диска и записываем в память.
7. Имитирование системных функций Nt / Zw по средствам syscall-ов, int2e в (старых версиях windows), таблица syscall-ов для всех систем, легко находится в гугле.
Остановимся на 7 методе так как Nt / Zw функции так же могут быть перехвачены, этот метод тоже далек от совершенства, но по моему мнению лучше из всего выше перечисленного.
Посмотрим на реализацию функции NtCreateFile в windows 7 sp1
происходит следующее:
— в еах лежит номер syscall-a.
— обнуляется ecx, (wow64 index?).
— в edx содержится указатель на параметры.
— далее вызов, корректировка стэка и возврат.
x64 вызов в win7 и win8
При помощи следующего x86 псевдокода «можно создать файл на диске».
//прототип функции NtCreateFile
typedef NTSTATUS (NTAPI * NTCREATEFILE) (OUT PHANDLE FileHandle,
IN ACCESS_MASK DesiredAccess,
IN POBJECT_ATTRIBUTES ObjectAttributes,
OUT PIO_STATUS_BLOCK IoStatusBlock,
IN PLARGE_INTEGER AllocationSize OPTIONAL,
IN ULONG FileAttributes,
IN ULONG ShareAccess,
IN ULONG CreateDisposition,
IN ULONG CreateOptions,
IN PVOID EaBuffer OPTIONAL,
IN ULONG EaLength);
unsigned char dNtCreateFile[] =
{0xb8,0x52,0x00,0x00,0x00,0x33,0xc9,0x8d,0x54,0x24,0x04,0x64,
0xff,0x15,0xc0,0x00,0x00,0x00,0x83,0xc4,0x04,0xc2,0x2c,0x00};
...
InitUnicodeString
InitializeObjectAttributes
...
DWORD oldp;
VirtualProtect(&dNtCreateFile, sizeof(dNtCreateFile), PAGE_EXECUTE_READ, &oldp);
auto func = (NTCREATEFILE) ((void*)dNtCreateFile);
Ntstatus = (func)(&fileHandle, DesiredAccess, ObjectAttritubes, ioStatusBlock, 0, FileAttributes, ShareAccess, CreateDisposition ,CreateOptions, Optional_Buffer, 0);
Данный псевдокод будет работать только на windows 7 sp1, однако dNtCreateFile можно формировать «динамично», используя метод из пункта 5, и тогда код с небольшими правками будет работать на всех системах начиная с winxp. Приветствуются любые размышления в комментариях, kernelmode — крайний случай.
Автор: Dinisoid