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

Содержание статьи

  • Декомпиляция ассемблерного кода
  • Наивная декомпиляция си-кода
  • IDA Copilot
  • Установка
  • Тестирование
  • aiDAPal
  • Gepetto
  • Выводы

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

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

Нач­нем с прос­того тес­та. Возь­мем ассем­блер­ный код и поп­росим 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 idcimport idaapiimport idautilsimport openaiimport reclient = 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 Nonedef 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 Nonedef 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-файл целиком и получить его исходный код пока что не удас­тся.

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *