Атака на чорний ящик. Реверс-інжиніринг віртуалізованого і мутованого коду


 
Захист власного програмного забезпечення від реверс інжинірингу досить стара проблема, свого часу терзала серця багатьох shareware розробників і не тільки. Зазвичай для таких цілей застосовують протектор, але наскільки б не був крутий протектор, завжди знайдуться люди які його розпиляють і зламають. Однак останнім часом протектори стали застосовувати технології видозміни коду (мутацію і віртуалізацію), які дозволяють з вихідного алгоритму зробити кашу, що зовні нагадує 'чорний ящик'. І дійсно зустрічаються люди, впевнені в тому, що віртуалізація і мутація виконуваного коду сучасними комерційними протекторами є якоюсь панацеєю. Ясна річ що будь безпечники швидше посміхнеться і не погодиться з таким твердженням, адже люди знаючі гірку ціну безпеки будь-які натяки на ідеальну захист швидше за все сприймуть як міф і маркетингову казку. У цій статті я розповім про власний досвід і бачення дослідження чорного ящика комерційних протекторів і можливі атаки на нього. Сподіваюся розуміння недоліків таких технологій, допоможе вам більш розумно й ефективно застосовувати їх на практиці чи не застосовувати взагалі.
 
 
0x00. Розбір механізмів захисту коду
Для початку давайте визначимо технології які ми будемо досліджувати:
 
1. Мутирование — це метод обфускаціі коду, при якому вихідний граф потоку управління розбивається додатковими вершинами, ветвлениями, доповнюється сміттєвими інструкціями, циклами, не порушуючи вихідного алгоритму програми. Часто вихідні інструкції мутує в деяку підмножину інших інструкцій виконують одну й ту ж роботу.
 
2. Віртуалізація — це метод обфускаціі коду, при якому вихідні інструкції алгоритму, транслюються в інструкції віртуальної машини, згенерованої протектором. На місце вихідного алгоритму вбудовується код, який під час виконання передає проміжні інструкції на вхід у віртуальну машину, що інтерпретує їх.
 
Обидва способи ускладнюють як статичний так і динамічний аналіз виконуваного коду і часто протектори допускають комбінування способів.
 
Далі в статті я буду розглядати безкоштовні демо-версії двох протекторів: VMProtect , Safengine , вони дозволяють мутувати, віртуалізувати і комбінувати обидва способи обфускаціі.
 
Для накладення технологій мутації і віртуалізації коду протектори надають такі способи:
 
1. На етапі розробки, через спеціальні маркери (SDK)
Розробник софту позначає у вихідному коді фрагмент захищається коду спеціальними функціями з SDK протектора, потім після компіляції, на етапі установки протектора, дані ділянки будуть виявлені, вирізані і обфусціровани.
 
 
int main ()
{
     VMProtectMutate("Critical_code_mut");
      ...   // critical code here
     VMProtectEnd();
     return 0;
}
Приклад позначки вихідного коду маркерами VMProtect'a
 
2. На етапі протекції, через налагоджувальні файли
Безпосередньо в процесі установки протектора, зчитуються налагоджувальні файли (pdb, map) і на їх основі визначається карта обьектов додатки. Далі розробник вибирає які функції потрібно захистити і як, після чого вони Целек вирізаються з коду і обробляються.
 
