Всем привет!

Я решил снова зайти в реверс-инжиниринг и написать данную статью.
Многие реверс-инженеры и аналитики используют привычный набор инструментов для дизассемблинга: Ghidra, IDA PRO, x64dbg, Cremniy, HxD.

И разумеется эти инструменты отлично справляются со своими задачами. Но я решил попробовать выполнить эксперимент: можно ли дизассемблировать и разобрать программы, используя только DeNuitkanizator и HxD.

Поэтому в данной статье я подробно всё опишу и в выводе будет сказано, что вышло, а что не получилось.

Обложка
Обложка

Что представляют из себя DeNuitkanizator и HxD?

DeNuitkanizator - анализатор Nuitka-сборок (а также PyInstaller и другие упаковщики) для извлечения метаданных, строк, модулей и структуры из скомпилированных .exe файлов.
Затем всю информацию просто выводит в папку DeNuitkanizator_Output.

Но у данной программы появилась технология: Asm-To-C. Она позволяет переводить ассемблерный код (x86/x64) в читаемый C-код. Основана на построчном преобразовании инструкций. Я вдохновился данной технологии у проекта на Github cisol

Интерфейс программы
Интерфейс программы

HxD - быстрый и бесплатный HEX-редактор. Она умеет работать с большими данными. Данная программа пригодится и для открытия .bin файлов в HEX-формате.

Интерфейс программы
Интерфейс программы

Что будем разбирать?

На разборе у нас будет две программы

hello.exe (3,65 МБ) - сделан в exe-файл через Nuitka

Исходный код программы:

print("Hello by 2M12")
input()
Вывод программы
Вывод программы

AnyDesk.exe (3,81 МБ) - нативный exe-файл. Версия 7.1.6.0

Интерфейс программы
Интерфейс программы

Разбор Hello.exe

Для начала нужно просто закинуть наш exe-файл в DeNuitkanizator.

Затем после успешного разбора мы получаем папки и два текстовых документа.

Вот что мы получили
Вот что мы получили

Теперь нам нужно запустить HxD и перейти по этому пути DeNuitkanizator_Output\hello_20260624_100536\Dumps\sections - путь может отличаться

И давайте откроем нашу .rsrc секцию

Вот все распакованные секции
Вот все распакованные секции

Обычно, когда используется onefile режим, то тогда DeNuitkanizator обозначает энтропию в 8.0 из 8.0. Всё дело в том, что там используется алгоритм сжатия zstd (ZStandard), и поэтому так и происходит.

Но у нас hello.exe был в режиме Standalone, поэтому хорошо поискав в HxD мы находим нашу строку:

Нашли ту самую строку из print
Нашли ту самую строку из print

Ну и помимо нашей строки есть различные функции print

Что ещё есть
Что ещё есть

Также с помощью DeNuitkanizator мы нашли замороженные модули, pe_header, и у нас есть дизассемблированный код (в C-переводе и просто ASM).

Ниже будут приведены отрывки из дизассемблированного кода:

C-перевод (первые 40 строк):

#include "environment.h"

