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

Идентификация глобальных переменных


Да, подумала Алиса, - вот это дерябнулась, так дерябнулась!

Программа, нашпигованная глобальными переменными, - едва ли на самое страшное проклятие хакеров, – вместо древа строгой иерархии, компоненты программы тесно переплетаются друг с другом и, чтобы понять алгоритм одного из них, – приходится "прочесывать" весь листинг в поисках перекрестных ссылок. А в совершенстве восстанавливать перекрестные ссылки не умеет ни один дизассемблер, - даже IDA!

Идентифицировать глобальные переменные очень просто, гораздо проще, чем все остальные конструкции языков высокого уровня. Глобальные переменные сразу же выдают себя непосредственной адресаций памяти, т.е. обращение к ним выглядит приблизительно так: "MOV EAX,[401066]", где 0x401066

и есть адрес глобальной переменной.

Сложнее понять: для чего эта переменная, собственно, нужна и каково ее содержимое на данный момент. В отличие от локальных переменных, глобальные – контекстно-зависимы. В самом деле, каждая локальная переменная инициализируется "своей" функцией и не зависит от того, какие функции были вызваны до нее. Напротив, глобальные переменные может модифицировать кто угодно и когда угодно, - значение глобальной переменной в произвольной точке программы не определено. Чтобы его выяснить, необходимо проанализировать все, манипулирующие с ней функции, и – более того – восстановить порядок из вызова. Подробнее этот вопрос будет рассмотрен в главе "___Построение дерева вызовов", - пока же разберемся с техникой восстановления перекрестных ссылок.

Техника восстановления перекрестных ссылок. В большинстве случаев с восстановлением перекрестных ссылок сполна справляется автоматический анализатор IDA, и делать это "вручную" практически никогда не придется. Однако бывает, что IDA ошибается, да и не всегда (и не у всех!) она бывает под рукой. Поэтому, совсем нелишне уметь справляться с глобальными переменными самому.

Отслеживание обращений к глобальным переменным контекстным поиском их смещения в сегменте кода [данных].


Непосредственная адресация глобальных переменных чрезвычайно облегчает поиск манипулирующих с ними машинных команд. Рассмотрим, например, такую конструкцию: "MOV EA,[0x41B904]". После ассемблирования она будет выглядеть так: "A1  04 B9 41 00". Смещение глобальной переменной записывается "как есть" (естественно, с соблюдением обратного порядка следования байт – старшие располагаются по большему адресу, а младшие – по меньшему).

Тривиальный контекстный поиск позволит выявить все обращения к интересующей вас глобальной переменной, достаточно лишь узнать ее смещение, переписать его справа налево и… вместе с полезной информацией получить какое-то количество мусора. Ведь не каждая число, совпадающее по значению со смещением глобальной переменной, обязано быть указателем на эту переменную. Тому же "04 B9 41 00" удовлетворяет, например, следующий контекст:

83EC04                       sub       esp,004

B941000000                   mov       ecx,000000041

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



Вот, например, встречается нам следующее: "…8D 81 04 B9 41 00 00…". Эту последовательность, за вычетом последнего нуля, можно интерпретировать так: "lea eax,[ecx+0х41B904]", но если предположить, что 0x8D принадлежит "хвосту" предыдущей команды, то получится следующее: "add d,[ecx][edi]*4,000000041", а, может быть, здесь и вовсе несколько команд…

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


Поэтому, приходится идти другим путем…

Образно машинный код можно изобразить в виде машинописного текста, напечатанного без пробелов. Если попробовать читать с произвольной позиции, мы, скорее всего, попадем на середину слова и ничего не поймем. Может быть, волей случая, первые несколько слогов и сложатся в осмысленное слово (а то и два!), но дальше пойдет сплошная чепуха. Например: "мамылараму". Ага, "мамы" – множественное число от "мама", подходит? Подходит. Дальше – "лараму". "Лараму" – это что, народный индийский герой такой со множеством родительниц? Или "Мамы ла Раму?" А как вам "Мамы Ла Ра Му" – в смысле три мамы "Ла, Ра и Му"? Да, скажите тоже, - вот, ерунда какая!!!

Смещаемся на одну букву вперед, оставляя "м" предыдущему слову. "А", - что ж, вполне возможно, это и есть союз "А", тем более что за ним идет осмысленное местоимение "мы", получается – "А мы Лараму" или "А мы Лара Му". Кто такой этот Лараму?!

