Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
книги хакеры / Майкл_Сикорски,_Эндрю_Хониг_Вскрытие_покажет!_Практический_анализ.pdf
Скачиваний:
18
Добавлен:
19.04.2024
Размер:
17.17 Mб
Скачать

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

w Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

.

 

 

 

 

 

.c

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-xcha

 

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

Глава 20. Анализ кода на C++  463

to

 

 

 

 

 

w Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

.

 

 

 

 

 

.c

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

Класс Socket содержит функцию для задания конечного адреса, но у него нет метода sendData, поскольку он не представляет собой какой-то конкретный тип сокета. Дочерний класс UDPSocket реализует функцию sendData , поэтому он способен отправлять данные. Кроме того, он может вызывать метод setDestinationAddr, определенный в классе Socket.

Влистинге 20.5 sendData вызывает функцию setDestinationAddr , хотя та

ине указана в классе UDPSocket. Это возможно благодаря тому, что родительский класс автоматически становится частью дочернего.

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

Обычные и виртуальные функции

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

Несколько популярных моделей программирования используют эту концепцию для существенного упрощения сложных задач. Чтобы проиллюстрировать полезность такого подхода, вернемся к примеру с сокетом из листинга 20.5. У нас есть код, который должен отправить данные по сети (sendData), но мы хотим иметь возможность выбирать между протоколами TCP и UDP. Чтобы этого добиться, можно просто создать родительский класс Socket с виртуальным методом sendData и два дочерних класса, UDPSocket и TCPSocket, которые переопределяют этот метод для отправки данных по соответствующему протоколу.

Внашем коде мы создаем объект типа Socket и указываем тот сокет, который будем использовать в том или ином случае. Функция sendData всегда будет вызываться из подходящего подкласса, будь то UDPSocket или TCPSocket; выбор будет делаться в зависимости от типа исходного объекта Socket.

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

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

впользу дочерней версии.

Втабл. 20.1 показан фрагмент кода, выполнение которого зависит от того, явля-

ется ли функция виртуальной.

 

 

 

 

hang

e

 

 

 

 

 

 

 

 

 

 

C

 

E

 

 

 

 

 

 

 

X

 

 

 

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

 

 

 

F

 

 

 

 

 

 

t

 

 

 

 

D

 

 

 

 

 

 

 

i

r

 

 

P

 

 

 

 

 

 

 

 

o

 

 

 

 

 

 

 

NOW!

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

w

 

 

to

 

 

464  Часть VI  •  Специальные темы

 

 

w Click

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

o

m

 

 

 

w

 

 

 

 

 

 

 

 

 

 

 

 

.

 

 

 

 

 

 

.c

 

 

 

 

 

p

 

 

 

 

g

 

 

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

 

 

 

-xcha

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Таблица 20.1. Пример исходного кода с виртуальными функциями

 

 

 

 

 

 

 

 

 

Обычная функция

 

Виртуальная функция

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

class A {

 

class A {

 

 

 

 

 

 

 

 

 

public:

 

public:

 

 

 

 

 

 

 

 

 

 

 

void foo() {

 

virtual void foo() {

 

 

 

 

 

 

 

 

 

 

 

printf(“Class A\n”);

 

printf(«Class A\n»);

 

 

 

 

 

 

 

 

 

 

 

}

 

}

 

 

 

 

 

 

 

 

 

};

 

 

 

};

 

 

 

 

 

 

 

 

 

class B : public A {

 

class B : public A {

 

 

 

 

 

 

 

 

 

public:

 

public:

 

 

 

 

 

 

 

 

 

 

 

void foo() {

 

virtual void foo() {

 

 

 

 

 

 

 

 

 

 

 

printf(“Class B\n”);

 

printf(«Class B\n»);

 

 

 

 

 

 

 

 

 

 

 

}

 

}

 

 

 

 

 

 

 

 

 

};

 

 

 

};

 

 

 

 

 

 

 

 

 

void g(A& arg) {

 

void g(A& arg) {

 

 

 

 

 

 

 

 

 

 

 

arg.foo();

 

arg.foo();

 

 

 

 

 

 

 

 

 

}

 

 

 

}

 

 

 

 

 

 

 

 

 

int _tmain(int argc, _TCHAR* argv[])

 

int _tmain(int argc, _TCHAR* argv[])

 

 

 

 

 

 

 

 

 

{

 

 

 

{

 

 

 

 

 

 

 

 

 

 

 

B b;

 

B b;

 

 

 

 

 

 

 

 

 

 

 

A a;

 

A a;

 

 

 

 

 

 

 

 

 

 

 

g(b);

 

g(b);

 

 

 

 

 

 

 

 

 

 

 

return 0;

 

return 0;

 

 

 

 

 

 

 

 

 

}

 

 

 

}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

w Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

.

 

 

 

 

 

.c

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

Код содержит два класса: A и B. Класс B переопределяет метод foo из класса A. В коде также есть функция для вызова метода foo за пределами любого из этих классов. Если эту функцию не сделать виртуальной, программа выведет строку Class A; в противном случае результатом будет строка Class B. Если не считать ключевых слов virtual в строках и , оба столбца содержат идентичный код.

