Как я могу обработать заголовок exe-файла при разборке с помощью iced_x86?

# #x86-64 #disassembly #portable-executable

Вопрос:

Я хочу разобрать список исполняемых файлов с форматом вывода для инструкций, которые я могу настроить. Как предыдущий веб-разработчик, iced-x86 с Node.js кажется, это простой способ начать. Я хочу использовать это.

Код, который я использую, в точности соответствует тому, что указано в документации для iced-x86: Дизассемблируйте (инструкции по декодированию и форматированию), хотя я загружаю Uint8Array из своего собственного exe — файла и устанавливаю RIP на 0x0, я не уверен, как следует определять это значение-оно кажется произвольным эквивалентом uint64_t exampleRip = 0x00007FFAC46ACDA4 :

 // iced-x86 features needed: --features "decoder nasm"
const { Decoder, DecoderOptions, Formatter, FormatterSyntax } = require("iced-x86");

/*
This code produces the following output:
00007FFAC46ACDA4 48895C2410           mov       [rsp 10h],rbx
00007FFAC46ACDA9 4889742418           mov       [rsp 18h],rsi
00007FFAC46ACDAE 55                   push      rbp
00007FFAC46ACDAF 57                   push      rdi
00007FFAC46ACDB0 4156                 push      r14
00007FFAC46ACDB2 488DAC2400FFFFFF     lea       rbp,[rsp-100h]
00007FFAC46ACDBA 4881EC00020000       sub       rsp,200h
00007FFAC46ACDC1 488B0518570A00       mov       rax,[rel 7FFA`C475`24E0h]
00007FFAC46ACDC8 4833C4               xor       rax,rsp
00007FFAC46ACDCB 488985F0000000       mov       [rbp 0F0h],rax
00007FFAC46ACDD2 4C8B052F240A00       mov       r8,[rel 7FFA`C474`F208h]
00007FFAC46ACDD9 488D05787C0400       lea       rax,[rel 7FFA`C46F`4A58h]
00007FFAC46ACDE0 33FF                 xor       edi,edi
*/

const exampleBitness = 64;
const exampleRipLo = 0xC46ACDA4;
const exampleRipHi = 0x00007FFA;
const exampleCode = new Uint8Array([
    0x48, 0x89, 0x5C, 0x24, 0x10, 0x48, 0x89, 0x74, 0x24, 0x18, 0x55, 0x57, 0x41, 0x56, 0x48, 0x8D,
    0xAC, 0x24, 0x00, 0xFF, 0xFF, 0xFF, 0x48, 0x81, 0xEC, 0x00, 0x02, 0x00, 0x00, 0x48, 0x8B, 0x05,
    0x18, 0x57, 0x0A, 0x00, 0x48, 0x33, 0xC4, 0x48, 0x89, 0x85, 0xF0, 0x00, 0x00, 0x00, 0x4C, 0x8B,
    0x05, 0x2F, 0x24, 0x0A, 0x00, 0x48, 0x8D, 0x05, 0x78, 0x7C, 0x04, 0x00, 0x33, 0xFF
]);
const hexBytesColumnByteLength = 10;

const decoder = new Decoder(exampleBitness, exampleCode, DecoderOptions.None);
// You have to enable the bigint feature to get i64/u64 APIs, not all browsers support BigInt
decoder.ipLo = exampleRipLo;
decoder.ipHi = exampleRipHi;
// This decodes all bytes. There's also `decode()` which decodes the next instruction,
// `decodeInstructions(count)` which decodes `count` instructions and `decodeOut(instruction)`
// which overwrites an existing instruction.
const instructions = decoder.decodeAll();

// Create a nasm formatter. It supports: Masm, Nasm, Gas (ATamp;T) and Intel (XED).
// There's also `FastFormatter` which uses less code (smaller wasm files).
//     const formatter = new FastFormatter();
const formatter = new Formatter(FormatterSyntax.Nasm);

// Change some options, there are many more
formatter.digitSeparator = "`";
formatter.firstOperandCharIndex = 10;

// Format the instructions
instructions.forEach(instruction => {
    const disasm = formatter.format(instruction);

    // Eg. "00007FFAC46ACDB2 488DAC2400FFFFFF     lea       rbp,[rsp-100h]"
    let line = ("0000000"   instruction.ipHi.toString(16)).substr(-8).toUpperCase()  
               ("0000000"   instruction.ipLo.toString(16)).substr(-8).toUpperCase();
    line  = " ";
    const startIndex = instruction.ipLo - exampleRipLo;
    exampleCode.slice(startIndex, startIndex   instruction.length).forEach(b => {
        line  = ("0"   b.toString(16)).substr(-2).toUpperCase();
    });
    for (let i = instruction.length; i < hexBytesColumnByteLength; i  )
        line  = "  ";
    line  = " ";
    line  = disasm;

    console.log(line);
});

// Free wasm memory
instructions.forEach(instruction => instruction.free());
formatter.free();
decoder.free();
 

Тем временем я также разбираю тот же файл с Гидрой, чтобы проверить свою работу.

Гидра выводит правильную разборку заголовка:

      //
     // Headers 
     // ram:00400000-ram:004001ff
     //
     assume DF = 0x0  (Default)
     IMAGE_DOS_HEADER_00400000                       XREF[1]:     004000b4(*)  
00400000 4d 5a 90        IMAGE_DO
         00 03 00 
         00 00 04 
   00400000 4d 5a           char[2]   "MZ"                    e_magic        
   00400002 90 00           dw        90h                     e_cblp        Bytes of last page
   00400004 03 00           dw        3h                      e_cp          Pages in file
   00400006 00 00           dw        0h                      e_crlc        Relocations
   00400008 04 00           dw        4h                      e_cparhdr     Size of header in 
   0040000a 00 00           dw        0h                      e_minalloc    Minimum extra para
   0040000c ff ff           dw        FFFFh                   e_maxalloc    Maximum extra para
   0040000e 00 00           dw        0h                      e_ss          Initial (relative)
   00400010 b8 00           dw        B8h                     e_sp          Initial SP value
   00400012 00 00           dw        0h                      e_csum        Checksum
   00400014 00 00           dw        0h                      e_ip          Initial IP value
   00400016 00 00           dw        0h                      e_cs          Initial (relative)
   00400018 40 00           dw        40h                     e_lfarlc      File address of re
   0040001a 00 00           dw        0h                      e_ovno        Overlay number
 

While iced_x86 outputs the following, clearing not correctly handling the header:

 0000000000000000 4D5A                 pop       r10
0000000000000002 90                   nop
0000000000000003 0003                 add       [rbx],al
0000000000000005 0000                 add       [rax],al
0000000000000007 000400               add       [rax rax],al
000000000000000A 0000                 add       [rax],al
000000000000000C FFFF                 (bad)
000000000000000E 0000                 add       [rax],al
0000000000000010 B800000000           mov       eax,0
0000000000000015 0000                 add       [rax],al
0000000000000017 004000               add       [rax],al
000000000000001A 0000                 add       [rax],al
 

Я недавно изучаю сборку; Я не знаю, как данные заголовка должны быть разобраны иначе, чем в остальных инструкциях. Как я могу с этим справиться? Есть ли функция iced_x86, которую я должен использовать?

Что я пробовал:

Я просматриваю документацию по этой теме и нашел это представление в C:

  struct DOS_Header 
 {
// short is 2 bytes, long is 4 bytes
     char signature[2] = { 'M', 'Z' };
     short lastsize;
     short nblocks;
     short nreloc;
     short hdrsize;
     short minalloc;
     short maxalloc;
     void *ss; // 2 byte value
     void *sp; // 2 byte value
     short checksum;
     void *ip; // 2 byte value
     void *cs; // 2 byte value
     short relocpos;
     short noverlay;
     short reserved1[4];
     short oem_id;
     short oem_info;
     short reserved2[10];
     long  e_lfanew; // Offset to the 'PE' signature relative to the beginning of the file
 }
 

Что кажется достаточно простым. Но я все еще не уверен, нужно ли мне писать код для этого в Node.js сценарий, или если библиотека iced_x86 уже поддерживает его.

Комментарии:

1. iced-x86-это дизассемблер. IIRC он не понимает ни одного исполняемого файла, включая PE (тот, о котором вы спрашиваете), а данные-это код, данные-это код. Я уверен, что существует множество библиотек PE для node.js в любом случае, физкультура довольно проста. Кроме того, обратите внимание, что ghidra не разбирает заголовок MZ, он просто показывает вам значение полей, что вы можете или не хотите делать (на самом деле это мало что добавляет, и есть специальные инструменты для проверки PE).

2. Возможно, вы захотите начать с дизассемблера с открытым исходным кодом , который понимает метаданные объектных файлов / исполняемых файлов, например llvm-objdump , или GNU Binutils objdump . Они оба переносимы не на x86, а на многие форматы файлов, поэтому они не будут такими «простыми». LLVM, будучи более новым, может иметь меньше накопленных ошибок, и он не GPL, поэтому вы можете использовать его как часть проектов, не связанных с GPL, в случае, если это имеет значение.

3. Дизассемблер objconv Агнера Фога специфичен для x86 и имеет открытый исходный код (GPL). agner.org/optimize/#objconv

4. @PeterCordes Спасибо, функциональность командной строки этого инструмента намного проще использовать, чем Ghidra