Полуперемещаемый код — HackZona.Ru

Полуперемещаемый код

Полуперемещаемый код

Тип статьи:
Со старой ХакЗоны.
Источник:
Эту часть правильнее было бы назвать «Вызов функций по абсолютному адресу», но это звучит
еще непонятнее, и куда менее амбициозно. Такую безумную идею я больше нигде не встречал и
считаю ее своим изобретением :)
Возможно, ты слышал, что вирмейкеры при разработке своих вирусов частенько добавляют в
них разные вредоносные функции. При этом они делают свой код «перемещаемым». Это
значит, что этот код сможет работать в любом месте памяти и не привязан жестко в каким-то
объектам (ведь вирмейкер фактически не знает в каком конкретном месте окажется его код
после переполнения буфера).Одна из основных проблем
перемещаемого кода — вызов внешних функций. Дело в том, что при вызове функции,
компилятор использует не абсолютный адрес, а смещение относительно места вызова функции.
Поясню на примере:
<br />
program Project1;<br />
uses Dialogs;<br />
begin<br />
ShowMessage('Пример');<br />
end.<br />

Ставим бряк(breakpoint) на ShowMessage и запускаем. Смотрим окно CPU (если ты уже забыл,
то его удобнее вызывать с помощью CTRL-ALT- C):
www.stranger.nextmail.ru/del4.jpg
По адресу $00451F71 лежит команда E8D2FBFFFF E8 это CALL (относительный ближний
вызов). Запускаем калькулятор, переключаем в HEX-режим (4 байта) и считаем:
$451F71 + $5 + $FFFFFBD2 = $451B48;
5 — длина команды CALL; FFFFFBD2 — смещение (не забывай, что младшие байты числа лежат
по младшим адресам). Получаем $451B48 и переходим по этому адресу (CTRL-G):
www.stranger.nextmail.ru/del5.jpg
Попали как раз в начало функции ShowMessage! Надеюсь я понятно объяснил относительную
адресацию.
Теперь представь, что мы переместили код, содержащий вызов функции ShowMessage, куда-
нибудь в кучу. И оказался ее вызов где-нибудь по адресу $00860828. Если снова посчитать
смещения перехода, то получится адрес $008603FF, естественно здесь никакого вызова
ShowMessage не будет, а будет ошибка доступа.
Таким образом, чтобы создать перемещаемый код, надо научиться вызывать функции не по
относительному смещению, а по абсолютному адресу. Что тебе это даст? Ты сможешь
переместить код своей функции в другое место, например в кучу, стек, в адресное пространство
какой-нибудь dllки, и спрячешь ее вызов от отладчика.
В общем идея состоит в том, чтобы вызывать функцию по указателю. А для того, чтобы
нормально использовать параметры, этот указатель будем приводить к прототипу этой самой
функции. Полное извращение. :) Смотри сам:
<br />
program Project1;<br />
uses Dialogs;<br />
// Прототип функции ShowMessage<br />
type _ShowMessage = procedure (const Msg: string);<br />
begin<br />
// Вызов функции по указателю<br />
_ShowMessage(@ShowMessage)('Test');<br />
end.<br />

Снова запускаем пример и смотрим, во что превратился вызов ShowMessage:
www.stranger.nextmail.ru/del6.jpg
Здесь в регистр EBX непосредственно заносится уже знакомый тебе адрес $451B48, по
которому расположена функция ShowMessage. Потом происходит CALL на это содержимое
регистра EBX. Никаких относительных смещений нет и этот код будет работать в любом месте
адресного пространства твоей программы!
Теперь настало время объяснить, почему я назвал этот код полуперемещаемым. Дело в том, что
этот код будет работать только в адресном пространстве твоей программы. Если ты
попробуешь внедрить его в другую прогу, то снова будет ошибка доступа. Почему? Да потому,
что абсолютный адрес $451B48 имеет значение только для твоей проги, а в другой по этому
адресу будет лежать что-то совсем другое, но точно не функция ShowMessage. Вот так-то.
Кроме того у метода вызова функций по абсолютному адресу есть еще одно совершенно
неочевидное ограничение. С API-функциями Windows никаких проблем нет. Но вот в Delphi
все функции делятся на настоящие и ненастоящие(встроенные). Отличить их легко и ты
сталкивался с этим не раз. Если навести мышку на имя функции и кликнуть по нему, удерживая
CTRL, то тебя забросит в место, где эта функция определяется, и ты сможешь посмотреть ее
код. Но это относится только к настоящим функциям.
Если ты сделаешь то же самое скажем с функцией AssignFile, то тебя забросит куда-то в начало
модуля System. Так по крайней мере в Delphi 7. Это пример ненастоящей (встроенной)
функции. На место ее вызова компилятор вставляет целый блок своего кода. Поэтому с
ненастоящими функциями вызов по указателю не прокатит. Если ты попробуешь сделать так:
<br />
type _AssignFile = procedure (var f: File; FileName: String);<br />
var f: File;<br />
begin<br />
<br />
_AssignFile(@AssignFile)(f,'test.txt');<br />
<br />
end.<br />

