[Информационная безопасность, Реверс-инжиниринг, IT-компании] Вскрытие покажет: анализируем драйвер Windows x64, защищенный VMProtect

Автор Сообщение
news_bot ®

Стаж: 6 лет 9 месяцев
Сообщений: 27286

Создавать темы news_bot ® написал(а)
05-Июл-2021 13:30


Анализ вредоносных программ, защищающих себя от анализа, — это всегда дополнительные трудности для вирусного аналитика. Программа может быть обфусцирована, чтобы избежать детектирования сигнатурными и эвристическими анализаторами антивирусов или затруднить специалисту ее статический анализ. Можно, конечно, запустить программу в виртуальной среде, но и от такого исследования ВПО могут иметь средства защиты. В общем, это постоянная борьба. Злоумышленники придумывают и дорабатывают свои методы обфускации, могут использовать их на этапе разработки программы или обрабатывать уже готовые, скомпилированные модули. Никто не мешает им воспользоваться готовыми продвинутыми решениями, которые созданы специально для защиты легитимного программного обеспечения от анализа и взлома.Одним из таких популярных решений уже давно является протектор VMProtect. После того как вирусописатели стали активно использовать для своих программ подобные взломанные протекторы, антивирусные компании создали "черные" и "серые списки" таких решений и начали детектировать образцы по самому коду протекторов. Сейчас наблюдается очередная волна активного использования VMProtect злоумышленниками для защиты вредоносного ПО от детектирования и анализа. Но и исследователи не стоят на месте: есть замечательные решения по деобфускации и девиртуализации VMProtect. Основное из них — VTIL Project исследователя Can Bölük. Но и оно, к сожалению, не является панацеей.Текущая волна использования VMProtect характеризуется активным применением протектора китайскими вирусописателями для защиты своих вредоносных драйверов Windows x64. Известно, что анализ подобных драйверов — головная боль вирусных аналитиков. Получив очередной такой драйвер на анализ, Андрей Жданов, специалист по проактивному поиску киберугроз Group-IB, решил поделиться достаточно простыми подходами, которые облегчат анализ этих вредоносных программ.
Что нам потребуется:1. The Interactive Disassembler (IDA) 7.0 и выше2. Виртуальная среда — гостевая ОС Windows 7 x64 или выше3. Python4. Volatility (я использовал Volatility 3)5. Unicorn
Этап 1: получение дампа драйвераЗагружаем драйвер в виртуальной среде. Для этого можно воспользоваться штатной утилитой sc.exe: sc create <svc_name> binpath= <driver_path> type= kernel start= demandИли загрузить драйвер с помощью утилиты DriverLoader, которая использует функцию NtLoadDriver:DriverLoader_x86-64.exe <driver_path> <svc_name>Если при загрузке возникли проблемы, связанные с цифровой подписью драйвера, — можно воспользоваться утилитой dseo013b.exe (Driver Signature Enforcement Overrider).После успешной загрузки снимаем полный дамп памяти. Если виртуальная машина (например, VMware) при снимке создает корректный дамп памяти, то можно обойтись и снимком памяти.Используем Volatility для извлечения всех модулей ядра из дампа:vol -f <dump_path> -o <dest_dir> Modules --dumpПроверяем, что среди них есть и наш драйвер, а остальные модули пока оставляем — они пригодятся нам позже.Мы получили дамп исследуемого драйвера. Это не исходный файл до обработки с помощью VMProtect — начальные значения данных утеряны — но его уже можно открыть в IDA и пытаться анализировать, хоть и не в полной мере.Этап 2. Получение списка вызовов импортируемых функций
Весь код дампа драйвера содержит вызовы, подобные call sub_F88004CFEFE9. Тело самой функции содержится в секции .vmp0 и представляет собой обфусцированный код с множеством условных и безусловных переходов и манипуляциями с регистрами. Таким образом VMProtect обфусцирует каждый вызов импортируемой функции в "защищенном" файле. Обычно вызов импортируемой функции выглядит так:FF 15 08 2A 00 00 call    cs:LoadLibraryAVMProtect заменяет его на следующий вызов:E8 08 72 03 00call    vmp_LoadLibraryAФункция vmp_LoadLibraryA в процессе работы получает фактический адрес функции LoadLibraryA и передает ей управление. Но, как мы видим, после вызова такой обфусцированной функции может оставаться байт, что надо учитывать при анализе в IDA. Возврат из обфусцированной функции в этом случае осуществляется правильно, на следующий после этого байта адрес.
На данном этапе необходимо получить список адресов таких функций. Для этого мы с помощью скрипта IDAPython осуществляем перебор всех функций секции .vmp0, вызов которых осуществляется извне, из другой секции.
def get_vmp_import_func_list():
    segm = ida_segment.get_segm_by_name('.vmp0')
    if (segm is None) or (segm.sclass != SEG_CODE):
        return None
    func_list = []
    ea = segm.start_ea
    while True:
        func = ida_funcs.get_next_func(ea)
        if (func is None):
            break
        ea = func.start_ea
        if (ea >= segm.end_ea):
            break
        xref = ida_xref.get_first_fcref_to(ea)
        if (xref == ida_idaapi.BADADDR):
            continue
        while (xref != ida_idaapi.BADADDR):
            if (xref >= segm.start_ea) and (xref < segm.end_ea):
                break
            xref = ida_xref.get_next_fcref_to(ea, xref)
        else:
            func_list.append(ea)
    return func_list
