Logo Море(!) аналитической информации!
IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware
Скидка до 20% на услуги дата-центра. Аренда серверной стойки. Colocation от 1U!

Миграция в облако #SotelCloud. Виртуальный сервер в облаке. Выбрать конфигурацию на сайте!

Виртуальная АТС для вашего бизнеса. Приветственные бонусы для новых клиентов!

Виртуальные VPS серверы в РФ и ЕС

Dedicated серверы в РФ и ЕС

По промокоду CITFORUM скидка 30% на заказ VPS\VDS

VPS/VDS серверы. 30 локаций на выбор

Серверы VPS/VDS с большим диском

Хорошие условия для реселлеров

4VPS.SU - VPS в 17-ти странах

2Gbit/s безлимит

Современное железо!

Книги: [Классика] [Базы данных] [Internet/WWW] [Сети] [Программирование] [UNIX] [Windows] [Безопасность] [Графика] [Software Engineering] [ERP-системы] [Hardware]
     

Секреты поваров компьютерной кухни или ПК: решение проблем

Касперски К.

Издано: 2003, BHV
Твердый переплет, 560 стр..

Аннотация
Статьи из книги

[Заказать книгу в магазине "Мистраль"]

Структура команд Intel 80x86

(рабочий фрагмент главы из второго издания "Фундаментальных основ хакерства")

Современные ассемблеры, абстрагируясь от несущественных деталей архитектуры компьютера, значительно упрощают процесс своего освоения и написания низкоуровневых программ, скрывая истинный формат машинных команд процессора. Если кто-то считает программирование на ассемблере "прямым" общением с компьютером, то он ошибается. Ассемблер - это язык высокого уровня (пускай и самый низкоуровневый среди остальных высокоуровневых языков).

Программируя непосредственно в машинных кодах, можно создавать чрезвычайно хитрый код, умело сопротивляющийся всяким попыткам его отладить или "прожевать" дизассемблером. Соответственно и изучить такой код будет можно лишь разобравшись в нем "вручную". Знаний одних лишь опкодов машинных команд, подчеркнутых из технической документации от разработчиков процессоров, может оказаться недостаточно. Документация не раскрывает всех тонкостей интерпретации команд микропроцессором и особенностей его поведения.

Наконец, владение машинными командами как нельзя кстати пригодится для написания собственных дизассемблеров, отладчиков и другого хакерского инструментария.

Структура машинных инструкций микропроцессов серии Intel 80x86 в общем случае выглядит как показано на рис. 1.5. Давайте рассмотрим его повнимательнее.

Рис. 1.5 Структура машинных инструкций микропроцессоров серии Intel 80x86

Префиксы

В начале каждой команды может располагаться до четырех префиксов (от лат. pre – впереди и fix – прикреплений) - особых однобайтовых кодов, уточняющих поведение команды, - а может не быть ни одного - тогда команда ведет себя "по умолчанию".

Условно префиксы делятся на четыре группы:

  1. 1. Префиксы блокировки и повторения:
    1. 0xF0 LOCK
    2. 0хF2 REPNZ (только для строковых инструкций).
    3. 0xF3 REP (только для строковых инструкций).
  2. 2. Префиксы переопределения сегмента:
    1. 0х2E CS:
    2. 0х36 SS:
    3. 0х3E DS:
    4. 0х26 ES:
    5. 0х64 FS:
    6. 0х65 GS:
  3. 3. Префикс переопределения размеров операндов:
    1. 0х66
  4. 4. Префикс переопределения размеров адреса:
    1. 0х67

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

Префиксы блокировки и повторения

Префикс "LOCK" указывает процессору на необходимость монопольного захвата шины на время выполнения следующей машинной команды. Это позволяет синхронизовать обращения к памяти, предотвращая одновременный доступ нескольких процессоров (или устройств - контроллера DMA например) к одной ячейке памяти.

Префикс LOCK функционирует исключительно со следующими командами: ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD и XCHG, причем один из них операндов должен непременно находится в памяти. Во всех остальных случаях процессор генерирует исключение #UD (INVALID_OPCODE). Это обстоятельство может использоваться разработчикам защитных механизмов, для "завуалированного" вызова обработчика соответствующего исключения. В обработчик же можно поместить "секретную" процедуру, скрыто выполняющую некоторые ответственные действия (например, сверяющую серийный номер). Конечно, зная, что LOCK вызывает исключение, можно найти его обработчик, а найдя - проанализировать, но идентификация структурных исключений не такое простое занятие и оно "по зубам" далеко не всем хакерам.

