Введение

При анализе вредоносного ПО, направленного на майнинг криптовалюты, интереснее всего исследовать именно загрузчики (лоадеры или дроперы) майнеров, чем сами майнеры, так как именно в загрузчиках в первую очередь реализуются техники, направленные на обход средств защиты и противодействие обнаружению

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

Этот механизм основывается на прямом вызове системных функций ОС Windows в обход использования стандартного пути через вызов функций библиотек kernel32.dll и ntdll.dll. Такой подход позволяет авторам вредоносного ПО обходить перехваты системных функций в пользовательском пространстве со стороны EDR-агентов, что позволяет снизить риск обнаружения вредоносного ПО.

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

Номер сискола определяется его расположением в ntdll.dll, то есть сискол с номером 0 будет расположен по самому наименьшему адресу в секции .text по сравнению с другими аналогичными функциями. 

При этом все экспортируемые системные функции в ntdll.dll являются оболочкой над инструкцией syscall (или прерывания int 2Eh), которая инициирует выполнение системной функции на уровне ядра:

Пример содержимого экспортируемой функции в ntdll.dll
Пример содержимого экспортируемой функции в ntdll.dll

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

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

Анализ

В процессе анализа вредоносного образца на начальных этапах встречаем такую функцию:

Функция, в которой используется syscall
Функция, в которой используется syscall

Которая содержит нетипичную для обычного ПО инструкцию syscall, но так как мы анализируем вредоносное ПО, то что-то нетипичное может стать, напротив, вполне уместным в данном контексте вредоносного функционала.

syscall принимает номер системного вызова из регистра rax/eax, так как в предшествующем коде не видно записей в rax, то, похоже, номер системного вызова возвращается функцией sub_4018f1 (назовем ее ResolveSysCall, так как на нее указывает несколько xref, и мы однозначно встретим ее еще). 

Внутри ResolveSysCall видно, что значение аргумента в цикле сравнивается со значениями какого-то массива, при этом в случае совпадения возвращается индекс элемента массива, то есть номер системного вызова.

Содержимое ResolveSysCall
Цикл в ResolveSysCall
Цикл в ResolveSysCall

Таким образом, становится однозначно ясно, что в данном вредоносном модуле используется техника по использованию системных вызовов напрямую.

SysWhispers2

В ResolveSysCall вся магия происходит в функции sub_4014b6, переименуем ее в ComputeSysCallsList и перейдем к ее анализу.

В этой функции мы встречаем следующее, пока не очень понятное, содержимое:
Содержимое функции ComputeSyscallsList
Содержимое функции ComputeSyscallsList

Которое станет более доступным после распознавания используемых структур и переименования соответствующих переменных.

А именно
Фрагмент 1
Фрагмент 1
Фрагмент 2
Фрагмент 2

В некоторых местах из-за представления декомпилятора становится не совсем понятно, что именно происходит на данном участке кода, но если параллельно погружаться в дизассемблированное представление, то такой механизм, как перемещение по структурам формата PE64 становится более доступным:

Участок анализа PE-заголовка в дизассемблированном и декомпилированном представлениях
Участок анализа PE-заголовка в дизассемблированном и декомпилированном представлениях

Лично мне не удалось понять, что в подсвеченной строке кода осуществляется поиск адреса таблицы экспорта, пока я не посмотрел на то, какие именно смещения используются в машинном коде. Здесь в очередной раз подтверждается старая истина, что декомпилированное представление помогает, когда нужно проанализировать что-то быстро, но при детальном изучении каких-то участков лучше посматривать в окно дизассемблера, не полагаясь только на декомпилятор.

Из всего увиденного становится ясно, что это компоненты инструмента SysWhispers2. Например, сильно бросается в глаза паттерн по поиску функций на основе сравнения с символами “Zw”, который совпадает с аналогичным участком сравнения в цикле с этой подстрокой в декомпилированном представлении:

Аналогичные участки кода в исходниках и декомпиляторе
Аналогичные участки кода в исходниках и декомпиляторе

SysWhipsers2 решает проблему с различиями номеров в каждой версии посредством анализа загруженной в память версии ntdll.dll и составления таблицы соответствия хешей имен функций их номерам, что представлено в коде выше.

