Хакер - Фундаментальные основы хакерства. Ищем операнды при взломе программ
hacker_frei
Крис Касперски, Юрий Язев
Содержание статьи
- Идентификация констант и смещений
- Определение типа непосредственного операнда
- Сложные случаи адресации или математические операции с указателями
- Порядок индексов и указателей
- Использование LEA для сложения констант
- Заключение
Когда мы занимаемся анализом ломаемой программы, пытаясь восстановить алгоритм ее работы, нам нужно определить типы операндов ассемблерных инструкций. Для этого есть несколько простых правил. Между тем среди операндов присутствуют константы и смещения, которые внешне очень похожи, но в то же время сильно различаются по способам и целям взаимодействия. Поэтому важно отделить одно от другого, так как такие «игры» — один из главных инструментов разработчиков защит.
Фундаментальные основы хакерства
Пятнадцать лет назад эпический труд Криса Касперски «Фундаментальные основы хакерства» был настольной книгой каждого начинающего исследователя в области компьютерной безопасности. Однако время идет, и знания, опубликованные Крисом, теряют актуальность. Редакторы «Хакера» попытались обновить этот объемный труд и перенести его из времен Windows 2000 и Visual Studio 6.0 во времена Windows 10 и Visual Studio 2019.
Ссылки на другие статьи из этого цикла ищи на странице автора.
ИДЕНТИФИКАЦИЯ КОНСТАНТ И СМЕЩЕНИЙ
Микропроцессоры серии 80x86 поддерживают операнды трех типов: регистр, непосредственное значение, непосредственный указатель. Тип операнда явно задается в специальном поле машинной инструкции, именуемом mod
, поэтому никаких проблем в идентификации типов операндов не возникает. Регистр — ну, все мы знаем, как выглядят регистры; указатель по общепринятому соглашению заключается в квадратные скобки, а непосредственное значение записывается без них. Например:
MOV ECX, EAX; ← регистровые операнды
MOV ECX, 0x666; ← левый операнд регистровый, правый — непосредственный
MOV [0x401020], EAX; ← левый операнд — указатель, правый — регистр
Кроме этого, микропроцессоры серии 80x86 поддерживают два вида адресации памяти: непосредственную и косвенную. Тип адресации определяется типом указателя. Если операнд — непосредственный указатель, то и адресация непосредственна. Если же операнд‑указатель — регистр, то такая адресация называется косвенной. Например:
MOV ECX,[0x401020] ← непосредственная адресация
MOV ECX, [EAX] ← косвенная адресация
Для инициализации регистрового указателя разработчики микропроцессора ввели специальную команду, вычисляющую значение адресного выражения addr
и присваивающую его регистру REG
, — LEA REG, [addr]
. Например:
LEA EAX, [0x401020] ; Регистру EAX присваивается значение указателя 0x401020
MOV 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
выражало собой объем бассейна, в который вода втекает через одну трубу, а вытекает через другую. Если заменить константу указателем, то объем бассейна станет равен... смещению метки в заново ассемблированной программе и все расчеты полетят к черту.
Таким образом, очень важно определить типы всех непосредственных операндов, и еще важнее определить их правильно. Одна ошибка может стоить программе жизни (в смысле работоспособности), а в типичной программе тысячи и десятки тысяч операндов!
Отсюда возникает два вопроса:
- Как вообще определяют типы операндов?
- Можно ли их определять автоматически (или на худой конец хотя бы полуавтоматически)?
Определение типа непосредственного операнда
Непосредственный операнд команды LEA
всегда указатель (исключение составляют ассемблерные «извращения»: чтобы сбить хакеров с толку, в некоторых защитах LEA
используются для загрузки константы).
Непосредственные операнды команд MOV
и PUSH
могут быть как константами, так и указателями. Чтобы определить тип непосредственного операнда, необходимо проанализировать, как используется его значение в программе. Для косвенной адресации памяти — это указатель, в противном случае — константа.
Например, мы встретили в тексте программы команду MOV EAX, 0x401020
. Что это такое: константа или указатель?
Ответ на вопрос дает строка MOV ECX, [EAX]
, подсказывающая, что значение 0x401020
используется для косвенной адресации памяти. Следовательно, непосредственный операнд не что иное, как указатель.
Существует два типа указателей — указатели на данные и указатели на функцию. Указатели на данные используются для извлечения значения ячейки памяти и встречаются в арифметических командах и командах пересылки (например, MOV
, ADD
, SUB
). Указатели на функцию используются в командах косвенного вызова и реже в командах косвенного перехода — CALL
и JMP
соответственно.
Следующий пример (const_pointers_cb
) откомпилируй с помощью C++Builder. В нем мы изучим разницу между константами и указателями:
int _tmain(int argc, _TCHAR* argv[])
{
static int a = 0x777;
int* b = &a;
int c = b[0];
}
Результат компиляции должен выглядеть приблизительно так:
main proc near
var_1C = dword ptr -1Ch
var_18 = qword ptr -18h
var_10 = qword ptr -10h
var_8 = dword ptr -8
var_4 = dword ptr -4
; Открытие кадра стека
push rbp
; Выделение 0x20 байт для локальных переменных
sub rsp, 20h
; Кадр стека указывает на дно стека
lea rbp, [rsp+20h]
Название смещения unk_451110
говорит о том, что значение по адресу 451110
имеет неопределенный тип.
Перейдем по нему и посмотрим, что там находится.
Так как число 0x777
не умещается в одном байте, компилятор разместил его в двух байтах. Следовательно, в RAX
помещается ссылка на это число.
lea rax, unk_451110
Хотя IDA представила данные программы так, как их приготовил компилятор, мы уже самостоятельно определили, что по смещению unk_451110
находится число, занимающее больше одного байта. Поэтому мы можем помочь IDA правильно отобразить данные. Для этого, перейдя по смещению, надо нажать клавишу с английской o, что соответствует команде: Edit → Operand Type → Offset → Offset (data segment). В результате смещение будет переименовано, а значение, на которое оно указывает, примет благородный вид: 777h
. Кроме того, команда преобразования неопределенных байтов в данные с db
(один байт) изменится на dq
(восемь байт).
Инициализация локальных переменных:
mov [rbp+var_4], 0
mov [rbp+var_8], ecx
mov [rbp+var_10], rdx
В RAX
расположена ссылка на значение, она копируется в переменную var_18
. Поскольку значение по ссылке unk_451110
находится в сегменте данных, можно сделать вывод, что var_18
— статическая переменная.
mov [rbp+var_18], rax
Копируем ссылку на переменную в памяти и таким образом получаем возможность изменить ссылку, но не значение.
mov rax, [rbp+var_18]
Загружаем содержимое локальной переменной var_18
в регистр ECX
. Отсюда можно сделать вывод, что в RAX
все‑таки указатель. Тогда локальная переменная var_18
тоже указатель!
mov ecx, [rax]
Присваиваем локальной переменной var_1C
значение, содержащееся в ECX
. А там хранится указатель на 0x777
.
mov [rbp+var_1C], ecx
mov [rbp+var_4], 0
; Функция возвращает ноль
mov eax, [rbp+var_4]
; Очищаем стек
add rsp, 20h
; Закрываем кадр стека
pop rbp
retn
main endp
Черт ногу сломит с этими указателями! Теперь рассмотрим пример func_pointers_cb
с косвенным вызовом функции (также скомпилированный с помощью C++Builder):
int func(int a, int b)
{
return a + b;
}
int _tmain(int argc, _TCHAR* argv[])
{
int (*zzz) (int a, int b) = func;
// Вызов функции происходит косвенно — по указателю zzz
zzz(0x666, 0x777);
return 0;
}
Результат компиляции должен выглядеть приблизительно так:
main proc near
var_1C = dword ptr -1Ch
var_18 = qword ptr -18h
var_10 = qword ptr -10h
var_8 = dword ptr -8
var_4 = dword ptr -4
; Открываем кадр стека
push rbp
; Выделяем 0x40 под локальные переменные
sub rsp, 40h
; Указатель кадра стека
lea rbp, [rsp+40h]
; В EAX заносим значение 0x666, пока непонятно для чего, но явно не для передачи
mov eax, 666h
; В R8D заносим значение 0x777
mov r8d, 777h
; Смотри! В R9 заносим указатель на функцию
lea r9, func(int,int)
; Инициализируем локальные переменные
mov [rbp+var_4], 0
mov [rbp+var_8], ecx
mov [rbp+var_10], rdx
; В var_18 помещаем указатель на функцию func
mov [rbp+var_18], r9
; Теперь ECX равна 0x666
mov ecx, eax
; а EDX — 0x777, регистры загружены и готовы для передачи параметров
mov edx, r8d
; Погляди-ка! Косвенный вызов функции!
call [rbp+var_18]
mov [rbp+var_4], 0
mov [rbp+var_1C], eax
mov eax, [rbp+var_4]
; Очищаем стек
add rsp, 40h
; Восстанавливаем регистр
pop rbp
retn
main endp
А вот и косвенно вызываемая функция func
. Исследуем ее, чтобы определить тип передаваемых ей непосредственных значений.
func(int, int) proc near
var_C = dword ptr -0Ch
var_8 = dword ptr -8
var_4 = dword ptr -4
IDA не определила аргументы, но мы‑то знаем, что они есть! Сейчас, когда параметры всегда передаются через регистры, различие между аргументами и локальными переменными — чистая формальность.
; Открываем кадр стека
push rbp
; Выделяем память для содержимого стека
sub rsp, 10h
; Кадр стека указывает на дно стека
lea rbp, [rsp+10h]
Присваиваем значение переменной var_4
, учитывая, что в регистре ECX
передается параметр, var_4
— аргумент:
mov [rbp+var_4], ecx
Присваиваем значение переменной var_8
, учитывая, что в регистре EDX
передается параметр, var_8
— аргумент:
mov [rbp+var_8], edx
; В ECX размещаем первое слагаемое
mov ecx, [rbp+var_4]
; Выполняем сложение с переменной, записывая сумму на место первого слагаемого
add ecx, [rbp+var_8]
; Значение суммы копируем в переменную var_C
mov [rbp+var_C], ecx
; В качестве результата возвращаем сумму
mov eax, [rbp+var_C]
; Удаляем содержимое стека
add rsp, 10h
; Закрываем кадр стека
pop rbp
retn
func(int, int) endp
Сложные случаи адресации или математические операции с указателями
C/C++ и некоторые другие языки программирования допускают выполнение над указателями различных арифметических операций, чем серьезно затрудняют идентификацию типов непосредственных операндов. В самом деле, если бы такие операции с указателями были запрещены, то любая математическая инструкция, манипулирующая с непосредственным операндом, однозначно указывала бы на его константный тип.
К счастью, даже в тех языках, где это разрешено, над указателями выполняется ограниченное число математических операций. Так, совершенно бессмысленно сложение двух указателей, а уж тем более умножение или деление их друг на друга. Вычитание — дело другое. Используя тот факт, что компилятор располагает функции в памяти согласно порядку их объявления в программе, можно вычислить размер функции, отнимая ее указатель от указателя на следующую функцию. Такой трюк встречается в упаковщиках (распаковщиках) исполняемых файлов, защитах с самомодифицирующимся кодом, но в прикладных программах используется редко.
Сказанное выше относилось к случаям «указатель + указатель». Между тем указатель может сочетаться и с константой. Причем такое сочетание настолько популярно, что процессоры серии 80x86 даже поддерживают для этого специальную адресацию — базовую. Пусть, к примеру, имеется указатель на массив и индекс некоторого элемента массива. Очевидно: чтобы получить значение этого элемента, необходимо сложить указатель с индексом, умноженным на размер элемента.
Вычитание константы из указателя встречается гораздо реже: этому соответствует меньший круг задач, и сами программисты избегают вычитания, поскольку оно нередко приводит к серьезным проблемам. Среди новичков популярен следующий прием: если им требуется массив, начинающийся с единицы, они, объявив обычный массив, получают на него указатель и... уменьшают его на единицу! Элегантно, не правда ли?
Но подумай, что произойдет, если указатель на массив будет равен нулю. Правильно, змея укусит свой хвост — указатель станет очень большим положительным числом. Вообще‑то под Windows NT массив гарантированно не может быть размещен по нулевому смещению, но не стоит привыкать к трюкам, привязанным к одной платформе и не работающим на других.
«Нормальные» языки программирования запрещают смешение типов, и правильно делают. Существует и еще одна фундаментальная проблема дизассемблирования — определение типов в комбинированных выражениях. Рассмотрим следующий пример:
MOV EAX, 0x...
MOV EBX, 0x...
ADD EAX, EBX
MOV ECX, [EAX]
Сумма двух непосредственных значений здесь используется для косвенной адресации. Ну, положим, оба они указателями быть не могут, исходя из самых общих соображений. Наверняка одно из непосредственных значений — указатель на массив (структуру данных, объект), а другое — индекс в этом массиве. Для сохранения работоспособности программы указатель необходимо заменить смещением метки, а вот индекс придется оставить без изменений (ведь индекс — это константа).
Как же различить, что есть что? Увы, нет универсального ответа, а в контексте приведенного выше примера это и вовсе невозможно!
Рассмотрим следующий пример, демонстрирующий определение типов в комбинированных выражениях (combined_exp_types
):
void MyFunc(char* a, int i)
{
a[i] = '\n';
a[i + 1] = 0;
}
int main()
{
static char buff[] = "Hello,Sailor!";
MyFunc(&buff[0], 5);
}
Результат компиляции с помощью Microsoft Visual C++ должен выглядеть так:
main proc near
; Выделение памяти для локальных переменных
sub rsp, 28h
; Определяем смещение — указатель на элемент в массиве
mov eax, 1
imul rax, 0
; В регистр RCX загружаем указатель на строку
lea rcx, buff ; "Hello, Sailor!"
add rcx, rax
; Указатель на строку копируется в RAX
mov rax, rcx
; Подготовка параметров:
; в регистр EDX помещается число 5 — второй параметр
mov edx, 5 ; i
; указатель на строку вновь копируется в RCX — первый параметр
mov rcx, rax ; a
; Параметры укомплектованы — вызываем функцию
call MyFunc(char *,int)
; Функция возвращает ноль
xor eax, eax
; Очистка стека
add rsp, 28h
retn
main endp
Мы можем с полной уверенностью сказать, где какой параметр, только в наших искусственных примерах. При изучении чужих программок такой уверенности, к сожалению, не будет. Поэтому, рассматривая параметры в функции ниже, мы, по идее, должны видеть их как два числовых аргумента. И наша задача разобраться, представляют ли они константы или указатели.
void MyFunc(char *, int) proc near
; Два параметра — все верно
arg_0 = qword ptr 8
arg_8 = dword ptr 10h
; Перенос значений параметров из регистров в память
mov [rsp+arg_8], edx ; Число 5
mov [rsp+arg_0], rcx ; Указатель на строку
; Копирование двойного слова со знаком в четверное
movsxd rax, [rsp+arg_8]
; Копирование четверного слова в четверное
mov rcx, [rsp+arg_0]
Сумма непосредственных значений используется для косвенной адресации памяти, значит, это константа и указатель. Но кто есть кто?
mov byte ptr [rcx+rax], 0Ah
Для ответа на этот вопрос нам необходимо понять смысл кода программы — чего же добивался программист сложением указателей? Предположим, что значение 5 — указатель. Логично? Да вот не очень‑то логично: если это указатель, то указатель на что?
Первые 64 килобайта адресного пространства Windows NT заблокированы для «отлавливания» нулевых и неинициализированных указателей. Ясно, что равным пяти указатель быть никак не может, разве что программист использовал какой‑нибудь очень извращенный трюк. А если указатель 0x140003038
(фактический адрес buff
)? Выглядит правдоподобным легальным смещением...
Кстати, что там у нас расположено? Секундочку...
.data:0000000140003038 buff db 'Hello, Sailor!',0
Теперь все сходится — функции передан указатель на строку "Hello, Sailor!" (значение 0x140003038
) и индекс символа этой строки (значение 5). Функция сложила указатель со строкой и записала в полученную ячейку символ "\n".
Следующая инструкция заносит значение аргумента arg_8
в регистр EAX
. Как мы установили, это константа:
mov eax, [rsp+arg_8]
; Инкрементируем значение в EAX на 1
inc eax
; Преобразуем двойное слово (значение в EAX) в четверное слово (значение в RAX)
cdqe
; Помещаем в RCX значение аргумента arg_0
; Как мы выяснили, оно представляет собой указатель на строку
mov rcx, [rsp+arg_0]
Сумма RCX
и RAX
используется для косвенной адресации памяти, точнее косвенно‑базовой, так как к указателю прибавляется еще и единица. В эту ячейку памяти заносится ноль. Другими словами, мы прописываем ноль за символом "\n".
mov byte ptr [rcx+rax], 0
retn
void MyFunc(char *, int) endp
Наши предположения подтвердились — функции передаются указатель на строку и индекс первого «отсекаемого» символа строки. А теперь скомпилируем тот же самый пример компилятором Embarcadero C++Builder и сравним, чем он отличается от Microsoft Visual C++:
; int __cdecl main(int argc, const char **argv, const char **envp)
public main
main proc near ; DATA XREF: __acrtused+29↑o
var_10 = qword ptr -10h
var_8 = dword ptr -8
var_4 = dword ptr -4
; Открытие кадра стека
push rbp
; Выделение 0х30 байт памяти для локальных переменных
sub rsp, 30h
; Копирование в RBP указателя на дно стека, ибо после вычитания получилась вершина,
; а после сложения — дно
lea rbp, [rsp+30h]
; В RAX — указатель на строку
lea rax, aHelloSailor ; "Hello, Sailor!"
; В R8D — значение 5
mov r8d, 5
; Инициализация локальных переменных...
mov [rbp+var_4], 0
; ...перебрасываем содержимое регистров в память
mov [rbp+var_8], ecx
mov [rbp+var_10], rdx
; Готовим параметры для передачи:
; в RCX копируем указатель на строку...
mov rcx, rax
; ...в EDX — значение 5
mov edx, r8d
; Вызов функции вместе с передачей параметров
call MyFunc(char *,int)
mov [rbp+var_4], 0
; Обнуляем EAX для возвращения нуля
mov eax, [rbp+var_4]
; Очищаем стек
add rsp, 30h
; Закрываем кадр стека
pop rbp
retn
main endp
; __int64 __fastcall MyFunc(char *, int)
public MyFunc(char *, int)
MyFunc(char *, int) proc near ; CODE XREF: main+2B↓p
var_C = dword ptr -0Ch
var_8 = qword ptr -8
; Открытие кадра стека
push rbp
; Выделение памяти
sub rsp, 10h
; Указатель кадра стека
lea rbp, [rsp+10h]
; Принятые параметры размещаем в локальных переменных
mov [rbp+var_8], rcx
mov [rbp+var_C], edx
; Первый параметр возвращаем в RCX
mov rcx, [rbp+var_8]
; Копирование двойного слова со знаком в четверное
movsxd rax, [rbp+var_C]
Сумма непосредственных значений используется для косвенной адресации памяти. Этот прием мы уже проходили, разбирая дизассемблерный листинг от Visual C++. Однако вопрос все тот же: как понять, где константа, а где указатель? Как и в предыдущем случае, необходимо проанализировать их значения.
mov byte ptr [rcx+rax], 0Ah
; Копирование значений из локальных переменных в регистры
mov rax, [rbp+var_8]
mov edx, [rbp+var_C]
; Увеличение var_C на 1, а Visual C++ в этом месте использовал инструкцию inc
add edx, 1
; Копирование двойного слова со знаком в четверное
movsxd rcx, edx
; Снова косвенная адресация памяти, чтобы поставить после символа новой строки "\n" 0
mov byte ptr [rax+rcx], 0
; Восстановление стека
add rsp, 10h
; Закрытие кадра стека
pop rbp
retn
MyFunc(char *, int) endp
По сравнению с листингом от Visual C++ листинг от C++Builder имеет минимальные различия. Раньше было не так... Даже не знаю, радоваться этому или огорчаться.
Порядок индексов и указателей
Открою маленький секрет: при сложении указателя с константой большинство компиляторов на первое место помещают указатель, а на второе — константу, каким бы ни было их расположение в исходной программе. Иначе говоря, выражения a[i]
, (a+i)[0]
, *(a+i)
и *(i+a)
компилируются в один и тот же код! Даже если извратиться и написать так: (0)[i+a]
, компилятор все равно выдвинет a
на первое место. Что это — ослиное упрямство, игра случая или фича? Ответ до смешного прост — сложение указателя с константой дает указатель! Поэтому результат вычислений всегда записывается в переменную типа «указатель».
Вернемся к последнему рассмотренному примеру (combined_exp_types_cb
), применив для анализа наше новое правило:
; Копирование значений из локальных переменных в регистры
mov rax, [rbp+var_8] ; В RAX теперь указатель на строку
mov edx, [rbp+var_C]
; Увеличение var_C на 1, а там (следовательно, теперь в регистре) значение 5
add edx, 1
; Копирование двойного слова со знаком в четверное
movsxd rcx, edx ; теперь значение 6 в RCX
Сложение RAX и RCX. Операция сложения указывает на то, что по крайней мере один из них константа, а другой — либо константа, либо указатель.
mov byte ptr [rax+rcx], 0
Ага! Сумма непосредственных значений используется для косвенной адресации памяти, значит, это константа и указатель. Но кто из них кто? С большой степенью вероятности RAX
— указатель (так оно и есть), поскольку он стоит на первом месте, а RCX
— индекс, так как он стоит на втором!
Использование LEA для сложения констант
Инструкция LEA
широко используется компиляторами не только для инициализации указателей, но и для сложения констант. Поскольку внутренне представление констант и указателей идентично, результат сложения двух указателей идентичен сумме тождественных им констант. То есть LEA EBX, [EBX+0x666] == ADD EBX, 0x666
, однако по своим функциональным возможностям LEA
значительно обгоняет ADD
. Вот, например, LEA ESI, [EAX*4+EBP-0x20]
, попробуй то же самое «скормить» инструкции ADD
!
Встретив в тексте программы команду LEA
, не торопись навешивать на возвращенное ею значение ярлык «указатель»: с не меньшим успехом он может оказаться и константой! Если «подозреваемый» ни разу не используется в выражении косвенной адресации — никакой это не указатель, а самая настоящая константа!
«Визуальная» идентификация констант и указателей
Вот несколько приемов, помогающих отличить указатели от констант.
- В 64-разрядных Windows-программах указатели могут принимать ограниченный диапазон значений. Доступный процессорам регион адресного пространства начинается со смещения
0x00000000 00010000
и простирается до смещения0x000003FF FFFFFFFF
. Поэтому все непосредственные значения, меньшие0x00000000 00010000
и большие0x000003FF FFFFFFFF
, представляют собой константы, а не указатели. Исключение составляет число ноль, обозначающее нулевой указатель. Некоторые защитные механизмы непосредственно обращаются к коду операционной системы, расположенному выше адреса0x000003FF FFFFFFFF
, где начинаются владения ядра.- Если непосредственное значение смахивает на указатель, посмотри, на что он указывает. Если по данному смещению находится пролог функции или осмысленная текстовая строка, скорее всего, мы имеем дело с указателем, хотя, может быть, это всего лишь совпадение.
- Загляни в таблицу перемещаемых элементов. Если адрес «подследственного» непосредственного значения есть в таблице, это, несомненно, указатель. Беда в том, что большинство исполняемых файлов неперемещаемы и такой прием актуален лишь для исследования DLL (а DLL перемещаемы по определению).
К слову сказать, дизассемблер IDA Pro использует все три описанных способа для автоматического опознавания указателей.
ЗАКЛЮЧЕНИЕ
Как мы увидели выше, правильное определение констант и смещений зависит от многих фундаментальных факторов: от операционной системы, ее разрядности, даже от языка ассемблера, в котором отражается процессорная архитектура! Эти понятия составляют основу любой программы. А рассмотренные в статье примеры показали важность их правильной идентификации.
Читайте ещё больше платных статей бесплатно: https://t.me/hacker_frei