Чому вирізається захищається код? Справа в тому що при розведенні коду, його розмір значно збільшується, а отже умістити у вихідному сегменті нові інструкції неможливо, тому і застосовується вирізання коду в власну ділянку пам'яті протектора.
  
 
 Спрощена схема мутації виконуваного коду функції void Test () {printf ("Hello»); }
 
 
0x01. Переваги та недоліки протекторів
Очевидною перевагою мутації звичайно ж є неможливість візуального дослідження алгоритму. Перед дослідником на перший погляд лежить просто тарілка з горою спагетті, розібрати яку в ручну позамежно складно, саме на це і роблять ставки сучасні протектори.
 
Не менш важливим є комбінування різних технік антіотладкі, антіпатчінга, антіхукінга, мутації і т.д. У сукупності все це веде до ускладнення процесів аналізу, але не зупиняє їх.
 
Недоліків у таких технологій теж вистачає. Перший з яких, це втрата продуктивності, адже мутіруемий код збільшується в сотні, а то і в тисячі разів. Чи не менше це стосується і віртуалізації, зазвичай віртуальна машина набагато важче мутіруемого коду. А якщо поєднати обидва підходи, то виходить маніакально роздутий код. Причому накладні витрати не завжди виправдовують таку обфускація, застосування мутують технологій однобоко направлено на усложние відновлення оригінального графа виконання програми.
 
Ось приклад трасування Примітивні функції:
 
 
void test()
{
	printf("This is protected message #1\n");
	printf("This is protected message #2\n");
}

До мутації:
 Trace log
main 00405A2A CALL 004059F0 ESP = 0018FEE0
main 004059F0 PUSH EBP ESP = 0018FEDC
main 004059F1 MOV EBP, ESP EBP = 0018FEDC
main 004059F3 SUB ESP, 40 ESP = 0018FE9C
main 004059F6 PUSH EBX ESP = 0018FE98
main 004059F7 PUSH ESI ESP = 0018FE94
main 004059F8 PUSH EDI ESP = 0018FE90
main 004059F9 PUSH OFFSET 0040ED10 ESP = 0018FE8C
main 004059FE CALL DWORD PTR DS: [<& MSVCR100D.printf>] EAX = 00000012, ECX = 84CB6CA9, EDX = 7418F4B8
main 00405A04 ADD ESP, 4 ESP = 0018FE90
main 00405A07 PUSH OFFSET 0040ECF8 ESP = 0018FE8C
main 00405A0C CALL DWORD PTR DS: [<& MSVCR100D.printf>] EAX = 00000014
main 00405A12 ADD ESP, 4 ESP = 0018FE90
main 00405A15 POP EDI ESP = 0018FE94
main 00405A16 POP ESI ESP = 0018FE98
main 00405A17 POP EBX ESP = 0018FE9C
main 00405A18 MOV ESP, EBP ESP = 0018FEDC
main 00405A1A POP EBP ESP = 0018FEE0, EBP = 0018FF30
main 00405A1B RETN ESP = 0018FEE4 

Після мутації протектором Safengine:
 Trace log
main 00405A2A CALL 004059F0 ESP = 0018FEE0
main 004059F0 JMP 004C9C6A
main 004C9C6A JMP 004C8391
main 004C8391 CALL 004C82D6 ESP = 0018FEDC
main 004C82D6 LEA ESP, [ESP +2] ESP = 0018FEDE
main 004C82DA LEA ESP, [ESP +2] ESP = 0018FEE0
main 004C82DE PUSH EBP ESP = 0018FEDC
main 004C82DF NEG BP EBP = 001800D0
main 004C82E2 JMP 004C812E
main 004C812E MOV EBP, ESP EBP = 0018FEDC
main 004C8130 STC
main 004C8131 SUB ESP, 40 ESP = 0018FE9C
main 004C8134 CALL 004C8006 ESP = 0018FE98
main 004C8006 JMP SHORT 004C7F96
main 004C7F96 LEA ESP, [ESP +4] ESP = 0018FE9C
main 004C7F9A PUSH EBX ESP = 0018FE98
main 004C7F9B CALL 004C7F80 ESP = 0018FE94
main 004C7F80 LEA ESP, [ESP +4] ESP = 0018FE98
main 004C7F84 PUSH ESI ESP = 0018FE94
main 004C7F85 JMP SHORT 004C7FB6
main 004C7FB6 PUSH EDI ESP = 0018FE90
main 004C7FB7 PUSH 0040ED10 ESP = 0018FE8C
main 004C7FBC CALL DWORD PTR DS: [40E19C] EAX = 00000012, ECX = 93D2AD8F, EDX = 7418F4B8
main 004C7FC2 JMP SHORT 004C7FA0
main 004C7FA0 STC
main 004C7FA1 JMP SHORT 004C7FDA
main 004C7FDA ADD ESP, 4 ESP = 0018FE90
main 004C7FDD CALL 004C7FC4 ESP = 0018FE8C
main 004C7FC4 LEA ESP, [ESP +4] ESP = 0018FE90
main 004C7FC8 PUSH 0040ECF8 ESP = 0018FE8C
main 004C7FCD CALL 004C7FE2 ESP = 0018FE88
main 004C7FE2 MOV BYTE PTR SS: [ESP], CH
main 004C7FE5 JMP SHORT 004C7FEB
main 004C7FEB LEA ESP, [ESP +4] ESP = 0018FE8C
main 004C7FEF CALL DWORD PTR DS: [40E19C] EAX = 00000014
main 004C7FF5 SETPE BH EBX = 7EFD0100
main 004C7FF8 XCHG BL, BH EBX = 7EFD0001
main 004C7FFA INC EBX EBX = 7EFD0002
main 004C7FFB JMP SHORT 004C805A
main 004C805A ADD ESP, 4 ESP = 0018FE90
main 004C805D POP EDI ESP = 0018FE94
main 004C805E MOV ESI, 4B536EDD ESI = 4B536EDD
main 004C8063 MOV SI, WORD PTR SS: [ESP] ESI = 4B530000
main 004C8067 JMP SHORT 004C801E
main 004C801E LEA EBX, [CDDFCA2F] EBX = CDDFCA2F
main 004C8024 POP ESI ESP = 0018FE98, ESI = 00000000
main 004C8025 CALL 004C8008 ESP = 0018FE94
main 004C8008 POP WORD PTR SS: [ESP] ESP = 0018FE96
main 004C800C MOV BX, WORD PTR SS: [ESP +1] EBX = CDDF0000
main 004C8011 XCHG BYTE PTR SS: [ESP], BL EBX = CDDF004C
main 004C8014 JMP SHORT 004C8040
main 004C8040 LEA ESP, [ESP +2] ESP = 0018FE98
main 004C8044 POP EBX EBX = 7EFDE000, ESP = 0018FE9C
main 004C8045 JMP SHORT 004C802A
main 004C802A MOV ESP, EBP ESP = 0018FEDC
main 004C802C LEA EBP, [EDI + EAX] EBP = 00000014
main 004C802F MOV BP, 3200 EBP = 00003200
main 004C8033 MOV EBP, CEF73787 EBP = CEF73787
main 004C8038 JMP 004C80ED
main 004C80ED POP EBP ESP = 0018FEE0, EBP = 0018FF30
main 004C80EE RETN ESP = 0018FEE4
-------- Logging stopped 
 
У наведеному прикладі мутація коду виконана на найменшому рівні складності. Safengine дозволяє збільшувати цю складність до 254 разів, що в підсумку може роздути ваш код з 10 інструкцій в набір сміття перевищує розмір вихідної програми в пару раз, що достатньо надмірно.
 
Так само до недоліком можна віднести випадки пошкодження програми, які на жаль на моїй пам'яті бували. Одна справа якщо такі збої виникнуть в звичайній програмі, що призведе до падіння і зовсім інше якщо падіння відбудеться в драйвері, що абсолютно не типово. Як відомо протектори можуть обробляти різні виконувані файли (exe, dll, ocx, sys).
 
Маркетингова політика теж часом залишає бажати кращого. Технічний сміття у вуха клієнтам створює ілюзію захищеності, не є добре. Адже розробники протекторів не пишуть в описах своїх продуктів що ця технологія хороша, але в ній є ось такий-то такий-то вилучила.
 
 
0x02. Проблема не повноти захисту коду
Нарешті перейдемо до питання, що ж заважає розробникам протекторів написати ще більш складну і стійкий захист? Відповідь досить проста — неповнота інформації. Отримуючи на вхід бінарний файл, навіть за наявності налагоджувальної інформації існує ряд обмежень, порушення яких призведе до не універсальні протектора, або пошкодження захищається додатки. Для прикладу таких обмежень поглянемо на структуру звичайного PE програми:
 
 
Raw Virtual Name
------------------------------------
00000000 00400000 PE header
00000200 00401000 Code sector
00000400 00402000 Data sector
00000600 00403000 Resources sector
 
Код і дані в додатку поміщаються в різні секції, але між ними є чіткі зв'язки. Таким чином кілька різних функцій можуть посилатися на один і той же блок даних, а дані можуть посилатися на інші дані і функції. Причому не завжди цей зв'язок є явною. Виходячи з цього, протектори не можуть вільно маніпулювати структурою виконуваного файлу: пересувати дані, розширювати, переміщати функції у вихідному сегменті і т.д. Хоча колись я робив спроби розширення секцій виконуваного файлу, але це окремий випадок не є універсальним. Тому протектори укладають свої дані в такий спосіб:
 
 
Raw Virtual Name
------------------------------------
00000000 00400000 PE header
00000200 00401000 Code sector
00000400 00402000 Data sector
00000600 00403000 Protector sector
00000800 00404000 Resources sector
 
Початкове місце розташування коду і даних не змінюється, хоча переміщення деяких інших секторів можливо (ресурси, релокації, ...). Захищуваний код вирізається і на його місце поміщається сміттєві інструкції, найчастіше ці інструкції є частиною мутованого графа виконання. Мутований граф і віртуальна машина поміщаються в сектор протектора.
 
Так само у зв'язку із збільшенням обчислювальної навантаження, протектори не можуть мутувати весь код програми. Тому вибір захищаються ділянок коду лягає на плечі програмісту. Але програміст не завжди може застосувати це накладення розумно. Наприклад накривши якийсь алгоритм шифрування мутацією, програміст забуду накрити мутацією всі виклики цього шифру, знайшовши які дослідник зможе роздобути вхідні дані цього алгоритму і на їх основі будувати припущення з влаштування шифру і можливо навіть класифікувати або відтворити його.
 
Все це призводить до витоку інформації з мутирование \ віртуалізованого коду і дозволяє виконувати атаки на нього. Знаючи зразкове місце розташування даних або якихось функцій ми можемо відстежувати звернення до них з чорного ящика, тим самим замість розплутування клубка, ми складаємо фантомний модель алгоритму. Звичайно такий підхід далеко не претендує на те щоб дати нам повну картину алгоритму програми, проте в деяких випадках цього досить достатньо.
 
 
0x03. Рибалимо в чорному ящику
Типовим засобом дослідження чорного ящика, є трассировщик. Однак траса обфусцірованного коду на 80-99% складається зі сміття, тому нам потрібно якось з цього сміття отримати тільки корисну інформацію. Цей процес чимось нагадує риболовлю. Уявімо що траса це озеро, трассировщик вудка, а умови трасування ця наживка. Використовуючи вищеописані недоліки протекторів, ми можемо підібрати правильну наживку і зловити правильну інформацію. Погляньмо як же це виглядає на практиці.
 
Припустимо у нас є наступна програма:
 
 
void array_fill(unsigned char *buf, size_t size)
{
    for (int i = 0; i < size; i++) {
        buf[i] = i;
        if (i > 0) {
            buf[i] ^= buf[i - 1];
        }
    }
}

int main()
{
    unsigned char buf[10];
    array_fill(buf, sizeof(buf));
    return 0;
}

Нехай на функцію array_fill () буде накладена і мутація і віртуалізація. Давайте оттрассіруем виклик функції array_fill ():
 
Початкове кількість кроків: 230
Кількість кроків після обфускаціі VMProtect: 83924
Кількість кроків після обфускаціі Safengine: 250382
 
Як видно з цифр розібрати трасу в ручну просто нереально. Тому скористаємося методом риболовлі.
 
Уявімо що ми нічого не знаємо про функції array_fill (). Дослідивши main () ми можемо точно сказати що її подвизов приймає на вході адресу буфера і його розмір, після чого згідно якомусь алгоритму буфер заповнюється інформацією. Тому ми задамо нашому трасувальникові правило, за яким ми будемо Залогуватися тільки звернення на читання \ запис до переданому в функцію буферу. Результат для всіх трьох варіантів додатків буде один і той же:
 Trace log
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7700, ECX = 0018FF34
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7701, ECX = 0018FF35
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF34
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF35
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7701, ECX = 0018FF35
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7702, ECX = 0018FF36
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF35
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF36
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7703, ECX = 0018FF36
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7703, ECX = 0018FF37
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF36
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF37
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7700, ECX = 0018FF37
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7704, ECX = 0018FF38
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF37
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF38
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7704, ECX = 0018FF38
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7705, ECX = 0018FF39
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF38
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF39
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7701, ECX = 0018FF39
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7706, ECX = 0018FF3A
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF39
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF3A
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7707, ECX = 0018FF3A
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7707, ECX = 0018FF3B
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF3A
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF3B
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7700, ECX = 0018FF3B
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7708, ECX = 0018FF3C
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF3B
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF3C
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7708, ECX = 0018FF3C
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7709, ECX = 0018FF3D
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF3C
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF3D 

Як видно не залежно від ступеня мутації коду і кол-ва сміття, ми змогли отримати чистий трасу описує всі звернення до буферу. Але чи зможемо ми відновити по ній вихідний алгоритм? Давайте спробуємо.
 
І так якщо придивитися в трасу стає видно цикл (це видно по повторюваним зверненнями до коду за адресою 004B7898):
 
 
; 1 step
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7700, ECX = 0018FF34
; 2 step
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7701, ECX = 0018FF35
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF34
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF35
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7701, ECX = 0018FF35
; 3 step
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7702, ECX = 0018FF36
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF35
main 004B7A8E MOV AL, BYTE PTR DS: [ECX] EAX = 004B7A89, ECX = 0018FF36
main 004B7898 MOV BYTE PTR DS: [ECX], AL EAX = 004B7703, ECX = 0018FF36
... 
і таких кроків всього 10, що відповідає розміру нашого буфера. Далі все досить просто, знаючи які значення беруться і які кладуться назад, наш алгоритм роботи лежить у нас практично на долоні. Єдине що тут потрібно вгадати, це застосування операції XOR, але в даному випадку це абсолютно не складно.
 
Звичайно даний приклад є штучним, на практиці ж доводиться стикатися з більш складними алгоритмами з вкладеними викликами і неявній логікою, отримання інформації про яких може опинитися в десятки разів складніше. У таких ситуаціях застосовуються більш складні трасувальники, деобфускатори, DBI і т.д. Проте все зводиться до видеранію інформації трасувальникові та її аналізу. Знаючи адреси до яких алгоритм може звернеться, ми збираємо досить корисної інформації про нього.
 
 
0x04. Аналіз вкладених викликів
Не менш важливою інформацією яку можна витягнути з чорного ящика, є інформація про вкладені виклики. Це можуть бути виклики WinAPI, бібліотечні функції, функції самого додатка. Така інформація допомогла б нам більш детально вивчити внутрішній устрій і залежності захищеного алгоритму.
 
У простому випадку для аналізу вкладених викликів, ми можемо скористатися відомою інформацією про структуру виконуваного файлу. Тобто знаючи в якому сегменті розташований мутований код, можна відстежити всі виходи з даного сегмента, які будуть відповідати викликам зовнішніх функцій. І це дійсно буде працювати для бібліотечних функцій, але проблеми можуть виникнути для виклику власних функцій програми. Як я говорив десь вище, вирізаючи захищається код, на його місце протектор може покласти частину власного коду (мутований граф, шматок віртуальної машини). Таким чином якщо виконання коду виходить з секції протектора в секцію коду програми, немає ніякої гарантії що це саме виклик вкладеної функції, а не виконання частини мутованого графа.

Вирішення цієї проблеми знову досить вірогідне. По перше нам буде потрібно гнучкий трассировщик, наприклад можна скористатися фреймворком Intel Pin , минулого разу я використовував трассировщик вбудований в OllyDbg. По друге потрібно створити грамотні трассіровочние правила, які дозволять нам Залогуватися тільки вкладені виклики, виключаючи сміття, розташований в секції коду. Якщо ми хочемо визначити виклик бібліотечної функції то нам достатньо створити правило, яке буде фіксувати передачу управління за межі дільниці пам'яті приналежному досліджуваного модулю. У більшості випадків це буде передача управління в якусь бібліотеку, хоча в деяких нестандартних ситуаціях передача управління може бути виконана в деякий базонезавісімий код протектора. Але такі окремі випадки ми розглядати не будемо. Однак що ж робити з вкладеними викликами в самій програмі?

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


Шаблоновий пролог функції

Тобто при поверненні управління з обфусціруемого коду (секції протектора), в секцію коду програми ми можемо перевіряти наявність інструкцій Int3, Retn розташованих до передбачуваного прологу і так само звіряти епілог із заздалегідь підготовленими сигнатурами. Це допоможе не завжди, так як компілятор може напхати чого завгодно в код, особливо після оптимізації, але це більше ніж нічого.

Так само я помітив один маленький недолік в обфускаціі протектором Safengine, який може бути присутнім і в інших протекторах, але якого немає в VMProtect. Недолік полягає в тому, що якщо протектор мутує якусь функцію і в ній є виклик іншої мутіруемой функції, то вкладена функція викликається не в мутіруемом коді, а через перехідник (інструкцію jmp) який розташований по її оригінальному адресою. Це так само є витоком інформації з мутованого коду і може бути використане для створення правила трасувальника. Хоча в VMProtect наприклад вкладена мутіруемая функція буде викликатися відразу в сегменті протектора, не дозволяючи нам таким чином визначати вкладений виклик. Можливо цей недолік існує тільки в демо версії Safengine.

Я вже не став засмічувати статтю исходниками трасувальника на Intel Pin, але якщо стаття вам сподобається, то можна і про трассировщик написати окремою статтею.

0x05. Відновлення графа виконання
Ми всі ходили навкруги, намагалися всіляко уникати повного аналізу мутованого коду, проте деобфускація і девіртуалізація далеко не міф. Просто на ділі такі технології досить складні і не пишуться на коліні. Я думаю фахівці з університетів та антивірусних лабораторій у всю вже освоїли даний напрямок і мають багатий набір засобів для їх реалізації. На жаль я не сильно знайомий з практикою застосування таких технологій і сказати про їх мені особливо нічого, крім того що вони є і що це круто :)

Якщо у вас є якісь знання і досвід в даному питанні буду вдячний якщо підкинете пару ссилочек в коментарях.

0x06. Висновок
Як бачите чорний ящик на ділі виявляється не таким вже чорним. За зібраними крупицях інформації, ми можемо частково відновлювати логіку захищеного алгоритму і деколи в достатньому обсязі.

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

Наостанок, бажаючим перевірити свої навички реверсу обфусцірованного коду, пропоную вирішити даний crackme .

Спасибі за увагу.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.