Содержание статьи
Декомпиляция ассемблерного кода
Начнем с простого теста. Возьмем ассемблерный код и попросим 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
можно догадаться, что это координаты x1
и x2
.
Разбирать подобную математику вручную было бы слишком утомительно. Обычно я просто смотрю в отладчике, что было с данными до и что стало после. А из этого уже делаю выводы о назначении функции.
Наивность текущего подхода — в зашкаливающем числе запросов. Модель GPT-4 за анализ двухсот небольших функций сожрала аж три бакса! Вероятно, модели попроще стоят дешевле. В любом случае подобный анализ применим только к конкретной функции. То есть загрузить exe-файл целиком и получить его исходный код пока что не удастся.
Продолжение доступно только участникам
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Присоединяйся к сообществу «Xakep.ru»!
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее