Взлом
Уважаемые гости! При посещении нашего сайта просим вас ознакомиться с разделами форума, прежде чем оставлять ваши объявления и т.п., а также при обращении за помощью просим быть внимательными: на сайте есть как проверенные специалисты, так и непроверенные. Если вы обратились к специалисту, который проверку НЕ проходил, рекомендуем воспользоваться услугой гарант-сервиса. Спасибо, что посетили XakerPlus!

REvil: Разбираем внутренности шифровальщика — от распаковки до восстановления API.

DataSQL

Новый
Пользователь.
Разбираем REvil. Как известный шифровальщик прячет вызовы WinAPI.
revil-768x461.jpeg

Слушай, история с 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. Хеши хранятся в зашифрованном виде, перед использованием преобразуются через простую арифметику, а затем сравниваются с хешами экспортируемых функций.

Это не остановит опытного реверсера, но:

· Замедляет автоматический анализ.
· Сбивает с толку простые детекторы.
· Требует написания скрипта для восстановления.

Если ты занимаешься анализом малвари — такие техники нужно знать. Они встречаются часто, и алгоритмы обычно простые. Пишешь скрипт, восстанавливаешь таблицу, и дальше работаешь как с обычной программой.
 


Сверху