Гайд TikTok заставил меня это сделать! Руководство по ленивой деобфускации строк в функции за 15 секунд для поколения Z

Разработчик
Статус
Оффлайн
Регистрация
1 Сен 2018
Сообщения
1,597
Реакции[?]
881
Поинты[?]
116K
Внимание: Впечатление от статьи может испортить качество текстового оформления на югейме. Статья в оригинале доступна на
Пожалуйста, авторизуйтесь для просмотра ссылки.

Вступление

Всем привет! Давно не было никакого материала от меня, хоть обещаний было много… Ну… как вам сказать…

Ладно, сейчас не об этом. Ручная деобфускация строк это довольно нудный и монотонный процесс, и заниматься этим когда ваш пик концентрации достигает 3 минуты невозможно… Может быть, вы также собираете мусор в своей квартире, который копится неделями? Ну и я об этом, поэтому сегодня мы за 15 минут напишем свой скрипт, который решит эту сложную проблему.

Почему в названии 15 секунд, а на деле 15 минут? Потому что конечное решение позволит вам деобфусцировать строки в функции за 15 секунд.
Автор показывает лишь базовую концепцию ленивого реверс-инжиниринга. Он не предоставляет универсальных решений для деобфускации всего, думайте своей головой.
Осматриваем пациента
В нашем арсенале сегодня будет два бинарных файла:
  • Пожалуйста, авторизуйтесь для просмотра ссылки.
    (снимок памяти был сделан yovimi для его кряка )
  • Pandora
На самом деле подобный ксоринг строк довольно распространён, и можно взять почти любое читерское ПО. Например: Legendware, ev0lve, undercial и т.д Главный фокус будет сделан на Fatality.

Fatality
Пример №1
Рассмотрим первый случай шифрование строк
Код:
mov     eax, 0B8CDCAC7h
mov     ecx, 0D2FA87CDh
mov     [esi+84h], ecx
mov     [esi+80h], eax
mov     eax, 0BAC7E9ABh
mov     ecx, 4D5D5218h
mov     edx, 0D2FAEBA2h
mov     edi, 0CCBEA3B7h
mov     [esi+54h], edx
mov     [esi+50h], edi
mov     [esi+8Ch], eax
mov     [esi+88h], ecx
mov     [esi+5Ch], eax
mov     [esi+58h], ecx
xor     eax, eax
lea     ecx, [esi+50h]  ;  Moving key to ecx
movaps  xmm0, xmmword ptr [esi+80h] ; Mov the crypted string to xmm0
xorps   xmm0, xmmword ptr [ecx] ; Dexor our string to xmm0
movaps  xmmword ptr [esi+80h], xmm0 ; Write our part of the string to the buffer
lea     ebx, [esi+80h]
push    ebx             ; Push string buffer
call    some_func
Если быть честным, то нас тут интересует всего три инструкции:
Код:
1:    movaps  xmm0, xmmword ptr [esi+80h]
2:    xorps   xmm0, xmmword ptr [ecx]
3:    movaps  xmmword ptr [esi+80h], xmm0
Давайте рассмотрим их более детально:
  1. Кладётся отрывок зашифрованной строки в xmm0
  2. Выполнятся конечная операция дешифрования, после которой в xmm0 содержатся отрывок строки длиной 128 бит.
  3. Записывается отрывок чистой строки в тоже место, где лежала зашифрованная

В декомпилированном виде это выглядит так:
Я потерял рассматриваемый нами кейс, поэтому константы будут отличаться, но это не важно!
C:
decrypted_part.m128_u64[0] = 0xDF8AE292606BC656ui64;

decrypted_part.m128_u64[1] = 0xD54021B748729F38ui64;

key.m128_u64[0] = 0xDF8AE292606BB573ui64;

key.m128_u64[1] = 0xD54021B748729F38ui64;

decrypted_part = _mm_xor_ps(decrypted_part, key);
Пример №2
Код:
* Начало пропущено в целях экономии пространства. *

mov     [esi+70h], edx

mov     [esi+7Ch], ecx

mov     [esi+78h], eax

lea     ecx, [esi+40h]

xor     eax, eax

movaps  xmm0, xmmword ptr [esi+0A0h]

xorps   xmm0, xmmword ptr [ecx]

movaps  xmmword ptr [esi+0A0h], xmm0

movaps  xmm0, xmmword ptr [esi+0B0h]

xorps   xmm0, xmmword ptr [ecx+10h]

movaps  xmmword ptr [esi+0B0h], xmm0

movaps  xmm0, xmmword ptr [esi+0C0h]

xorps   xmm0, xmmword ptr [ecx+20h]

movaps  xmmword ptr [esi+0C0h], xmm0

movaps  xmm0, xmmword ptr [esi+0D0h]

xorps   xmm0, xmmword ptr [ecx+30h]

movaps  xmmword ptr [esi+0D0h], xmm0
Выглядит громоздко, и страшно?

Я с вами полностью согласен, но если разбирать небольшими частями, то в результате это всё тот-же
Пожалуйста, авторизуйтесь для просмотра ссылки.
. Но нас интересует вовсе не это! Тут стоит увидеть, что большая строка собирается частями:
Код:
* ... *

1: movaps  xmmword ptr [esi+0A0h], xmm0

* ... *

2: movaps  xmmword ptr [esi+0B0h], xmm0

* ... *

Рассмотрим детальнее
  1. Начало нашей строки находится по [esi + 0x0A0] .Туда записываются первые 16 байт
  2. Теперь записывается в сомнительное место [esi + 0x0B0]. Отнимем это смещение от нашего начального: 0x0B0 - 0x0A0 = 0x10 = 16. Регистр xmm0 также размером 16 байт, получается оно записывает строку частями!

В декомпилированном виде это выглядит так:
C:
string_part_1.m128_u64[0] = 0xBC8F8D82BFD7CBC3ui64;

string_part_1.m128_u64[1] = 0xDBA4C9C52234267Bui64;


string_part_2.m128_u64[0] = 0xDC928B87F2020A7i64;

string_part_2.m128_u64[1] = 0xD09CD5214CD292AFui64;


string_part_3.m128_u64[0] = 0x72A2250BB64109ABi64;

string_part_3.m128_u64[1] = 0x7E5E9E717B0F4253i64;


string_part_4.m128_u64[0] = 0x169A107835C7D17Ci64;

string_part_4.m128_u64[1] = 0x21D2CB6129134B1Ei64;


key_1.m128_u64[0] = 0xD2FAEBA2CCBEA3B7ui64;

key_1.m128_u64[1] = 0xBAC7E9AB4D5D5218ui64;


key_2.m128_u64[0] = 0x6FE951D4114F00C9i64;

key_2.m128_u64[1] = 0xB4F9B94D2DB1B2CAui64;


key_3.m128_u64[0] = 0x1CCD0566D9336F8Bi64;

key_3.m128_u64[1] = 0x5776EA1F126E320Ci64;


key_4.m128_u64[0] = 0x169A107835C7D15Di64;

key_4.m128_u64[1] = 0x21D2CB6129134B1Ei64;


string_part_1 = _mm_xor_ps(string_part_1, key_1);

string_part_2 = _mm_xor_ps(string_part_2, key_2);

string_part_3 = _mm_xor_ps(string_part_3, key_3);

string_part_4 = _mm_xor_ps(string_part_4, key_4);

some_func(&string_part_1);
Составляем шаблон
Написание шаблона
Чтобы найти все экземпляры шифрования строк нужно составить шаблон. Мы будем составлять шаблон, опираясь всего на две инструкции, потому что мы слишком ленивы, чтобы делать его более точным.

1: 0F 28 86 80 00 00 00 | movaps xmm0, xmmword ptr [esi+80h]
2: 0F 57 02 | xorps xmm0, xmmword ptr [edx]

Разбираем эти инструкции по частям:
Данное объяснение не является полностью правильным, оно было специально упрощено автором для более простого восприятия
  1. 0F 28- это опкод самой инструкции, т.е **movaps xmm0, 86 80 00 00 00** - это константа, откуда будет выполнено перемещение в xmm0
  2. 0F 57 - это также опкод инструкции, т.е **xorps xmm0 02**- это константа, с кем будет выполнена операция xor

Составлять шаблон будем вида текста в шестнадцатеричном формате ( IDA Style ).
  1. Нужно отсеять константы, потому что они могут отличаться у разных экземпляров.
  2. Нужно отметить, это опкоды инструкций, которые остаются постоянными для всех экземпляров. В нашем случае это 0F 28 для инструкции movaps и 0F 57 для инструкции xorps.

Теперь мы можем составить шаблон для поиска всех экземпляров шифрования строк:
0F 28 ? ? ? ? ? 0F 57

Этот шаблон будет искать все вхождения, где первая инструкция - movaps xmm0, а вторая инструкция - xorps xmm0.
Теперь можно использовать этот шаблон для поиска всех мест в коде, где происходит дешифрование строк.

Тестирование шаблона
Поскольку мы слишком ленивы, чтобы написать собственное решение для тестирования шаблона, мы воспользуемся IDA Pro и их поиском по последовательности байтов.
image (1).png

