惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
月光博客
月光博客
雷峰网
雷峰网
WordPress大学
WordPress大学
博客园 - 司徒正美
Last Week in AI
Last Week in AI
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Visual Studio Blog
H
Help Net Security
Engineering at Meta
Engineering at Meta
Google DeepMind News
Google DeepMind News
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
S
Security @ Cisco Blogs
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
爱范儿
爱范儿
W
WeLiveSecurity
J
Java Code Geeks
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
T
Threatpost
The Cloudflare Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
N
Netflix TechBlog - Medium
Latest news
Latest news
V2EX - 技术
V2EX - 技术
小众软件
小众软件
T
The Blog of Author Tim Ferriss
A
Arctic Wolf
B
Blog RSS Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
I
InfoQ
C
Check Point Blog
N
News | PayPal Newsroom
Cyberwarzone
Cyberwarzone
V
V2EX
TaoSecurity Blog
TaoSecurity Blog
P
Privacy & Cybersecurity Law Blog
Microsoft Security Blog
Microsoft Security Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
D
DataBreaches.Net
F
Fortinet All Blogs
阮一峰的网络日志
阮一峰的网络日志
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
IT之家
IT之家
K
Kaspersky official blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Google DeepMind News
Google DeepMind News
C
CXSECURITY Database RSS Feed - CXSecurity.com
www.infosecurity-magazine.com
www.infosecurity-magazine.com

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Вырезаем 99% мусорного кода с помощью SSA в Binary Ninja (Flare-On 12)
farvend · 2026-06-22 · via Все публикации подряд на Хабре

Сложный

11 мин

0

Заманка

Скачать задания

Мы из вот такой огроменной (это лишь малая её часть) функции, в ней 4185 (!) строк декомпиляции

Изначальный main

Изначальный main

Получим вот это

Деобфусцированный main

Деобфусцированный main

А тут всего 35 строк

Вступление

Проходя недавний Flare-On 12, я открыл 7 задание, нашел main и увидел следующую картину:

Ужасная куча вычислений

Ужасная куча вычислений

И ещё тысячи строк подобного бреда, а логики не видно. Так что весь этот мусор нужно как-то убрать.

Что делать?

Если посмотреть на граф потока управления, то видно, что он довольно простой (я его показывать не буду, так как он настолько длинный, что блоки там просто не видны), так что это не часть control flow flattening.

Можно посмотреть все вызовы функций через

>>> len(current_function.call_sites)
31

(Я использую Binary Ninja, этот и последующие скрипты будут для него)

Видим довольно скромное количество вызовов, значит, скорее всего, мусорных вызовов нет. Если потыкать по этим вызовам, то можно убедиться, что параметры получаются только через другие вызовы напрямую. Вот что я имею ввиду:

Вызовы функций, которые не используют тяжёлые вычисления

Вызовы функций, которые не используют тяжёлые вычисления

Получается, весь этот гигантский пласт математики можно просто вырезать без каких-либо последствий для работы программы. Возникает логичный вопрос: если этот код ни на что не влияет, почему декомпилятор не выкинул его?

А ответ в том, что всё-таки эти вычисления влияют на глобальные переменные, но только чтоб это понять нужно полистать декомпиляцию. В мусорных кусках происходит примерно это:

int64_t var1 = data_1001; // data_1001 нигде в полезном не используется
# куча преобразований var1, может быть использования других переменных
data_1004 = var1_new; // data_1004 нигде в полезном не используется

Так что по-сути все эти вычисления могут влиять на поведение программы (запись в глобальные переменные), но вот только эти переменные скорее всего нужны только для запутывания кода

Однако я этот вывод сделал лишь на одном куске main, в других функциях и других кусках может быть эти вычисления всё-таки нужны.

Подготавливаемся проверять гипотезу

Получается нам нужно доказать в main, что параметры вызовов функций зависят от малой части переменных main. Один из самых стабильных и рабочих вариантов, который мне пришёл в голову, так это как-то отследить использования переменной в вызове функции до её создания, но как мы будем это делать?

Перерыв на теорию

Можно, конечно, парсить ассемблер, но это было бы ужасно сложно (перезаписи регистров, переменные относительно rbp и т.д.), поэтому нам нужен инструмент, который :

  • Предоставляет переменные, а не регистры

  • Позволяет сказать как конкретное состояние переменной получилось.

