Как починить CFG 🔧
В процессе реверса ВПО мы встречаемся со случаями, когда обфускация мешает понять общий алгоритм работы. Один из примеров — создание двух последовательных противоположных условных переходов в одну точку.
Схематично это выглядит так:
То есть IDA при анализе кода идет сначала по ветке
Далее, встретив противоположный условный переход, она снова идет по ветке
При живом исполнении в независимости от состояния флагов будет выполнен переход на операнд, то есть выполнение будет как на скриншоте 2, если мы подправим control flow.
Трудность при исследовании таких шеллкодов в том, что переходов может быть много и каждый раз анализ будет о них спотыкаться. Можно попробовать починить вручную, но если таких блоков будут сотни?
✍️ Чтобы справиться с этим, напишем несложный скрипт на
Сначала составим список противоположных условных переходов и положим его в функцию, которая будет сверять две последовательные инструкции со списком:
Будем последовательно проходить каждую инструкцию до тех пор, пока не встретим нужные либо не упремся в лимит.
🧐 С помощью метода
С помощью
Затем исправляем условные переходы и просим IDA проанализировать новый код. Это необходимо, чтобы можно было найти дальнейшие блоки обфускации:
🗂 Методом
Таким образом, этот скрипт позволяет из обфусцированного кода получить нормальный ASM-код, как на скриншоте 3.
Иногда после него остаются единичные нераспознанные байты, но их легко поправить. Из этого кода можно создать функцию, декомпилировать ее и проанализировать (как на скриншоте 4).
Изучайте IDAPython, пишите скрипты. Happy reversing!
#tip #reverse #idapython
@ptescalator
В процессе реверса ВПО мы встречаемся со случаями, когда обфускация мешает понять общий алгоритм работы. Один из примеров — создание двух последовательных противоположных условных переходов в одну точку.
Схематично это выглядит так:
start:
jnX labelA
jX labelA
labelA:
<bytes>
То есть IDA при анализе кода идет сначала по ветке
False
и создает код там, откладывая ветку True
на потом. Встретив инструкцию jnX
, IDA создает код сразу после текущей.Далее, встретив противоположный условный переход, она снова идет по ветке
False
и создает код на следующем адресе, построив мусорную инструкцию, после которой дизассемблировать уже нельзя. Тогда IDA возвращается к отложенной на потом очереди и берет адрес оттуда, но беда в том, что там код уже создан, а значит анализ завершается (хорошо видно на скриншоте 1).При живом исполнении в независимости от состояния флагов будет выполнен переход на операнд, то есть выполнение будет как на скриншоте 2, если мы подправим control flow.
Трудность при исследовании таких шеллкодов в том, что переходов может быть много и каждый раз анализ будет о них спотыкаться. Можно попробовать починить вручную, но если таких блоков будут сотни?
✍️ Чтобы справиться с этим, напишем несложный скрипт на
IDAPython
, который исправит проблему автоматически. Задача — найти эти блоки и пропатчить их.Сначала составим список противоположных условных переходов и положим его в функцию, которая будет сверять две последовательные инструкции со списком:
def c_jumps(addr, n_addr):
ops = [
("jz", "jnz"),
("jnz", "jz"),
("je", "jne"),
("jne", "je"),
...
]
if (ida_ua.print_insn_mnem(addr), ida_ua.print_insn_mnem(n_addr)) in ops:
return True
return False
Будем последовательно проходить каждую инструкцию до тех пор, пока не встретим нужные либо не упремся в лимит.
def deobf(start, limit=BADADDR):
while addr != BADADDR:
n_addr = ida_search.find_code(addr, ida_search.SEARCH_DOWN)
if n_addr == BADADDR:
break
if not c_jumps(addr, n_addr):
addr = n_addr
continue
🧐 С помощью метода
find_code
из модуля ida_search
находим следующий адрес, на котором есть код, а с помощью функции c_jumps
проверяем, являются ли инструкции на этом и следующем адресе противоположными прыжками. Найдя их, мы должны проверить, указывают ли эти прыжки на одну точку (то есть равны ли их операнды):
o1 = get_operand_value(addr, 0)
o2 = get_operand_value(n_addr, 0)
if o1 != o2:
addr = n_addr
continue
insn = ida_ua.insn_t()
l1 = ida_ua.decode_insn(insn, addr)
l2 = ida_ua.decode_insn(insn, n_addr)
С помощью
get_operand_value
получаем значение операндов (у jX и jXX он один) и проверяем их равенство. Чтобы определить длину отрезка, который нужно пропатчить, с помощью decode_insn
из ida_ua
находим длины инструкций.Затем исправляем условные переходы и просим IDA проанализировать новый код. Это необходимо, чтобы можно было найти дальнейшие блоки обфускации:
after_addr = n_addr + l2
ida_bytes.patch_bytes(addr, bytes([0x90] * (l1 + l2 + 1)))
ida_auto.auto_wait()
ida_bytes.del_items(after_addr, ida_bytes.DELIT_EXPAND)
ida_auto.auto_wait()
ida_ua.create_insn(o1)
addr = o1
ida_auto.auto_wait()
🗂 Методом
patch_bytes
из ida_bytes
мы патчим инструкции, с помощью auto_wait из ida_auto
просим IDA проанализировать новый код, затем, используя del_items
, удаляем мусорные инструкции, созданные при первичном анализе, и снова анализируем. С помощью create_insn
создаем валидную инструкцию там, куда указывали условные переходы, и переанализируем в последний раз.Таким образом, этот скрипт позволяет из обфусцированного кода получить нормальный ASM-код, как на скриншоте 3.
Иногда после него остаются единичные нераспознанные байты, но их легко поправить. Из этого кода можно создать функцию, декомпилировать ее и проанализировать (как на скриншоте 4).
Изучайте IDAPython, пишите скрипты. Happy reversing!
#tip #reverse #idapython
@ptescalator
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥18👍7💯6👾3
Статический резолв импортов 👨💻
Динамический резолв импортов по хеш-суммам в ВПО — тема заезженная, но для проведения статического анализа необходимо разметить имена и прототипы API. Чтобы из того, что представлено на скрине 1, получить то, что на скрине 2, и не мучаться с ручной разметкой, можно написать скрипт IDAPython.
😠 На примере DodgeBox рассмотрим резолв, который заключается в вычислении адреса API-функции и помещении его в глобальную структуру. Реализация — на скрине 3.
Алгоритм хеширования опустим, поскольку здесь он не столь важен. Перед началом необходимо подготовить словарь с именами функций WinAPI и их хеш-суммами. Для этого выберем те библиотеки, что используются в бинаре. Здесь есть имена DLL в открытом виде, но иногда — только хеш-суммы, в этом случае можно составить словарь из всех системных DLL. Наш словарь — на скрине 4.
Далее убеждаемся, что члены в структуре заполняются последовательно, чтобы автоматически извлечь из кода хеш-суммы, имена модулей. Внимание — первый член структуры пропускается. Создаем структуру нужного размера (скрин 5), чтобы она вместила все функции.
Чтобы извлечь хеш-суммы и имена библиотек, пишем функцию, которая пройдет по всем вызовам
Функция
На выходе
Функция
Функция
Однако на самом деле функция возвращает объект типа
Структура API после вызова скрипта представлена на скрине 6. Если применить ее к глобальной переменной, резолв превратится в то, что видно на скрине 7, и можно будет удобно анализировать бинарь статически, не запуская отладчик.
Есть, конечно, соблазн написать функцию
#tip #reverse #idapython
@ptescalator
Динамический резолв импортов по хеш-суммам в ВПО — тема заезженная, но для проведения статического анализа необходимо разметить имена и прототипы API. Чтобы из того, что представлено на скрине 1, получить то, что на скрине 2, и не мучаться с ручной разметкой, можно написать скрипт IDAPython.
Алгоритм хеширования опустим, поскольку здесь он не столь важен. Перед началом необходимо подготовить словарь с именами функций WinAPI и их хеш-суммами. Для этого выберем те библиотеки, что используются в бинаре. Здесь есть имена DLL в открытом виде, но иногда — только хеш-суммы, в этом случае можно составить словарь из всех системных DLL. Наш словарь — на скрине 4.
Далее убеждаемся, что члены в структуре заполняются последовательно, чтобы автоматически извлечь из кода хеш-суммы, имена модулей. Внимание — первый член структуры пропускается. Создаем структуру нужного размера (скрин 5), чтобы она вместила все функции.
Чтобы извлечь хеш-суммы и имена библиотек, пишем функцию, которая пройдет по всем вызовам
get_proc_by_hash
и извлечет ее аргументы.def get_hashes(resolve_API_addr, get_proc_by_hash_addr):
result = []
func: ida_funcs.func_t
func = ida_funcs.get_func(resolve_API_addr)
cur = func.start_ea
while cur < func.end_ea:
if get_operand_value(cur, 0) == get_proc_by_hash_addr:
result.append(get_args(cur))
cur = ida_search.find_code(cur, SEARCH_DOWN)
return result
Функция
get_args
поднимается на несколько шагов вверх от операции call
и извлекает аргументы.def get_args(call_addr):
func_name_hash = None
lib_name = None
cur = call_addr
True:
if print_insn_mnem(cur) == "mov" and print_operand(cur, 0) == "r8d":
func_name_hash = get_operand_value(cur, 1) & 0xFFFFFFFF
elif print_insn_mnem(cur) == "lea" and print_operand(cur, 0) == "rcx":
lib_name = ida_bytes.get_strlit_contents(get_operand_value(cur, 1), -1, STRTYPE_C_16).decode()
if func_name_hash and lib_name:
return lib_name, func_name_hash
cur = ida_search.find_code(cur, SEARCH_UP)
На выходе
get_hashes
получим список, который используем для заполнения структуры API. Основная функция будет выглядеть так:struc: ida_struct.struc_t = ida_struct.get_struc(ida_struct.get_struc_id("API"))
funcs = get_hashes(0x180007A90, 0x1800078E0)
for i in range(1, len(funcs) + 1):
lib_name, func_name_hash = funcs[i - 1]
member: ida_struct.iss.onember_t = struc.members[i]
func_name = get_func_name(lib_name, func_name_hash, winapi_hashes_dict)
if func_name:
ida_struct.set_member_name(struc, member.soff, func_name)
func_tinfo = get_func_tinfo(func_name)
if func_tinfo:
ida_struct.set_member_tinfo(struc, member, 0, func_tinfo, 0)
Функция
get_func_name
проста в реализации, она находит в словаре имя API по хеш-сумме. А вот get_func_tinfo
более интересна: она создает объект, содержащий прототип функции, который мы также применим к члену структуры.def get_func_tinfo(func_name):
tinfo = ida_typeinf.get_named_type(None, func_name, 0)
if tinfo:
type_s = tinfo[1]
field_s = tinfo[2]
t = ida_typeinf.tinfo_t()
t.deserialize(None, type_s, field_s)
t.create_ptr(t)
return t
else:
return None
Функция
ida_typeinf.get_named_type
получает информацию о типе, который содержится в Type Library (*.til)
. Вызов выглядит так:Python>get_func_tinfo("GetWindowsDirectoryW")
UINT (__stdcall *)(LPWSTR lpBuffer, UINT uSize)
Однако на самом деле функция возвращает объект типа
ida_typeinf.tinfo_t
.Структура API после вызова скрипта представлена на скрине 6. Если применить ее к глобальной переменной, резолв превратится в то, что видно на скрине 7, и можно будет удобно анализировать бинарь статически, не запуская отладчик.
Есть, конечно, соблазн написать функцию
make_beautifully
, которая сама вычитает офсеты, создаст структуру и члены внутри нее, но об этом в другой раз.#tip #reverse #idapython
@ptescalator
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥23❤10👍10
Как починить CFG. Часть вторая 🛠
Ранее мы рассказывали про то, как восстановить Control Flow Graph (CFG) в случае его обфускации. Однако часто при анализе даже необфусцированного ВПО можно встретить случаи, когда CFG некоторых функций генерируется с ошибками. Одним из таких примеров является написанное на Delphi ВПО, где из-за особенностей обработки исключений часто можно встретить картину, как на скриншоте 1.
🤔 Если присмотреться внимательнее (скриншот 2), то можно заметить, что в блоке (1) на стек сохраняется адрес одного из последующих блоков (3), после чего выполняется некоторая логика (зачастую — освобождение ресурсов или объектов) и происходит прыжок на сохраненный ранее адрес (2).
Из-за того что в блоке 2 присутствует дополнительная ссылка, IDA не может однозначно определить адрес, куда будет осуществлен прыжок в
Создадим класс хука, который будет ожидать событие
Напишем функцию поиска сохраняемого на стек адреса
Добавим инициализацию хука при запуске скрипта и загрузим его в IDA. Запустим повторный анализ бинарного файла. В результате получим исправленный граф (скриншот 3).
Оставшиеся одиночные блоки — это вызовы обработчиков исключений; в данном случае они не влияют на ход работы программы. Стоит помнить, что пока хук активен, он будет автоматически вызываться даже при разметке нового кода, который не был размечен ранее.
Happy reversing! 💫
#tip #reverse #idapython
@ptescalator
Ранее мы рассказывали про то, как восстановить Control Flow Graph (CFG) в случае его обфускации. Однако часто при анализе даже необфусцированного ВПО можно встретить случаи, когда CFG некоторых функций генерируется с ошибками. Одним из таких примеров является написанное на Delphi ВПО, где из-за особенностей обработки исключений часто можно встретить картину, как на скриншоте 1.
🤔 Если присмотреться внимательнее (скриншот 2), то можно заметить, что в блоке (1) на стек сохраняется адрес одного из последующих блоков (3), после чего выполняется некоторая логика (зачастую — освобождение ресурсов или объектов) и происходит прыжок на сохраненный ранее адрес (2).
Из-за того что в блоке 2 присутствует дополнительная ссылка, IDA не может однозначно определить адрес, куда будет осуществлен прыжок в
jmp eax
. Чтобы исправить эту проблему, напишем небольшой хук, который будет проверять и автоматически патчить подобные места в коде.Создадим класс хука, который будет ожидать событие
ev_ana_insn
. Для начала необходимо убедиться, что это действительно интересующая нас последовательность, после чего пройтись «вверх» и попытаться найти сохраненный на стек адрес. Затем пропатчить jmp eax
на jmp short address
.class DelphiJmpEaxFixer(idaapi.IDP_Hooks):
def lookup_push_insn(self, start: int, limit: int = 30) -> int | None:
...
def ev_ana_insn(self, insn: idaapi.insn_t) -> bool:
#
b = bytes(idaapi.get_bytes(insn.ea - 1, 3))
if idaapi.is_tail(idaapi.get_flags(insn.ea)):
return True
# pop eax | 58
# jmp eax | ff e0
# ensure all pop & jmp seq
if b[0] != 0x58 or b[1] != 0xFF or b[2] != 0xE0:
return False
print(f"Got jmp short eax at {insn.ea:x}")
pushed_address = self.lookup_push_insn(insn.ea)
if pushed_address is None:
return False
delta = pushed_address - insn.ea
if delta < 0 or delta > 128:
return False
print(f"{delta=}")
asm_call = f"jmp short {delta}"
assembled = idaapi.AssembleLine(insn.ea, 0, 0, True, asm_call)
if assembled is None:
return False
return idaapi.patch_bytes(insn.ea, assembled)
Напишем функцию поиска сохраняемого на стек адреса
lookup_push_insn
. В ней найдем предположительную верхнюю границу блока 2 и проверим, что блок имеет единственную ссылку. Дополнительно ограничим диапазон поиска, в целях оптимизации.def lookup_push_insn(self, start: int, limit: int = 30) -> int | None:
ptr: int = start
insn = idaapi.insn_t()
jmp_ref = idaapi.BADADDR
for _ in range(limit, 0, -1):
prev_addr = idaapi.decode_prev_insn(insn, ptr)
if prev_addr == idaapi.BADADDR:
break
ptr = prev_addr
_refs = [xref for xref in idautils.CodeRefsTo(ptr, False)]
# If we found refs it's likely an upper basic block address
# it must be a single jmp ref
if _refs:
if len(_refs) != 1:
return None
jmp_ref = next(iter(_refs))
break
if jmp_ref == idaapi.BADADDR:
return None
ref_insn_sz = idaapi.decode_insn(insn, jmp_ref)
if ref_insn_sz == 0 or insn.itype != idaapi.NN_jmp:
return None
addr = idaapi.decode_prev_insn(insn, ptr)
if addr == idaapi.BADADDR or insn.itype != idaapi.NN_push:
return None
return insn.Op1.value
Добавим инициализацию хука при запуске скрипта и загрузим его в IDA. Запустим повторный анализ бинарного файла. В результате получим исправленный граф (скриншот 3).
Оставшиеся одиночные блоки — это вызовы обработчиков исключений; в данном случае они не влияют на ход работы программы. Стоит помнить, что пока хук активен, он будет автоматически вызываться даже при разметке нового кода, который не был размечен ранее.
hook_instance = DelphiJmpEaxFixer()
hook_instance.hook()
Happy reversing! 💫
#tip #reverse #idapython
@ptescalator
Please open Telegram to view this post
VIEW IN TELEGRAM
Please open Telegram to view this post
VIEW IN TELEGRAM
🔥21👍11❤8👏3🆒2