все заметки

x64 дизассемблер на php

2024.12.21 (редактировано: 2024.12.25)

Это не полноценный дизассмеблер. Это разбор инструкций и подсчет их размера. То есть, не учитываются данные: смещения, значения, регистры. Учитывается только название инструкции - мнемоника.

У кого нет времени на описание, переходим сразу к онлайн версии.
Или скачать: x64disasm.php, instructions.php
Или посмотреть: x64disasm.php, instructions.php

Внимание. Мало слов, много примеров.

Если нет понимания про префиксы, ModR/M и SIB, то отличное описание на OSDev Wiki.

Здесь только то, что нужно в данном проекте.

За основу возьмем базу инструкций с опкодами и битностью из iced

Кратко

1. Префиксы наследия (legacy)
1.1. 0x66
1.2. 0x67
1.3. 0x66, 0xF2, 0xF3

2. Новые префиксы
2.1. REX
2.2. VEX
2.3. EVEX
2.4. MVEX
2.5. XOP

3. ModR/M, но не ModR/M

4. База инструкций
4.1. Префикс внутри
4.2. Третий опкод внутри
4.3. Байт константа после ModR/M
4.4. Пересечения опкодов
4.5. Исключение

5. Как пользовать

6. Заключение

А теперь подробнее. Погнали...

1. Префиксы наследия (legacy)

0x26, 0x2E, 0x36, 0x3E, 0x64, 0x65, 0x66, 0x67, 0xF0, 0xF2, 0xF3.

Нас интересуют 0x66, 0x67, 0xF2, 0xF3, только они меняют размер инструкции.

Они могут идти друг за другом, бывает встречается и такое 66 67 66 66 66 2E 0F 1F 84 00 00 00 00 00

1.1. 0x66 - указывает на использование 16 битной инструкции, если такая имеется:

ADD EAX,[RAX] ; 03 00    < o32 03 /r\nADD AX,[RAX]  ; 66 03 00 < o16 03 /r

Но в ней не указано количество байтов. А вот здесь указано:

ADD EAX,11111111 ; 05 11 11 11 11 < o32 05 id\nADD EAX,1111     ; 66 05 11 11    < o16 05 iw

В iced существует такая же проблема с Jxx как и в IDA. Учитывается префикс 0x66.

Для примера JZ в iced.

JE rel16\no16 0F 84 cw\n16/32/64-bit

Подразумевается, что 66 0F 84 00 00 интерпретируется как JZ word.

Но на самом деле это не так, 0x66 префикс никак не влияет на эту инструкцию:

JZ dword ; 66 0F 84 00 00 00 00 < o32 0F 84 cd\nJZ dword ; 0F 84 00 00 00 00    < o32 0F 84 cd

Это выяснилось при тестировании дизассемблера, он жестко спотыкался об эти байты.

Кстати, так же происходит и с JMP инструкций, но IDA справляется с этим.

JMP rel16\no16 E9 cw\n16/32/64-bit

Итого:

JMP dword ; 66 E9 00 00 00 00 < o32 E9 cd\nJMP dword ; E9 00 00 00 00    < o32 E9 cd

1.2. 0x67 - указывает на использование 16 битной адресации, если такая имеется:

MOV AL,[1111111111111111] ; A0 11 11 11 11 11 11 11 11\nMOV AL,[11111111]         ; 67 A0 11 11 11 11

1.3. 0x66, 0xF2, 0xF3 - и префикс, и опкод в одном лице.

Если у инструкции нет такого префикса (опкода), он все равно может встречаться в коде:

SLDT [RAX] ; F2 0F 00 00\nSLDT [RAX] ; 0F 00 00

А есть инструкции которые не будут идентифицированы без этого префикса:

POPCNT EAX,[RAX] ; F3 0F B8 00\n#UD              ; 0F B8 00

А вот где могут быть все четыре (с и без префикса) варианта:

PSHUFHW xmm1,[RAX],0 ; F3 0F 70 00 00\nPSHUFLW xmm1,[RAX],0 ; F2 0F 70 00 00 \nPSHUFD xmm1,[RAX],0  ; 66 0F 70 00 00\nPSHUFW mm1,[RAX],0   ; 0F 70 00 00

2. Новые префиксы

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

$instructions['EVEX']['7D.66.0F38']['W0']['512']['/r xxx'] = 'VPERMT2B zmm1 {k1}{z}, zmm2, zmm3/m512';
  • 7D - опкод, идет сразу после префикса

Это реальный байт, а вот далее уже обрабатываются биты, у каждого префикса свои, но суть одна:

  • 66 - еще один опкод/префикс (66, F3, F2)
  • 0F38 - map (0F, 0F38, 0F3A, MAP5, MAP6, X8, X9, XA)
  • W0 - prefix.W (W0, W1, WIG)
  • 512 - длина битности (128, 256, 512, L0, L1, LZ, LIG)

2.1. REX

Его значение может быть 0x40-0x4F (в x86 это INC/DEC). Нагляднее побитно:

+---+---+---+---+---+---+---+---+\n| 0   1   0   0 | W | R | X | B |\n+---+---+---+---+---+---+---+---+

Этот префикс сигнализирует как использовать инструкцию (32 или 64 бит) и расширение для ModR/M и SIB.

Для примера возьмем из предыдущего раздела. Для данных инструкций меняется только регистр:

PSHUFHW xmm1,[R8],0 ; F3 49 0F 70 00 00\nPSHUFLW xmm1,[R8],0 ; F2 41 0F 70 00 00\nPSHUFD xmm1,[R8],0  ; 66 4F 0F 70 00 00\nPSHUFW mm1,[R8],0   ; 43 0F 70 00 00

А теперь о инструкции где есть размеры.

Если REX.W == 1 (>=0x48) то используется 64 битная инструкция и значения уже берутся исходя из этого, иначе по умолчанию 32 бита.

MOV EAX,11111111         ; B8 11 11 11 11\nMOV EAX,11111111         ; 40 B8 11 11 11 11 (REX = 0x40, W == 0)\nMOV RAX,1111111111111111 ; 4E B8 11 11 11 11 11 11 11 11 (REX = 0x4E, REX.W == 1)

REX префиксы могут идти друг за другом, ничего при этом не меняя.

2.2. VEX

Новый вид префикса для 64 битов.

  • 0xC5 - 2 байта (C5 XX) на префикс
  • 0xC4 - 3 байта (C4 XX XX) на префикс

Одну инструкцию можно представить в двух вариантах VEX:

; VEX.LIG.F3.0F.WIG 5F /r\nVMAXSS xmm0, xmm15, [RAX] ; C5 82 5F 00\nVMAXSS xmm0, xmm15, [RAX] ; C4 A1 82 5F 00

Но, это возможно, если так называемая карта (map) у инструкции равна 0F. Это значение единственный в двухбайтовом префиксе.

В трехбайтовом префиксе есть еще 0F38 и 0F3A.

2.3. EVEX/MVEX

Тот же VEX, только четыре байта начинайющихся с 0x63: 63 XX YY XX

Для орпеделения префикса нужно из YY байта проверить [2] бит:

  • [2] = 0 - MVEX
  • [2] = 1 - EVEX

2.4. XOP

Это подарок от AMD.

Байт идентификатор 0x8F, но необходимо проверять следующий байт и биты, чтобы [5:0] >= 01000

Иначе это может быть POP:

POP [RDI] ; 8F 07\nXOP       ; 8F 08

А если рассматривать через призму ModR/M, то это /1 (0b01), /2 (0b10), /3 (0b11).

Все остальное то же самое.

3. Вместо /r и /[0-7]{1}

Новая вводная от Интел это следующий байт (ModR/M) от опкода: проверка полей mod, reg, r/m.

  • 11:rrr:000 - [7:6] - обязательно равно 11, [5:3] - любое, [2:0] - обязательно равно 000
  • !(11):000:bbb - [7:6] - не равно 11, [5:3] - обязательно равно 000, [2:0] - любое
  • !(11):rrr:bbb - [7:6] - не равно 11, [5:3] - любое, [2:0] - любое

Если условия не выполнены, то это не наша инструкция.

4. База инструкций

Что за великолепие в этой прекрасной базе инструкций? Спросите вы.
Почему максимум два опкода на инструкцию, хотя их может быть три.
Почему префиксы (и не только) внутри?
Да, это база выстрадана на практике. И на примерах это видно.

4.1. Префикс внутри

$instructions['0F.01']['^F3 /5 xxx'] = 'RSTORSSP m64';\n$instructions['0F.01']['/7 xxx'] = 'INVLPG m';\n

То есть, в следующем байте после F3.0F.01 ModR/M, reg должен быть равен 101 (/5). Если это не так, а допустим 111 (/7), то мы теряемся, ибо других инструкций в нашем массиве нет.

Если брать без префикса, то там уже как раз есть /7 (111) и мы получаем мнемонику нашей инструкции.

Допустим:

  • F3 0F 01 28 (reg=101) - мы получаем RSTORSSP m64 (RSTORSSP qword ptr ds:[rax])
  • F3 0F 01 38 (reg=111) - мы ничего не получаем, потому что есть префикс F3, но по документации он не нужен, а может быть. (INVLPG byte ptr ds:[rax])

4.2. Третий опкод внутри

Пример как байт за опкодом (ModR/M) может все менять:

$instructions['0F.1E']['^F3 |FB . xxx'] = 'ENDBR32';\n$instructions['0F.1E']['^F3 |FA . xxx'] = 'ENDBR64';\n$instructions['0F.1E']['^F3 /1 xxx'] = 'RDSSPD r32';\n$instructions['0F.1E']['/r x32'] = 'RESERVEDNOP r/m32, r32';\n
  • F3 0F 1E FA (reg=111) - ENDBR64
  • F3 0F 1E CA (reg=001) - RDSSPD EDX
  • F3 0F 1E D2 (reg=010) - RESERVEDNOP EDX/EDX
  • F3 0F 1E DA (reg=011) - RESERVEDNOP EDX/EBX

4.3. Байт константа после ModR/M

$instructions['0F.0F']['/r $B6 xxx'] = 'PFRCPIT2 mm, mm/m64';\n$instructions['0F.0F']['/r $B4 xxx'] = 'PFMUL mm, mm/m64';\n
  • 0F 0F 00 B6 - pfrcpit2 mm0, qword ptr ds:[rax]
  • 0F 0F 00 B4 - pfmul mm0, qword ptr ds:[rax]
  • 0F 0F 00 B5 - invalid

4.4. Пересечения опкодов

$instructions['9B']['|D9 /6 x32'] = 'FSTENV m28byte';\n$instructions['9B']['|D9 /7 xxx'] = 'FSTCW m2byte';
  • 9B D9 30 - FSTENV [rax]
  • 9B D9 38 - FSTCW [rax]

Для 9B D9 только два ModR/M.reg /6 и /7, и если будет иначе, то все строится иначе.
То есть, 9B D9 00 будет:

WAIT        ; 9B\nFLD d,[RAX] ; D9 00

Из этого

$instructions['9B']['. xxx'] = 'WAIT';\n$instructions['D9']['/0 xxx'] = 'FLD m32fp';

Такие пересечения есть у опкодов начинающихся на: 9B, C6, C7, D8, D9, DA, DB, DC, DD, DE, DF

Все они скомпонованы и отсортированы по аналогии. Кстати, /[0-7] приоритетнее /r.

4.5. Исключение

Есть такие инструкции:

  • 0F 20 /r - MOV r64, cr
  • 0F 21 /r - MOV r64, dr
  • 0F 22 /r - MOV cr, r64
  • 0F 23 /r - MOV dr, r64

Все понятно, ModR/M... Но, оказывается, в данной инструкции игнорируются ModR/M.mod ([7:6] биты).
То есть, вообще не учитываются, надо внедрять исключение, ибо от этих бит зависит длинна инструкции. Получается, что это не совсем /r.
Короче, было принято волевое решение изобрести велосипед и насильно присваивать ModR/M.mod=11, дабы избежать изменения количество байт в инструкции.
По аналогии с 11:rrr:bbb получилось вот такая конструкция ~11:rrr:bbb.

$instructions['0F.20']['~11:rrr:bbb xxx'] = 'MOV r64, cr';\n$instructions['0F.21']['~11:rrr:bbb xxx'] = 'MOV r64, dr';\n$instructions['0F.22']['~11:rrr:bbb xxx'] = 'MOV cr, r64';\n$instructions['0F.23']['~11:rrr:bbb xxx'] = 'MOV dr, r64';

5. Как пользовать

<?php\n\ninclude 'x64disasm.php';\n\n$data = "\x00\x01\x02\x03";\n$data = x64disasm($data);\n\nprint_r($data);\n

Получаем

Array\n(\n\t[0] => ADD r/m8, r8\n\t[2] => ADD r8, r/m8\n)

6. Остальное

Почему PHP? Нам никто не мешает махать красными флагами.
И этот код будет работать на любой версии PHP.

Точки в опкодах? Для удобства восприятия.

Почему строковый тип? Для удобства восприятия.

Зачем? Расширить знания (потом расскажу).

Github? Да, но потом. Или может быть лучше поддержать отечество и использовать gitverse.ru...

...

еще по теме реверс инжиниринг