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

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

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

Продолжение доступно только участникам

Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».

Присоединяйся к сообществу «Xakep.ru»!

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

  • Подпишись на наc в Telegram!

    Только важные новости и лучшие статьи

    Подписаться

  • Подписаться
    Уведомить о
    0 комментариев
    Межтекстовые Отзывы
    Посмотреть все комментарии