• [ Регистрация ]Открытая и бесплатная
  • Tg admin@ALPHV_Admin (обязательно подтверждение в ЛС форума)

Статья Декомпилятор на нейронках. Скрещиваем IDA Pro с ChatGPT

stihl

Moderator
Регистрация
09.02.2012
Сообщения
1,167
Розыгрыши
0
Реакции
510
Deposit
0.228 BTC
stihl не предоставил(а) никакой дополнительной информации.
Языковые модели способны взять на себя рутинную часть работы реверс‑инженера. Они объяснят, как работает блок кода, и подскажут удачные имена функций и переменных. Рассмотрим широкий спектр облегчающих анализ инструментов — от локальных языковых моделей до агентов, способных к рассуждению и запуску пользовательского кода.

Декомпиляция ассемблерного кода​

Начнем с простого теста. Возьмем ассемблерный код и попросим GPT декомпилировать в псевдо-C.

Декомпилируй ассемблерный код из IDA Pro в псевдо-C, как Hex-Rays.

Код:
waveOut_work    proc near

                push    ebx
                mov     ebx, offset pwh

loc_409F0E:
                push    20h ; ' '       ; cbwh
                push    ebx             ; pwh
                push    ds:hwo          ; hwo
                call    __imp_waveOutUnprepareHeader
                add     ebx, 20h ; ' '
                cmp     ebx, offset byte_412DE4
                jb      short loc_409F0E
                push    ds:hwo          ; hwo
                call    __imp_waveOutClose
                pop     ebx
                retn
waveOut_work    endp

Правильный ответ от Hex-Rays:


Код:
MMRESULT waveOut_work()
{
  struct wavehdr_tag *v0; // ebx

  v0 = &pwh;
  do
    waveOutUnprepareHeader(hwo, v0++, 0x20u);
  while ( v0 < (struct wavehdr_tag *)&byte_412DE4 );
  return waveOutClose(hwo);
}

Ответ GPT-4o:

Код:
void waveOut_work()
{
    WAVEHDR *pwh = (WAVEHDR *) &pwh;  // Начало массива структур WAVEHDR
    while ((BYTE *)pwh < (BYTE *)0x412DE4)
    {
        waveOutUnprepareHeader(hwo, pwh, sizeof(WAVEHDR));
        pwh++;
    }
    waveOutClose(hwo);
}

IDA предполагает, что функция возвращает MMRESULT, хотя смысла получать ответ от waveOutClose нет и void — правильный тип. Имя WAVEHDR понятней, чем wavehdr_tag, хотя это одна и та же структура.

Пока ответ GPT лучше, попробуем с участком кода побольше.

Декомпилируй ассемблерный код из IDA Pro в псевдо-C, как Hex-Rays.

Код:
sub_40C5A4      proc near

String          = dword ptr  4

                push    ebx
                push    esi
                mov     ebx, [esp+8+String]
                mov     esi, 105h
                cmp     word ptr [ebx], 21h ; '!'
                jnz     short loc_40C5D0
                xor     ebx, ebx
                call    __imp_GetVersion
                test    eax, 80000000h
                mov     esi, 3
                jnz     short loc_40C5FB
                mov     esi, 0Bh
                jmp     short loc_40C5FB

loc_40C5D0:
                cmp     word ptr [ebx], 3Fh ; '?'
                jnz     short loc_40C5DF
                xor     ebx, ebx
                mov     esi, 3
                jmp     short loc_40C5FB

loc_40C5DF:
                movsx   ecx, byte ptr [ebx]
                test    ds__ctype+1)[ecx], 4
                jz      short loc_40C5FB
                push    ebx             ; String
                call    j__atoi
                mov     ebx, eax
                mov     esi, 1
                add     esp, 4

loc_40C5FB:
                push    ebx             ; dwData
                push    esi             ; uCommand
                push    offset FullPath_HLP ; lpszHelp
                push    ds:g_hWnd_0     ; hWndMain
                call    __imp_WinHelpA
                pop     esi
                pop     ebx
                retn    4
sub_40C5A4      endp

Ответ Hex-Rays:

Код:
BOOL __stdcall sub_40C5A4(char *String)
{
  char *v1; // ebx
  UINT v2; // esi

  v1 = String;
  v2 = 261;
  if ( *(_WORD *)String == 33 )
  {
    v1 = 0;
    v2 = 3;
    if ( (GetVersion() & 0x80000000) == 0 )
      v2 = 11;
  }
  else if ( *(_WORD *)String == 63 )
  {
    v1 = 0;
    v2 = 3;
  }
  else if ( (*((_BYTE *)_ctype + *String + 1) & 4) != 0 )
  {
    v1 = (char *)j__atoi(String);
    v2 = 1;
  }
  return WinHelpA(g_hWnd_0, FullPath_HLP, v2, (ULONG_PTR)v1);
}

Ответ GPT-4o:

Код:
int __stdcall sub_40C5A4(LPCSTR String)
{
    int dwData = 0;
    UINT uCommand = 0x105; // Значение по умолчанию

    if (*(WORD *)String == '!')
    {
        // Если строка начинается с '!'
        GetVersion(); // результат используется только для проверки платформы
        if (GetVersion() & 0x80000000)
            uCommand = 3;
        else
            uCommand = 0x0B;
    }
    else if (*(WORD *)String == '?')
    {
        // Если строка начинается с '?'
        uCommand = 3;
    }
    else if (isxdigit((unsigned char)*String)) // или isdigit, если __ctype+1[ecx] == 4 означает цифру
    {
        dwData = atoi(String);
        uCommand = 1;
    }

    WinHelpA(g_hWnd_0, FullPath_HLP, uCommand, dwData);
    return 0;
}

И вот у модели начинаются галлюцинации. Зачем‑то поставила два вызова GetVersion. Откуда‑то взялся возвращаемый ноль, хотя из кода очевидно, что EAX берется из WinHelpA, следовательно, тип возврата должен быть BOOL.

Тем не менее модель удачно распознала имена переменных, преобразовала константы к нужным типам и даже увидела макрос isdigit. Если бы не галлюцинации, инструменту бы цены не было. Круто, но Hex-Rays пока еще рано списывать со счетов.


Наивная декомпиляция си-кода​

Считается, что с анализом кода на си GPT справляется лучше. Проверим это простым скриптом на IDAPython, работающим через API от OpenAI.

Код:
import idc
import idaapi
import idautils
import openai
import re

client = openai.OpenAI(
  api_key="sk-proj-*"
)

def ask_gpt_about_function(c_code):
    prompt_text = f"""

Here is a C-like decompiled function from a binary:

{c_code}

Please suggest a concise and descriptive name for this function (using snake_case), and provide a brief explanation of what it does.

Код:
Respond strictly in the following format:
Function name: <name>
Description: <short explanation>
"""
    try:
        response = client.chat.completions.create(
            model="gpt-4",
            messages=[
                {"role": "user", "content": prompt_text}
            ],
            temperature=0.5
        )

        return response.choices[0].message.content
    except Exception as e:
        print(f"[!] GPT API error: {e}")
        return None

def apply_result_to_function(ea, gpt_response):
    match = re.search(r"Function name:\s*(\w+).?Description:\s(.*)", gpt_response, re.DOTALL)
    if match:
        name = "gpt_" + match.group(1)
        desc = match.group(2).strip()

        if ida_name.force_name(ea, name, idc.SN_AUTO):
            print(f"[+] Renamed function at {hex(ea)} to: {name}")
        else:
            print(f"[!] Could not rename function to: {name}")

        idc.set_func_cmt(ea, desc, 0)
    else:
        print(f"[!] Could not parse GPT response:\n{gpt_response}")

def is_user_defined_name(ea):
    return not idc.get_name(ea).startswith("sub_")

def get_decompiled_code(ea):
    try:
        cfunc = idaapi.decompile(ea)
        return str(cfunc)
    except Exception as e:
        print(f"[!] Failed to decompile function at {hex(ea)}: {e}")
        return None

def main():
    for func_ea in idautils.Functions():


        print(f"[*] Processing function at {hex(func_ea)} {is_user_defined_name(func_ea)}")

        if is_user_defined_name(func_ea):
            continue

        code = get_decompiled_code(func_ea)
        if not code:
            continue

        gpt_result = ask_gpt_about_function(code)
        if gpt_result:
            apply_result_to_function(func_ea, gpt_result)

main()

Код получает адреса распознанных функций и, если функция еще не была переименована, отправляет декомпилированный Hex-Rays код запросом к GPT. В ответе мы ожидаем увидеть новое имя функции и краткое описание, что она делает. Далее скрипт переименовывает функцию и добавляет комментарий с описанием ее работы.

Код:
// This function swaps the second and third bytes of the input array and then masks the second byte with a value from a global byte array and a bitwise AND operation. The result is stored back into the second byte of the input array.
int __fastcall gpt_swap_and_mask_bytes_0(int a1, int a2, _BYTE *a3)
{
  int result; // eax

  LOBYTE(a2) = a3[2];
  a3[2] = a3[3];
  a3[3] = a2;
  LOBYTE(a2) = a3[1];
  result = a2;
  LOBYTE(result) = byte_4110E4[(int)(unsigned __int8)a2 >> 4] | a2 & 0xF;
  a3[1] = result;
  return result;
}

Для маленьких функций без контекста придуманные GPT названия практически бесполезны. Если бы мы предварительно указали имена аргументов и правильные типы, анализ был бы точнее. Еще одна проблема скрипта — порядок функций. Сначала надо передавать на анализ вызываемые функции и только затем вызывающие, чтобы расширить контекст последних. Для этого надо создать граф вызовов и постепенно идти от самых дальних узлов наверх.

Попробуем отдать GPT большую изолированную функцию, понятную без внешнего контекста.

Код:
// This function copies and inverts the pixels from one area of the image to another. The source and destination areas, as well as the dimensions, are determined by the input parameters. The pixels are processed in reverse order, and zero pixels are skipped.
char __stdcall gpt_copy_inverted_pixels(int a1, int a2, int a3, int a4, int a5, int a6, int a7, int a8, int a9)
{
  char *v9; // edi
  char *v10; // esi
  int v11; // edx
  int v12; // ebp
  int v13; // ecx
  char result; // al

  v9 = (char *)lpBits + a4 + dword_412CD0 * (dword_412CD4 - 1 - a3);
  v10 = (char *)(a9 + a8 + a6 + a6 * -a7 - 1);
  v11 = a2 - a4 + dword_412CD0;
  v12 = a1 - a3;
  dword_412CD8 = a2 - a4;
  do
  {
    v13 = dword_412CD8;
    while ( 1 )
    {
      result = *v10--;
      if ( !result )
        break;
LABEL_4:
      *v9++ = result;
      if ( !--v13 )
        goto LABEL_8;
    }
    while ( 1 )
    {
      ++v9;
      if ( !--v13 )
        break;
      result = *v10--;
      if ( result )
        goto LABEL_4;
    }
LABEL_8:
    v9 -= v11;
    v10 += a2 - a4 + a6;
  }
  while ( v12-- > 1 );
  return result;
}

Работа над байтами без вызова внешнего кода декодируется достаточно бодро. GPT понял, что алгоритм работает с двумя областями изображения. Действительно, по коду v12 = a1 - a3 можно догадаться, что это координаты x1 и x2.

Разбирать подобную математику вручную было бы слишком утомительно. Обычно я просто смотрю в отладчике, что было с данными до и что стало после. А из этого уже делаю выводы о назначении функции.

Наивность текущего подхода — в зашкаливающем числе запросов. Модель GPT-4 за анализ двухсот небольших функций сожрала аж три бакса! Вероятно, модели попроще стоят дешевле. В любом случае подобный анализ применим только к конкретной функции. То есть загрузить exe-файл целиком и получить его исходный код пока что не удастся.


IDA Copilot​

Вместо обычного запроса «прокомментируй функцию» IDA Copilot использует механизм «агентов», способных выполнять цепочки действий (например, получить код → анализировать → переименовать). Для этого используется фреймворк LangChain. Ему передается первоначальный промпт с задачей и набор «инструментов» — Python-функций, которые языковая модель вызывает по необходимости.

Код:
tools = [
    self.__GetAddressInfoTool(),
    self.__GetDefinitionTool(),
    self.__GetPseudocodeTool(),
    self.__SetFunctionCommentTool(),
    self.__SetFunctionDefinitionTool(),
    self.__SetFunctionNameTool(),
    self.__GetIsMyWorkDoneTool(ea)
]

class __GetAddressInfoTool(BaseTool):
    name = 'get_address_info'
    description = ('Given a hex address or function name, show its information. '
                   'Input Format: <hex_address_or_function_name>. '
                   'Input Example1: sub_140007080. '
                   'Input Example2: 0x140007080.')

У каждого инструмента есть имя и описание, а также функция _run, принимающая строку с аргументом от модели.


Установка​

В requirements.txt указаны старые версии библиотек, их установка ломает другие плагины. Так что советую переставить их после работы с плагином вручную.

При запуске выясняется еще одна проблема: плагин запрашивает модель gpt-3.5-turbo-0613, которая давно не существует. Заменяю в коде актуальной.

Код:
class Copilot:
    def run(self, temperature=0.5, model='gpt-4o'):

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

Код:
default_prompt_en = PromptTemplate(
    input_variables=['binary_description'],
    template="""

You are Copilot, a professional reverse engineer currently conducting an in-depth analysis of a binary file. You are using IDA Pro and have observed a specific function's decompiled pseudocode.

Your task is to perform a comprehensive analysis of this pseudocode to better understand its purpose and logic. Please follow the guidelines below:

1. Function Purpose Analysis: Describe in detail what this function does and what its purpose is. Add comments in English directly to the function, each prefixed with Copilot Comment: so they are clearly distinguishable.
2. Function Signature Correction: Based on your understanding of the code logic, infer and correct any function signatures that may have been incorrectly or ambiguously decompiled by IDA Pro. Explain in detail why you made this decision.
3. Function Naming Analysis: Thoroughly analyze the current function and all related functions it calls. Rename any functions starting with sub_ to more meaningful names, and provide clear explanations for each renaming.

Description:
Код:
{binary_description}

Please continue your analysis until get_is_my_work_done tells you the work is complete.
""")

Поначалу все работает нормально, но затем я натыкаюсь на ограничения по времени:

Rate limit reached for gpt-4o on tokens per min (TPM): Limit 30000, Used 26575, Requested 3712.
Добавляем задержку для каждой функции:


Код:
import time

    class __GetIsMyWorkDoneTool(BaseTool):
        def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> Any:
            time.sleep(1)

    class __SetFunctionNameTool(BaseTool):
        def _run(self, query: str, run_manager: Optional[CallbackManagerForToolRun] = None) -> Any:
            time.sleep(1)

Теперь с плагином можно работать.


Тестирование​

Код:
BOOL show_help()
{
  BOOL result; // eax

  if ( g_hWnd_0 )
  {
    WinHelpA(g_hWnd_0, FullPath_HLP, 2u, 0);
    return DestroyWindow(g_hWnd_0);
  }
  return result;
}


Возьмем небольшую функцию и проследим цепочку рассуждений Copilot.

- Для начала анализа необходимо получить псевдокод show_help
- Для этого вызывается действие get_pseudocode

- Псевдокод отображает справку через WinHelpA
- Следуя гайдлайну:
- Добавим комментарий к псевдокоду
- Исправим прототип функции при необходимости
- Проанализируем и поменяем имена функций

- Я собираюсь добавить комментарий:
- Вызываю действие set_function_comment
- Получаю текст комментария и ставлю его на функцию

- Комментарий успешно добавлен, теперь анализ прототипов:
- Текущий прототип BOOL show_help()
- Код возвращает BOOL от DestroyWindow, все верно

- Теперь проведем анализ имени:
- Имя сообщает об отображении справки
- Переименование не требуется
- WinHelpA и DestroyWindow — Windows API
- Переименование не требуется

- Убедимся, что все задачи исполнены:
- Вызываем get_is_my_work_done
- Получаем ошибку 'NoneType' object has no attribute 'startswith'
- Вызываем get_is_my_work_done еще раз
- Получаем ту же самую ошибку
- Похоже, ошибка имеет постоянный характер
- Это мешает проверке завершения всех задач
- Однако, исходя из текста гайдлайна, все задачи исполнены

- Анализ завершен
Это краткий пересказ, полный лог занимает шесть килобайт. Идея плагина крутая, хотя реализация сыровата. Запросов к GPT довольно много, но результат работы того стоит. Инструмент недоделан, однако на его основе можно написать агент под свои задачи.


aiDAPal​

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

Натравив плагин на реальную функцию, через раз получаю ошибку ERROR - aiDAPal: error unpacking response. Значит, модель не всегда отдает правильные ответы. Попробую перехватить ее вход и выход, чтобы оценить масштаб проблемы. Добавляю свои print в оригинальный метод.

Код:
def do_analysis(code,model_name):
    url = ollama_url
    headers = {"Content-Type": "application/json"}
    payload = {"model": model_name, "prompt": code, "stream": False,"format":"json"}
    print('Prompt:', code)
    res = requests.post(url, headers=headers, json=payload)
    try:
        t = res.json()['response']
        t = json.loads(t)
        print('Response:', t)
        return t
    except:
        # rarely this occurs, leftover from early on
        logging.error(f'aiDAPal: error unpacking response\n{res.json()["response"]}')
        return None

Запускаю анализ функции.

Код:
Prompt:
/*
0x401a74 is __imp_GetVersion //
0x417404 is g_hWnd_0 //
0x401b08 is __imp_WinHelpA //
*/
BOOL __stdcall sub_40C5A4(char *String)
{
  char *v1; // ebx
  UINT v2; // esi

  v1 = String;
  v2 = 261;
  if ( *(_WORD *)String == 33 )
  {
    v1 = 0;
    v2 = 3;
    if ( (GetVersion() & 0x80000000) == 0 )
      v2 = 11;
  }
  else if ( *(_WORD *)String == 63 )
  {
    v1 = 0;
    v2 = 3;
  }
  else if ( (*((_BYTE *)_ctype + *String + 1) & 4) != 0 )
  {
    v1 = (char *)j__atoi(String);
    v2 = 1;
  }
  return WinHelpA(g_hWnd_0, FullPath_HLP, v2, (ULONG_PTR)v1);
}

Response:
{
   "function":"sub_40C5A4",
   "comment":"This function interprets a string input and performs specific actions based on the first character of the string. It primarily calls WinHelpA with different parameters depending on the initial character of the input string.",
   "variables":[
      {
         "original_name":"String",
         "new_name":"input_string"
      },
      {
         "original_name":"v1",
         "new_name":"temp_variable"
      },
      {
         "original_name":"v2",
         "new_name":"command_flag"
      }
   ],
}

Ответ практически идеален, разве что имя функции не поменяла. Попробуем с другим кодом.
Код:
Prompt: int __stdcall sub_409590(int a1)
{
  unsigned int v1; // esi
  int result; // eax
  unsigned int *v3; // edx

  v1 = *(_DWORD *)(a1 + 12);
  result = dword_417300 + 8 * dword_417204 - 8;
  if ( v1 < *(_DWORD *)(result + 4) )
  {
    v3 = (unsigned int *)(result + 4);
    do
    {
      v3 -= 2;
      result -= 8;
    }
    while ( *v3 > v1 );
  }
  return result;
}

Response:
{
   "function":"sub_409590",
   "comment":"This function seems to be calculating an index based on a given input value and a set of predefined values. It uses several pointer arithmetic operations and conditional logic to find the index, possibly for use in accessing or manipulating data in memory.",
   "variables":[
      {
         "original_name":"a1",
         "comment":"Input parameter"
      },
      {
         "original_name":"v1",
         "comment":"Value calculated from input parameter"
      },
      {
         "original_name":"result",
         "comment":"Calculated index or address"
      },
      {
         "original_name":"v3",
         "comment":"Pointer used in loop to compare and update index"
      }
   ]
}

Теперь вместо переименования переменных модель добавляет к ним комментарий. Спасибо, но не от всей души. Код в этом месте ломается, так как не может найти в ответе обязательное поле new_name.