Сдвигаемся еще на одну букву и читаем "мыла", а за ним "раму". Заработало! А "ам" стало быть, хвост от "мама".

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

Отличия констант от указателей или продолжаем разгребать мусор дальше. Вот, наконец, мы избавились от ложных срабатываний, бессмысленность которых очевидна с первого взгляда. Куча мусора заметно приуменьшилась, но… в ней все еще продолжают встречаться такие штучки как "PUSH 0x401010".


Что такое 0x401010

– константа или смещение? С равным успехом может быть и то, и другое. Пока не доберемся до манипулирующего с ней кода, мы вообще не сможем сказать ничего вразумительного. Если манипулирующий код обращается к 0x401010 по значению, - это константа (выражающая, например, скорость улепетывания Пяточка от Слонопотама), а если по ссылке – это указатель (в данном контексте смещение).

Подробнее эту проблему мы еще обсудим в главе "Идентификация констант и смещений", пока же заметим с большим облегчением, что минимальный адрес загрузки файла в Windows 9x равен 0x400000, и немного существует констант, выражаемых таким большим числом.

Замечание: минимальный адрес загрузки Windows NT равен 0x10000, однако, чтобы программа могла успешно работать и под NT, и под 9x, она должна грузиться не ниже 0x400000.

 Кошмары 16-разрядного режима. В 16-разрядном режиме отличить константу от указателя не так-то просто, как в 32-разрядном режиме! В 16-разрядном режиме под данные отводится один (или несколько) сегментов размером 0x10000

байт и допустимые значения смещений заключены в узком интервале [0x0, 0xFFFF], причем у большинства переменных смещения очень невелики и визуально неотличимы от констант.

Другая проблема – один сегмент чаще всего не вмещает в себя всех данных и приходится заводить еще один (а то и больше). Два сегмента – это еще ничего: один адресуется через регистр DS, другой – через ES и никаких трудностей в определении "это указатель на переменную какого

сегмента" не возникает. Например, если нас интересуют все обращения к глобальной переменной X, расположенной в основном сегменте по смещению 0x666, то команду MOV AX, ES:[0x666], мы сразу же откинем в мусорную корзину, т.к. основной сегмент адресуется через DS (по умолчанию), а здесь – ES. Правда, обращение может происходить и в два этапа. Например: "MOV BX,0x666/xxx---xxx/MOV AX,ES:[BX]", увидев "MOV BX,0x666" мы не только не можем определить сегмент, но и даже сказать – смещение ли это вообще? Впрочем, это не сильно затрудняет анализ…



Хуже, если сегментов данных в программе добрый десяток (а, что, может же потребоваться порядка 640 килобайт статической памяти?). Никаких сегментных регистров на это не хватит, и их переназначения будут происходить многократно. Тогда, чтобы узнать к какому именно сегменту происходит обращение, потребуется определить значение сегментного регистра. А как его определить? Самое простое – прокрутить экран дизассемблера немного вверх, ища глазами инициализацию данного сегментного регистра, помня то том, что она может осуществляться не только командой MOV segREG, REG, но довольно частенько и POP! Например, PUSH ES/POP DS

равносильно MOV DS, ES

– правда, команды MOV segREG, segREG

в "языке" микропроцессоров 80x86, увы, нет. Как нет команды MOV segREG, CONST, и ее приходится эмулировать вручную либо так: MOV AX, 0x666/MOV ES,AX, либо так: PUSH 0x666/POP ES.

Как хорошо, что 16-разрядный режим практически полностью ушел в прошлое, унося в песок истории все свои проблемы. Не только программисты, но и хакеры с переходом на 32-разрядный режим  вздыхают с облегчением.

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

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

Мне могут возразить – а зачем вообще явно передавать глобальную переменную функции? Любая функция и без этого может к ней обратится. Не спорю. Да, может, но только если знает об этом заранее. Вот, скажем, есть у нас функция xchg, обменивающая свои аргументы местами, и есть две глобальные переменные, которые позарез приспичило обменять. Функции xchg доступны все глобальные переменные, но она "не знает" какие из них необходимо обменивать (и необходимо ли это вообще?), вот и приходится ей явно передавать глобальные переменные как аргументы.