Для этого прекрасно подходит MLIL из Binary Ninja в SSA форме! Давайте объясню что это

MLIL

В Binary Ninja есть промежуточные представления, которые предназначены для лифтинга машинного кода в псевдо Си. Одно из них — Medium Level Intermediate Language. Оно как раз абстрагирует регистры и стек, оставляя только переменные, также у вызовов функций появляются аргументы. Первое требование выполнено, что со вторым?

SSA форма

Наша проблема — переменная может перезаписываться кучу раз, что для анализа неудобно. Для решения этой проблемы существует SSA форма, которая есть для каждого промежуточного представления в Binary Ninja.

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

А для получения использований и определения есть API, крутяк!

>>> current_function.mlil.ssa_form.get_ssa_var_definition(example_var)
<MediumLevelILSetVarSsa: rcx_2606#6 = &var_218>

Также у Binary Ninja в SSA форме при вызовах функций или взаимодействием с глобальной памятью создаются версии памяти. Вот как это выглядит:

// mem#7 — новая версия памяти, mem#6 — то, в какой версии это происходит
rax_5206#6, mem#7 = string::new_autolen(rcx_2606#6, rdx_750#2) @ mem#6

Проверяем гипотезу

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

Найти количество всех переменных:

>>> len(current_function.mlil.ssa_vars)
26980

Найти все вызовы:

>>> def is_call(instr):
... 	if isinstance(instr, MediumLevelILCallSsa):
... 		return instr
... 	return None
... 
... # traverse рекурсивно обходит дерево выражений. Мы передаём ему функцию-фильтр: 
... # если она возвращает None, узел пропускается, а если возвращает сам объект, 
... # то он попадает в итератор.
... for call in current_function.mlil.ssa_form.traverse(is_call):
... 	print(call)
mem#6 = 0x1402268c0(rcx_1029#1065, 0x140) @ mem#5
mem#7 = 0x14002e020(rcx_1030#1066) @ mem#6
rax_2057#2268, mem#8 = 0x140081590(rcx_1031#1067) @ mem#7
rax_3125#11037, mem#60 = 0x140226560() @ mem#59
rax_3126#11038, mem#61 = 0x14022e3d0(rcx_1563#5349) @ mem#60
...

Найти аргументы функции:

