top of page
ISSP

Оптимізатор LLVM для деобфускації нативного коду або як читати нечитабельний код

Оновлено: 27 жовт. 2022 р.

Проблема аналізу обфускованого коду не є новою. Обфускація – значна зміна надмірності коду, тому навіть прості методи можуть ускладнити аналіз, в тому числі, нескладних алгоритмів.


У цій статті ми пропонуємо розглянути метод деобфускації на основі роботи оптимізатора LLVM. Особливістю цього підходу є його універсальність. Оптимізація відбувається над LLVM-IR, що дозволяє успішно використовувати одні й самі методи у різних архітектурах.


Специфіка оптимізатора LLVM


LLVM - проєкт для спрощення створення компіляторів з урахуванням платформо-незалежної системи кодування машинних інструкцій, в основі якої лежить RISC архітектура. Оскільки LLVM-IR - проміжна мова для цієї архітектури, то всі перетворення (оптимізації) відбуваються саме над байткодом LLVM-IR.


Теоретично при використанні різних способів декомпіляції нативного коду в LLVM-IR можливі вкрай серйозні оптимізації навіть скомпільованого коду.


Особливістю цього методу деобфускації є відносна універсальність методології (з поправкою на можливості "ліфтера" native-LLVM).


Серед безкоштовно доступних продуктів, які можуть виконувати ліфтинг, можна виділити кілька цікавих проєктів:


1. RetDec https://github.com/avast/retdec – декомпілер, який заснований якраз на ліфтингу нативного коду в LLVM-IR. Саме його ми і будемо використовувати як приклад.


2. LLvm-mctoll https://github.com/microsoft/llvm-mctoll - не розглядався як варіант через відсутність підтримки x86 (при цьому x86-64 є).


3. Mcsema https://github.com/lifting-bits/mcsema – досить цікавий проєкт для “перенесення” коду на інші архітектури. Головний мінус – використовується досить складна схема перенесення коду з “віртуальним” процесором оригінальної архітектури, яка ще більше ускладнює структуру. Це суттєво обмежує можливості подальшої оптимізації та деобфускації.


4. Dagger https://github.com/repzret/dagger - загалом непоганий проєкт, який, однак, має проблеми з ліфтингом глобальних змінних, а також не передбачає ліфтинг стекових змінних, що обмежує його використання з метою оптимізації коду.


Оптимізатор LLVM - практична реалізація


Для практичної демонстрації давайте візьмемо наочний приклад заплутаного руками коду, який скомпільований з відключеною оптимізацією.

LLVM optimizer code snippet
LLVM optimizer code snippet

Після компіляції це буде виглядати ось так

LLVM optimizer hand-crafted code after compilation
LLVM optimizer hand-crafted code after compilation

Після проходу оптимізатора ми отримуємо наступний результат

LLVM optimizer hand-crafted code after compilation and optimization
LLVM optimizer hand-crafted code after compilation and optimization

Давайте розбиратися, як усе працює. Насамперед нам потрібно вибрати блок коду, який ми хочемо оптимізувати. В даному випадку це лише одна функція, розташована за адресою 0x140001000-0x14000109B (за умови відключеного ALSR).


Наступне завдання, що стоїть перед нами – отримати перетворений код цієї функції, з чим успішно може впоратися декомпілер RetDec. Для кращого ефекту необхідно виправити конфігурацію.


Консоль запуску RetDec виглядає так


python retdec.py [filename].exe --select-ranges 0x140001000-0x14000109B --select-decode-only --stop-after bin2llvmir


На виході ми матимемо [filename].ll файл. Його потрібно транслювати в асемблер, що робиться за допомогою утиліти LLVM compiler (LLC).


llc -o [filename].asm -x86-asm-syntax=intel -filetype=asm -march=x86-64 -mtriple x86_64-win32 [filename].ll


Щоб асемблерний файл на виході мав адекватну структуру, його можна зібрати за допомогою fasm. Для цього потрібно задіяти певний набір фільтрів (що в основному стосуються синтаксису) і що важливіше - виправити імена бібліотечних викликів на адреси (особливості ліфтингу за допомогою RetDec).


У прикладі це не використовується, а наведена реалізація працює лише під x86. За бажання можна досить просто виправити регулярні вирази.


Далі запускаємо процес трансляції асемблера в "байтики".


FASM.EXE [fixed_asm_filename].asm


Фінальний етап - взяти отриманий bin файл і записати дані з нього на те ж місце всередині виконуваного файлу, зайве “забити” 0xCC. У пітоні за допомогою lief це виглядає ось так.


binary.patch_address(function_start, list(compiled_function))


Оптимізатор LLVM- Як перетворити запропоновану концепцію на реальний продукт


Описаний приклад обфускації є досить простим. З ним непогано можуть впоратися такі декомпілятори, як IDA, Ghidra або використаний нами RetDec.


Водночас запропонована концепція може використовуватися для більш зручного динамічного аналізу, адже оптимізація сильно спрощує граф виконання та прибирає зайве.


Потенційно описаний механізм може прибирати ollvm (при проведенні певних модифікацій), або навіть віртуалізацію - він працює на трасі VMProtect, проте потрібні подальші вдосконалення.


Крім того, можливе використання цього підходу в застарілому софті для оптимізації вже скомпільованих блоків коду.


Як приклад візьмемо наступний код


LLVM optimizer - Assembler code example
LLVM optimizer - Assembler code example

Після оптимізації ми бачимо ось таку картину

LLVM optimizer - Assembler code example after optimization
LLVM optimizer - Assembler code example after optimization

Таким чином, оптимізатор непогано впорався з "оптимізацією циклу", перетворивши операцію циклу на константу.


Найбільшою проблемою при практичному використанні цього методу може стати "кривий" ліфтинг native to LLVM-IR, оскільки RetDec насамперед декомпілятор, а не ліфтер.


Тому цей метод – це скоріше концепція, аніж приклад “бойового” інструменту, хоча вже є успішні кейси щодо його використання для закриття завдань, з якими не справлялася IDA. Але це поки що носить ситуативний характер і кожен випадок вимагає індивідуального доопрацювання.


Висновки - як можна використовувати оптимізатор LLVM


У цій статті ми розглянули метод деобфускації на основі роботи оптимізатора LLVM, який дозволяє використовувати одні й ті ж методи оптимізації на різних архітектурах. Ми запропонували просту практичну реалізацію цього методу деобфускації та обговорили ключові питання, пов’язані з цим методом.


Хочете дізнатися більше про ефективні інструменти reverse engineering? Ознайомтеся з нашою статтею Reverse Engineering with Ghidra.


Більше про професійні послуги ISSP з кібер розслідувань та роботу ISSP лабораторії за посиланням.

Comments


bottom of page