В случае с обычными функциями выбор конкретного вызова происходит во время компиляции. Когда компилируются два фрагмента кода, представленных в табл 20.1, объект получает класс A. Теоретически компилятор мог выбрать подкласс A, но на этом этапе нам уже известен тип объекта, поэтому функция foo будет вызываться из класса A. По этой причине код в левом столбце выводит строку Class A.

Если речь идет о виртуальных функциях, решение о том, какая из них будет вызвана, принимается во время выполнения. Если используется объект класса A, код вызовет функцию именно из этого класса. То же самое относится и к классу B. Поэтому код в правом столбце выводит строку Class B.

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

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

w Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

.

 

 

 

 

 

.c

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-xcha

 

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

Глава 20. Анализ кода на C++  465

to

 

 

 

 

 

w Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

.

 

 

 

 

 

.c

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

Использование таблиц виртуальных методов

При обработке кода на языке C++ компилятор добавляет специальные структуры данных для поддержки виртуальных функций. Эти структуры называются таблицами виртуальных методов или просто vtable. Они представляют собой обычные массивы указателей на функции. У каждого класса, который использует виртуальные методы, есть своя таблица, и каждый метод в ней имеет отдельную запись.

Втабл. 20.2 показан ассемблерный код двух версий функции g, представленной

втабл. 20.1. Слева находится обычная версия для вызова foo, а справа — виртуальная.

Таблица 20.2. Ассемблерный код примера из табл. 20.1

Вызов обычной функции

Вызов виртуальной функции

 

 

 

 

 

 

00401000

push

ebp

00401000

push

ebp

00401001

mov

ebp, esp

00401001

mov

ebp, esp

00401003

mov

ecx, [ebp+arg_0]

00401003

mov

eax, [ebp+arg_0]

00401006

call

sub_401030

00401006

mov

edx, [eax]

0040100B

pop

ebp

00401008

mov

ecx, [ebp+arg_0]

0040100C

retn

 

0040100B

mov

eax, [edx]

 

 

 

0040100D

call

eax

 

 

 

0040100F

pop

ebp

 

 

 

00401010

retn

 

 

 

 

 

 

 

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

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

На рис. 20.2 показано, как с помощью виртуальной функции делается выбор

впользу того или иного участка кода в табл. 20.2. Первые четыре байта объекта являются указателем на vtable. Первые четыре байта внутри vtable представляют собой указатель на код первой виртуальной функции.

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

всписке.

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

 

 

 

 

hang

e

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

C

 

E

 

 

 

 

 

 

 

 

 

 

 

 

X

 

 

 

 

 

 

 

 

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

 

 

 

 

 

 

 

 

F

 

 

 

 

 

 

t

 

 

 

 

 

 

 

 

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

 

 

 

 

w

 

 

to

 

 

466  Часть VI  •  Специальные темы

w Click

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

o

m

 

w

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

.

 

 

 

 

 

.c

 

 

 

 

 

 

 

 

 

 

 

p

 

 

 

 

g

 

 

 

 

 

 

 

 

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

 

 

 

 

 

 

 

 

-xcha

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

hang

e

 

 

 

 

 

 

 

C

 

E

 

 

 

 

X

 

 

 

 

 

 

-

 

 

 

 

 

d

 

 

F

 

 

 

 

 

 

t

 

 

D

 

 

 

 

 

 

 

i

 

 

 

 

 

 

 

 

 

r

P

 

 

 

 

 

NOW!

o

 

 

 

 

 

 

 

 

 

 

 

 

BUY

 

 

 

 

 

 

to

 

 

 

 

 

w Click

 

 

 

 

 

m

 

 

 

 

 

 

w

 

 

 

 

 

 

 

 

 

 

w

 

 

 

 

 

 

 

o

 

 

.

 

 

 

 

 

.c

 

 

 

p

 

 

 

 

g

 

 

 

 

 

df

 

 

n

e

 

 

 

 

 

-x cha

 

 

 

 

Рис. 20.2. Объект на языке C++ с таблицей виртуальных методов (vtable)

Распознавание таблицы виртуальных методов

Чтобы понять, куда ведет вызов, нам нужно определить тип объекта и установить местоположение vtable. Доступ к адресу vtable обычно происходит недалеко от оператора new, относящегося к конструктору (об этом понятии мы поговорим в следующем разделе).

Таблица виртуальных методов выглядит как массив указателей на функции. В листинге 20.6 показан пример vtable для класса с тремя виртуальными функция­ ми. При просмотре такой таблицы перекрестную ссылку должна иметь только первая ее запись. Доступ к остальным элементам выполняется по сдвигу относительно начала таблицы: прямого доступа к ним нет.

ПРИМЕЧАНИЕ

Вэтом примере строка с меткой off_4020F0 является началом vtable; не спутайте это с таблицами переключения сдвигов, рассмотренными в главе 6.

Втаблице переключения сдвиги не связаны с ответвлениями и помечены как loc_###### вместо sub_######.

Листинг 20.6. Таблица виртуальных методов в IDA Pro

004020F0

off_4020F0

dd offset sub_4010A0

004020F4

 

dd offset sub_4010C0

004020F8

 

dd offset sub_4010E0

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

Установив местоположение vtable и виртуальных функций, можно использовать эту информацию для их анализа. Вы должны знать, что все методы внутри найденной вами таблицы принадлежат одному и тому же классу и что все они каким-то образом связаны между собой. На основе vtable можно также определить, являются ли два метода родственными.