Разбираем REvil. Как известный шифровальщик прячет вызовы WinAPI.
Слушай, история с Travelex была громкой. Атака, шифровальщик REvil, миллионные требования выкупа. Но меня тогда заинтересовало не это. Меня заинтересовало, как REvil обфусцирует свои вызовы WinAPI. Не банальный динамический импорт, а настоящая обфускация — хеши функций, которые еще и в процессе работы преобразуются.
Я взял образец, покопался в нем. В этой статье расскажу, как я его распаковывал, как вытаскивал таблицу хешей и как превращал эти хеши обратно в нормальные имена функций. Спойлер: математика там простая, но реализация хитрая.
---
Первая разведка: что показывает DiE
Загружаю файл в Detect It Easy (DiE). DiE говорит: упаковщик не обнаружен. Странно. Смотрю секции — названия как у UPX, но сигнатура явно искажена. Энтропия высокая. Скорее всего, UPX, но с измененными сигнатурами, чтобы сбить с толку автоматические детекторы.
Значит, наша задача — распаковать вручную.
---
Распаковка: идем через VirtualAlloc
Загружаю файл в x64dbg. Начинаю с точки входа. Ставлю брейк на VirtualAlloc — это стандартный прием при распаковке. Почему? Потому что упаковщик должен выделить память под распакованный код.
После вызова VirtualAlloc в eax возвращается адрес выделенной памяти. Ставлю брейк на возврате:
Смотрю, что попало в eax. Теперь ставлю однократную точку останова на запись по этому адресу. Жму F9.
Срабатывает. Попадаю в цикл копирования:
Прокручиваю руками несколько итераций. В памяти начинает проявляться знакомая сигнатура:
MZ — это DOS-заголовок. Значит, в память пишется PE-файл. Мы на месте.
Теперь смотрю, куда ведет jmp eax в конце распаковщика. Останавливаюсь на этом переходе:
Шагаю вперед. Попадаю в распакованный код. Снимаю дамп и загружаю в IDA Pro.
---
Первые шаги в IDA
Точка входа выглядит просто:
Захожу в sub_40369D. Внутри:
call dword_41CB64 — это вызов по адресу из таблицы. Смотрю, что лежит по dword_41CB64:
Число, похожее на хеш. Таблица таких чисел идет дальше. Импорта нет — все вызовы идут через эти хеши. Значит, нужно расшифровать, каким функциям они соответствуют.
---
Функция преобразования хешей
Иду в sub_406A4D. Там:
Видно, что берется хеш из таблицы, передается в sub_405DCF, и результат сохраняется обратно. Весь массив обрабатывается.
Иду в sub_405DCF. Код большой, переключаюсь на декомпилятор IDA (F5). Получаю псевдокод. Суть простая:
Входной параметр a1 — это хеш из таблицы. Выходной — преобразованный хеш, который дальше сравнивается с хешами от имен экспортируемых функций.
Алгоритм можно выразить так:
Проверяю на одном из значений таблицы. Подходит. Теперь нужно понять, как хешируются имена функций.
---
Алгоритм хеширования имен
Смотрю дальше в sub_405DCF. Там идет работа с таблицей экспорта:
Смещения 0x3C и 0x78 — это стандартные поля PE-заголовка. v13 — базовый адрес загруженной DLL. Значит, код парсит экспортную таблицу. sub_405BAE — это функция, которая хеширует имя функции. Смотрю в нее:
Вот он, алгоритм. На Python:
Теперь у нас есть всё: исходные хеши из семпла, функция их преобразования и функция хеширования имен. Осталось сопоставить.
---
Восстанавливаем таблицу вызовов
Пишем скрипт для IDA (на Python). Вместо ручного перебора используем автоматику.
1. Вычитываем все хеши из таблицы dword_41C9F8.
2. Преобразуем каждый через transform_hash.
3. Генерируем хеши для имен функций из известных DLL (kernel32, user32, advapi32).
4. Сопоставляем.
5. Вставляем в IDA комментарии с именами функций.
Вот упрощенная версия:
После выполнения у нас есть список соответствий. Теперь можно вручную (или автоматически) заменить вызовы по хешам на нормальные имена.
---
Что в итоге
REvil использует не самый сложный, но действенный метод обфускации API. Хеши хранятся в зашифрованном виде, перед использованием преобразуются через простую арифметику, а затем сравниваются с хешами экспортируемых функций.
Это не остановит опытного реверсера, но:
· Замедляет автоматический анализ.
· Сбивает с толку простые детекторы.
· Требует написания скрипта для восстановления.
Если ты занимаешься анализом малвари — такие техники нужно знать. Они встречаются часто, и алгоритмы обычно простые. Пишешь скрипт, восстанавливаешь таблицу, и дальше работаешь как с обычной программой.
Слушай, история с Travelex была громкой. Атака, шифровальщик REvil, миллионные требования выкупа. Но меня тогда заинтересовало не это. Меня заинтересовало, как REvil обфусцирует свои вызовы WinAPI. Не банальный динамический импорт, а настоящая обфускация — хеши функций, которые еще и в процессе работы преобразуются.
Я взял образец, покопался в нем. В этой статье расскажу, как я его распаковывал, как вытаскивал таблицу хешей и как превращал эти хеши обратно в нормальные имена функций. Спойлер: математика там простая, но реализация хитрая.
---
Первая разведка: что показывает DiE
Загружаю файл в Detect It Easy (DiE). DiE говорит: упаковщик не обнаружен. Странно. Смотрю секции — названия как у UPX, но сигнатура явно искажена. Энтропия высокая. Скорее всего, UPX, но с измененными сигнатурами, чтобы сбить с толку автоматические детекторы.
Значит, наша задача — распаковать вручную.
---
Распаковка: идем через VirtualAlloc
Загружаю файл в x64dbg. Начинаю с точки входа. Ставлю брейк на VirtualAlloc — это стандартный прием при распаковке. Почему? Потому что упаковщик должен выделить память под распакованный код.
После вызова VirtualAlloc в eax возвращается адрес выделенной памяти. Ставлю брейк на возврате:
Код:
008F9552| FF55 B4 | call dword ptr ss:[ebp-4C] | VirtualAlloc 008F9555| 8945 F0 | mov dword ptr ss:[ebp-10],eax | <---- мы здесь
Смотрю, что попало в eax. Теперь ставлю однократную точку останова на запись по этому адресу. Жму F9.
Срабатывает. Попадаю в цикл копирования:
Код:
00279DA4| 8A11 | mov dl,byte ptr ds:[ecx] | 00279DA6| 8810 | mov byte ptr ds:[eax],dl | 00279DA8| 40 | inc eax | 00279DA9| 41 | inc ecx | 00279DAA| 4F | dec edi | 00279DAB| 75 F7 | jne 279DA4 |
Прокручиваю руками несколько итераций. В памяти начинает проявляться знакомая сигнатура:
Код:
003C0000 4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00 MZ..........y..
MZ — это DOS-заголовок. Значит, в память пишется PE-файл. Мы на месте.
Теперь смотрю, куда ведет jmp eax в конце распаковщика. Останавливаюсь на этом переходе:
Код:
00569C23| FFE0 | jmp eax |
Шагаю вперед. Попадаю в распакованный код. Снимаю дамп и загружаю в IDA Pro.
---
Первые шаги в IDA
Точка входа выглядит просто:
Код:
public start start proc near push 0 call sub_40369D push 0 call sub_403EEF pop ecx retn start endp
Захожу в sub_40369D. Внутри:
Код:
sub_40369D proc near call sub_406A4D push 1 call dword_41CB64 call sub_40489C test eax, eax jz short loc_4036BD
call dword_41CB64 — это вызов по адресу из таблицы. Смотрю, что лежит по dword_41CB64:
Код:
.data:0041CB64 dword_41CB64 dd 40D32A7Dh
Число, похожее на хеш. Таблица таких чисел идет дальше. Импорта нет — все вызовы идут через эти хеши. Значит, нужно расшифровать, каким функциям они соответствуют.
---
Функция преобразования хешей
Иду в sub_406A4D. Там:
Код:
loc_405BD6: push dword_41C9F8[esi] call sub_405DCF mov dword_41C9F8[esi], eax add esi, 4 pop ecx cmp esi, 230h jb short loc_405BD6
Видно, что берется хеш из таблицы, передается в sub_405DCF, и результат сохраняется обратно. Весь массив обрабатывается.
Иду в sub_405DCF. Код большой, переключаюсь на декомпилятор IDA (F5). Получаю псевдокод. Суть простая:
Код:
int__cdecl sub_405DCF(int a1) { v1 = (unsigned int)a1 ^ (((unsigned int)a1 ^ 0x76C7) << 16) ^ 0xAFB9; // ... v15 = v1 & 0x1FFFFF; // ... }
Входной параметр a1 — это хеш из таблицы. Выходной — преобразованный хеш, который дальше сравнивается с хешами от имен экспортируемых функций.
Алгоритм можно выразить так:
Python:
def transform_hash(hash_val): tmp = hash_val ^ ((hash_val ^ 0x76C7) << 16) ^ 0xAFB9 return tmp & 0x1FFFFF
Проверяю на одном из значений таблицы. Подходит. Теперь нужно понять, как хешируются имена функций.
---
Алгоритм хеширования имен
Смотрю дальше в sub_405DCF. Там идет работа с таблицей экспорта:
Код:
v17= (IMAGE_EXPORT_DIRECTORY *)(v13 + *(_DWORD )((_DWORD *)(v13 + 0x3C) + v13 + 0x78)); v18= (int)v17->AddressOfNames + v13; v20= (int)v17->AddressOfFunctions + v13; v23= v17->NumberOfNames; while( (sub_405BAE(v14 + *(_DWORD *)(v18 + 4 * v16)) & 0x1FFFFF) != v15 ) { if ( ++v16 >= v23 ) return 0; }
Смещения 0x3C и 0x78 — это стандартные поля PE-заголовка. v13 — базовый адрес загруженной DLL. Значит, код парсит экспортную таблицу. sub_405BAE — это функция, которая хеширует имя функции. Смотрю в нее:
Код:
int__cdecl sub_405BAE(const char *name) { int result = 0x2b; for ( ; name; ++name ) result = (unsigned __int8)name + 0x10F * result; return result & 0x1FFFFF; }
Вот он, алгоритм. На Python:
Python:
def hash_name(name): result = 0x2b for c in name: result = ord(c) + 0x10f * result return result & 0x1FFFFF
Теперь у нас есть всё: исходные хеши из семпла, функция их преобразования и функция хеширования имен. Осталось сопоставить.
---
Восстанавливаем таблицу вызовов
Пишем скрипт для IDA (на Python). Вместо ручного перебора используем автоматику.
1. Вычитываем все хеши из таблицы dword_41C9F8.
2. Преобразуем каждый через transform_hash.
3. Генерируем хеши для имен функций из известных DLL (kernel32, user32, advapi32).
4. Сопоставляем.
5. Вставляем в IDA комментарии с именами функций.
Вот упрощенная версия:
Python:
import idaapi import idc
def transform_hash(hash_val): tmp = hash_val ^ ((hash_val ^ 0x76C7) << 16) ^ 0xAFB9 return tmp & 0x1FFFFF
def hash_name(name): result = 0x2b for c in name: result = ord(c) + 0x10f * result return result & 0x1FFFFF
Список функций для проверки (в реальности нужно брать из всех загруженных DLL)
api_names = [
"CreateFileA", "WriteFile", "ReadFile", "CloseHandle",
"VirtualAlloc", "VirtualProtect", "GetProcAddress", "LoadLibraryA"
]
Создаем словарь: хеш -> имя
hash_to_name = {} for name in api_names: h = hash_name(name) hash_to_name[h] = name
Вычитываем таблицу хешей из адреса 0x41C9F8 (в IDA)
start_addr = 0x41C9F8 size= 0x230 # длина таблицы в байтах for i in range(0,size, 4): orig_hash = idc.get_wide_dword(start_addr + i) real_hash = transform_hash(orig_hash) if real_hash in hash_to_name: print(f"0x{orig_hash:08X} -> {hash_to_name[real_hash]}") # Можно добавить комментарий в код # idc.set_cmt(call_addr, hash_to_name[real_hash], 0)
После выполнения у нас есть список соответствий. Теперь можно вручную (или автоматически) заменить вызовы по хешам на нормальные имена.
---
Что в итоге
REvil использует не самый сложный, но действенный метод обфускации API. Хеши хранятся в зашифрованном виде, перед использованием преобразуются через простую арифметику, а затем сравниваются с хешами экспортируемых функций.
Это не остановит опытного реверсера, но:
· Замедляет автоматический анализ.
· Сбивает с толку простые детекторы.
· Требует написания скрипта для восстановления.
Если ты занимаешься анализом малвари — такие техники нужно знать. Они встречаются часто, и алгоритмы обычно простые. Пишешь скрипт, восстанавливаешь таблицу, и дальше работаешь как с обычной программой.