void func() {
_0x140001000:
    MEMORY(uint64_t, rsp+8) = rbx; /* mov qword ptr [rsp + 8], rbx */
    MEMORY(uint64_t, rsp+16) = rsi; /* mov qword ptr [rsp + 0x10], rsi */
    PUSH64(rdi); /* push rdi */
    TMP64(rsp, -, 0x30); SET_ZF(64); SET_CF_SUB(rsp, 0x30); SET_AF_0(rsp, 0x30); SET_OF_SUB(rsp, 0x30, 64, 0x8000000000000000); SET_SF(64); SET_PF(); rsp = tmp64; /* sub rsp, 0x30 */
    rdi = rcx; /* mov rdi, rcx */
    rcx = (uint64_t)&MEMORY(uint64_t, rip+150047); /* lea rcx, [rip + 0x24a1f] */
    /* call qword ptr [rip + 0x24941] */
    r8 = (uint64_t)&MEMORY(uint64_t, rip+150026); /* lea r8, [rip + 0x24a0a] */
    rcx = rdi; /* mov rcx, rdi */
    rdx = (uint64_t)&MEMORY(uint64_t, rip+226032); /* lea rdx, [rip + 0x372f0] */
    MEMORY(uint64_t, rip+226017) = rax; /* mov qword ptr [rip + 0x372e1], rax */
    /* call 0x14001d820 */ PUSH64((uint64_t)&&_ret_140001037); goto _0x14001d820; _ret_140001037:;
    rbx = MEMORY(uint64_t, rip+230341); /* mov rbx, qword ptr [rip + 0x383c5] */
    rsi = MEMORY(uint64_t, rip+226862); /* mov rsi, qword ptr [rip + 0x3762e] */
    tmp64 = rbx & rbx; SET_ZF(64); SET_SF(64); SET_PF(); cf = 0; of = 0; /* test rbx, rbx */
    if(!zf) goto _0x140001080; /* jne 0x140001080 */
    ecx ^= ecx; SET_ZF(32); SET_SF(32); SET_PF(); cf = 0; of = 0; /* xor ecx, ecx */
    /* call 0x140015340 */ PUSH64((uint64_t)&&_ret_140001051); goto _0x140015340; _ret_140001051:;
    rdx = -1; /* mov rdx, -1 */
    rcx = rax; /* mov rcx, rax */
    /* call qword ptr [rip + 0x246fa] */
    MEMORY(uint64_t, rip+230299) = rax; /* mov qword ptr [rip + 0x3839b], rax */
    tmp64 = rax & rax; SET_ZF(64); SET_SF(64); SET_PF(); cf = 0; of = 0; /* test rax, rax */
    if(zf) goto _0x14000117f; /* je 0x14000117f */
    TMP64(MEMORY(uint64_t, rax), +, 1); SET_ZF(64); SET_AF_INC(64); SET_OF_INC_DEC_NEG(64, 0x8000000000000000); SET_SF(64); SET_PF(); MEMORY(uint64_t, rax) = tmp64; /* inc qword ptr [rax] */
    rbx = MEMORY(uint64_t, rip+230280); /* mov rbx, qword ptr [rip + 0x38388] */
_0x140001080:
    TMP64(rbx, -, MEMORY(uint64_t, rip+226017)); SET_ZF(64); SET_CF_SUB(rbx, MEMORY(uint64_t, rip+226017)); SET_AF_0(rbx, MEMORY(uint64_t, rip+226017)); SET_OF_SUB(rbx, MEMORY(uint64_t, rip+226017), 64, 0x8000000000000000); SET_SF(64); SET_PF(); /* cmp rbx, qword ptr [rip + 0x372e1] */
    if(zf) goto _0x1400010b8; /* je 0x1400010b8 */
    rax = MEMORY(uint64_t, rip+230408); /* mov rax, qword ptr [rip + 0x38408] */
    tmp64 = rax & rax; SET_ZF(64); SET_SF(64); SET_PF(); cf = 0; of = 0; /* test rax, rax */
    if(!zf) goto _0x1400010a9; /* jne 0x1400010a9 */
    rcx = (uint64_t)&MEMORY(uint64_t, rip+166044); /* lea rcx, [rip + 0x2889c] */
    /* call qword ptr [rip + 0x24876] */
    MEMORY(uint64_t, rip+230383) = rax; /* mov qword ptr [rip + 0x383ef], rax */
_0x1400010a9:

Всё, что закомментировано - неподдерживаемые пока мнемоники.

Ассемблер (первые 40 строк):

0x140001000: mov      qword ptr [rsp + 8], rbx       
0x140001005: mov      qword ptr [rsp + 0x10], rsi    
0x14000100a: push     rdi                            
0x14000100b: sub      rsp, 0x30                      
0x14000100f: mov      rdi, rcx                       
0x140001012: lea      rcx, [rip + 0x24a1f]           
0x140001019: call     qword ptr [rip + 0x24941]      [CALL]
0x14000101f: lea      r8, [rip + 0x24a0a]            
0x140001026: mov      rcx, rdi                       
0x140001029: lea      rdx, [rip + 0x372f0]           
0x140001030: mov      qword ptr [rip + 0x372e1], rax 
0x140001037: call     0x14001d820                    [CALL]
0x14000103c: mov      rbx, qword ptr [rip + 0x383c5] 
0x140001043: mov      rsi, qword ptr [rip + 0x3762e] 
0x14000104a: test     rbx, rbx                       
0x14000104d: jne      0x140001080                    [JMP]
0x14000104f: xor      ecx, ecx                       
0x140001051: call     0x140015340                    [CALL]
0x140001056: mov      rdx, -1                        
0x14000105d: mov      rcx, rax                       
0x140001060: call     qword ptr [rip + 0x246fa]      [CALL]
0x140001066: mov      qword ptr [rip + 0x3839b], rax 
0x14000106d: test     rax, rax                       
0x140001070: je       0x14000117f                    [JMP]
0x140001076: inc      qword ptr [rax]                
0x140001079: mov      rbx, qword ptr [rip + 0x38388] 
0x140001080: cmp      rbx, qword ptr [rip + 0x372e1] 
0x140001087: je       0x1400010b8                    [JMP]
0x140001089: mov      rax, qword ptr [rip + 0x38408] 
0x140001090: test     rax, rax                       
0x140001093: jne      0x1400010a9                    [JMP]
0x140001095: lea      rcx, [rip + 0x2889c]           
0x14000109c: call     qword ptr [rip + 0x24876]      [CALL]
0x1400010a2: mov      qword ptr [rip + 0x383ef], rax 
0x1400010a9: mov      rdx, rax                       
0x1400010ac: mov      rcx, rbx                       
0x1400010af: call     qword ptr [rip + 0x24613]      [CALL]
0x1400010b5: mov      rbx, rax                       
0x1400010b8: mov      rdx, rsi                       
0x1400010bb: mov      rcx, rbx

Как видите всё было успешно извлечено с помощью Capstone + Asm-To-C. Но важно учитывать, что всё равно нужно уметь сортировать мусор (да он есть, ведь Capstone - не рекурсивный дизассемблер, пока что).

А вот информация по секциям:

.data: VA=0x00032000 RawSize=24,064 VirtSize=31,840 Entropy=2.21/8.0 Rights=0xc0000040 
.pdata: VA=0x0003a000 RawSize=8,192 VirtSize=7,920 Entropy=5.20/8.0 Rights=0x40000040 
.rdata: VA=0x00025000 RawSize=52,736 VirtSize=52,594 Entropy=6.16/8.0 Rights=0x40000040 
.reloc: VA=0x004b6000 RawSize=2,048 VirtSize=1,860 Entropy=5.19/8.0 Rights=0x42000040 
.rsrc: VA=0x0003c000 RawSize=4,692,480 VirtSize=4,692,412 Entropy=5.55/8.0 Rights=0x40000040 
.text: VA=0x00001000 RawSize=146,432 VirtSize=146,284 Entropy=6.15/8.0 Rights=0x60000020 EXEC

А ещё обратите внимание на pe_headers.txt. Там присутствует упоминания версии python:

----------Imported symbols----------

[IMAGE_IMPORT_DESCRIPTOR]
0x2EB10    0x0   OriginalFirstThunk:            0x2FEC8   
0x2EB10    0x0   Characteristics:               0x2FEC8   
0x2EB14    0x4   TimeDateStamp:                 0x0        [Thu Jan  1 00:00:00 1970 UTC]
0x2EB18    0x8   ForwarderChain:                0x0       
0x2EB1C    0xC   Name:                          0x3168E   
0x2EB20    0x10  FirstThunk:                    0x252D8   

python311.dll.PyImport_ImportFrozenModule Hint[406]
python311.dll.PyErr_ExceptionMatches Hint[180]
python311.dll._PyErr_FormatFromCause Hint[1172]
python311.dll.PyObject_GC_Del Hint[622]
python311.dll.PyObject_CallFunctionObjArgs Hint[606]
python311.dll.PyLong_AsLong Hint[447]
python311.dll.PyObject_ClearWeakRefs Hint[615]
python311.dll.PyCode_Type Hint[84]
python311.dll.PyUnicode_AsUTF8 Hint[890]
python311.dll.PyUnicode_AsWideCharString Hint[897]
python311.dll.PyUnicode_FromFormat Hint[936]

Разбор AnyDesk.exe

Теперь давайте также закинем файл в наш DeNuitkanizator и подождём результата

Перейдём по пути DeNuitkanizator_Output\AnyDesk_20260624_160750\Dumps

Путь где Overlay
Путь где Overlay

И теперь откроем overlay.bin через HxD.

Видно чья цифровая подпись
Видно чья цифровая подпись

Видно, что подпись сделана DigiCert . То есть один из крупнейших центров сертификации.

А ещё обратите внимание, что (видимо для подписи) используется RSA-4096 + SHA-384

RSA-4096 + SHA-384
RSA-4096 + SHA-384

Откроем теперь DeNuitkanizator_Output\AnyDesk_20260624_160750\Strings\all_utf8.txt

Заметили Buildbot
Заметили Buildbot

Заметим систему CI/CD Buildbot. И он кстати написан на Python😉
Я слышал его часто применяют в сложных сборках из-за гибкости.

А также у нас есть и pe_headers.txt (первые 39 строк):

----------DOS_HEADER----------

[IMAGE_DOS_HEADER]
0x0        0x0   e_magic:                       0x5A4D    
0x2        0x2   e_cblp:                        0x90      
0x4        0x4   e_cp:                          0x3       
0x6        0x6   e_crlc:                        0x0       
0x8        0x8   e_cparhdr:                     0x4       
0xA        0xA   e_minalloc:                    0x0       
0xC        0xC   e_maxalloc:                    0xFFFF    
0xE        0xE   e_ss:                          0x0       
0x10       0x10  e_sp:                          0xB8      
0x12       0x12  e_csum:                        0x0       
0x14       0x14  e_ip:                          0x0       
0x16       0x16  e_cs:                          0x0       
0x18       0x18  e_lfarlc:                      0x40      
0x1A       0x1A  e_ovno:                        0x0       
0x1C       0x1C  e_res:                         
0x24       0x24  e_oemid:                       0x0       
0x26       0x26  e_oeminfo:                     0x0       
0x28       0x28  e_res2:                        
0x3C       0x3C  e_lfanew:                      0xD0      

----------NT_HEADERS----------

[IMAGE_NT_HEADERS]
0xD0       0x0   Signature:                     0x4550    

----------FILE_HEADER----------

[IMAGE_FILE_HEADER]
0xD4       0x0   Machine:                       0x14C     
0xD6       0x2   NumberOfSections:              0x6       
0xD8       0x4   TimeDateStamp:                 0x634E8DEE [Tue Oct 18 11:28:46 2022 UTC]
0xDC       0x8   PointerToSymbolTable:          0x0       
0xE0       0xC   NumberOfSymbols:               0x0       
0xE4       0x10  SizeOfOptionalHeader:          0xE0      
0xE6       0x12  Characteristics:               0x122     
Flags: IMAGE_FILE_32BIT_MACHINE, IMAGE_FILE_EXECUTABLE_IMAGE, IMAGE_FILE_LARGE_ADDRESS_AWARE

PE Headers служит "паспортом" для программ, и по факту объясняет Windows как запускать программу. Данный заголовок получается с помощью библиотеки pefile.

А вот информация по секциям:

.data: VA=0x00c8e000 RawSize=3,949,056 VirtSize=3,949,964 Entropy=8.00/8.0 Rights=0xc0000040 
.itext: VA=0x00004000 RawSize=0 VirtSize=13,142,528 Entropy=0.00/8.0 Rights=0xc0000080 
.rdata: VA=0x00c8d000 RawSize=1,024 VirtSize=762 Entropy=5.64/8.0 Rights=0x40000040 
.reloc: VA=0x01058000 RawSize=1,024 VirtSize=768 Entropy=1.18/8.0 Rights=0x42000040 
.rsrc: VA=0x01053000 RawSize=18,944 VirtSize=18,512 Entropy=6.02/8.0 Rights=0x40000040 
.text: VA=0x00001000 RawSize=10,752 VirtSize=10,293 Entropy=6.51/8.0 Rights=0x60000020 EXEC

Совет!

Если вы видите в entropy.txt, что у какой-либо секции повышенная энтропия (8.0 самая максимальная) - то скорее всего файлы были сжаты с помощью различных алгоритмов (например gzip).

Пример энтропий секций у AnyDesk
Пример энтропий секций у AnyDesk

Ниже будут приведены отрывки из дизассемблированного кода:

C-перевод (первые 40 строк):

#include "environment.h"

void func() {
    PUSH64(ebp); /* push ebp */
    ebp = esp; /* mov ebp, esp */
    eax = MEMORY(uint32_t, ebp+8); /* mov eax, dword ptr [ebp + 8] */
    edx = MEMORY(uint32_t, ebp+16); /* mov edx, dword ptr [ebp + 0x10] */
    PUSH64(esi); /* push esi */
    esi = ecx; /* mov esi, ecx */
    ecx = MEMORY(uint32_t, ebp+12); /* mov ecx, dword ptr [ebp + 0xc] */
    MEMORY(uint32_t, esi) = eax; /* mov dword ptr [esi], eax */
    eax ^= eax; SET_ZF(32); SET_SF(32); SET_PF(); cf = 0; of = 0; /* xor eax, eax */
    PUSH64(edi); /* push edi */
    edi = MEMORY(uint32_t, ebp+24); /* mov edi, dword ptr [ebp + 0x18] */
    MEMORY(uint32_t, esi+8) = eax; /* mov dword ptr [esi + 8], eax */
    MEMORY(uint32_t, esi+20) = eax; /* mov dword ptr [esi + 0x14], eax */
    MEMORY(uint32_t, esi+24) = eax; /* mov dword ptr [esi + 0x18], eax */
    MEMORY(uint32_t, esi+28) = eax; /* mov dword ptr [esi + 0x1c], eax */
    MEMORY(uint32_t, esi+32) = eax; /* mov dword ptr [esi + 0x20], eax */
    MEMORY(uint32_t, esi+36) = eax; /* mov dword ptr [esi + 0x24], eax */
    MEMORY(uint32_t, esi+40) = eax; /* mov dword ptr [esi + 0x28], eax */
    MEMORY(uint32_t, esi+44) = eax; /* mov dword ptr [esi + 0x2c], eax */
    eax = (uint64_t)&MEMORY(uint32_t, ebp+8); /* lea eax, [ebp + 8] */
    PUSH64(eax); /* push eax */
    PUSH64(0x40); /* push 0x40 */
    PUSH64(MEMORY(uint32_t, ebp+28)); /* push dword ptr [ebp + 0x1c] */
    MEMORY(uint32_t, esi+12) = edx; /* mov dword ptr [esi + 0xc], edx */
    edx = MEMORY(uint32_t, ebp+20); /* mov edx, dword ptr [ebp + 0x14] */
    PUSH64(edi); /* push edi */
    MEMORY(uint32_t, esi+4) = ecx; /* mov dword ptr [esi + 4], ecx */
    MEMORY(uint32_t, esi+16) = edx; /* mov dword ptr [esi + 0x10], edx */
    /* call dword ptr [ecx + 0x18] */
    tmp32 = eax & eax; SET_ZF(32); SET_SF(32); SET_PF(); cf = 0; of = 0; /* test eax, eax */
    if(!zf) goto _0x401058; /* jne 0x401058 */
    MEMORY(uint32_t, esi+8) = 9; /* mov dword ptr [esi + 8], 9 */
    goto _0x401131; /* jmp 0x401131 */
_0x401058:
    PUSH64(ebx); /* push ebx */
    ebx = MEMORY(uint32_t, esi+16); /* mov ebx, dword ptr [esi + 0x10] */
    TMP32(ebx, -, 0x40); SET_ZF(32); SET_CF_SUB(ebx, 0x40); SET_AF_0(ebx, 0x40); SET_OF_SUB(ebx, 0x40, 32, 0x80000000); SET_SF(32); SET_PF(); /* cmp ebx, 0x40 */

Всё, что закомментировано - неподдерживаемые пока мнемоники.

Ассемблер (первые 40 строк):

0x401000: push     ebp                            
0x401001: mov      ebp, esp                       
0x401003: mov      eax, dword ptr [ebp + 8]       
0x401006: mov      edx, dword ptr [ebp + 0x10]    
0x401009: push     esi                            
0x40100a: mov      esi, ecx                       
0x40100c: mov      ecx, dword ptr [ebp + 0xc]     
0x40100f: mov      dword ptr [esi], eax           
0x401011: xor      eax, eax                       
0x401013: push     edi                            
0x401014: mov      edi, dword ptr [ebp + 0x18]    
0x401017: mov      dword ptr [esi + 8], eax       
0x40101a: mov      dword ptr [esi + 0x14], eax    
0x40101d: mov      dword ptr [esi + 0x18], eax    
0x401020: mov      dword ptr [esi + 0x1c], eax    
0x401023: mov      dword ptr [esi + 0x20], eax    
0x401026: mov      dword ptr [esi + 0x24], eax    
0x401029: mov      dword ptr [esi + 0x28], eax    
0x40102c: mov      dword ptr [esi + 0x2c], eax    
0x40102f: lea      eax, [ebp + 8]                 
0x401032: push     eax                            
0x401033: push     0x40                           
0x401035: push     dword ptr [ebp + 0x1c]         
0x401038: mov      dword ptr [esi + 0xc], edx     
0x40103b: mov      edx, dword ptr [ebp + 0x14]    
0x40103e: push     edi                            
0x40103f: mov      dword ptr [esi + 4], ecx       
0x401042: mov      dword ptr [esi + 0x10], edx    
0x401045: call     dword ptr [ecx + 0x18]         [CALL]
0x401048: test     eax, eax                       
0x40104a: jne      0x401058                       [JMP]
0x40104c: mov      dword ptr [esi + 8], 9         
0x401053: jmp      0x401131                       [JMP]
0x401058: push     ebx                            
0x401059: mov      ebx, dword ptr [esi + 0x10]    
0x40105c: cmp      ebx, 0x40                      
0x40105f: jae      0x40106d                       
0x401061: mov      dword ptr [esi + 8], 1         
0x401068: jmp      0x401130                       [JMP]
0x40106d: mov      eax, dword ptr [esi + 0xc]  

Заключение

Как видите, программы возможно разбирать с помощью двух инструментов: DeNuitkanizator и HxD. Но важно понимать, что одного DeNuitkanizator'а может быть недостаточно!

У нас получилось дизассемблировать программы, извлечь разную информацию из секций, посмотреть заголовок PE, найти строчку из hello.exe.

В любом случае это был эксперимент, и я настоятельно рекомендую DeNuitkanizator комбинировать с Ghidra, x64dbg, Cremniy или IDA PRO.


Статья про DeNuitkanizator

Официальный сайт DeNuitkanizator