[Информационная безопасность, Реверс-инжиниринг, IT-компании] Вскрытие покажет: анализируем драйвер Windows x64, защищенный VMProtect
Автор
Сообщение
news_bot ®
Стаж: 6 лет 9 месяцев
Сообщений: 27286
Анализ вредоносных программ, защищающих себя от анализа, — это всегда дополнительные трудности для вирусного аналитика. Программа может быть обфусцирована, чтобы избежать детектирования сигнатурными и эвристическими анализаторами антивирусов или затруднить специалисту ее статический анализ. Можно, конечно, запустить программу в виртуальной среде, но и от такого исследования ВПО могут иметь средства защиты. В общем, это постоянная борьба. Злоумышленники придумывают и дорабатывают свои методы обфускации, могут использовать их на этапе разработки программы или обрабатывать уже готовые, скомпилированные модули. Никто не мешает им воспользоваться готовыми продвинутыми решениями, которые созданы специально для защиты легитимного программного обеспечения от анализа и взлома.Одним из таких популярных решений уже давно является протектор 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
===========
Похожие новости:
- [Информационная безопасность] (не) Безопасный дайджест: атаки на титанов, парольное изобилие и слежка на грани фола
- [Информационная безопасность, Хранение данных, Умный дом] Исследование: умная колонка Amazon Echo Dot не удаляет пароли при сбросе
- [Разработка под iOS, Беспроводные технологии, Смартфоны, IT-компании] Исследователь нашёл ещё одно название сети Wi-Fi, которое отключает беспроводной модуль в iPhone
- [Читальный зал, Научно-популярное, Искусственный интеллект, Научная фантастика] Сначала я подумал: «Это безумие»: реальный план использования романов для предсказания войн (перевод)
- [Информационная безопасность, Компьютерное железо, Накопители] Ещё одна 0-Day-уязвимость угрожает многим пользователям Western Digital (перевод)
- [Управление персоналом, Финансы в IT, IT-компании] Джим Уайтхёрст ушёл с поста главы IBM через 14 месяцев после вступления в должность
- [Информационная безопасность, Мессенджеры, Законодательство в IT] Telegram начал блокировать один из крупнейших сервисов для частного поиска данных граждан «Глаз Бога»
- [Управление персоналом, IT-компании, Удалённая работа] Внутренний опрос работников Apple показал, что им больше нравится удалёнка
- [Информационная безопасность, Администрирование доменных имен, Логические игры] Как у меня увели домен. Продолжение
- [Компьютерное железо, Настольные компьютеры, IT-компании] Gigabyte представила похожий на видеокарту твердотельный накопитель объёмом 32 ТБ и со скоростью чтения 28 ГБ/с
Теги для поиска: #_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-Ноя 12:45
Часовой пояс: UTC + 5
Автор | Сообщение |
---|---|
news_bot ®
Стаж: 6 лет 9 месяцев |
|
Анализ вредоносных программ, защищающих себя от анализа, — это всегда дополнительные трудности для вирусного аналитика. Программа может быть обфусцирована, чтобы избежать детектирования сигнатурными и эвристическими анализаторами антивирусов или затруднить специалисту ее статический анализ. Можно, конечно, запустить программу в виртуальной среде, но и от такого исследования ВПО могут иметь средства защиты. В общем, это постоянная борьба. Злоумышленники придумывают и дорабатывают свои методы обфускации, могут использовать их на этапе разработки программы или обрабатывать уже готовые, скомпилированные модули. Никто не мешает им воспользоваться готовыми продвинутыми решениями, которые созданы специально для защиты легитимного программного обеспечения от анализа и взлома.Одним из таких популярных решений уже давно является протектор 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
Весь код дампа драйвера содержит вызовы, подобные 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 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 # 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 =========== Источник: habr.com =========== Похожие новости:
Блог компании Group-IB ), #_informatsionnaja_bezopasnost ( Информационная безопасность ), #_reversinzhiniring ( Реверс-инжиниринг ), #_itkompanii ( IT-компании ) |
|
Вы не можете начинать темы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Вы не можете отвечать на сообщения
Вы не можете редактировать свои сообщения
Вы не можете удалять свои сообщения
Вы не можете голосовать в опросах
Вы не можете прикреплять файлы к сообщениям
Вы не можете скачивать файлы
Текущее время: 22-Ноя 12:45
Часовой пояс: UTC + 5