>>> current_il_expr
<MediumLevelILCallSsa: mem#6 = 0x1402268c0(rcx_1029#1065, 0x140) @ mem#5>
>>> current_il_expr.params
[<MediumLevelILVarSsa: rcx_1029#1065>, <MediumLevelILConst: 0x140>]

Найти весь путь от использования (в нашем случае — использование в качестве аргумента функции) переменной до её первого определения:

>>> example_var = current_il_expr.dest
... 
... func = current_function
... mlil = func.mlil
... ssa = mlil.ssa_form
... 
... pending: List[SSAVariable] = []
... important_vars = set()
... 
... def add_important(var):
... 	if var in important_vars:
... 		return
... 	important_vars.add(var)
... 	if isinstance(var, SSAVariable):
... 		pending.append(var)
... 	else:
... 		for def_site in func.mlil.get_var_definitions(var):
... 			ssa_def = def_site.ssa_form
... 			if ssa_def is not None:
... 				for used_var in ssa_def.vars_read + ssa_def.vars_written:
... 					add_important(used_var)
... 		for use_site in func.mlil.get_var_uses(var):
... 			ssa_use = use_site.ssa_form
... 			if ssa_use is not None:
... 				for used_var in ssa_use.vars_written:
... 					add_important(used_var)
... 
... add_important(example_var)
... 
... while len(pending) > 0:
... 	current_var = pending.pop()
... 	def_instr = ssa.get_ssa_var_definition(current_var)
... 	if def_instr is not None:
... 		for used_var in def_instr.vars_read:
... 			add_important(used_var)
... 
... print(important_vars)
{<SSAVariable: rdx_1209 version 1338>, <SSAVariable: rcx_1030 version 1066>, <var void var_168>, <SSAVariable: rcx_2604 version 2134>, <SSAVariable: rdx_1025 version 815>, <SSAVariable: rcx_1031 version 1067>, <SSAVariable: rcx_5181 version 3617>, <SSAVariable: rcx_1029 version 1065>}

Кстати, тут важно сделать небольшую ремарку. Хоть мы и работаем с SSA-формой, внутри неё всё равно могут попадаться обычные (не SSA) переменные. Обычно это происходит, когда в коде берется адрес переменной (ptr = &var).

Как только декомпилятор видит взятие адреса, он понимает, что теперь переменная может быть изменена по указателю из любого места. Отследить её строгие версии, как требует SSA, становится невозможно, поэтому Binary Ninja помечает её как aliased и оставляет обычной переменной.

Из-за этого мы не можем просто пойти назад по графу. Поэтому к таким переменным я применяю консервативный подход: я анализирую не только её определения, но и все использования, читая результаты таких выражений.

Собираем проверку гипотезы

Чтобы выполнять такие большие скрипты прямо из VSCode нужно поставить плагин Binja-RPyC, так их гораздо удобнее писать.

func: Function = bv.get_function_at(0x14020ba00) # main
ssa: MediumLevelILFunction = func.mlil.ssa_form

def add_important(var):
	if var in important_vars:
		return
	important_vars.add(var)

	if isinstance(var, SSAVariable):
		pending.append(var)
	else:
		for def_site in func.mlil.get_var_definitions(var):
			ssa_def = def_site.ssa_form
			if ssa_def is not None:
				for used_var in ssa_def.vars_read + ssa_def.vars_written:
					add_important(used_var)
		for use_site in func.mlil.get_var_uses(var):
			ssa_use = use_site.ssa_form
			if ssa_use is not None:
				for used_var in ssa_use.vars_written:
					add_important(used_var)

pending: List[SSAVariable] = []

important_vars = set()

for call_site in func.call_sites:
	for mlil in call_site.mlils:
		for call in mlil.ssa_form.traverse(lambda b: b if isinstance(b, MediumLevelILCallSsa) else None):
			call: MediumLevelILCallSsa = call
			if ".synthetic_builtins" == bv.get_sections_at(call.dest.value.value)[0].name:
				continue
			for var in call.vars_read + call.vars_written:
				add_important(var)

while len(pending) > 0:
	current_var = pending.pop()
	
	def_instr = ssa.get_ssa_var_definition(current_var)
	if def_instr is not None:
		for used_var in def_instr.vars_read:
			add_important(used_var)

print(len(important_vars) / len(ssa.ssa_vars))

Тут мы считаем количество важных переменных (те, которые используются в параметрах функций) и находим их соотношение к общему количеству SSA переменных. Мы должны получить 1 если используются все переменные, иначе должны получить число меньше 1. А мы получаем аж 0.002446256486286138 (это 0.24%), то есть 99.76% переменных (и примерно кода) просто нам не нужны и мы это доказали!

Пишем деобфускатор

Теперь имея реальные переменные мы можем вырезать те выражения, которые не используют эти переменные.

Как переписывать декомпиляцию

Binary Ninja позволяет при помощи системы Workflow менять любое промежуточное представление (на данный момент кроме HLIL).

Как найти использованные переменные

Для этого есть прекрасное апи. Можно взять выражение и без танцев с бубном просто их получить

>>> current_il_expr.vars_read
[<SSAVariable: rcx_2607 version 2137>, <SSAVariable: rdx_751 version 685>]
>>> current_il_expr.vars_written
[<SSAVariable: rax_5207 version 4557>]

Как писать Workflow

Чтобы Binary Ninja начала использовать наш алгоритм, его нужно зарегистрировать как кастомный этап анализа

import binaryninja as bn
import importlib
import json

def _trampoline(analysis_context):
    import deobfuscator_logic
    importlib.reload(deobfuscator_logic)
    deobfuscator_logic.run(analysis_context)

configuration = json.dumps({
    "name": "extension.deobfuscator.slicing",
    "title": "Deobfuscator DCE Slicing",
    "description": "Dead code elimination via backward slicing on MLIL SSA",
    "eligibility": {
        "auto": {
            "default": True
        }
    }
})

wf = bn.Workflow("core.function.metaAnalysis").clone("extension.deobf")

wf.register_activity(bn.Activity(configuration, action=_trampoline))

wf.insert("core.function.generateHighLevelIL", [
    "extension.deobfuscator.slicing"
])

wf.register()

Здесь куча бойлерплейта. Думаю стоит здесь выделить 2 вещи:

  1. В wf.insert написан core.function.generateHighLevelIL чтобы наш проход выполнился перед созданием HLIL (по официальной документации рекомендуется использовать wf.insert_after(“core.function.generateMediumLevelIL”, но и так окей). Это значит, что наш скрипт почистит MLIL прямо перед тем, как декомпилятор начнёт строить из него финальный Си-псевдокод

  2. Workflow в Binary Ninja после загрузки нельзя изменить. По умолчанию, если бы я писал основной код прямо в функции-колбэке, мне приходилось бы перезапускать декомпилятор, а это неудобно.

    Используя трюк с importlib.reload, я вынес всю логику в отдельный файл deobfuscator_logic.py. Теперь я могу редактировать код деобфускатора, нажимать в Binary Ninja кнопку Reanalyze, и она выполнит мой новый код. Это мне сократило кучу времени.

Пишем deobfuscator_logic.py

Повторю наш план: мы хотим вырезать те выражения, которые не используют помеченные нами переменные.

import binaryninja as bn
from binaryninja import *
import time

def get_important_vars(bv: BinaryView, func: Function) -> Set[SSAVariable]:
    ssa: MediumLevelILFunction = func.mlil.ssa_form
    pending: List[SSAVariable] = []
    important_vars: Set[SSAVariable] = set()

    def add_important(var):
        if var in important_vars:
            return
        
        if isinstance(var, SSAVariable):
            important_vars.add(var)
            pending.append(var)
        else:
            for def_site in func.mlil.get_var_definitions(var):
                ssa_def = def_site.ssa_form
                if ssa_def is not None:
                    for used_var in ssa_def.vars_read + ssa_def.vars_written:
                        add_important(used_var)
            for use_site in func.mlil.get_var_uses(var):
                ssa_use = use_site.ssa_form
                if ssa_use is not None:
                    for used_var in ssa_use.vars_written:
                        add_important(used_var)

    for param_var in func.parameter_vars:
        add_important(param_var)

    for call_site in func.call_sites:
        for mlil in call_site.mlils:
            for call in mlil.ssa_form.traverse(
                lambda b: b if isinstance(b, MediumLevelILCallSsa) else None
            ):
                try:
                    if ".synthetic_builtins" == bv.get_sections_at(call.dest.value.value)[0].name:
                        continue
                except IndexError as e:
                    pass
                for var in call.vars_read + call.vars_written:
                    add_important(var)

    while len(pending) > 0:
        current_var = pending.pop()
        def_instr = ssa.get_ssa_var_definition(current_var)
        if def_instr is not None:
            for used_var in def_instr.vars_read:
                add_important(used_var)

    return important_vars

def run(context: bn.AnalysisContext):
    func: Function = context.function
    # Наш проход съедает много ресурсов, а анализ на питоне однопоточный, что будет сильно влиять на производительность
    do_deobf = False
    for block in func.basic_blocks:
        if len(block) > 1000:
            do_deobf = True
            break
    if not do_deobf:
        return
    
    mlil = context.mlil
    ssa: MediumLevelILFunction = mlil.ssa_form

    good_vars = get_important_vars(context.view, func)

    # Собираем ненужные выражения
    bad_expr = set()
    for instr in ssa.instructions:
        written = instr.vars_written + instr.vars_read
        if (written and (all(v not in good_vars for v in written) and not isinstance(instr, MediumLevelILIf))):
            non_ssa = instr.non_ssa_form
            if non_ssa is not None:
                bad_expr.add(non_ssa.expr_index)

    # Стираем эти выражения
    for expr in bad_expr:
        mlil.replace_expr(expr, mlil.nop())

    # ОБЯЗАТЕЛЬНО, ИНАЧЕ ДЕКОМПИЛЯЦИЯ НЕ ИЗМЕНИТСЯ 
    mlil.finalize()
    mlil.generate_ssa_form()

get_important_vars — по-сути копия кода из проверки гипотезы. Единственный новый код — это run.

Ставим деобфускатор

Теперь у нас есть 2 файла — загрузчик (тот, что с _trampoline) и сам анализатор (deobfuscator_logic.py), кидаем их в папку плагинов, её можно открыть вот так

Как открыть папку с плагинами

Как открыть папку с плагинами

Кидаем туда 2 наших файла и перезапускаем Binary Ninja.

После перезапуска загружаем файл и лезем в настройки (CTRL + ,), ищем function workflow и ставим extension.deobf вместо core.function.metaAnalysis

И всё, у нас всё работает!

... но всё ли?

В main всё действительно хорошо

Декомпиляция main

Декомпиляция main

Однако если мы зайдём в функцию по адресу 140081590, то увидим это

Мистические переменные

Мистические переменные

У нас нет определений переменных в условиях!

Чтобы это исправить, добавим ещё один проход. Он будет анализировать аргументы ветвлений. Если эти аргументы как-то зависят от уже известной нам «полезной» переменной, то мы помечаем весь этот путь вычислений как важный, чтобы декомпилятор его не удалил. Код этого прохода:

def backprop_conds(
	bv: BinaryView,
	func: Function,
	good_vars: Set[SSAVariable],
) -> Set[SSAVariable]:
	ssa: MediumLevelILFunction = func.mlil.ssa_form

	pending: List[SSAVariable] = []
	important_vars: Set[SSAVariable] = set()

	# cache: var -> reaches_good?
	reaches_cache = {}

	def add_important(var):
		if var in important_vars:
			return

		if isinstance(var, SSAVariable):
			important_vars.add(var)
			pending.append(var)
		else:
			# обычная Variable / aliased var
			for def_site in func.mlil.get_var_definitions(var):
				ssa_def = def_site.ssa_form
				if ssa_def is not None:
					for used_var in ssa_def.vars_read + ssa_def.vars_written:
						add_important(used_var)

	def reaches_good(var, seen=None) -> bool:
		"""
		Проверяет: если идти назад от var по def-use цепочке,
		можно ли дойти до уже известных good_vars или newly-important vars.
		"""
		if seen is None:
			seen = set()

		if var in seen:
			return False
		seen.add(var)

		if var in reaches_cache:
			return reaches_cache[var]

		# Уже хорошая переменная из основного слайсинга
		if var in good_vars:
			reaches_cache[var] = True
			return True

		# Переменная, которую мы уже добавили как важную из другого IF
		if var in important_vars:
			reaches_cache[var] = True
			return True

		if isinstance(var, SSAVariable):
			def_instr = ssa.get_ssa_var_definition(var)
			if def_instr is None:
				reaches_cache[var] = False
				return False

			# Если любая зависимость определения ведёт к good — var тоже good-dependent
			for parent in def_instr.vars_read:
				if reaches_good(parent, seen):
					reaches_cache[var] = True
					return True

			reaches_cache[var] = False
			return False

		else:
			# Обычная Variable: пробуем пройтись по её определениям
			for def_site in func.mlil.get_var_definitions(var):
				ssa_def = def_site.ssa_form
				if ssa_def is None:
					continue

				for parent in ssa_def.vars_read + ssa_def.vars_written:
					if reaches_good(parent, seen):
						reaches_cache[var] = True
						return True

			reaches_cache[var] = False
			return False

	def drain_pending():
		"""
		Обычный backward slicing от уже добавленных important_vars.
		"""
		while pending:
			current_var = pending.pop()

			def_instr = ssa.get_ssa_var_definition(current_var)
			if def_instr is not None:
				for used_var in def_instr.vars_read:
					add_important(used_var)

	changed = True

	while changed:
		changed = False

		for block in ssa:
			for instr in block:
				if instr.operation != MediumLevelILOperation.MLIL_IF:
					continue

				cond_vars = list(instr.vars_read)
				if not cond_vars:
					continue

				# Если хотя бы одна переменная условия назад доходит до good_vars,
				# считаем IF настоящим и сохраняем всю цепочку условия.
				if any(reaches_good(v) for v in cond_vars):
					before = len(important_vars)

					for v in cond_vars:
						add_important(v)

					drain_pending()

					if len(important_vars) != before:
						changed = True
						reaches_cache.clear()

	return important_vars

И добавим в run:

good_vars = get_important_vars(context.view, func)
ifs = backprop_conds(context.view, func, good_vars)
good_vars.update(ifs)

Всё, работает!

Рабочий деобфускатор

Рабочий деобфускатор

Итоги

Вот так, объединив SSA-форму и крутое API Binary Ninja, мы сократили функцию с >4000 мусорных строк до 35 строк чистой логики. Дальше реверсить этот таск стало делом техники.

Полный исходный код скрипта я выложил на Gist.

Подписывайтесь на телегу. Туда кидаю миништуки, всякие свои скрипты и инструменты: https://t.me/vector35_fan_club