Образцы серьезной малвари и вымогателей часто содержат интересные методики заражения, скрытия активности и нестандартные отладочные приемы. В вирусах типа Potato или вымогателях вроде SynAsk используется простая, но мощная техника скрытия вызовов WinAPI. Об этом мы и поговорим, а заодно напишем рабочий пример скрытия WinAPI в приложении.

Итак, есть несколько способов скрытия вызовов WinAPI.

  1. Виртуализация. Важный код скрывается внутри самодельной виртуальной машины.
  2. Прыжок в тело функции WinAPI после ее пролога. Для этого нужен дизассемблер длин инструкций.
  3. Вызов функций по их хеш-значениям.

Все остальные техники — это разные вариации или развитие трех этих атак. Первые две встречаются нечасто — слишком громоздкие. Как минимум приходится всюду таскать с собой дизассемблер длин и прологи функций, рассчитанные на две разные архитектуры. Вызов функций по хеш-именам прост и часто используется в более-менее видной малвари (даже кибершпионской).

Наша задача — написать легко масштабируемый мотор для реализации скрытия вызовов WinAPI. Они не должны читаться в таблице импорта и не должны бросаться в глаза в дизассемблере. Давай напишем короткую программу для экспериментов и откомпилируем ее для x64.

#include <Windows.h>

int main() {
  HANDLE hFile = CreateFileA("C:\\test\\text.txt",
    GENERIC_READ,
    FILE_SHARE_READ,
    NULL,
    OPEN_EXISTING,
    FILE_ATTRIBUTE_NORMAL,
    NULL);
  Sleep(5000);
  return 0;
}

Как видишь, здесь используются две функции WinAPI — CreateFileA и Sleep. Функцию CreateFileA я решил привести в качестве примера не случайно — по ее аргументу "C:\\test\\text.txt" мы ее легко и найдем в уже обфусцированном виде.

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

  • предпочитать краткость кода (/Os),
  • отключить проверку безопасности (/Gs-),
  • отключить отладочную информацию,
  • в настройках компоновщика отключить внесение случайности в базовый адрес (/DYNAMICBASE:NO),
  • включить фиксированный базовый адрес (/FIXED),
  • обозначить самостоятельно точку входа (в нашем случае это main),
  • игнорировать все стандартные библиотеки (/NODEFAULTLIB),
  • отказаться от манифеста (/MANIFEST:NO).

Эти действия помогут уменьшить размер программы и избавить ее от вставок неявного кода. В моем случае получилось, что программа занимает 3 Кбайт. Ниже — ее полный листинг.

public start
start proc near

dwCreationDisposition= dword ptr -28h
dwFlagsAndAttributes= dword ptr -20h
var_18= qword ptr -18h

sub   rsp, 48h
and   [rsp+48h+var_18], 0
lea   rcx, FileName       ; "C:\\test\\text.txt"
xor   r9d, r9d            ; lpSecurityAttributes
mov   [rsp+48h+dwFlagsAndAttributes], 80h ; dwFlagsAndAttributes
mov   edx, 80000000h      ; dwDesiredAccess
mov   [rsp+48h+dwCreationDisposition], 3 ; dwCreationDisposition
lea   r8d, [r9+1]         ; dwShareMode
call  cs:CreateFileA
mov   ecx, 1388h          ; dwMilliseconds
call  cs:Sleep
xor   eax, eax
add   rsp, 48h
retn

start endp

Как видишь, функции WinAPI явно читаются в коде и видны в таблице импорта приложения.

Приложение в программе PE-bear
Приложение в программе PE-bear

Теперь давай создадим модуль, который поможет скрывать от любопытных глаз используемые нами функции WinAPI. Напишем таблицу хешей функций.

static DWORD hash_api_table[] = {
  0xe976c80c, // CreateFileA
  0xb233e4a5, // Sleep
}

Как хешировать

В статье нет смысла приводить алгоритм хеширования — их десятки, и они доступны в Сети, даже в Википедии. Могу посоветовать алгоритмы, с возможностью выставления вектора начальной инициализации (seed), чтобы хеши функций были уникальными. Например, подойдет алгоритм MurmurHash.

