Издательский дом ООО "Гейм Лэнд"ЖУРНАЛ ХАКЕР #118, ОКТЯБРЬ 2008 г.

Надругательство над API: Модификация API-функций конкретным приложением

Леонид «Cr@wler» Исупов (crawlerhack@rambler.ru)

Вспомни, тебе когда-нибудь требовалось изменить функциональность API, вызываемой из системной библиотеки? При этом нередко случается так, что модификация DLL «в лоб» (с сохранением изменений в файле библиотеки), невозможна. К счастью, есть другие методы, позволяющие эффективно менять код API-функций по своему усмотрению. Без особых трудностей модифицировать можно даже системную DLL. Заинтересовался? Тогда читай.

Существует два подхода к реверсингу динамически загружаемых библиотек. Первый – отлаживать dll «непосредственно», то есть – загружая ее в отладчик с помощью некоторого вспомогательного процесса. Второй – отлаживать необходимую библиотеку в контексте реального процесса. У второго способа есть неоспоримое преимущество – на конкретную API-функцию можно поставить точку останова и при передаче программой параметров в стек проследить за механизмом ее работы. Именно поэтому я предлагаю при отладке dll пользоваться вторым методом.

Самый надежный метод

С методом отладки определились. Возникает вопрос, – каким образом применить изменения, которые мы совершим во время отладки? Вариантов масса. Почти каждому придет на ум сохранить измененную dll. Этот вариант практически никогда не годится! Подумай сам: как будут чувствовать себя программы, которые используют модифицированную dll? Думаю, что многочисленных ошибок не избежать. К тому же, антивирусы могут заподозрить что-то неладное. Есть альтернативный путь: сохранить dll под другим именем и изменить таблицу импорта целевой программы так, чтобы вызывалась API-функция не оригинальной, а модифицированной библиотеки. Но тогда придется таскать за собой лишний файл, что тоже не всегда приемлемо. Третий вариант: модифицируем dll, загруженную в память, прямо из программы, которая ее использует. Хотя этот метод тоже может расцениваться антивирусами как действие вредоносного характера, он менее заметен. Наконец, четвертый, самый надежный (но и самый сложный) метод – перенос исполняемого кода конкретной API-функции в тело программы. Тут антивирусам придраться будет не к чему. Так как первые два метода довольно просты, да и скрыть «деяние» невозможно, рассмотрим два последних способа.

Патчинг DLL с применением VirtualProtect

Попробуем внедриться в тело dll, загруженной процессом, и изменить ее, что называется, «изнутри». Отлаживать будем файлик, который вызывает API-функцию MessageBoxA. Ты можешь найти программу, которую отлаживал я, на нашем DVD, но вообще, подойдет любой. Попробуем поменять местами параметры, которые принимает функция. Сначала взглянем на них, протрассировав по <F7> до инструкции «call user32.MessageBoxA» и попав в тело библиотеки user32.dll:

7E3A05BC PUSH 0 ;LanguageID = 0
7E3A05BE PUSH DWORD PTR SS:[EBP+14];|Style
7E3A05C1 PUSH DWORD PTR SS:[EBP+10];|Title
7E3A05C4 PUSH DWORD PTR SS:[EBP+C]; |Text
7E3A05C7 PUSH DWORD PTR SS:[EBP+8]; |hOwner
7E3A05CA CALL user32.MessageBoxExA; MessageBoxExA

Поменяем местами атрибуты «Title» и «Text» (то есть заголовок окна и его текст). Для этого просто поменяем инструкцию, расположенную по адресу 7E3A05C1 на инструкцию, которая находится по адресу 7E3A05C7, – и наоборот. Я думаю, ты знаешь, что внести необходимые изменения можно в окне, которое появляется по двойному щелчку левой кнопки мыши на нужной команде. Теперь выделим три инструкции, начиная с адреса 7E3A05C1, и из меню правой кнопки мыши выберем «Binary -> Binary copy» для получения машинных кодов измененных инструкций (почему три инструкции, а не две, я объясню позже).

В буфер обмена будет помещен следующий код: FF 75 0C FF 75 10 FF 75 08. Последний байт нам не нужен, – его отрезаем. Останется восемь необходимых нам байт. Второе значение, которое понадобится в ходе выполнения задачи: адрес, начиная с которого размещены измененные инструкции: 7E3A05C1.

