Как разобрать .exe всего двумя инструментами: практический разбор с DeNuitkanizator и HxD

от автора

Всем привет!

Я решил снова зайти в реверс-инжиниринг и написать данную статью.
Многие реверс-инженеры и аналитики используют привычный набор инструментов для дизассемблинга: 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

ссылка на оригинал статьи https://habr.com/ru/articles/1051484/