то компилятор просто откажется тебя понимать и нивкакую не будет компилировать код. Имей
это ввиду. Решить эту проблему можно, сделав функцию-переходник, которая будет уже
вызывать встроенную функцию. В демо-проекте токой переходник сделан для функции
FreeMem.
Еще одна засада поджидает тебя при использовании строк. В нашем примере в функцию
ShowMessage передается непосредственно указатель на строку, но так бывает далеко не всегда.
Строки в Delphi это совсем непростые динамические структуры данных, для обработки которых
компилятор вставляет специальный код. Там происходит выделение памяти в куче, всякие
контроли границ, обработка исключений и т.п. Лучше всего типом String не пользоваться, а
создавать строку непосредственно в функции в виде символьного массива. Примерно так:
<br />
var<br />
str: array[0..4] of Char;<br />
<br />
str[0] := 'T';<br />
str[1] := 'e';<br />
str[2] := 's';<br />
str[3] := 't';<br />
str[4] := #0;<br />

Это конечно неудобно, но зато надежно. Еще вариант передавать указатели на строки с
параметрах функции. Есть и будут другие подводные камни, например при обработке
исключительных ситуаций. Поэтому не забывай про отладчик и постоянно контролируй, чего
тебе Delphi там накомпилировала.
В качестве рабочего примера мы сейчас соорудим секретную функцию, которая будет
прятаться в стеке и записывать имя пользователя в файл.
// Пользовательская функция освобождения памяти
// Заменим этой функцией ненастоящую функцию FreeMem
<br />
procedure FreeMemory(P: Pointer);<br />
begin<br />
FreeMem(P);<br />
end;<br />
<br />
// Секретная функция, которая будет работать в стеке<br />
//Получает в параметрах указатели на строки сообщений<br />
function Secret(pTitle: PChar; pMessage: PChar): Boolean; stdcall;<br />
type<br />
// Прототипы использованных Функций<br />
_AllocMem = function(Size: Cardinal): Pointer;<br />
...........<br />
_MessageBox = function (hWnd: HWND; lpText, lpCaption: PChar;<br />
uType: UINT): Integer; stdcall;<br />
var<br />
FileName: array[0..5] of Char;<br />
FileHandle: Integer;<br />
UserName: PChar;<br />
NameLength: Cardinal;<br />
begin<br />
Result := False;<br />
//Резервируем память под имя пользователя.<br />
NameLength := 256; // Максимальная длина имени пользователя<br />
UserName := _AllocMem(@AllocMem)(NameLength);<br />
// Получаем имя пользователя<br />
_GetUserName(@GetUserName)(UserName,NameLength);<br />
// Создаем строку с именем файла 'х.txt'<br />
FileName[0] := 'x';<br />
FileName[1] := '.'; // Строки надежнее всего создавать так.<br />
FileName[2] := 't';<br />
FileName[3] := 'x';<br />
FileName[4] := 't';<br />
FileName[5] := #0 ; // Не забываем терминальный нуль<br />
// Создаем файл для записи. В модуле SysUtils есть функция FileCreate, но<br />
// она использует в параметрах тип String, что приводит к вставке<br />
// незапланированного кода, поэтому ее не используем.<br />
FileHandle := Integer(_CreateFile(@CreateFile)(@FileName,GENERIC_WRITE,<br />
0, nil, CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, 0));<br />
dec(NameLength); // Обрубаем хвостовой нуль<br />
//Пишем в файл имя пользователя<br />
if _FileWrite(@FileWrite)(FileHandle,UserName^, NameLength) = NameLength<br />
then Result := True;<br />
// Освобождаем ресурсы.<br />
_FileClose(@FileClose)(FileHandle);<br />
// Вызов ненастоящей функции FreeMem мы заменим<br />
// на нашу настоящую функцию FreeMemory<br />
_FreeMemory(@FreeMemory)(UserName);<br />
// Хвалимся успехами<br />
if Result then _MessageBox(@MessageBox)(0,pMessage,pTitle,0);<br />
end;<br />

Часть прототипов функций и различные проверки опущены для краткости (смотри их в
прилагаемом демо-проекте). Имя пользователя получаем как обычно с помощью функции
GetUserName и записываем его в файл x.txt. В конце выскакивает окно сообщения с текстом,
который передали функции в параметрах. Функция объявлены как stdcall, чтобы четко
определиться, что параметры передаются через стек (а не через регистры, как в Delphi по
умолчанию), чтобы удобнее было за ними следить в отладчике.
Теперь остается зарезервировать память в стеке, переместить туда нашу функцию и передать на
нее управление. Для резервирования памяти просто объявим локальный байтовый массив, т.к.
все локальные переменные резервируются в стеке.
<br />
procedure Main;<br />
var<br />
buf: array[0..511] of Byte; // Место в стеке c запасом<br />
<br />

Ну вот. В стеке у нас есть место на полкилобайта с запасом на всякий случай. Осталось
скопировать туда функцию Secret. Для копирования можно использовать CopyMemory или
Move.
Move(Secret,buf,FuncLen);
Стоп! А чему равна FuncLen — длина функции Secret? Здесь придется делать допущение, что в
скомпилированном коде функции будут располагаться в том же порядке, как мы написали в
исходнике (и это действительно так). Поэтому если сделаем так:
<br />
program Project1;<br />
...<br />
function Secret(pTitle: PChar; pMessage: PChar): Boolean; stdcall;<br />
...<br />
procedure Main;<br />
begin<br />
//Здесь копируем в стек функцию Secret<br />
...<br />
end;<br />
begin<br />
Main;<br />
end.<br />

и получается, что длину функции можно будет вычислить путем вычитания адреса функции
Secret из адреса функции Main:
<br />
FuncLen := Cardinal(@Main) - Cardinal(@Secret);<br />

И еще один момент. Раньше можно было без проблем выполнять код, расположенный в стеке,
но теперь с этим стали жестоко бороться и запрещать исполняемый стек программными а
аппаратными средствами. (если хочешь знать больше, то ищи статьи про DEP — Data Execution
Prevention) По идее это должно привести с уменьшению количества атак типа buffer overflow
(переполнение буфера). Поэтому, чтобы код работал везде, надо явно разрешить исполнение
кода в странице памяти стека (куда мы копируем функцию). Станица памяти имеет размер 2 кб
и с помощью VirtualProtect нам нужно изменить атрибуты 2-х страниц, на тот случай, если
функция окажется в конце первой страницы.
<br />
VirtualProtect(buf, 2, PAGE_EXECUTE_READWRITE, OldPageProtection);<br />

В общем виде код функции Main выглядит так:
<br />
procedure Main;<br />
var<br />
FuncLen: Cardinal; // Размер кода функции<br />
buf: array[0..511] of Byte; // Место в стеке c запасом<br />
MovedFunction: _Secret; // Указатель на функцию в стеке<br />
OldPageProtection: Cardinal;// СТарые аттрибуты страницы памяти<br />
title, ok_string: String; // Строки для всплывающего окна<br />
begin<br />
title := 'Пример выполнения кода в стэке';<br />
ok_string := 'РАБОТА В СТЕКЕ: Файл успешно создан.';<br />
// Загоняем функцию Secret в стэк<br />
FuncLen := Cardinal(@Main) - Cardinal(@Secret);<br />
Move(Secret,buf,FuncLen);<br />
MovedFunction := @buf; //Устанавливаем указатель на начало функции в стеке<br />
// Устанавливаем в стеке аттрибут выполнения кода<br />
if not VirtualProtect(@buf,2,PAGE_EXECUTE_READWRITE,OldPageProtection)<br />
then Exit;<br />
try<br />
// Запускаем функцию в стеке<br />
if MovedFunction(PChar(title), PChar(ok_string)) then<br />
ShowMessage('Работа в стеке прошла успешно.')<br />
else<br />
ShowMessage('Ошибка при работе в стеке.');<br />
// Восстанавливаем аттрибуты страниц<br />
VirtualProtect(@buf,FuncLen,OldPageProtection,OldPageProtection);<br />
except<br />
ShowMessage('Hello hacker!');<br />
end;<br />
end;<br />

Код полностью откомментирован. Вопросов быть не должно. Вызов секретной функции
находится в блока trу..except. Это надо делать в любом случае. Мало ли что может произойти у
пользователя на компе. Но нам обработка исключений потребуется для совершенно
конкретного случаю ловли взломщика! J
Компилим экзешник (до сих пор использую Delphi 7) и запускаем его. Появляются подряд два
окошка, которые сообщают, что все идет нормально, а каталоге программы создается файл x.txt
с именем пользователя.
Теперь попробуем поймать вызов секретной функции. Мы знаем, что имя пользователя
получается с помощью функции GetUserName. Поэтому загружаем экзешник в отладчик
OllyDbg и ставим точку останова на функции GetUserNameA (bpx GetUserNameA в командной
строке). Запускаем на выполнение — вываливается сообщение «Hello hacker!», а отладчик
всплывает где-то в ntdll.dll Файл x.txt не создается. Тоже самое происходит при попытке
отладить секретную функцию с помощью стандартного отладчика Delphi. Но вот если
поставить железный бряк (hardware on execution), то отладчик послушно всплывает посередине
стека и позволяет полностью протрассировать код секретной функции.
Получился довольно хитрый антиотладочный прием против начинающих взломщиков.
Разберемся, как он работает? Обязательно! Когда отладчик ставит бряк, то он ставит на это
место специальную инструкцию int 3 (опкод CC), которая вызывает отладочное исключение.
Это исключение отладчик отлавливает и сразу заменяет это CC на тот байт, который стоял на
этом месте до установки бряка.
А теперь смотри, что получается у нас. Вот окрестности вызова GetUserNameA в стеке. Серым
подсвечена инструкция, которую заменит отладчик.
www.stranger.nextmail.ru/del7.jpg
Точка останова (инструкция int3) ставится в секции кода на вызове функции GetUserNameA, но
этот код никогда не исполняется. Когда мы копируем функцию в стек, то вместе с остальным
кодом копируется и инструкция int3.
www.stranger.nextmail.ru/del8.jpg
Но об этой новой точке останова отладчик ничего не знает, т.к. он ее не устанавливал! Поэтому
int 3 в коде секретной функции не удаляется. Процессор ее выполняет и возникает исключение.
Его-то и ловит блок try..except и приветствует неудавшегося взломщика. И кроме того посмотри
как изуродовался код после int3. В случае с железным бряком никакого изменения кода не
происходит, поэтому трассировка проходит нормально. Собственно таким образом я и получил
эти скриншоты.Если тебе все еще показалось мало описанных в этой статье извращений, то никто не мешает
взять на вооружение технику модификации кода, описанную в первой части. Изменяй код
скопированной с стек функции, как твоей душе будет угодно. Здесь я этого делать не стал,
чтобы совсем не запутать. В результате идет лесом не только отладчик, но еще и дизассемблер.
Можно даже прикрутить к проге простенький дизассембер и удалять из скопированного кода
точки останова. Хотя, имхо, это уже стрельба из пушки по воробьям, т.к. железный бряк сведет
на нет все усилия.


©StraNger from Network Angels Team
Нравится
Не нравится

3 комментария

20:49
+1
01:53
Ты не являешься автором данной статьи. Ее написал парень в журнале Romul из VR Online Team.
06:10
хе)
тогда напиши ему и спроси кто действительно автор =0