Целых 1274! Но стоит отметить, что наш паттерн не точный, и мы нашли еще часть простой математики никак не связанной с дешифрованием строк.

Начинаем писать полу-эмулятор
Для нашего ленивого деобфускатора мы будем использовать Python, ведь его биндинги имеет абсолютно каждая библиотека.

Обсуждения работы
Первая версия нашего деобфускатора будет работать в пределах одной функции, и полностью игнорировать control flow.
Принцип работы:
Пункт 4а не будет использован в этой статье. Он оставлен читателю как идея для самостоятельной реализации.
  1. Находим по шаблону наши инструкции
  2. Считаем длину movps+ **xorps**, чтобы эмулятор всегда понимал до какого адреса ему нужно эмулировать
  3. Инициализируем эмулятор, устанавливаем хуки, и начинаем эмуляцию
  4. Если инструкция подразумевает какой-то прыжок, или вызов - пропускаем.
    1. Если инструкция подразумевает какой-то прыжок, или вызов - создаём дополнительную копию эмулятора, которая прыгает, если адрес прыжка находится в пределах функции. Так мы сможем обходить весь control flow, и проэмулировать её полностью!
  5. После завершения эмуляции - читаем **xmm0**, и выводим пользователю.
Хотя этот метод не является идеальным и может не сработать во всех случаях, он предоставляет простой и быстрый способ деобфускации строк в функции.

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

Вспомогательные функции

Итак, начнём! Первым делом нам нужна функция которая будет загружать наш бинарный файл
Python:
def open_binary(path):
    with open(path, "rb") as f:
        return f.read()

Для сканирования бинарного файла на шаблоны сначала нам нужно преобразовать наш текст в шестнадцатеричном формате в массив байтов.
“?” - будем интерпретировать как -1
Python:
def convert_pattern_to_decimal(pattern):

    split_pattern = pattern.split()


    decimal_values = [-1 if '?' in hex_char else int(hex_char, 16) for hex_char in split_pattern]


    return decimal_values

Теперь сама функция сканирования бинарного файла на шаблон:
Python:
def pattern_scan_in_binary(binary, pattern):

    pattern_length = len(pattern)

    return [

        i for i in range(len(binary) - pattern_length + 1)

        if all(p == -1 or b == p for b, p in zip(binary[i:i+pattern_length], pattern))

    ]
Дополнительно нам нужна функция которая пройдётся дизассемблером по всему бинарному файлу, и сохранит это всё в словарь, чтобы в будущем посчитать размер нужных нам инструкций
Главная возможность этой функции также остается за читателем! Например можно находить начало функции с помощью словаря, и благодаря этому сканировать абсолютно весь бинарный файл.
Python:
def get_instruction_dict(capstone, binary, addr, size):

    instruction_dict = {}


    for instruction in capstone.disasm(binary[addr:addr+size], addr):

        instruction_dict[instruction.address] = instruction

   

    return instruction_dict

Проверим?
Python:
def main():

    binary = get_binary(BINARY_PATH)

    capstone = Cs(CS_ARCH_X86, CS_MODE_32)

    print("[?] Capstone version: {}".format(cs_version()))

    print("[+] Binary size: 0x{:X}".format(len(binary)))


    bytes_pattern = convert_pattern_to_decimal(PATTERN)


    instruction_dict = get_instruction_dict(capstone, binary, 0x0, len(binary))

    if len(instruction_dict) == 0:

        print("[-] Can't get instruction dict")

        return


        pattern_occurrences = pattern_scan_in_binary(memory_function, bytes_pattern)

        print("[+] Found {} occurrences".format(len(pattern_occurrences)))

image (4).png

Работает! Наш сканер шаблонов не потерял ни одного экземпляра, и сравнился с IDA Pro.

Подготовка к эмуляции
Есть одна проблема. Сканер находит начало инструкции movaps после совпадения с нашим шаблоном. Однако для эмуляции нам необходимо, чтобы адрес был началом инструкции после xorps.

Чтобы найти правильный адрес:
  1. Начинаем с начала movaps . Это адрес сразу после совпадения шаблонов.
  2. Дизассемблируем инструкции одну за другой, добавляя их длину.
  3. Останавливаемся при достижении инструкции xorps .
  4. Общая длина от movapsдо xorps это наше смещение
  5. Добавляем это смещение к начальному адресу.

В итоге после этих шагов мы получаем конечный адрес эмуляции.
Python:
# Set the initial offset to the location of the pattern occurrence in the function's memory

offset = occurrence


# Try to disassemble the first instruction at the offset

instruction = next(capstone.disasm(memory_function[offset:], offset), None)