Итак, почему же мы скопировали машинный код, состоящий из восьми байт, хотя модифицировали инструкции только в пределах шести байт? Все очень просто – оперировать с данными мы будем, используя четырехбайтные регистры. Соответственно, и писать в память мы будем по 4 байта. Минимальное число, кратное четырем и большее шести, - восьмерка. Приступим к модификации программы.

Перезапускаем отлаживаемый процесс. Вызов функции MessageBoxA, располагающийся по адресу 0040100E, заменяем на безусловный переход: jmp 00401026 (начиная с этого адреса, будет располагаться написанный нами код). К сожалению, системные библиотеки, располагающиеся в контексте процесса, защищены от записи (страницы памяти, в которой размещена dll, имеют атрибут «readable»). Нам необходимо выставить атрибут «RWE», – разрешить запись в область памяти. Легче всего проделать это при помощи вызова системной функции VirtualProtect с соответствующими параметрами, переданными в стек. Вот ее прототип:

function VirtualProtect(
lpAddress: Pointer;// начальный адрес области памяти
dwSize: DWORD;// размер области памяти
flNewProtect: DWORD;// новые атрибуты защиты
lpflOldProtect: Pointer// указатель на переменную, содержащую старые атрибуты защиты
): BOOL; stdcall; overload;

Первый параметр – адрес, с которого начинается область памяти, атрибуты страниц которой подлежат модификации. Второй параметр – размер этоой области памяти (в нашем случае – 8 байт). Третий – байт-код, представляющий собой новые атрибуты для области памяти. Из справочной литературы или с помощью такой утилиты, как LordPE, узнаем, что значение, соответствующее атрибуту «RWE», будет равно 40h. Наконец, последний параметр – это указатель на переменную, которая содержит старые атрибуты. В качестве этого параметра в стек можно положить любой адрес, по которому находится нулевая переменная. Я выбрал адрес 00401060. Естественно, в стек все параметры кладутся, начиная с последнего.

Чуть раньше мы получили машинный код измененных библиотечных инструкций. Поместим их в теле нашей программы. Выделяй восемь байт, начиная с адреса 00401100, и нажимай <ctrl+e>. После чего в нижнем поле появившегося окна вписывай сохраненные нами ранее значения. Подтверждай изменения нажатием на кнопку «Ok». Самое время написать код, который будет «патчить» системную библиотеку. Здесь все предельно просто: в регистр EAX при помощи команды MOV будем помещать данные, которые необходимо записать поверх оригинального кода API-функции. В регистр EBX с помощью той же инструкции MOV положим адрес, по которому будем производить запись (его мы также получили ранее, – 7E3A05C1). Производить запись будем при помощи инструкции MOV [EBX], EAX. После этих манипуляций необходимо вызвать функцию MessageBoxA, которая была модифицирована, и передать управление на инструкцию, следующую сразу за командой безусловного перехода на наш код. Последнее замечание – необходимо сохранить все регистры перед началом выполнения нашего кода и возвратить их в исходное состояние после его выполнения при помощи пары команд pushad/popad.

В результате получим следующий код:

00401026 PUSHAD; сохраняем регистры в стек
; далее следуют четыре параметра, передаваемые в стек перед вызовом VirtualProtect
00401027 PUSH 00401060; указатель на нулевой байт - параметр "lpflOldProtect"
0040102C MOV EAX,40; Помещаем в EAX параметр "flNewProtect", который разрешает запись в область памяти, где располагается наша DLL
00401031 PUSH EAX; кладем параметр в стек
00401032 PUSH 8; параметр "dwSize", который представляет собой размер области памяти
00401034 PUSH 7E3A05C1; параметр "lpAddress" - начальный адрес области памяти
00401039 CALL VirtualProtect; вызываем VirtualProtect
0040103E MOV EAX,[401100]; помещаем в eax первые 4 байта машинного кода
00401043 MOV EBX,7E3A05C1; в ebx помещаем адрес, по которому будет записано машинное слово
00401048 MOV [EBX],EAX; записываем машинный код в тело API-функции
0040104A MOV EAX,[401104]; помещаем в eax 4 последних байта машинного кода
0040104F MOV EBX,7E3A05C5; в ebx помещаем адрес, по которому будет записано машинное слово
00401054 MOV [EBX],EAX; записываем машинный код в тело API-функции
00401056 POPAD; восстанавливаем регистры перед вызовом MessageBoxA
00401057 CALL MessageBoxA
0040105C JMP 00401013; переходим к дальнейшему выполнению программы

После того, как введешь его, пронаблюдай результат выполнения программы. Два атрибута API-функции MessageBoxA поменялись местами, причем в результате инлайн-патчинга кода библиотеки, располагающейся в памяти.

Воровство кода

Теперь представим на минуту, что у нас нет доступа к изменению атрибутов страниц памяти. Пускай даже и есть, – любой антивирус может заподозрить, что процесс совершает какую-то гадость, если изменяет параметры памяти, где расположена системная библиотека. Здесь придется придумать нечто особенное. Я предлагаю перенести инструкции API-функции, которую необходимо пропатчить, в свободную область секции кода нашей программы. Делается это элементарно: с помощью той же инструкции mov, только помещенной в цикл.

Для примера попробуем перенести в секцию кода и пропатчить ту же самую функцию, что мы рассматривали ранее, – MessageBoxA. Сначала выберем адрес, где будет базироваться перемещаемый код API-функции. Пусть это – 00401100. Теперь откроем уже рассмотренный нами файл ex.exe под OllyDbg и протрассируем код вплоть до вызова MessageBoxA (куда зайдем по <F7>). Сделав несложный подсчет, определяем, что API-функция занимает в памяти 48h байт. Запомним! Также запишем на клочке бумаги базовый адрес функции – 7E3A058A. Код, который будет считывать из памяти машинный код API и размещать его в секции кода программы, расположим по адресу 00401026. А значит, вызов

0040100E CALL <JMP.&user32.MessageBoxA>

Заменим инструкцией перехода к нашему коду:

0040100E JMP 00401026

Вот как будет выглядеть цикл, копирующий тело API в секцию кода нашей программы:

00401026 PUSHAD; сохраняем регистры в стек
00401027 MOV EAX,7E3A0588; базовый адрес API-функции минус 2 байта
0040102C MOV ECX,48; ecx - счетчик цикла, помещаем в регистр размер API-функции
00401031 MOV BX,[EAX+ECX]; помещаем в двухбайтовый регистр BX 2 байта из тела API-функции
00401035 MOV [ECX+401100],BX; записываем содержимое BX в память по адресу ECX+401100
0040103C SUB ECX,2; уменьшаем значение счетчика на 2
0040103F JNZ 00401031; продолжаем выполнять цикл, если ECX не равен нулю

Как видишь, код простой, но дам кое-какие комментарии. Почему мы поместили в регистр EAX не базовый адрес, а базовый адрес -2? В последней итерации цикла значение счетчика ECX будет равно 2, а 2+7E3A0588 и дает базовый адрес. Следующие инструкции последовательно, по 2 байта, копируют тело API-функции (соответственно, и счетчик мы установили с шагом 2 – при помощи инструкции «SUB ECX, 2»).

Казалось бы, все сделано. Но запуск такого кода обречен на неудачу. Почему? Мы перенесли функцию, которая жестко привязана к базовому адресу, а это значит, что вызовы и инструкции типа «ret» становятся недействительными. Чтобы придать перенесенному коду работоспособность, придется его пропатчить. Для начала получим машинные коды, которые будем «накладывать» на перемещенную API-функцию, исправляя проблемные места. Поставь точку останова по адресу 00401041 и запусти программу на исполнение (<F9>). После смело переходи по адресу 00401100, чтобы посмотреть, какие области перенесенного кода нуждаются в патчинге.

Нас интересуют четыре инструкции, которые мешают нормальной работе функции. Первая – команда помещения в стек заголовка окна, параметр не передается. Вот как выглядит инструкция:

00401136 PUSH [EBP+14]

Та же ситуация – и с инструкцией, передающей параметр-хэндл родительского окна.

0040113F PUSH [EBP+8]

Обе этих команды при патчинге будем заменять инструкцией PUSH 0. Машинный код этой инструкции выглядит как 6A00. Намотаем это на ус. Следующая инструкция, которая была испорчена при переносе кода API-функции, – вызов MessageBoxExA:

00401142 CALL 00401174

Ее также следует пропатчить! Машинный код, необходимый для этого, получим так: дважды нажмем на инструкцию, расположенную по адресу 00401142, левой кнопкой мыши и в появившемся окне введем «call MessageBoxExA» (без кавычек). После этого нажмем на исправленную инструкцию правой кнопкой мыши и из контекстного меню выберем «Binary -> Binary copy». Вот полученный четырехбайтный машинный код: E8B5F4F97D.
Последняя инструкция, с которой следует «разобраться», – это операция возврата:

00401148 RETN 10

Заменим ее переходом к основному коду программы («JMP 00401013») и затем получим машинные коды этой инструкции при помощи той же команды «Binary -> Binary copy». Вот код этой инструкции: E9C6FEFFFF.

Самое время написать код, который будет совершать патчинг. Если учесть, что у нас есть машинные коды инструкций и адреса, по которым необходимо их записать, то исправляющий код будет выглядеть очень просто. Единственная ремарка: в память патч-код будет помещаться «задом наперед», начиная с «хвоста» регистра и заканчивая его «головой». Значит, в регистр машинные коды будем заносить, предварительно «перевернув их».

Для первых трех инструкций патч-код будет выглядеть следующим образом:

00401041 MOV EAX,FF90006A; помещаем патч-код для первой инструкции (задом наперед) в регистр EAX
00401046 MOV EBX,00401136 помещаем в EBX адрес, по которому будем записывать машинный патч-код
0040104B MOV [EBX],EAX; записываем содержимое регистра EAX по адресу, помещенному в EBX
; аналогично - для второй и третьей инструкций:
0040104D MOV EAX,E890006A
00401052 MOV EBX,0040113F
00401057 MOV [EBX],EAX
00401059 MOV EAX,7DF9F4B5
0040105E MOV EBX,00401143
00401063 MOV [EBX],EAX

Патч-код для инструкции «ret» состоит из пяти байт, а регистр – четырехбайтовый. Разобьем операцию патчинга на два подхода:

00401065 MOV EAX,FFFEC6E9; помещаем в EAX четыре байта патч-кода
0040106A MOV EBX,00401148; помещаем адрес в EBX
0040106F MOV [EBX],EAX; записываем патч-код
00401071 MOV EAX,0FF; помещаем в EAX оставшийся, последний, байт патч-кода
00401076 MOV EBX,0040114C; в EBX помещаем адрес
0040107B MOV [EBX],EAX; патчим

Код почти готов. Осталось восстановить регистры командой POPAD и передать управление перенесенной в секцию кода и исправленной API-функции MessageBoxA (инструкцией JMP 00401102). Вот как выглядит написанный нами код целиком:

00401026 PUSHAD
00401027 MOV EAX,7E3A0588
0040102C MOV ECX,48
00401031 MOV BX,[EAX+ECX]
00401035 MOV [ECX+401100],BX
0040103C SUB ECX,2
0040103F JNZ 00401031
00401041 MOV EAX,FF90006A
00401046 MOV EBX,00401136
0040104B MOV [EBX],EAX
0040104D MOV EAX,E890006A
00401052 MOV EBX,0040113F
00401057 MOV [EBX],EAX
00401059 MOV EAX,7DF9F4B5
0040105E MOV EBX,00401143
00401063 MOV [EBX],EAX
00401065 MOV EAX,FFFEC6E9
0040106A MOV EBX,00401148
0040106F MOV [EBX],EAX
00401071 MOV EAX,0FF
00401076 MOV EBX,0040114C
0040107B MOV [EBX],EAX
0040107D POPAD
0040107E JMP 00401102

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

Конец?!

Естественно, это не конец, ибо существует масса интересных идей. Технические возможности огромны, ум реверсера остер, а совершенных защитных механизмов не существует. О многих методах патчинга API мы еще расскажем, до других ты, наверняка, додумаешься сам. Все зависит только от твоего воображения. Удачи во взломах!

Содержание