Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Physical Address
304 North Cardinal St.
Dorchester Center, MA 02124
Когда мы занимаемся анализом ломаемой программы, пытаясь восстановить алгоритм ее работы, нам нужно определить типы операндов ассемблерных инструкций. Для этого есть несколько простых правил. Между тем среди операндов присутствуют константы и смещения, которые внешне очень похожи, но в то же время сильно различаются по способам и целям взаимодействия. Поэтому важно отделить одно от другого, так как такие «игры» — один из главных инструментов разработчиков защит.
Фундаментальные основы хакерства
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
Микропроцессоры серии 80×86 поддерживают операнды трех типов: регистр, непосредственное значение, непосредственный указатель. Тип операнда явно задается в специальном поле машинной инструкции, именуемом mod
, поэтому никаких проблем в идентификации типов операндов не возникает. Регистр — ну, все мы знаем, как выглядят регистры; указатель по общепринятому соглашению заключается в квадратные скобки, а непосредственное значение записывается без них. Например:
MOV ECX, EAX; ← регистровые операндыMOV ECX, 0x666; ← левый операнд регистровый, правый — непосредственныйMOV [0x401020], EAX; ← левый операнд — указатель, правый — регистр
Кроме этого, микропроцессоры серии 80×86 поддерживают два вида адресации памяти: непосредственную и косвенную. Тип адресации определяется типом указателя. Если операнд — непосредственный указатель, то и адресация непосредственна. Если же операнд‑указатель — регистр, то такая адресация называется косвенной. Например:
MOV ECX,[0x401020] ← непосредственная адресацияMOV ECX, [EAX] ← косвенная адресация
Для инициализации регистрового указателя разработчики микропроцессора ввели специальную команду, вычисляющую значение адресного выражения addr
и присваивающую его регистру REG
, — LEA REG, [addr]
. Например:
LEA EAX, [0x401020] ; Регистру EAX присваивается значение указателя 0x401020MOV ECX, [EAX] ; Косвенная адресация — загрузка в ECX двойного слова, ; расположенного по смещению 0x401020
Правый операнд команды LEA
всегда представляет собой ближний (near) указатель (исключение составляют случаи использования LEA
для сложения констант — подробнее об этом см. в одноименном пункте). И все было бы хорошо… да вот, оказывается, внутреннее представление ближнего указателя эквивалентно константе того же значения. Отсюда LEA EAX, [0x401020]
равносильно MOV EAX, 0x401020
. В силу определенных причин MOV
значительно обогнал в популярности LEA
, практически вытеснив последнюю инструкцию из употребления.
Отказ от LEA
породил фундаментальную проблему ассемблирования — проблему OFFSET’a. В общих чертах ее суть заключается в синтаксической неразличимости констант и смещений (ближних указателей). Конструкция MOV EAX, 0x401020
может грузить в EAX
и константу, равную 0x401020
(пример соответствующего C-кода: a=0x401020
), и указатель на ячейку памяти, расположенную по смещению 0x401020
(пример соответствующего C-кода: a=&x
). Согласись, a=0x401020
совсем не одно и то же, что a=&x
! А теперь представь, что произойдет, если в повторно ассемблированной программе переменная х
окажется расположена по иному смещению, а не 0x401020
? Правильно — программа рухнет, ибо указатель a
по‑прежнему указывает на ячейку памяти 0x401020
, но здесь теперь «проживает» совсем другая переменная!
Почему переменная может изменить свое смещение? Основных причин тому две. Во‑первых, язык ассемблера неоднозначен и допускает двоякую интерпретацию. Например, конструкции ADD EAX, 0x66
соответствуют две машинные инструкции: 83 C0 66
и 05 66 00 00 00
длиной три и пять байт соответственно. Транслятор может выбрать любую из них, и не факт, что ту же самую, которая была в исходной программе (до дизассемблирования). Неверно «угаданный» размер вызовет смещение всех остальных инструкций, а вместе с ними и данных. Во‑вторых, смещение не замедлит вызвать модификацию программы (разумеется, речь идет не о замене JZ
на JNZ
, а о настоящей адаптации или модернизации), и все указатели тут же «посыплются».
Вернуть работоспособность программы помогает директива offset
. Если MOV EAX, 0x401020
действительно загружает в EAX
указатель, а не константу, по смещению 0x401020
следует создать метку, именуемую, скажем, loc_401020
. Также нужно MOV EAX, 0x401020
заменить на MOV EAX, offset loc_401020
. Теперь указатель EAX
связан не с фиксированным смещением, а с меткой!
А что произойдет, если предварить директивой offset
константу, ошибочно приняв ее за указатель? Программа откажет или станет работать некорректно. Допустим, число 0x401020
выражало собой объем бассейна, в который вода втекает через одну трубу, а вытекает через другую. Если заменить константу указателем, то объем бассейна станет равен… смещению метки в заново ассемблированной программе и все расчеты полетят к черту.
Таким образом, очень важно определить типы всех непосредственных операндов, и еще важнее определить их правильно. Одна ошибка может стоить программе жизни (в смысле работоспособности), а в типичной программе тысячи и десятки тысяч операндов!
Отсюда возникает два вопроса:
Типы операндов
Источник: xakep.ru