Замечание: Операционная система Windows 2000 содержит ошибку - при встрече с некорректным LOCK функция GetExceptionCode возвращает не STATUS_ILLEGAL_INSTRUCTION (код C000001D) - как того следовало бы ожидать, а никому не известный код C000001E, определение которого в заголовочных файлах SDK вообще отсутствует! Поэтому, разработчикам защит следует "вручную" обрабатывать такую ситуацию.

Дополнение: Как вы думаете, что произойдет если указать несколько префиксов LOCK перед одной командой? Документация от Intel об этой тонкости умалчивает, ссылаясь на неопределенное поведение, однако, тривиальный эксперимент убеждает, что несколько префиксов LOCK работают точно так, как один.


main()
{
  static char s0[]="Hello, World!\n";
  static char s1[]="Hello, Sailor!\n";
  struct exception_pointers *zzz;
  int eip;

  hakamaka()
  {
    printf(s0);
    __asm{lock nop}
    printf(s1);
  }

  main()
  {
    struct exception_pointers *zzz;
    int eip;
    __try{  hakamaka();}

    __except((zzz=_exception_info(),
      eip=zzz->ContextRecord->Eip,
      zzz->ContextRecord->Eip=++eip,
      strcpy(s1,"Hello, Hacker!\n"))?-1:-1){}
}

Листинг 44 Использование префикса LOCK для вызова структурного исключения

Результат дизассемблирования файла, сгенерированного компилятором Microsoft Visual C++ 6.0 в режиме максимальной оптимизации будет выглядеть так (ниже приведен лишь ключевой фрагмент):


hakamaka  proc near    ; CODE XREF: main+2Dp
  push  offset aHelloWorld ; "Hello, World!\n"
  call  _printf
  ; printf("Hello, World!\n");

  lock nop
  ; При беглом просмотре листинга эту "фишку"
  ; можно и пропустить. Ну lock nop ну и что? А не
  ; захочет выполнять процессор эту инструкцию и
  ; сгенерирует исключение. Но даже если на LOCK NOP
  ; и обратить внимание – попробуй-ка, разберись какой
  ; код получит управление! Идентификация обработчиков
  ; структурных исключений чрезвычайной сложна и уже
  ; одна она представляет неслабую головоломку,
  ; посильную только умудренным кодокопателям

  push  offset aHelloSailor ; "Hello, Sailor!\n"
  call  _printf
  add  esp, 8
  ; Кажется, что данный код выводит строку
  ; printf("Hello, Sailor!\n"), но это не так!
  ; Префикс LOCK вызывая обработчик структурного
  ; исключения, передает ему "бразды правления" и
  ; не факт, что он "захочет" возвратить управление.
  ; В данном случае управление все же возвращается
  ; на команду, следующую за LOCK, но предварительно
  ; строка "Hello, Sailor!" замещается строкой
  ; "Hello, Hacker!"
  ; А в реальных защитах таким образом скрыто
  ; выполняется расшифровка кода или другие критичные
  ; к раскрытию манипуляции.

  retn
hakamaka  endp

Листинг 45 Фрагмент дизассемблированной защиты, основанной на префиксе LOOK

Префиксы повторений

Два префикса повторений REP (синонимы - REPE, REPZ) и REPNE (синоним - REPNZ) повторяют следующую за ними строковую инструкцию ECX [CX] раз, каждый раз уменьшая ECX [CX] на единицу, но не выставляя флаг нуля (ZF) при обращении ECX [CX] в нуль, - вернее, префиксы повторения вообще не воздействуют на флаги.

Префиксы повторений могут функционировать лишь со следующими командами: INS, MOVS, OUTS, LOADS, STOS, CMPS и SCAS, причем, проверка условия продолжения цикла осуществляется только с двумя командами – CMPS и SCAS. Т. е. "REPNE LOADS" работает аналогично "REPE LOADS" и независимо от рода считываемых данных всегда повторяется ECX [CX] раз.

Когда же префиксы REPE или REPNE используется не со строковыми инструкциями, они выполняют следующую за ней команду всего лишь раз, не уменьшая при этом содержимого регистра ECX [CX]! Так, например, на первый взгляд следующий код "REP SHL EAX, CL" должен сдвинуть содержимое EAX на бит влево, однако же на самом деле сдвиг равен CL бит! бит влево, однако же на самом деле сдвиг равен CL бит!

Префиксы переопределения сегмента

Префиксы переопределения сегмента указывают какой именно сегментной регистр следует использовать для обращения к ячейке памяти. Префиксы переопределения сегмента применимы только к командам явно обращающимся к памяти, - т. е. имеющими адресное поле mod, содержимое которого не равно "11", а в остальных случаях они игнорируются. Так, конструкции "DS:REP" и "CS:PUSH AX" означают то же самое, что и "REP" и "PUSH EAX", хотя начинающие хакеры могут предположить, что префикс "DS" заставляет инструкцию REP искать адрес возврата по указателю "DS:ESP", а "PUSH", соответственно, заталкивать EAX по адресу CS:ESP-4. Такой трюк, действительно, встречается в некоторых защитных механизмах, создатели которых стремились сбить взломщиков с толку.

Другой примем - использование нескольких префиксов с одной командной, например, "DS:FS:FG:CS:MOV AX,[100]". Микропроцессоры Intel и AMD используют для адресации ячейки последний префикс в цепочке (в данном случае CS), однако их разработчики этого не гарантируют (официально, как сказано выше, использование двух и более префиксов одной и той же категории запрещено). Вполне возможно, что следующие модели процессоров будут при встрече такой конструкции выбрасывать исключение или обрабатывать ее по-иному, поэтому, крайне не рекомендуется использовать этот трюк в собственных защитах.

Замечание: дизассемблер IDA Pro вплоть до версии 4.17 включительно не "переваривает" множественные префиксы и вряд ли начнет "переваривать" их в последующих версиях.

В частности, "CS:DS:ES:MOV [EAX], 0x666" IDA Pro дизассемблирует так:

seg000:00000100  db      2Eh, 3Eh
;                ; Это ^^^^^^^^^^
                    ; префиксы CS и DS соответственно
seg000:00000100  mov     dword ptr es:[eax], 66666666h
;                                                 ^^
; IDA Pro извлекла последний префикс в цепочке
; и использовала его для адресации

Листинг 46 Использование множественных префиксов сбивает дизассемблер с толку

Вообще-то, это не создает никаких проблем, ибо в "высшем смысле" IDA Pro поступает правильно, т.к. действительно процессор "съедает" только последний префикс в цепочке, а остальные - игнорирует. Просто, не надо бояться, увидев в коде странные "DB", - правда убедитесь, что это именно префиксы, а не что-то другое!

Префикс переопределения размера операндов

Префикс переопределения размера операндов работает как триггер - если он встречается в 32-разрядном коде, то процессор интерпретирует операнды следующий за ним инструкции как 16-разнядные и, соответственно, наоборот.

Дело в том, что схема адресации, принятая в микропроцессорах серии Intel 80x86 не позволяет отличать 16- и 32- разрядные регистры: EAX кодируется точно как и его "младший брат" - AX. Не стоит кидать камни в разработчиков и обвинять Intel в "кривизне". Во-первых, такая ситуация вызвана требованием обратной совместимости, а во-вторых, в адресных полях битам и так тесно, как селедкам в бочке и ни одной свободной комбинации нулей и единичек для кодирования разрядности операндов там нет. К тому же, 386+ микропроцессоры оптимизированные именно на работу с 32-разрядными данными и 32-разрядным кодом: 16-разрядные операнды в 32-разрядном режиме или 16-разрядный режим сегодня - почтенное достояние старины. Поэтому, использование префикса для выбора разрядности операндов - вполне оправданно.

Префикс переопределения размера работает и с командами, имеющие неявные операнды. Рассмотрим, как работает конструкция "66:RETN". Казалось бы, коль инструкция RETN не имеет операндов, то префикс 66 можно игнорировать. На самом деле RETN работает с неявным операндом и, судя, по распечатке микрокода работает она так:


IF instruction = near return
 THEN;
  IF OperandSize = 32
   THEN
    IF top 12 bytes of stack not within stack limits
     THEN #SS(0); FI;
    EIPPop();
   ELSE (* OperandSize = 16 *)
    IF top 6 bytes of stack not within stack limits
     THEN #SS(0) FI;
    tempEIPPop();
    tempEIPtempEIP AND 0000FFFFH;
    IF tempEIP not within code segment limits
     THEN #GP(0); FI;
    EIPtempEIP;
 FI;

FI

Листинг 47 Псевдокод, раскрывающий алгоритм работы машинной команды RETN

Ага, говорим мы голос Тиггера вдруг вышедшего на свет из чащи чертополоха, - если это не ошибка разработчиков процессора, то классная фишка! Правда, в среде 32-разрядного кода она уже не такая и классная, какой была под MS-DOS, но все по порядку.

В реальном и 16-разрядном режиме указатель команд обрезается до 16 бит, поэтому на первый взгляд 66:RET сработает корректно. Корректно-то, оно корректно, да не совсем - стек ведь оказывается несбалансированным! Из него вместо одного слова RET снимает целых два! Так нетрудно получить и исключение 0xC - исчерпание стека. Если это сделать умышленно, поручив обработку исключения нечто полезное, то такое приложение не удаться отладить ни Soft-Ice, ни даже эмулирующим отладчиком cup386! Остальные отладчики так же либо виснут, либо совершают очень далекий переход по EIP и, попав в "космос", так же виснут, либо снимают одно слово и не вызывают ни исключения ни его обработчика, а если этот обработчик расшифровывает некоторые части программы, то под отладчиком они остаются зашифрованными и все вновь виснет. В общем труба

В 32-разрядном режиме префикс 66h приводит к переходу по адресу (word ptr SS:[ESP]), со снятием одного слова из стека вместо двух. А в Windows NT первые 64 килобайта адресного пространства закрыты для обращения и при попытке исполнения первой же команды в этом регионе тут же вылетит знаменитое "access violation". В принципе, его можно перехватить и поручить обработчику нечто полезное, но - один нюанс - в Windows 9x закрыты лишь первые 4 Кб адресного пространства, а в остальные можно читать, писать и даже исполнять в них свой код (правда, Microsoft категорически не приветствует это делать), поэтому, необходимо удостовериться, что биты 815 адреса возврата равны нулю. Это ограничивает свободу разработчика защитного механизма, кроме того, слишком заметно, - потому, практически никем не используется.

Рассмотрим следующий пример:


  func();
  main()
  {
    func();
  }

  .586
  .MODEL FLAT
  .CODE
  _func proc
    DB 66h
    RET
  _func endp
  END

Листинг 48 Пример защиты, основанный на некорректной обработке инструкции возврата

Результат его прогона отладчиком "SED", запущенным под управлением Windows NT должен выглядеть приблизительно так:

F:\.PHCK\src>debugprocess disasm.66ret.exe
SED - Simple Experimental Debuger By KPNC
Exception "STATUS ACCESS VIOLATION" (C0000005) @01028h
CONTINUABLE EXCEPTION ARISE! TERMINATE...

Если префикс переопределения размера стоит перед инструкцией, не имеющей операндов, то он игнорируется. Некоторые дизассемблеры, в том числе и IDA Pro не дизассемблируют такие префиксы, оставляя их в виде "сырого" дампа. Например:"0x66:CLI" будет дизассемблировано так:

  seg000:0100                 db      66h 
  seg000:0100                 cli 

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

Префикс переопределения размеров адреса

Префикс переопределения размеров адреса очень похож на префикс переопределения размеров операнда, за тем исключением, что указывает на длину адресного поля, а не операнда.

Например, если команду 32-разрядного режима "MOV [0x00000666], AX" записать как "DB 67;MOV [0x0666], AX" ее адресное поле сократится с 4-х до 2-х байт, и, учитывая расход одного байта на префикс, мы выиграем один байт. Правда, значительно проиграем в скорости, ибо код с префиксом исполняется по крайней мере на один такт медленнее - так стоит ли игра свеч?

Другое дело - 16-разрядный режим. Если войти в protect mode, увеличить лимиты размера сегментов и, "забыв" восстановить их, вернуться обратно в real-mode, мы сможем адресовать все 4 гигабайта оперативной памяти любым из сегментных регистров! Правда, это, так называемый, "unreal mode" особой популярности не сыскал, ибо требовал девственно - чистой MS-DOS без EMS-драйверов, скрытно переводящий процессор в защищенный режим, и запускающих DOS на виртуальной машине под строгим надзором драйвера. Сами же EMS-драйвера требовались многим программам и отказ от их использования был в большинстве случаев невозможен.

Ограничения количества префиксов

В документации Intel встречается упоминание об ограничении количества префиксов на команду - официально их должно быть не более четырех. Что ж, это стыкуется с другим ограничением - не более одного префикса из каждой группы. А групп всего четыре - стало быть и префиксов не должно быть более четырех. На самом же деле, как мы уже могли убедиться, использование двух и более префиксов из одной и той же группы вполне допустимо (хотя и не рекомендуется для критически ответственных программ), но если ограничение на это "больше" или количество префиксов может достигать и миллиона?

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

Разумеется, это обстоятельство можно использовать для преднамеренного вызова обработчика исключения.

Опкод

О поле "опкод" официальная документация не сообщает практически ничего - говориться лишь что оно занимает от одного до двух байт, а при необходимости "оттяпывает" и три бита поля "Reg/Opcode". И все? Не густо. И это при том, что коды машинных инструкций имеют вполне определенную, регулярную структуру, пускай со сложным сводом правил и исключений но все же Хотя бы из чистого любопытства можно провести сравнительный анализ и выявить общие закономерности.

Ну, вот например, взгляните на разновидности инструкции ADD (имеются ввиду разновидности опкода, а не полей адресации):

Таблица 1 Под инструкцией ADD "прячется" отнюдь не одна машинная команда
инструкция опкод инструкция опкод

ADD AL, imm8

04

ADD AL, 66h

04 66

ADD AX, immD

05

ADD AX, 666h

05 66 06

ADD r/m8, r8

00

ADD DL, CL

00 CA

ADD r/md,rd

01

ADD EDX, ECX

01 CA

ADD r8, r/m8

02

ADD CL, DL

02 CA

ADD rd, r/md

03

ADD ECX, EDX

03 CA

ADD r/m8,imm8

80 /0

ADD byte ptr [DX], 66

80 00000001 66

ADD r/md,immd

81 /0

ADD dword prt [EDX], 666

81 00000001 66060000

ADD r/md,imm8

83 /0

ADD byte ptr [EDX], 66

83 00000001 66

Если расписать таким же образом и остальные команды, а затем проанализировать их, то обнаружатся некоторые закономерности:

  1. если размер операнда инструкции - 8 бит, то ее опкод всегда четный, т.е. младший бит сброшен, напротив, если размер операнда соответствует разрядности сегмента, то младший бит равен единице, - значит, младший бит - есть бит размера операндов!
  2. если инструкция имеет два операнда, ни один из которых не является непосредственным значением, то возникает естественная необходимость уточнить какой из них операнд-источник, а какой - операнд-приемник. (В случае непосредственного значения все вопросы отпадают - операнд, представленный непосредственным значением, может быть только источником). Легко видеть, что порядок операндов задается не только полем адресации, но и самим опкодом. Сравните: ADD r/m8, r8 [опкод – 00] и ADDD r8, r/m8 [опкод - 02]. Похоже, что первый, считая от нуля, бит - есть бит порядка операндов и это предположение подтверждается на остальных инструкциях!

Сказанное, однако, справедливо не для всех команд процессора, но верно для некоторого контингента, который не так уж и мал: ADD, ADC, AND, XOR, OR, SUB, SBB, CMP и др.

 Вообще-то, данная функция не входит в библиотеку стандартного Cи, но может быть реализована самостоятельно, путем создания собственного менеджера динамической памяти.

Аннотация
Статьи из книги

[Заказать книгу в магазине "Мистраль"]

Бесплатный конструктор сайтов и Landing Page

Хостинг с DDoS защитой от 2.5$ + Бесплатный SSL и Домен

SSD VPS в Нидерландах под различные задачи от 2.6$

✅ Дешевый VPS-хостинг на AMD EPYC: 1vCore, 3GB DDR4, 15GB NVMe всего за €3,50!

🔥 Anti-DDoS защита 12 Тбит/с!

VPS в России, Европе и США

Бесплатная поддержка и администрирование

Оплата российскими и международными картами

🔥 VPS до 5.7 ГГц под любые задачи с AntiDDoS в 7 локациях

💸 Гифткод CITFORUM (250р на баланс) и попробуйте уже сейчас!

🛒 Скидка 15% на первый платеж (в течение 24ч)

Новости мира IT:

Архив новостей

IT-консалтинг Software Engineering Программирование СУБД Безопасность Internet Сети Операционные системы Hardware

Информация для рекламодателей PR-акции, размещение рекламы — adv@citforum.ru,
тел. +7 495 7861149
Пресс-релизы — pr@citforum.ru
Обратная связь
Информация для авторов
Rambler's Top100 TopList This Web server launched on February 24, 1997
Copyright © 1997-2000 CIT, © 2001-2019 CIT Forum
Внимание! Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав. Подробнее...