if instruction is None:

    print("[-] No instruction found.")

    return


# Initialize size with the size of the first instruction

size = instruction.size


while instruction.mnemonic != 'xorps':

    # Increment the offset by the size of the previous instruction

    offset += size


    # Disassemble the instruction at the new offset

    instruction = next(capstone.disasm(memory_function[offset:], offset), None)


    # Break the loop if no more instructions available

    if instruction is None:

        break


    # Add size of the current instruction to total size

    size += instruction.size

Ох, мы совсем забыли инициализировать наш эмулятор, это вы виноваты!
Python:
def setup_main_emulator(binary_data):

    """ Initialize main emulator with binary data. """

    emulator = Uc(UC_ARCH_X86, UC_MODE_32)


    page_size = 0x1000  # Size for memory page.

    alignment = (len(binary_data) + page_size - 1) & -page_size  # Ensure address is aligned correctly.


    print("[+] Preparing to allocate binary for Unicorn Emulator. Size: 0x{:X}".format(alignment))

        allocate_addr = BINARY_ALLOCATED_ADDRESS

    emulator.mem_map(allocate_addr, alignment)  # Map memory in emulator.


    # Write the binary to the mapped memory.

    emulator.mem_write(allocate_addr, binary_data)


    stack_addr = 0x0

    stack_size = 0x100000  # 1MB size of stack.

    emulator.mem_map(stack_addr, stack_size)  # Map memory for stack.


    return emulator
Эмулируем

Для начала нам нужно написать промежуточный хук, чтобы проверять какую инструкцию эмулятор собирается исполнять. В общем процесс выглядит примерно так:
  1. Читаем размер инструкции по текущему адресу, после чего дизассемблируем её
  2. Если инструкция call / jmp/ jne/ je/ jg/ jae/ jbe/jb то перезаписываем EIP на ( current EIP + размер инструкции )
  3. Если инструкция ret то заканчиваем эмуляцию, потому что дальше будет не валидная эмуляция
Python:
def instruction_hook(emu, address, size, user_data):


    if address < EMULATE_FUNC_START or address > EMULATE_FUNC_END:

        emu.emu_stop()


    insn_mem = emu.mem_read(address, size)

    instruction = next(md.disasm(insn_mem, address))


    if instruction.mnemonic in ['call', 'jmp', 'jne', 'je', 'jg', 'jae', 'jbe', 'jb']:

        emu.reg_write(uc.x86_const.UC_X86_REG_EIP, address + size)

    elif instruction.mnemonic == 'ret':

        emu.emu_stop()
Нам нужен еще один хук, чтобы эмуляция не выдавала ошибку, если натыкалась на не аллоцированную память в куче.
Python:
def hook_invalid_memory_access(mu, access, address, size, value, user_data):

        print(f"[+] Memory read at 0x{address:x} is not mapped, mapping it.")

        # Map memory

        mu.mem_map(address & ~(0x1000 - 1), 0x1000)

        # Return True to indicate we want to continue emulation

        return True
Теперь конечная цель это проэмулировать нашу функцию от начала до нужной нам инструкции, и извлечь результаты с xmm0.
Весь скрипт написан так, чтобы читатель имел возможность для его модификации, и улучшения, пользуясь рекомендациями со статьи.
Давайте рассмотрим подробнее, что нам нужно сделать в функции эмуляции:
  1. Очистить стек
  2. Очистить все регистры
  3. Установить хуки
  4. Начать эмуляцию
  5. После завершения эмуляции прочитать xmm0, еcли это предоставляется возможным
