ESCalator
5.47K subscribers
334 photos
1 video
8 files
143 links
Tips and tricks от команды экспертного центра безопасности Positive Technologies (PT ESC)
Download Telegram
Как починить CFG 🔧

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

Схематично это выглядит так:


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), чтобы она вместила все функции.

Чтобы извлечь хеш-суммы и имена библиотек, пишем функцию, которая пройдет по всем вызовам 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
🔥2310👍10
Как починить CFG. Часть вторая 🛠

Ранее мы рассказывали про то, как восстановить 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👍118👏3🆒2