Код:
Prompt: int __stdcall sub_408F20(char *String)
{
  return sub_40C5A4(String);
}

Response:
{
   "function":"int sub_408F20(char *String)",
   "comment":"This function is a simple wrapper for another function, 'sub_40C5A4'. It takes a character string as an argument and returns the result of calling 'sub_40C5A4' with that string.",
   "variables":[
      {
         "originalName":"String",
         "newName":"inputString"
      }
   ]
}

Response:
{
   "function":"int __fastcall sub_408F20(_DWORD *String)",
   "comment":"This function is a simple wrapper for another function named 'sub_40C5A4'. It takes a pointer to a character string as its argument, calls 'sub_40C5A4' with that argument, and returns the result of that call."
}

Перезапустив анализ пару раз на новом коде, видим, что модель отдает неполные данные и откровенно галлюцинирует, добавляя __fastcall.

Задумка была крутой, но скромный размер модели не позволяет использовать ее в работе, разве что в цикле запрашивать ответы, пока не выдаст верный. Но для этого потребуется приличный GPU, чтобы не ждать годами.


Gepetto​


Плагин работает на IDA Pro 7.4 или старше. Первоначальная разработка финансировалась «Лабораторией Касперского», сейчас проект нашел другого спонсора. Поддерживает множество моделей, в том числе локальных. Настройка через config.ini. Хотя для работы с GPT достаточно добавить свой ключ в переменную окружения OPENAI_API_KEY.

После установки в меню Edit появился новый пункт Gepetto. Он предоставляет две команды: Rename variables и Explain function.

Для теста я выбрал самую жирную функцию и попросил объяснить, как она работает. На выходе меня ждал объемный комментарий на три килобайта. Язык настраивается в конфиге, но я оставил английский по умолчанию.

// Note that without more context on
// what this function is part of or specific documentation on the external
// functions it calls, creating an accurate name involves some degree of
// speculation.

GPT выжал максимум из доступного, но жалуется на нехватку контекста — информации о вызываемых функциях, что логично. Действительно, анализ лучше начинать «снизу» и постепенно идти «наверх». Тогда новые имена вызываемых функций будут подспорьем для анализа вызывающего их кода.

Код:
char sub_4081B8()
{
  int rowIndex; // ecx
  int columnIndex; // ebx
  _BYTE *currentCell; // edx
  char returnCode; // [esp+4h] [ebp-4h]

  rowIndex = 0;
  columnIndex = 0;
  returnCode = 0;
  for ( currentCell = (_BYTE *)dword_4173D4; !*currentCell; ++currentCell )
  {
    if ( rowIndex && *(currentCell - 1) == 6
      || rowIndex < dword_4173D8 && currentCell[1] == 6
      || columnIndex && currentCell[dword_4173DC] == 6
      || columnIndex < dword_4173E0 && currentCell[dword_4171E8] == 6 )
    {
      returnCode = 4;
      *currentCell = 4;
    }
LABEL_12:
    if ( ++rowIndex >= dword_4171E8 )
    {
      if ( ++columnIndex >= dword_4171EC )
        return returnCode;
      rowIndex = 0;
    }
  }
  if ( *currentCell != 2
    || (!rowIndex || *(currentCell - 1) != 6)
    && (rowIndex >= dword_4173D8 || currentCell[1] != 6)
    && (!columnIndex || currentCell[dword_4173DC] != 6)
    && (columnIndex >= dword_4173E0 || currentCell[dword_4171E8] != 6) )
  {
    goto LABEL_12;
  }
  dword_412C94 = rowIndex;
  dword_412C98 = columnIndex;
  return 1;
}

Теперь проверим, как работает автоматическое именование переменных. GPT назвал возвращаемое значение returnCode, распознал работу с таблицей и правильно именовал индексы. Весьма недурно.


Комментарий детально описал, что делает функция, и даже предложил ей удачное имя.

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


Выводы​

Языковые модели отлично справляются с анализом ассемблерного или си‑кода. Они экономят мозговые усилия аналитика и позволяют ему концентрироваться на действительно важных вещах. Так можно переделать гораздо больше работы за то же самое время.
 
Activity
So far there's no one here