Python:
def emulate_main_routine_with_hooks(emu, start_addr, end_addr):

    stack_size = 1024 * 1024  # 1MB


    # Clear stack

    emu.mem_write(0x0, b"\\x00" * stack_size)

   

    # Initialize stack

    emu.reg_write(uc.x86_const.UC_X86_REG_ESP, stack_size - 1)


    # Initialize machine registers

    for reg in [uc.x86_const.UC_X86_REG_EAX, uc.x86_const.UC_X86_REG_EBX, uc.x86_const.UC_X86_REG_ECX,

                uc.x86_const.UC_X86_REG_EDX, uc.x86_const.UC_X86_REG_ESI, uc.x86_const.UC_X86_REG_EDI,

                uc.x86_const.UC_X86_REG_EBP, uc.x86_const.UC_X86_REG_EFLAGS, uc.x86_const.UC_X86_REG_XMM0, uc.x86_const.UC_X86_REG_XMM1]:

        emu.reg_write(reg, 0x0)

   

    # Add hooks

    emu.hook_add(uc.UC_HOOK_CODE, new_hook_code)

    emu.hook_add(uc.UC_HOOK_MEM_READ_UNMAPPED | uc.UC_HOOK_MEM_WRITE_UNMAPPED, hook_invalid)


    try:

        emu.emu_start(start_addr, end_addr)

    except Exception as err:

        print("[-] Exception: {}".format(err))

        return


    xmm0 = emu.reg_read(uc.x86_const.UC_X86_REG_XMM0)


    if xmm0 == 0:

        print("[-] XMM0 is zero. Failed to decrypt string")

        return


    xmm0_high = p64(xmm0 >> 64)

    xmm0_low = p64(xmm0 & ((1 << 64) - 1))


    try:

        xmm0_high_str = xmm0_high.decode("utf-8")

        xmm0_low_str = xmm0_low.decode("utf-8")

                print("[+] 0x{:X} | {} ".format(end_addr, xmm0_low_str + xmm0_high_str)

    except:

                print("[-] 0x{:X} | Failed to decrypt string", end_addr)

        return


В конечном итоге наша главная функция будет выглядеть так:
Python:
def main():

    binary = get_binary(BINARY_PATH)

    capstone = Cs(CS_ARCH_X86, CS_MODE_32)

    print("[?] Capstone version: {}".format(cs_version()))

    print("[+] Binary size: 0x{:X}".format(len(binary)))


    bytes_pattern = convert_pattern_to_decimal(PATTERN)


    instruction_dict = get_instruction_dict(capstone, binary, 0x0, len(binary))

    if len(instruction_dict) == 0:

        print("[-] Can't get instruction dict")

        return


    memory_function = binary[get_file_offset(EMULATE_FUNC_START): get_file_offset(EMULATE_FUNC_START) + EMULATE_FUNC_SIZE]


    pattern_occurrences = pattern_scan_in_binary(memory_function, bytes_pattern)

    print("[+] Found {} occurrences".format(len(pattern_occurrences)))

   

    emulator = setup_main_emulator(binary)


    for occurrence in pattern_occurrences:

        print("[+] Found at 0x{:X}".format(occurrence))


            # Set the initial offset to the location of the pattern occurrence in the function's memory

            offset = occurrence


            # Try to disassemble the first instruction at the offset

            instruction = next(capstone.disasm(memory_function[offset:], offset), None)


            if instruction is None:

            print("[-] No instruction found.")

            return


            # Initialize size with the size of the first instruction

            size = instruction.size


            while instruction.mnemonic != 'xorps':

            # Increment the offset by the size of the previous instruction

            offset += size


            # Disassemble the instruction at the new offset

            instruction = next(capstone.disasm(memory_function[offset:], offset), None)


            # Break the loop if no more instructions available

            if instruction is None:

                break


            # Add size of the current instruction to total size

            size += instruction.size


            offset += size

            emulate_main_routine_with_hooks(emulator, EMULATE_FUNC_START, EMULATE_FUNC_START + offset )


       print("[+] Done. Decrypted all strings in function")
Заключение
Тестируем наш скрипт
Давайте выполним наш скрипт на небольшую функцию, и проверим его работоспособность.
image (2).png
Да! Он действительно работает, хоть и не обрабатывает все случаи. Базовые возможности его улучшения описаны в статье, но это всего лишь базовые! Всё ограничивается вашей фантазией.

Конец?
В этой статье мы рассмотрели метод автоматического поиска и эмуляции дешифрования строк с помощью Python. Несмотря на то, что этот метод является базовым, он демонстрирует, как сочетание статического анализа и динамической эмуляции может позволить дальше отдыхать на диване, вместо того, чтобы тратить десятки минут на ручное дешифрование строк.

До следующей встречи, через 152 дня.

Мой шизоблог:
Пожалуйста, авторизуйтесь для просмотра ссылки.

Спасибо:
  • Easton21: За помощь в редактировании статьи
  • kaazedev: За критику текста
  • Разработчикам Unicorn / Capstone: За возможность настолько абстрактно писать код
 
Последнее редактирование:
Разработчик
Статус
Оффлайн
Регистрация
1 Сен 2018
Сообщения
1,597
Реакции[?]
881
Поинты[?]
116K
EFI_COMPROMISED_DATA
лучший в мире
Статус
Оффлайн
Регистрация
26 Янв 2018
Сообщения
920
Реакции[?]
1,632
Поинты[?]
85K
и какой смысл в том, что ты скинул этот скрипт? объяснения сами себя напишут?
я не обесценивал твою статью. я лишь скинул скрипт, который умеет чуть побольше
 
Сверху Снизу