Давай условимся, что у нас макрос хеширования будет иметь прототип HASH_API(name, name_len, seed), где name — имя функции, name_len — длина имени, seed — вектор начальной инициализации. Так что все значения хеш-функций у тебя будут другими, не как в статье!

Поскольку мы договорились писать легко масштабируемый модуль, определимся, что функция получения WinAPI у нас будет вида

LPVOID get_api(DWORD api_hash, LPCSTR module);

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

LPVOID parse_export_table(HMODULE module, DWORD api_hash) {
  PIMAGE_DOS_HEADER     img_dos_header;
  PIMAGE_NT_HEADERS     img_nt_header;
  PIMAGE_EXPORT_DIRECTORY     in_export;

  img_dos_header  = (PIMAGE_DOS_HEADER)module;
  img_nt_header = (PIMAGE_NT_HEADERS)((DWORD_PTR)img_dos_header + img_dos_header->e_lfanew);
  in_export = (PIMAGE_EXPORT_DIRECTORY)((DWORD_PTR)img_dos_header + img_nt_header->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_EXPORT].VirtualAddress);

По ходу написания этой функции я буду пояснять, что к чему, потому что путешествие по заголовку PE-файла — дело непростое (у динамической библиотеки будет именно такой заголовок). Сначала мы объявили используемые переменные, с этим не должно было возникнуть проблем. 🙂 Далее, в первой строчке кода, мы получаем из переданного в нашу функцию модуля DLL ее IMAGE_DOS_HEADER. Вот его структура:

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», увеличит личную накопительную скидку и позволит накапливать профессиональный рейтинг Xakep Score! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


Check Also

В королевстве PWN. Препарируем классику переполнения буфера в современных условиях

Сколько раз и в каких только контекстах не писали об уязвимости переполнения буфера! Однак…

5 комментариев

  1. Аватар

    hiddy

    17.12.2018 at 21:26

    А что на счет ручных системных вызовов? (syscall, sysenter).
    Хотелось бы побольше от Вас статей по защите/реверсу всего вот этого.
    И спасибо за качественный контент.

  2. Аватар

    le

    19.01.2019 at 21:08

    Ок, api_name вычислили, а name_len?

  3. Аватар

    b27etula

    28.02.2019 at 13:38

    «таблицы импортов передаваемых» — может таблицу экспорта?

  4. Аватар

    helter

    04.03.2019 at 10:42

    вспомнился очень старый лоадер испльзующий данную технику, только на плюсах и с использованием template`ов ( сводить к холивару с vs c++ не буду 😉 , да и не спора ради коментарий(мой выбор в данной ситуации совпадает с автором)). в статье, как и в вспомнившемся мне исходнике, напрягает одно — описывать прототипы функций. ну не удобно же.но это легко решается(с парой оговорок). готовый код скидывать не буду, озвучу идею.
    winapi`шки будем дефайнить так:

    #define Sleep(…) ExecFunc(DllId, 0x3d9972f5, NUMARGS(__VA_ARGS__) ,##__VA_ARGS__)/

    (для примера взата Sleep). вообщем dllid — это номер длл в заранее подготовленой таблице (для работы нам нужно не так и много, штук пять длл хватит на все случаи жизни, это мой подход, можно и просто имя длл писать) следом идет хэш имени winapi .и для всех апишек мы используем лишь дефайны, и единственную функцию :

    LPVOID ExecFunc(DWORD module, DWORD hash, LPVOID p1, …)
    {
    😉 тут как раз есть оговорка, я в своей реализации ограничил число параметров 20.(кто захочет реализовать, поймет думаю почему )
    но этож только концепт, и идея для размышлений 😉
    }

    с таким подходом очень удобно генерировать необходимые дефайны с хэшами скриптом(передавая параметром длл какую нужно распарсить), и подключать заголовком. при этом отпадает необходимость в описании прототипа на каждую нововведеную в код апишку.

  5. Аватар

    b27etula

    23.04.2019 at 14:22

    e_lfanew — это RAW адрес, а не никакой RVA, автор статьи не знает терминологию.

Оставить мнение