В итоге получаем список RVA (Relative Virtual Address) таких функций в текстовом файле:0002C1300002C29D0002C4490002C51C0002C58E0002C5D30002C65E0002C668Этап 3. Получение оригинальных адресов импортируемых функцийЧтобы получить адреса оригинальных импортируемых функций, воспользуемся кодом самих обфусцированных функций VMProtect. Для этого загрузим полученный дамп драйвера как shellcode в отладчике x64dbg в виртуальной среде. Для запуска в качестве shellcode можно воспользоваться готовой утилитой или разработать свою, которая просто выделяет память (VirtualAlloc), копирует туда shellcode и передает ему управление. Однако здесь следует сделать замечание: это справедливо для дампа, где RVA и позиции в файле совпадают. В противном случае необходимо загружать дамп как PE-файл, по секциям.Передавать управление на заголовок MZ драйвера мы, конечно, не будем, а поместим на это место код вызова каждой обфусцированной функции. Будем пошагово отлаживать ее код и в конечном итоге извлекать оригинальный адрес импортируемой функции. С помощью x64dbgpy и скрипта на Python можно полностью автоматизировать этот процесс: сначала скрипт считывает из текстового файла список RVA обфусцированных функций, а по окончании сохраняет уже в другой текстовый файл список RVA и соответствующих им оригинальных адресов импортируемых функций:0002C130 FFFFF80002A4A6C00002C29D FFFFF80002A4A6C00002C449 FFFFF80002BEECC00002C51C FFFFF80002A4A6C00002C58E FFFFF80002D1FAC40002C5D3 FFFFF80002A4A4000002C65E FFFFF80002A483300002C668 FFFFF80002A97718......Текст функции получения оригинального адреса импортируемой функции с использованием x64dbgpy:
def get_original_import_addr(vmp_import_addr):
    start_addr = GetRIP()
    save_rsp = GetRSP()
    # call $+vmp_import_addr
    WriteByte(start_addr, 0xE8)
    WriteDword(start_addr + 1, vmp_import_addr - 5)
    orig_import_addr = None
    for _ in range(MAX_STEPS):
        StepIn()
        rip = GetRIP()
        inst = ReadByte(rip)
        # retn ?
        if (inst == 0xC3) or (inst == 0xC2):
            rsp = GetRSP()
            orig_import_addr = ReadQword(rsp)
            break
    SetRIP(start_addr)
    SetRSP(save_rsp)
    return orig_import_addr
Такой способ получения не очень сложный, однако требует запускать отладчик в виртуальной среде и использовать дополнительные программы для загрузки дампа как shellcode.Более предпочтительным вариантом будет использование эмулятора вместо отладчика. Реализация на Python с использованием эмулятора Unicorn:
# callback for tracing instructions
def hook_code(uc, address, size, orig_addr_wrapper):
    inst = uc.mem_read(address, 1)
    # retn ?
    if (inst[0] != 0xC3) and (inst[0] != 0xC2):
        return
    esp = uc.reg_read(UC_X86_REG_ESP)
    addr_size = 0
    if (UC_MODE == UC_MODE_64):
        addr_size = 8
        fmt = '<Q'
    elif (UC_MODE == UC_MODE_32):
        addr_size = 4
        fmt = '<L'
    if (addr_size != 0):
        addr = uc.mem_read(esp, addr_size)
        orig_addr_wrapper[0], = struct.unpack(fmt, addr)
    uc.emu_stop()
