Хакер - Разбираем REvil. Как известный шифровальщик прячет вызовы WinAPI
hacker_frei
Nik Zerof
Не так давно атаке подверглась международная система денежных переводов Travelex, и виновником этого оказался шифровальщик REvil, чем и привлек мое внимание. Забегая вперед, скажу, что в этом трояне использованы простые, но эффективные методы обфускации, которые не позволяют нам так просто увидеть используемые им вызовы WinAPI. Давай посмотрим, как устроен этот энкодер изнутри.
По доброй традиции загрузим семпл в DiE и поглядим, что он нам покажет.

DiE считает, что файл ничем не упакован. Хотя постой-ка, давай переключимся на показания энтропии секций.

Судя по названиям секций, файл упакован UPX, но их энтропия выглядит очень странно. Почему тогда DiE не распознал упаковщик? Ну, например, сигнатура UPX может быть намеренно искажена, чтобы запутать дизассемблеры. Так или иначе, перед нами упакованный файл, поэтому загружаемся в отладчик x64dbg. Давай поставим точку останова на функцию VirtualAlloc, которая мелькает у нас в окрестностях точки входа, и запустим троян.
INFO
Есть несколько функций WinAPI, бряки на которые нужно устанавливать по умолчанию при распаковке неизвестного пакера, ибо механизмы распаковки достаточно стандартны:
- VirtualAlloc — используется при выделении памяти для пейлоада;
- VirtualProtect — используется для установки атрибутов доступа к памяти;
- CreateProcessInternalW — при создании нового процесса, в эту функцию в итоге передается управление;
- ResumeThread — используется для продолжения выполнения при инъекциях.
Брякаемся на функции и выходим из нее в наш код. В итоге видим такую картину:
008F9552 | FF55 B4 | call dword ptr ss:[ebp-4C] | VirtualAlloc 008F9555 | 8945 F0 | mov dword ptr ss:[ebp-10],eax | <---- мы находимся здесь 008F9558 | 8365 DC 00 | and dword ptr ss:[ebp-24],0 | 008F955C | 8B85 58FFFFFF | mov eax,dword ptr ss:[ebp-A8] | 008F9562 | 0FB640 01 | movzx eax,byte ptr ds:[eax+1] |
Осматриваемся дальше, видим интересный кусок кода в конце функции, в которой мы оказались:
00569C10 | 8985 5CFFFFFF | mov dword ptr ss:[ebp-A4],eax | 00569C16 | 8B85 5CFFFFFF | mov eax,dword ptr ss:[ebp-A4] | 00569C1C | 0385 68FFFFFF | add eax,dword ptr ss:[ebp-98] | 00569C22 | C9 | leave | 00569C23 | FFE0 | jmp eax | Интересный переход!
Не забываем: при отработке функции VirtualAlloc адрес выделенной памяти находится в eax. Ставим точку останова на этот переход, попутно переходим на дамп (адрес в eax) и смотрим, что будет происходить в выделенной памяти. Для этого ставим на начале этой памяти однократную точку останова на запись, и отладчик останавливается на цикле записи данных в память. Вот так выглядит часть цикла:
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..........yy..
Отпускаем отладчик и останавливаемся на jmp eax, делаем шаг вперед — и мы в распакованном файле! Теперь можно снимать дамп и загружать его в IDA Pro. Выполнив эту нехитрую процедуру, мы увидим код стартовой функции:
public start start proc near push 0 call sub_40369D push 0 call sub_403EEF pop ecx retn start endp
Исследование функций и возможностей нашего вредоноса надо с чего-то начинать, поэтому заходим в первый call. Там нас ждет уже более интересный код:
sub_40369D proc near call sub_406A4D // Перед вызовом функции по хешу есть только одна подпрограмма; очевидно, все самое интересное спрятано здесь :) push 1 call dword_41CB64 // Хм, что это? call sub_40489C test eax, eax jz short loc_4036BD ... ...
Видим вызов подпрограммы sub_406A4D, далее вызов такого рода: call dword_41CB64. Очевидно, что если все «оставить как есть», то здесь приложение упадет при выполнении, потому что dword_41CB64 ведет на таблицу такого рода (это только часть таблицы!):
.data:0041CB64 dword_41CB64 dd 40D32A7Dh ; DATA XREF: sub_40369D+7↑r .data:0041CB68 dword_41CB68 dd 0C97676C4h ; DATA XREF: sub_403EE1+6↑r .data:0041CB6C dword_41CB6C dd 0D69D6931h ; DATA XREF: sub_403BC0+15↑r .data:0041CB70 dword_41CB70 dd 8AABE016h ; DATA XREF: sub_406299+C0↑r ... ...
Кроме того, в нашем образе таблица импорта пустая: разумеется, функции WinAPI получаются динамически, имена функций не хранятся в открытом виде, и, похоже, используются их хеши. На самом деле «Хакер» уже описывал подобную технику обфускации вызовов WinAPI, следовательно, нашим постоянным читателям будет проще разобраться в устройстве REvil. Итак, ныряем в функцию sub_406A4D, видим там один безусловный переход и следуем дальше в sub_405BCD. Практически в начале функции видим очень интересный код:
loc_405BD6: // Кладем на стек элемент из таблицы хешей, на которую указывает ESI push dword_41C9F8[esi] // Работаем над этими данными, обработанное значение вернется в EAX call sub_405DCF // Возвращаем обратно mov dword_41C9F8[esi], eax // Идем по списку дальше (шагаем по четыре байта) add esi, 4 pop ecx cmp esi, 230h jb short loc_405BD6
Разумеется, мы не можем не заглянуть в функцию sub_405DCF. Там мы видим целую портянку кода, поэтому придется переключиться в декомпилированный псевдокод, чтобы не погрязнуть в этом болоте с головой. Конечно, если ты гуру ассемблера и тебе не составляет труда читать много кода на этом языке, можешь оставить все как есть, а лично мне привычнее псевдокод IDA Pro.
Функция большая, поэтому полностью приводить ее в статье не имеет смысла, но мы можем сконцентрироваться на ее основных частях. Работу функции можно разделить на два этапа. Первый — трансформация имеющихся хеш-сумм, указанных в программе. Второй — получение из таблицы экспорта системных библиотек имен функций, хеширование и сверка с шаблонами, полученными из таблицы, которую мы уже видели.
Парсинг таблицы экспорта системной библиотеки на псевдокоде выглядит таким образом:
v17 = (IMAGE_EXPORT_DIRECTORY *)(v13 + *(_DWORD *)(*(_DWORD *)(v13 + 0x3C) + v13 + 0x78));
v21 = (int)v17->AddressOfNameOrdinals + v13;
v18 = (int)v17->AddressOfNames + v13;
v22 = (int)v17->AddressOfNames + v13;
v20 = (int)v17->AddressOfFunctions + v13;
v23 = v17->NumberOfNames;
if ( !v23 )
return 0;
while ( (sub_405BAE(v14 + *(_DWORD *)(v18 + 4 * v16)) & 0x1FFFFF) != v15 ){
v18 = v22;
if ( ++v16 >= v23 )
return 0;
}
Почему именно этот кусок псевдокода привлек мое внимание? Разумеется, бросаются в глаза такие смещения, как 0x3C или 0x78. Кроме того, переменная v13, работающая с этими числами, приводится к типу DWORD*, говоря нам, что мы смотрим на некое смещение. Разумеется, все указывает на заголовок PE-файла:
0x00 WORD emagic Magic DOS signature MZ (0x4d 0x5A) 0x02 WORD e_cblp Bytes on last page of file 0x04 WORD e_cp Pages in file 0x06 WORD e_crlc Relocations 0x08 WORD e_cparhdr Size of header in paragraphs 0x0A WORD e_minalloc Minimum extra paragraphs needed 0x0C WORD e_maxalloc Maximum extra paragraphs needed 0x0E WORD e_ss Initial (relative) SS value 0x10 WORD e_sp Initial SP value 0x12 WORD e_csum Checksum 0x14 WORD e_ip Initial IP value 0x16 WORD e_cs Initial (relative) CS value 0x18 WORD e_lfarlc File address of relocation table 0x1A WORD e_ovno Overloay number 0x1C WORD e_res[4] Reserved words (4 WORDs) 0x24 WORD e_oemid OEM identifier (for e_oeminfo) 0x26 WORD e_oeminfo OEM information; e_oemid specific 0x28 WORD e_res2[10] Reserved words (10 WORDs) 0x3c DWORD e_lfanew Offset to start of PE header
В коде мы видим смещение 0x3c, которое соответствует полю e_lfanew. Двигаясь далее по e_lfanew по смещению 0x78 (смотрим псевдокод), мы видим вот такое поле:
0x78 DWORD Export Table RVA of Export Directory
Значит, идет разбор таблицы экспорта, что говорит о динамическом получении WinAPI.
Чтобы IDA Pro «понимала» структуру таблицы экспорта, ее необходимо объявить в Local Types, нажав Shift + F1. После этого на переменной v17 нужно скомандовать из контекстного меню Convert to struct*. Структура таблицы экспорта PE-файла выглядит таким образом:
struct IMAGE_EXPORT_DIRECTORY {
long Characteristics;
long TimeDateStamp;
short MajorVersion;
short MinorVersion;
long Name;
long Base;
long NumberOfFunctions;
long NumberOfNames;
long *AddressOfFunctions;
long *AddressOfNames;
long *AddressOfNameOrdinals;
}
Как раз здесь мы видим используемые поля: *AddressOfFunctions, *AddressOfNames и *AddressOfNameOrdinals. По псевдокоду понятно, что хеши из уже имеющихся в коде получаются таким образом:
int __cdecl sub_405DCF(int (*a1)(void)){ // Передача аргумента
... // Много строк, которые можно пропустить
v1 = (unsigned int)a1 ^ (((unsigned int)a1 ^ 0x76C7) << 16) ^ 0xAFB9;
... //
v15 = v1 & 0x1FFFFF;
... //
}
Да, в теле семпла используются не «готовые» хеши, их еще предстоит привести в правильный вид. Если отбросить все лишнее, мы получим следующий алгоритм:
hash_api_true = (hash ^ ((hash ^ 0x76C7) << 16) ^ 0xAFB9) & 0x1FFFFF
где hash — переданный в качестве аргумента функции хеш из таблицы. Хорошо, что IDA подсвечивает одинаковые переменные, иначе анализ семпла занял бы намного больше времени. В псевдокоде этот хеш хранится в переменной под именем a1, которая является аргументом функции.
Если пропустить через этот алгоритм указанные в коде хеши (помнишь таблицу?), получаем «правильные» хеши, которые будут сравниваться с полученными из таблицы экспорта системной библиотеки, точнее, из имен экспортируемых функций. Псевдокод получения хеша из символьного имени функции будет выглядеть на Python так:
def hash_from_name(name):
result = 0x2b
for x in name:
result = ord(c) + 0x10f * result
return result & 0x1FFFFF
Вызов функции:
hash_from_name(name) # Name — переменная, содержащая символьное имя функции
Итак, нам осталось лишь пропустить всю таблицу представленных в семпле псевдохешей через алгоритм hash_api_true и составить уже таблицу «правильных» хешей. Далее нужно пропустить список функций WinAPI, состоящий из обычных символьных имен, через алгоритм hash_from_name, получив хешированные имена. Заключительная часть — надо сопоставить эти два списка, таким образом декодируя имена и хеш-представления. Разумеется, удобнее всего это сделать с помощью питоновского скрипта для IDA, а не вручную.
Можно ли сделать быстрее?
Конкретно в данном случае REvil строит таблицу деобфусцированных функций сразу целиком. Поэтому, загрузив семпл в отладчик, можно исполнить подпрограмму получения и деобфускации WinAPI-функций, после чего отладчик автоматом подставит уже расшифрованные имена функций API в код. Затем можно снять дамп и работать уже с ним, функции будут присутствовать на своих местах. Но этот способ подходит далеко не всегда. Например, если деобфускация выполняется не сразу со всем списком используемых функций, а с каждой функцией по отдельности, по мере ее вызова, этот прием уже не сработает. Кстати, именно так и было сделано в статье про обфускацию вызовов WinAPI.
Заключение
В этой статье мы разобрали, каким образом восстановить вызовы WinAPI, которые обфусцированы методом обращения по их хешам. Как видишь, такой метод обфускации достаточно легко преодолевается при помощи отладчика или дизассемблера, при том что реверсера не останавливают математические манипуляции с хешем. Подобные вещи прекрасно видны в псевдокоде и замедляют исследование семпла разве что на пару минут. На самом деле подобные приемы направлены на противодействие автоматическим системам обнаружения, но практически никак не затрудняют анализ «вручную».
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei