Заклинание кода: Гимель

Дата публикации 4 дек 2002

Заклинание кода: Гимель — Архив WASM.RU

Если взглянуть на типичную программу на ассемблере, то можно заметить, что некоторых инструкций там больше, чем других. Одна из таких инструкций MOV. Эта инструкция отвечает за копирование информации - например, значения ячейки памяти в регистр.

К примеру, мы хотим поместить в EAX число 1. Сделать это просто, нужный опкод в данном случае 0B8h. А как задаётся число, которое надо поместить в регистр? Над этой проблемой билось не одно поколение заклинателей кода, но затем один из них, расшифровав письмена в Книге Двойных Слов, рассказал, что нужное число просто помещается сразу после опкода. То есть заклинание выглядит следующим образом:

Код (Text):
  1.  
  2.         db      0B8h    ; mov eax,1
  3.         dd      1

Обратите внимание: единица вставлена с помощью директивы dd, а в результате это будет последовательность байт 1,0,0,0. Также обратите внимание, что в последовательности байт единица идёт вначале. Это потому, что в архитектуре процессоров семейства x86 байты слов и двойных слов располагаются от младшего к старшему, а единица, безусловно, находится в младшем байте. Таким образом, я могу переписать вышеприведённое заклинание так:

Код (Text):
  1.  
  2.         db      0B8h    ; mov eax, 1
  3.         db      1,0,0,0

Оба заклинания делают одно и то же, но я привел их оба, чтобы вы лучше представляли себе, как они устроены на уровне байт. А вместо единицы можно подставить любое 32-битное число.

А что делать, если нужно поместить какое-либо число не в EAX, а в EDI? Данный опкод повинуется правилу, о котором было рассказано в предыдущей главе: к базовому опкоду (0B8h) следует прибавить номер регистра (номер EDI - 7). Это даёт 0B8h+7=0BFh. Давайте проверим это на примере простого заклинания:

Код (Text):
  1.  
  2. format PE console
  3. entry start
  4.  
  5. include '..\..\include\kernel.inc'
  6. include '..\..\include\user.inc'
  7. include '..\..\include\macro\stdcall.inc'
  8. include '..\..\include\macro\import.inc'
  9.  
  10. section '.data' data writeable readable
  11.  
  12. _d      db '%d',0
  13.  
  14. section '.code' code executable readable
  15.  
  16. start:
  17.  
  18.         db      0BFh    ; mov edi,100
  19.         dd      100
  20.  
  21.         push    edi
  22.         push    _d
  23.         call    [printf]
  24.         add     esp,8
  25.  
  26.         invoke  ExitProcess,0
  27.  
  28. section '.idata' import data readable writeable
  29.  
  30.                 library kernel32,'kernel32.dll',\
  31.                         msvcrt,'msvcrt.dll'
  32. kernel32:       import  ExitProcess,'ExitProcess'
  33. msvcrt:         import  printf,'printf'

Компиляция и последующий запуск программы должны показать, что заклинание работает.

Конечно, заклинатель кода, овладевший копированием числа в регистры общего назначения, может захотеть большего: копирования содержимого регистра в регистр или, чего гляди, регистра в память. Это, разумеется, возможно, но прежде необходимо усвоить несколько вещей.

В языке ассемблера для копирования непосредственного значения (оно же константа, оно же литерал) в регистр, как и содержимого регистра в другой регистр или в память, применяется одна мнемоника - MOV. В тоже время эти варианты имеют разные опкоды. К этому моменту вы уже должны были это подозревать, но теперь, когда я прямо сказал об этом, возможно, волосы на вашей голове (и прочих частях тела) встали дыбом. Или, по меньшей мере, зашевелились. К сожалению (а может и к счастью), это так (не то, что волосы зашевелились, а то, что опкоды разные). Причем зачастую к этим опкодам применяются разные правила.

Начнём с простого случая, с MOV EAX,EDX. Как нам составить заклинание? Базовый опкод инструкции "MOV регистр,регистр" - 08Bh, но правило, которое мы применяли в прошлые разы, здесь не поможет, ведь здесь два операнда, а не один.

Чтобы составить требуемое заклинание, нам придётся обратиться к помощи дополнительного байта, применяемого как раз в таких случаях. Этот байт называется ModR/M, и, если он есть, то располагается сразу после опкода. Сам ModR/M делится на три поля следующим образом:

  • Биты 6-7 - Mod
  • Биты 3-5 - Reg/Opcode (пока что я буду называть это поле просто Reg)
  • Биты 0-2 - R/M

Здесь поля Reg и R/M обычно служат для указания операндов. Помня вышеизложенное, можно легко составить заклинание - в Reg помещаем номер первого регистра, в R/M - номер второго. А в Mod помещаем 3 (почему, пока говорить не буду, ибо вы можете не выдержать обрушившегося на вас знания и цифровые духи будут пировать на ваших костях).

Для удобства ниже даны номера регистров в двоичном формате:

  • EAX - 000b
  • ECX - 001b
  • EDX - 010b
  • EBX - 011b
  • ESP - 100b
  • EBP - 101b
  • ESI - 110b
  • EDI - 111b

А 3, которое помещается в поле Mod, в двоичной системе счисления равна 11b. Вот и само заклинание для MOV EAX,EDX:

Код (Text):
  1.  
  2.         db      08Bh,11000010b

Первый байт (08Bh) - это опкод. Второй байт можно логически разделить так: 11(Mod)-000(Reg=EAX)-010(R/M=EDX).

Теперь рассмотрим инструкцию "ADD регистр,регистр". Её базовый опкод равен 03h и она повинуется только что рассмотренному правилу, поэтому мы можем легко составить заклинание, например, для ADD ECX,EBX:

Код (Text):
  1.  
  2.         db      03h,11001011b ; 03h(Опкод), 11(Mod)-001(Reg=ECX)-011(R/M=EBX)

Теперь давайте составим заклинание для следующей программы:

Код (Text):
  1.  
  2.         mov     eax,10
  3.         mov     edx,15
  4.         add     eax,edx

В результате в eax должно получиться 25. Вот текст такой программы для тех, кто пока не сумел ввести его сам:

Код (Text):
  1.  
  2. format PE console
  3. entry start
  4.  
  5. include '..\..\include\kernel.inc'
  6. include '..\..\include\user.inc'
  7. include '..\..\include\macro\stdcall.inc'
  8. include '..\..\include\macro\import.inc'
  9.  
  10. section '.data' data writeable readable
  11.  
  12. _d      db '%d',0
  13.  
  14. section '.code' code executable readable
  15.  
  16. start:
  17.  
  18.         db      0B8h            ; mov eax, 10
  19.         dd      10
  20.         db      0BAh            ; mov edx, 15
  21.         dd      15
  22.         db      3,11000010b     ; add eax, edx
  23.  
  24.         push    eax
  25.         push    _d
  26.         call    [printf]
  27.         add     esp,8
  28.  
  29.         invoke  ExitProcess,0
  30.  
  31. section '.idata' import data readable writeable
  32.  
  33.                 library kernel32,'kernel32.dll',\
  34.                         msvcrt,'msvcrt.dll'
  35. kernel32:       import  ExitProcess,'ExitProcess'
  36. msvcrt:         import  printf,'printf'

Если вы внимательно читали то, о чём говорилось в предыдущей главе, то вспомните, что PUSH EAX можно заменить на 50h. А как заменить PUSH _d? _d - это имя (оно же адрес) переменной, то есть 32-битное число. Опкод "PUSH число" 68h, поэтому соответствующее заклинание такое:

Код (Text):
  1.  
  2.         db      68h
  3.         dd      _d

ADD ESP,8 заколдовать тоже несложно. Базовый опкод "ADD регистр,число" 81h. Однако для этой инструкции не применимы заклинания, которые мы творили ранее, ведь здесь операндами выступают не два регистра, а только один регистр и число-константа. Как же быть? Очень просто: в Reg записывается 0, а в R/M - номер регистра. В Mod, как и ранее, помещаем 11b, а число должно идти сразу после опкода и байта ModR/M. В итоге получается следующая программа:

Код (Text):
  1.  
  2. format PE console
  3. entry start
  4.  
  5. include '..\..\include\kernel.inc'
  6. include '..\..\include\user.inc'
  7. include '..\..\include\macro\stdcall.inc'
  8. include '..\..\include\macro\import.inc'
  9.  
  10. section '.data' data writeable readable
  11.  
  12. _d      db '%d',0
  13.  
  14. section '.code' code executable readable
  15.  
  16. start:
  17.  
  18.         db      0B8h            ; mov eax, 10
  19.         dd      10
  20.         db      0BAh            ; mov edx, 15
  21.         dd      15
  22.         db      3,11000010b     ; add eax, edx
  23.  
  24.         db      50h             ; push eax
  25.         db      68h             ; push _d
  26.         dd      _d
  27.  
  28.         call    [printf]
  29.  
  30.         db      81h,11000100b   ; add esp, 8
  31.         dd      8
  32.  
  33.         invoke  ExitProcess,0
  34.  
  35. section '.idata' import data readable writeable
  36.  
  37.                 library kernel32,'kernel32.dll',\
  38.                         msvcrt,'msvcrt.dll'
  39. kernel32:       import  ExitProcess,'ExitProcess'
  40. msvcrt:         import  printf,'printf'

Надеюсь, вы усвоили материал данной главы. При необходимости проведите курс медитаций или (в крайнем случае) пишите мне по адресу aquila@wasm.ru.

В следующей главе я окончу свой рассказ о байте ModR/M и расскажу о секретах еще одного байта - SIB. © Aquila / WASM.RU


0 1.058
archive

archive
New Member

Регистрация:
27 фев 2017
Публикаций:
532