А это значит, что всех обращений к глобальным переменным простым контекстным поиском мы не нейдем. Самое печальное – не найдет их и IDA Pro (да и как бы она их могла найти? для этого ей потребовался бы полноценный эмулятор процессора или хотя бы основных команд), на чем мы и убедимся в следующем примере:

#include <stdio.h>

int a; int b; // Глобальные переменные a и b

// Функция, обменивающая значения аргументов

xchg(int *a, int *b)

{

int c; c=*a; *b=*a; *b=c;

//     ^^^^^^^^^^^^^^^^^^ косвенное обращение к аругментам по указателю

// если аргументы функции – глобальные переменные, то они будут адресоваться

// не прямо, а косвенно

}

main()

{

a=0x666; b=0x777; // Здесь – непосредственное обращение к глобальным переменным

xchg(&a, &b); // Передача глобальной переменной по ссылке

}

Листинг 121 Явная передача глобальных переменных

Результат компиляции компилятором Microsoft Visual C++ должен выглядеть так:

main         proc near           ; CODE XREF: start+AFp

push   ebp

mov    ebp, esp

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

mov    dword_405428, 666h

; Инициализируем глобальную переменную dword_405428

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

; адресация

mov    dword_40542C, 777h

; Инициализируем глобальную переменную dword_40542C

push   offset dword_40542C

; Смотрите! Передаем функции смещение глобальной переменной dword_40542C как

; аргумент (т.е. другими словами, передаем ее по ссылке)

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

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

push   offset dword_405428

; Передаем функции смещение глобальной переменной dword_405428

call   xchg

add    esp, 8

pop    ebp

retn

main         endp

xchg         proc near           ; CODE XREF: main+21p

var_4        = dword      ptr -4



arg_0        = dword      ptr  8

arg_4        = dword      ptr  0Ch

push   ebp

mov    ebp, esp

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

push   ecx

; Выделяем память для локальной переменной var_4

mov    eax, [ebp+arg_0]

; Загружаем а EAX содержимое аргумента arg_0

mov    ecx, [eax]

; Смотрите! Косвенное обращение к глобальной переменной!

; А еще говорят – будто бы таких не бывает!

; Разумеется, определить, что обращение происходит именно к глобальной

; переменной (и какой именно глобальной переменной) можно только анализом

; кода вызывающей функции

mov    [ebp+var_4], ecx

; Копируем значение *arg_0 в локальную переменную var_4

mov    edx, [ebp+arg_4]

; Загружаем в EDX содержимое аргумента arg_4

mov    eax, [ebp+arg_0]

; Загружаем в EAX содержимое аргумента arg_0

mov    ecx, [eax]

; Копируем в ECX значение аргумента *arg_0

mov    [edx], ecx

; Копируем в [arg_4] значение arg_0[0]

mov    edx, [ebp+arg_4]

; Загружаем в EDX значение arg_4

mov    eax, [ebp+var_4]

; Загружаем в EAX значение локальной переменной var_4 (хранит *arg_0)

mov    [edx], eax

; Загружаем в *arg_4 значение *arg_0

mov    esp, ebp

pop    ebp

retn

xchg         endp

dword_405428 dd 0                ; DATA XREF: main+3w main+1Co

                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

dword_40542C dd 0                ; DATA XREF: main+Dw main+17o

                                        ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

; IDA

нашла все ссылки на обе глобальные переменные

; Первые две: main+3w

и main+Dw на код инициализации

;      ('w' – от "write" – т.е. в обращение на запись)

: Вторые две: main+1Co

и main+17o

;      ('o' – от "offset" – т.е. получение смещения глобальной переменной)

Листинг 122

Если среди перекрестных ссылок на глобальную переменную присутствуют ссылки с суффиксом 'o', обозначающие взятие смещения (аналог ассемблерной директивы offset), то сразу же вскидывайте свои ушки на макушку – раз offset, значит, имеет место передача глобальной переменной по ссылке.


А ссылка – это косвенная адресация. А косвенная адресация – это ласты: утомительный ручной анализ и никаких чудес прогресса.

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

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

Всякая непосредственно адресуемая ячейка памяти – глобальная (статическая) переменная (см. исключения ниже), но не всякая глобальная (глобальная) переменная всегда адресуется непосредственно.


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