Помимо SysWhispers2 авторы вредоносного ПО могут использовать другие версии или похожие техники (например, Hell’s Gate или Halo’s Gate).

Теперь, найдя исходный проект, можно посмотреть его, чтобы сверить результаты своего исследования соответствующих участков вредоносного модуля с оригинальным кодом.

Немного автоматизации

Так как SysWhispers2 может время от времени встречаться в ходе анализа различных модулей, можно написать скрипт для упрощения анализа таких вредоносов, который также будет высчитывать хеши имен всех системных функций и указывать имена функций в соответствующих участках. Можно также добавить составление аналогичной таблицы соответствия номеров сисколов их именам, вдруг пригодится в будущем (тем более исходники на C такой логики у нас уже есть).

Итоговый скрипт с использованием IDAPy приобретает следующий вид (конечно, если кто-то видит недостатки кода, с которыми не может мириться, прошу прощения):

Код
import idautils
import idaapi
import pefile


# вычисление хеша
def hash(name):
    position = 0
    # seed-фраза для каждой сборки своя
    seed = 0x7d895397
    while name[position]:      
        seed ^= (((seed << 24) | (seed >> 8)) + (int.from_bytes(name[position:position+2], "little"))) & 0xffffffff
        position += 1
    return(seed)

  
# поиск по номеру сискола соответствия между хешом и именем сискола
def get_func_name(hashes, syscalls, hash_value):
    syscall = list(hashes.keys())[list(hashes.values()).index(hash_value)]
    return(syscalls[syscall])

  
# объявляем словари: временный для полученных сисколов,
# словарь с упорядоченными сисколами,
# и словарь соответствия номера сискола хешу    
SysCallsTableTmp = {}
SysCallsTable = {}
SysCallsTable_hashes = {}


# возьмем ntdll системы, так как в каждой версии ОС
# номера сисколов могут отличаться,
# далее заполняем словарь именами сисколов и их адресами
pe = pefile.PE("C:\\Windows\\System32\\ntdll.dll")

for entry in pe.DIRECTORY_ENTRY_EXPORT.symbols:
    try:
        if b"Zw" in entry.name:
            SysCallsTableTmp[entry.name] = hex(pe.OPTIONAL_HEADER.ImageBase + entry.address)
    except:
        continue

        
# так как номер сискола прямо пропорционально соответствует его адресу
# (сискол с номером 0 будет расположен по самому наименьшему адресу),
# упорядочиваем новый словарь согласно адресам,
# то есть получаем непосредственно номера сисколов и хеши их имен.
# Если бы нам не нужны были номера сисколов, сортировкой можно не заниматься,
# а просто определить соответствие имен и их хешей
SysCallsTable_sorted = sorted(SysCallsTableTmp.items(), key = lambda syscall: syscall[1])


# допишем нулевой байт, чтобы это укладывалось в логику хеширования
for i in range(len(SysCallsTableTmp)):
    SysCallsTable[hex(i)] = SysCallsTable_sorted[i][0]
    SysCallsTable_hashes[hex(i)] = hash(SysCallsTable[hex(i)] + b"\x00")

    
# используем поиск всех вызовов функции поиска сисколов ResolveSyscalls
# по адресу 0x4018F1 в нашем случае, чтобы получить искомые хеши 
# и расставить комментарии с полученными именами функций
xrefs = XrefsTo(0x4018F1)
for i in xrefs:
    ea = prev_head(i.frm)
    if "ecx" in generate_disasm_line(ea, 0):
        name = get_func_name(SysCallsTable_hashes, SysCallsTable, get_operand_value(ea, 1) & 0xffffffff)
        set_cmt(ea, name.decode("utf-8"), 1)

После работы скрипта мы получим следующий вид участков, где используются сисколы (с указанием имени функции, которая будет вызываться):

Пример 1
Пример 1
Пример 2
Пример 2

Таким образом, мы получили небольшой инструмент, который позволяет при использовании SysWhispers2 по хешу имени сискола определять, что именно за системная функция будет вызываться. Теперь, если нам снова попадется образец, который использует SysWhispers2, мы сможем гораздо быстрее понять функционал анализируемого вредоносного модуля.

Комментарии (0)