def get_orig_import_func_list(dump_data, vmp_func_list):
    orig_addr_wrapper = [0]
    image_size = (len(dump_data) + 0xFFFF) & ~0xFFFF
    try:
        # Initialize emulator
        mu = Uc(UC_ARCH_X86, UC_MODE)
        # tracing all instructions with customized callback
        mu.hook_add(UC_HOOK_CODE, hook_code, orig_addr_wrapper)
        # map memory for this emulation
        mu.mem_map(BASE_ADDR, image_size + STACK_SIZE)
        # write machine code to be emulated to memory
        mu.mem_write(BASE_ADDR, dump_data)
    except UcError as e:
        print('Unicorn Engine Error: %s' % e)
        return None
    orig_func_list = []
    for vmp_func_rva in vmp_func_list:
        try:
            # write vmp function call code
            call_code = b'\xE8' + struct.pack('<L', vmp_func_rva - 5)
            mu.mem_write(BASE_ADDR, call_code)
            # initialize stack
            mu.reg_write(UC_X86_REG_ESP,
                         BASE_ADDR + image_size + STACK_SIZE // 2)
            orig_addr_wrapper[0] = 0
            # emulate machine code in infinite time
            mu.emu_start(BASE_ADDR, BASE_ADDR + len(dump_data))
            if (orig_addr_wrapper[0] != 0):
                orig_func_list.append((vmp_func_rva,
                                       orig_addr_wrapper[0]))
        except UcError as e:
            print('Unicorn Engine Error: %s' % e)
    return orig_func_list
Этап 4. Получение списка импортируемых функций, корректировка имен в IDAЗдесь нам пригодятся извлеченные на первом этапе модули ядра: они содержат фактические адреса экспортируемых функций. С помощью разработанного скрипта на Python мы получаем общий список всех экспортируемых функций извлеченных модулей ядра:F880014D2D50 NdisAdjustBufferLength F880014AD370 NdisAdjustNetBufferCurrentMdl F880014AD240 NdisAdvanceNetBufferDataStart F880014E9910 NdisAdvanceNetBufferListDataStart F880014B65C0 NdisAllocateBuffer F880014B6630 NdisAllocateBufferPool ......Различия в форме адресации на втором и третьем этапах, например, F880014D2D50 и FFFFF880014D2D50, обусловлены использованием канонической формы адреса, в соответствии с которой 47-й бит копируется в остальные 48-63 биты (аналогично расширению знака). При сравнении адресов надо учитывать этот факт и сразу приводить к канонической форме адреса.С помощью другого скрипта Python из двух последних списков формируем список импортируемых функций для IDA:0002C130 KeReleaseSpinLock 0002C29D KeReleaseSpinLock 0002C449 ExFreePoolWithTag 0002C51C KeReleaseSpinLock 0002C58E PsLookupProcessByProcessId 0002C5D3 KeAcquireSpinLockRaiseToDpc 0002C65E IofCompleteRequest 0002C668 _strnicmp ......А в завершение скрипт IDAPython в соответствии с этим списком корректирует имена всех обфусцированных вызовов импортируемых функций драйвера в дизассемблере IDA.В результате всех этих действий получаем вполне пригодный для анализа код драйвера.
===========
Источник:
habr.com
===========

Похожие новости: Теги для поиска: #_informatsionnaja_bezopasnost (Информационная безопасность), #_reversinzhiniring (Реверс-инжиниринг), #_itkompanii (IT-компании), #_analiz (анализ), #_drajvery (драйверы), #_windows, #_analiz_koda (анализ кода), #_reversinzhiniring (реверс-инжиниринг), #_reversing (реверсинг), #_forensics, #_kriminalistika (криминалистика), #_blog_kompanii_groupib (
Блог компании Group-IB
)
, #_informatsionnaja_bezopasnost (
Информационная безопасность
)
, #_reversinzhiniring (
Реверс-инжиниринг
)
, #_itkompanii (
IT-компании
)
Профиль  ЛС 
Показать сообщения:     

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

Текущее время: 22-Ноя 06:13
Часовой пояс: UTC + 5