Всем привет!
Я решил снова зайти в реверс-инжиниринг и написать данную статью.
Многие реверс-инженеры и аналитики используют привычный набор инструментов для дизассемблинга: 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
Также с помощью 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.bin через HxD.
Видно, что подпись сделана DigiCert . То есть один из крупнейших центров сертификации.
А ещё обратите внимание, что (видимо для подписи) используется RSA-4096 + SHA-384
Откроем теперь DeNuitkanizator_Output\AnyDesk_20260624_160750\Strings\all_utf8.txt
Заметим систему 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).
Ниже будут приведены отрывки из дизассемблированного кода:
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.
ссылка на оригинал статьи https://habr.com/ru/articles/1051484/