Надругательство над 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 Поменяем местами атрибуты «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( Первый параметр – адрес, с которого начинается область памяти, атрибуты страниц которой подлежат модификации. Второй параметр – размер этоой области памяти (в нашем случае – 8 байт). Третий – байт-код, представляющий собой новые атрибуты для области памяти. Из справочной литературы или с помощью такой утилиты, как LordPE, узнаем, что значение, соответствующее атрибуту «RWE», будет равно 40h. Наконец, последний параметр – это указатель на переменную, которая содержит старые атрибуты. В качестве этого параметра в стек можно положить любой адрес, по которому находится нулевая переменная. Я выбрал адрес 00401060. Естественно, в стек все параметры кладутся, начиная с последнего. Чуть раньше мы получили машинный код измененных библиотечных инструкций. Поместим их в теле нашей программы. Выделяй восемь байт, начиная с адреса 00401100, и нажимай <ctrl+e>. После чего в нижнем поле появившегося окна вписывай сохраненные нами ранее значения. Подтверждай изменения нажатием на кнопку «Ok». Самое время написать код, который будет «патчить» системную библиотеку. Здесь все предельно просто: в регистр EAX при помощи команды MOV будем помещать данные, которые необходимо записать поверх оригинального кода API-функции. В регистр EBX с помощью той же инструкции MOV положим адрес, по которому будем производить запись (его мы также получили ранее, – 7E3A05C1). Производить запись будем при помощи инструкции MOV [EBX], EAX. После этих манипуляций необходимо вызвать функцию MessageBoxA, которая была модифицирована, и передать управление на инструкцию, следующую сразу за командой безусловного перехода на наш код. Последнее замечание – необходимо сохранить все регистры перед началом выполнения нашего кода и возвратить их в исходное состояние после его выполнения при помощи пары команд pushad/popad. В результате получим следующий код: 00401026 PUSHAD; сохраняем регистры в стек После того, как введешь его, пронаблюдай результат выполнения программы. Два атрибута 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; сохраняем регистры в стек Как видишь, код простой, но дам кое-какие комментарии. Почему мы поместили в регистр 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 Патч-код для инструкции «ret» состоит из пяти байт, а регистр – четырехбайтовый. Разобьем операцию патчинга на два подхода: 00401065 MOV EAX,FFFEC6E9; помещаем в EAX четыре байта патч-кода Код почти готов. Осталось восстановить регистры командой POPAD и передать управление перенесенной в секцию кода и исправленной API-функции MessageBoxA (инструкцией JMP 00401102). Вот как выглядит написанный нами код целиком: 00401026 PUSHAD Если все было сделано правильно, перенесенная API будет работать. Этот метод замечателен тем, что мы можем как угодно модифицировать код API, не имея возможности изменить атрибуты страниц памяти, где расположена системная библиотека. Конец?!Естественно, это не конец, ибо существует масса интересных идей. Технические возможности огромны, ум реверсера остер, а совершенных защитных механизмов не существует. О многих методах патчинга API мы еще расскажем, до других ты, наверняка, додумаешься сам. Все зависит только от твоего воображения. Удачи во взломах! |