Фундаментальные основы хакерства

Идентификация объектов, структур и массивов


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

Чарлз Перси Сноу «ЭЙНШТЕЙН»

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

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

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

#include <stdio.h>

#include <string.h>

struct zzz

{

char s0[16];

int a;

float f;

};



func(struct zzz y)

// Понятное дело, передачи структуры по значению лучше избегать,

// но здесь это сделано умышленно для демонстрации скрытого создания

// локальной переменной

{

printf("%s %x

%f\n",&y.s0[0], y.a, y.f);

}

main()

{

struct zzz y;

strcpy(&y.s0[0],"Hello,Sailor!");

y.a=0x666;

y.f=6.6;

func(y);

}

Листинг 45 Пример, демонстрирующий уничтожение структур на стадии компиляции

Результат его компиляции в общем случае должен выглядеть так:

main         proc near           ; CODE XREF: start+AFp

var_18       = byte ptr -18h

var_8        = dword      ptr -8

var_4        = dword      ptr -4


; члены структуры неотличимы от обычных локальных переменных

push   ebp

mov    ebp, esp

sub    esp, 18h

; резервирование места в стеке для структуры

push   esi

push   edi

push   offset aHelloSailor ; "Hello,Sailor!"

lea    eax, [ebp+var_18]

; Указатель на локальную переменную var_18

; следующая за ней переменная расположена по смещению 8

; следовательно, 0x18-0x8=0x10 – шестнадцать байт – именно столько

; занимает var_18, что намекает на то, что она – строка

; (см. "Идентификация литералов и строк")

push   eax

call   strcpy

; копирование строки из сегмента данных в локальную переменную-член структуры

add    esp, 8

mov    [ebp+var_8], 666h

; занесение в переменную типа DWORD

значения 0x666

mov    [ebp+var_4], 40D33333h

; а это значение в формате float равно 6.6

; (см. "Идентификация аргументов функций")

sub    esp, 18h

; резервируем место для скрытой локальной переменной, которая используется

; компилятором для передачи функции экземпляра структуры по значению

; (см. "Идентификация локальных переменных – регистровых и временныех

переменныех")

mov    ecx, 6

; будет скопировано 6 двойных слов, т.е. 24 байта

; 16 – на строку и по четыре на float

и int

lea    esi, [ebp+var_18]

; получаем указатель на копируемую структуру

mov    edi, esp

; получаем указатель на только что созданную скрытую локальную переменную

repe movsd

; копируем!

call   func

; вызываем функцию

; передачи указателя на скрытую локальную переменную не происходит – она

; и так находится на верху стека.

add    esp, 18h

pop    edi

pop    esi

mov    esp, ebp

pop    ebp

retn  

main         endp

Листинг 46

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

main()

{

char s0[16];

int a;

float f;

strcpy(&s0[0],"Hello,Sailor!");

a=0x666;

f=6.6;

}



Листинг 47 Пример, демонстрирующий сходство структур с обычными локальными переменными

И сравним результат компиляции с предыдущим:

main         proc near           ; CODE XREF: start+AFp

var_18       = dword      ptr -18h

var_14       = byte ptr -14h

var_4        = dword      ptr -4

; Ага, кажется есть какое-то различие! Действительно, локальные переменные помещены

; в стек не в том порядке, в котором они были объявлены в программе, а как это

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

; в порядке их объявления.

; Но, поскольку, при дизассемблировании оригинальный порядок следования переменных

; не известен, определить "правильно" ли они расположены или нет, увы,

; не представляется возможным

push   ebp

mov    ebp, esp

sub    esp, 18h

; резервируем 0x18 байт стека (как и предыдущем примере)

push   offset aHelloSailor ; "Hello,Sailor!"

lea    eax, [ebp+var_14]

push   eax

call   strcpy

add    esp, 8

mov    [ebp+var_4], 666h

mov    [ebp+var_18], 40D33333h

; смотрите: код аккуратно совпадает байт в байт! Следовательно, невозможно

; автоматически отличить структуру от простого скопища локальных переменных

mov    esp, ebp

pop    ebp

retn  

main         endp

func         proc near           ; CODE XREF: main+36p

var_8        = qword      ptr -8

arg_0        = byte ptr  8

arg_10       = dword      ptr  18h

arg_14       = dword      ptr  1Ch

; смотрите: хотя функции передается только один аргумент – экземпляр структуры –

; в дизассемблерном тексте он не отличим от последовательной засылки в стек

; нескольких локальных переменных! Поэтому, восстановить подлинный прототип

; функции невозможно!

push   ebp

mov    ebp, esp

fld    [ebp+arg_14]

; загрузить в стек FPU вещественное целое, находящееся по смещению

; 0x14 относительно указателя eax

sub    esp, 8

; зарезервировать 8 байт пол локал. перемен.



fstp   [esp+8+var_8]

; перепихнуть считанное вещественное значение в локальную переменную

mov    eax, [ebp+arg_10]

push   eax

; прочитать только что "перепихнутую" вещественную переменную

; и затолкать ее в стек

lea    ecx, [ebp+arg_0]

; получить указатель на первый аргумент

push   ecx

push   offset aSXF  ; "%s %x %f\n"

call   printf

add    esp, 14h

pop    ebp

retn  

func         endp

Листинг 48

Выходит, отличить структуру от обычных переменных невозможно? Неужто исследователю придется самостоятельно распознавать "родство" данных и связывать их "брачными узами", порой ошибаясь и неточно воспроизводя исходный текст программы?

Как сказать… И да, и нет одновременно. "Да": экземпляр структуры, использующийся в той же единице трансляции в которой он был объявлен, "развертывается" еще на стадии компиляции в самостоятельные переменные, обращение к которым происходит индивидуально по их фактическим адресам (возможно относительным). "Нет", – если в области видимости находится один лишь указатель на экземпляр структуры. – Тогда обращение ко всем членам структуры происходит через указатель на этот экземпляр структуры (т.к. структура не присутствует в области видимости, например, передается другой функции по ссылке, вычислить фактические адреса ее членов на стадии компиляции невозможно).

Постойте, но ведь точно так происходит обращение и к элементам массива, – базовый указатель указывает на начало массива, к нему добавляется смещение искомого элемента относительно начала массива (индекс элемента, умноженный на его размер), – результат вычислений и будет фактическим указателем на искомый элемент!

Единственное фундаментальное отличие массивов от структур состоит в том, что массивы гомогенны

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

(состоящими из элементов различных типов). Таким образом, задача идентификации структур и массивов сводится: во-первых, к выделению ячеек памяти, адресуемых через общий для всех них базовый указатель, и, во-вторых, определению типа этих переменных ___(см.


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

С другой стороны, если программисту вздумается подсчитать зависимость выпитого пива от дня недели, он может выделить для учета либо массив day[7], либо завести структуру struct week{int Monday; int Tuesday;….}. И в том, и в другом случае сгенерированный компилятором код будет одинаков, да не только код, но и смысл! В этом контексте структура неотличима от массива и физически, и логически, - выбор той или иной конструкции – дело вкуса.

Так же возьмите себе на заметку, что массивы, как правило, длинны, а обращение к их элементам часто сопровождается различными математическими операциями, совершаемыми над указателем. Далее – обработка элементов массива как правило осуществляется в цикле, а члены структуры по обыкновению "разбираются" индивидуально (хотя некоторые программисты позволяют себе вольность обращаться со структурой как с массивом). Еще неприятнее, что Си/Си++ допускают (если не сказать провоцируют) явное преобразование типов и… ой, а ведь в этом случае, при дизассемблировании не удастся установить: имеем ли мы дело с объединенными под одну крышу разнотипными данными (т.е. структуру), или же это массив, c "ручным" преобразованием типа своих элементов. Хотя, строго говоря, после подобных преобразований массив превращается в самую настоящую структуру! (Массив по определению гомогенен, и данные разных типов хранить не может).

Модифицируем предыдущий пример, передав функции не саму структуру, а указатель на нее и посмотрим, что за код сгенерировал компилятор.

funct  proc near           ; CODE XREF: sub_0_401029+29p

var_8        = qword      ptr -8

arg_0        = dword      ptr  8

; ага! Функция принимает только один аргумент!

push   ebp

mov    ebp, esp

mov    eax, [ebp+arg_0]



; загружаем переданный функции аргумент в EAX

fld    dword ptr [eax+14h]

; загружаем в стек FPU вещественное значение, находящееся по смещению

; 0x14 относительно указателя EAX

; Таким образом, во-первых, EAX (аргумент, переданный функции) – это указатель

; во-вторых, это не просто указатель, а базовый указатель, использующийся

; для доступа к элементам структуры или массива.

; Запомним тип первого элемента (вещественное значение) и продолжим анализ

sub    esp, 8

; резервируем 8 байт пол локальные переменные

fstp   [esp+8+var_8]

; перепихиваем считанное вещественное значение в локальную переменную var_8

mov    ecx, [ebp+arg_0]

; Загружаем в ECX значение переданного функции указателя

mov    edx, [ecx+10h]

; загружаем в EDX значение, лежащее по смещению 0x10

; Ага! Это явно не вещественное значение, следовательно, мы имеем дело со

; структурой

push   edx

; заталкиваем только что считанное значение в стек

mov    eax, [ebp+arg_0]

push   eax

; получаем указатель на структуру (т.е. на ее первый член)

; и запихиваем его в стек. Поскольку ближайший элемент

; находится по смещению 0x10, то первый элемент структуры по-видимому

; занимает все эти 0x10 байт, хотя это и не обязательно – возможно остальные

; члены структуры просто не используются. Установить: как все обстоит на самом

; деле можно, обратившись к вызывающей (материнской) функции, которая и

; инициализировала эту структуру, но и без этого, мы можем восстановить

; ее приблизительный вид

; struct xxx{

; char x[0x10] || int x[4] || __int16[8] || __int64[2];

; int y;

; float z;

; }

push   offset aSXF  ; "%s %x %f\n"

; строка спецификаторов, позволяет уточнить типы данных – так, первый элемент

; это, бесспорно, char x[x010], поскольку, он выводится как строка,

; следовательно наше предварительное предположение о формате структуры –

; верное!

call   printf

add    esp, 14h

pop    ebp

retn  

funct  endp



main   proc near           ; CODE XREF: start+AFp

var_18       = byte ptr -18h

var_8        = dword      ptr -8

var_4        = dword      ptr -4

; смотрите: на первый взгляд мы имеем дело с несколькими локальными переменными,

; но давайте не будем торопиться с их идентификацией!

push   ebp

mov    ebp, esp

sub    esp, 18h

; Открываем кадр стека

push   offset aHelloSailor ; "Hello,Sailor!"

lea    eax, [ebp+var_18]

push   eax

call   unknown_libname_1

; unknown_libmane_1 – это strcpy и понять это можно даже не анализируя ее код.

; Функция принимает два аргумента – указатель на локальный буфер из 0x10 байт

; (размер 0x10 получен вычитанием смещения ближайшей переменной от смещения

; самой этой переменной относительно карда стека) такой же точно прототип

; и у strcmp, но это не может быть strcmp, т.к. локальный буфер

; не инициализирован, и он может быть только буфером-приемником

add    esp, 8

; выталкиваем аргументы из стека

mov    [ebp+var_8], 666h

; инициализируем локальную переменную var_8 типа DWORD

mov    [ebp+var_4], 40D33333h

; инициализируем локальную переменную var_4 типа... нет, не DWORD

; (хотя она и выглядит как DWORD), - проанализировав, как эта переменная

; используется в функции funct, которой она передается, мы распознаем

; в ней вещественное значение размером 4 байта. Стало быть это float

; (подробнее см. "Идентификация аргументов функций")

lea    ecx, [ebp+var_18]

push   ecx

; Вот теперь – самое главное! Функции передается указатель на локальную

; переменную var_18, - строковой буфер размером в 0x10 байт,

; но анализ вызываемой функции позволил установить, что она обращается не

; только к первым 0x10 байтам стека материнской функции, а ко всем – 0x18!

; Следовательно, функции передается не указатель на строковой буфер,

; а указатель на структуру

;

; srtuct x{

;      char var_18[10];

;      int var_8;

;      float var_4

; }

;



; Поскольку, типы данных различны, то это – именно структура, а не массив.

call   funct

add    esp, 4

mov    esp, ebp

pop    ebp

retn  

sub_0_401029 endp

Листинг 49

::Идентификация объектов. Объекты языка Си++ - это, по сути дела, структуры, совмещающие в себе данные, методы их обработки (функции то бишь), и атрибуты защиты (типа public, friend…).

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

Теперь обо всем этом подробнее. Итак, объект (вернее, экземпляр объекта) – что он собой представляет? Пусть у нас есть следующий объект:

class MyClass{

void demo_1(void);

int a;

int b;

 public:

virtual void demo_2(void);

int c;

};

MyClass zzz;

Листинг 50 Пример, демонстрирующий строение объекта

Экземпляр объекта zzz "перемелется" компилятором в следующую структуру (см. рис 13):



Рисунок 13 0х008 Представление экземпляра объекта в памяти.

Перед исследователем встают следующие проблемы: как отличить объекты от простых структур? Как определить размер объектов? Как определить какая функция к какому объекту принадлежит? Как…. Погодите, погодите, не все сразу! Начнем, отвечать на вопросы по порядку согласно социалистической очереди.

Вообще же, строго говоря, отличить объект от структуры невозможно в силу того, что объект и есть структура с членами приватными по умолчанию. При объявлении объектов можно пользоваться и ключевым словом "struct", и ключевым словом "class". Причем, для классов, все члены которых открыты, предпочтительнее использовать именно "struc", т.к.


члены структуры уже публичны по умолчанию. Сравните два следующих примера:

struct MyClass{                   class MyClass{

void demo(void);                  void demo_private(void);

int x;                            int y;

 private:                         public:

void demo_private(void);          void demo(void);

int y;                            int x;

};                                };

Листинг 51 Классы – это структуры с членами приватными по умолчанию

Одна запись отличается от другой лишь синтаксически, а код, генерируемый компилятором, будет идентичен! Поэтому, с надеждой научиться отличать объекты от структур следует как можно скорее расстаться.

ОК, условимся считать объектами структуры, содержащие одну или более функций, вот только как определить какая функция какому объекту принадлежит? С виртуальными функциями все просто – они вызываются косвенно, через указатель на виртуальную таблицу, помещаемый компилятором в каждый экземпляр объекта, к которому принадлежит данная виртуальная функция. Не виртуальные функции вызываются по их фактическому адресу, равно как и обычные функции, не принадлежащие никакому объекту. Положение безнадежно? Отнюдь нет! Каждой функции-члену объекта передается неявный аргумент – указатель this, ссылающийся на экземпляр объекта, к которому принадлежит данная функция. Экземпляр объекта это, правда, не сам объект, но нечто очень тесно с ним связанное, поэтому, восстановить исходную структуру объектов дизассемблируемой программы – вполне реально (подробнее об этом см. "Объекты и экземпляры")

Размер объектов

определяется теми же указателями this – как разница соседний указателей (если объекты расположены в стеке или в сегменте данных). Если же экземпляры объектов создаются оператором new (как часто и бывает), то в код помещается вызов функции new, принимающий в качестве аргумента количество выделяемых байт, - это и есть размер объекта.

Вот, собственно, и все. Остается добавить, что многие компиляторы, создавая экземпляр объекта, не содержащего ни данных, ни виртуальных функций, все равно выделяют под него минимальное количество памяти (обычно один байт), хотя никак его не используют.


call   demo

; Вот мы и добрались до вызова функции demo

– открываем хвост Тигре!

; Пока не ясно, что эта функция делает (символьное имя дано ей для наглядности)

; но известно, что она принадлежит экземпляру объекта, на который

; указывает ECX. Назовем этот экземпляр 'a'. Далее – поскольку

; функция, вызывающая demo (т.е. функция в которой мы сейчас находимся), не

; принадлежит к 'a' (она же его сама и создала – не мог же экземпляр объекта

; сам "вытянуть себя за волосы"), то функция demo

– это public-функция.

; Неплохо для начала?

mov    dword ptr [esi], 777h

; так, так... мы помним, что ESI указывает на экземпляр объекта, тогда

; выходит, что в объекте есть еще один public-член, это переменная

; типа int.

; По предварительным заключениям объект выглядел так:

; class myclass{

public:

void demo(void); // void –т.к. функция ничего не принимает и не возвращает

; int x;

;}

pop    esi

retn  

main         endp

demo         proc near           ; CODE XREF: main+Fp

; вот мы в функции demo – члене объекта A

push   esi

mov    esi, ecx

; Загружаем в ECX – указатель this, переданный функции

push   offset aMyclass     ; "MyClass\n"

call   printf

add    esp, 4

; Выводим строку на экран...это не интересно, но вот дальше…

mov    ecx, esi

call   demo_private

; Опля, вот он, наш Тигра! Вызывается еще одна функция! Судя по this,

; эта функция нашего объекта, причем вероятнее всего имеющая атрибут private,

; поскольку вызывается только из функции самого объекта.

mov    dword ptr [esi+4], 666h

; Так, в объекте есть еще одна переменная, вероятно, приватная. Тогда,

; по современным воззрениям, объект должен выглядеть так:

; class myclass{

void demo_provate(void);

int y;

; public:

void demo(void); // void –т.к. функция ничего не принимает и не возвращает

int x;

; }

;

; Итак, мы не только идентифицировали объект, но даже восстановили его

; структуру! Пускай, не застрахованную от ошибок (так, предположение



; о приватности "demo_private" и "y" базируется лишь на том, что они ни разу

; не вызывались извне объекта), но все же – не так ООП страшно, как его

; малюют и восстановить если не подлинный исходный текст программы, то хотя бы

; какое-то его подобие вполне возможно!

pop    esi

retn  

demo         endp

demo_private proc near           ; CODE XREF: demo+12p

; приватная функция demo. – ничего интересного

             push   offset aPrivate     ; "Private\n"

             call   printf

             pop    ecx

             retn  

demo_private endp

Листинг 53

::Объекты и экземпляры. В коде, сгенерированном компилятором, никаких объектов и в помине нет, – одни лишь экземпляры объектов. Вроде бы – да какая разница-то? Экземпляр объекта разве не есть сам объект? Нет, между объектом и экземпляром существует принципиальная разница. Объект – это структура, в то время как экземпляр объекта (в сгенерированном коде!) – подструктура этой структуры. Т.е. пусть имеется объект А, включающий в себя функции a1 и a2. Далее, пусть создано два его экземпляра – из одного мы вызываем функцию a1, а из другого – a2. С помощью указателя this мы сможем выяснить лишь то, что одному экземпляру принадлежит функция a1, а другому – a2. Но установить – являются ли эти экземпляры экземплярами одного объекта или экземплярами двух разных объектов – невозможно! Ситуация усугубляется тем, что в производных классах наследуемые функции не дублируются (во всяком случае, так поступают "умные" компиляторы, хотя… в жизни случается всякое). Возникает двузначность – если с одним экземпляром связаны функции a1 и a2, а с другим - a1, a2 и a3, то это могут быть либо экземпляры одного класса (просто из первого экземпляра функция a3 не вызывается), то ли второй экземпляр – экземпляр класса, производного от первого. Код, сгенерированный компилятором, в обоих случаях будет идентичным! Приходится восстанавливать иерархию классов по смыслу и назначению принадлежащих им функций… понятное дело, приблизиться к исходному коду сможет только провидец (ясновидящий).



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

::мой адрес – не дом и не улица! Где "живут" структуры, массивы и объекты? Конечно же, в памяти! А поконкретнее? Конкретнее: существуют три типа размещения: в стеке

(автоматическая память), сегменте данных (статическая память) и куче (динамическая память). И каждый тип со своим "характером". Возьмем стек – выделение памяти неявное, фактически происходящее на этапе компиляции, причем гарантированно определяется только общий объем памяти, выделенный под все локальные переменные, а определить: сколько занимает каждая из них – невозможно в принципе. Не верите? А вот скажем, пусть будет такой код: "char a1[13]; char a2[17]; char a3[23]". Если компилятор выровняет массивы по кратным адресам (а это делают многие компиляторы), то разница смещений ближайших друг к другу массивов может и не быть равна их размеру. Единственная надежда восстановить подлинный размер – найти в коде проверки на выход за границы массива (если они есть – их часто не бывает). Второе (самое неприятное) – если один из массивов не используется, а только объявляется, то не оптимизирующие компиляторы (и даже некоторые оптимизирующие!) могут, тем не менее, отвести для него стековое пространство. Он вплотную примкнет к предыдущему массиву и… гадай – то ли размер массива такой, то ли в его конец "вбухан" неиспользуемый массив? Ну, с массивами куда бы еще ни шло, а вот со структурами и объектами дела обстоят намного хуже. Никому и в голову не придет помещать в программу код, отслеживающий выход за пределы структуры (объекта). Такое невозможно в принципе (ну разве что программист слишком вольно работает с указателями)!

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


К счастью, идентификацию объектов облегчает наличие в них указателя на виртуальную таблицу, но ведь не факт, что любая таблица указателей на функции – есть виртуальная таблица! Может, это просто массив указателей на функции, определенный самим программистом? Вообще-то, при наличии опыта такие ситуации можно легко распознать (см. "Идентификация виртуальных функций"), но все-таки они достаточно неприятны.

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

Наконец, объекты (структуры, массивы), расположенные в куче – просто сказка для анализа! Отведение памяти осуществляется функцией, явно принимающей количество выделяемых байт в качестве своего аргумента, и возвращающей указатель, гарантированно указывающий на начало экземпляра объекта (структуры, массива). Радует и то, что обращение к элементам всегда происходит через базовый указатель, даже если объявление совершается в области видимости (иначе и быть не может – фактические адреса выделяемых блоков динамической памяти не известны на стадии компиляции).

__дописать – восстановление структуры многомерных массивов


Содержание раздела