Введение в крэкинг с нуля, используя OllyDbg - Глава 33 — Архив WASM.RU
Что такое IAT, и как её починить
Перед тем, как начинать, как безумные, чинить IAT’ы, необходимо введение в то, что такое вообще IAT. Мы рассмотрим на примере оригинального крэкми CrueHead’а и упакованного варианта, где располагается IAT и что делает с ним упаковщик.
Ок, сначала общая идея, для чего нужен IAT.
Дело в том, что у каждой API-функции есть свой адрес, например, откроем оригинальный крэкми CrueHead’а в OllyDbg и напечатаем:
![]()
Видим, что на моей машине адрес этой функции 77D50EA, если вы пойдёте по соответствующему адресу на вашей машине, то у одних это может быть тот же адрес, у других нет, зависит от вашей версии Windows, от установленных обновлений, которых, как мы знаем множество, и каждая новая версия DLL, содержащая API-функцию (в данном случае это User32.dll) обычно меняет адрес.
![]()
Почти каждый раз, когда Microsoft выпускает новую версию этой DLL, меняются адреса API-функций, входящих в неё, поэтому если я запрограммирую в крэкми CrueHead’а, чтобы при совершении перехода на API-функцию MessageBoxA, это был переход на 77D504EA, то на моей машине это будет прекрасно работать, а также у тех, у кого та же версия User32.dll, но у остальных, у кого не Windows XP или стоит другая версия User32.dll, нужной функции по этому адресу не будет, и произойдёт ошибка.
Тогда вопрос заключается в том, что операционная система должна предоставлять какой-то способ, чтобы мой крэкми работал на других Windows и с другими версиями DLL.
Для этого были созданы знаменитые таблицы под названием IT (import table – таблица импортов) и IAT (Import Address Table – таблица импортированных адресов).
Не надо бояться этих страшных названий – в этой главе мы подробно рассмотрим, где они находятся в распакованной программе и для чего служат.
![]()
Теперь делаем SEARCH FOR-ALL INTERMODULAR CALLS и видим вызовы, которые идут в другие модули или DLL, то есть могут быть вызовами API-функций.
![]()
Здесь видим различные вызовы, например функции MessageBoxA, если сделаем двойной щелчок по первой из них:
![]()
Видим, что на самом деле это вызов CALL 40143A, и OllyDbg нам поясняет, что произойдёт переход на API-функцию MessageBoxA. Она заключает MessageBoxA в угловые скобки, чтобы показать, что вызов прямой, а не косвенный.
CALL 40143A
![]()
Видим, что среди опций OllyDbg во вкладке DISASM есть SHOW SIMBOLIC ADDRESSES. Если уберём её, то всё становится более ясным.
![]()
Видим, что справа также остаются пояснения с параметрами и названием API-функции, куда будет совершён переход, но теперь CALL действительно похож на прямой переход.
CALL 40143A
В SEARCH FOR INTERMODULARS CALLS
![]()
Видим, что в реальности три вызова MessageBoxA – это вызовы 40143A, посмотрим, что там находится.
![]()
Здесь видим, что вся загвоздка тут, чтобы добраться до API-функции, делает косвенный JMP, который таким образом получает значение, куда надо на самом деле перейти, из 4031AC.
JMP [4031AC]
![]()
Также видим, что если включить эту опцию, то понять, что это косвенный переход сложнее, поэтому её лучше выключать, когда работаем с IAT.
И вот этот трюк: программа переходит на API-функцию с помощью косвенного JMP, который получает адрес API-функции из 4031AC, и видим, что для остальных функций есть похожие косвенные переходы.
Кажется, вопрос о переносимости между разными компьютерами начинает проясняться. Когда программе нужно перейти на какую-либо API-функцию, делается косвенный переход, считывается адрес, по которому та располагается, из некоего рода хранилища. Посмотрим через DUMP, что это за хранилище.
![]()
Здесь, и правда, видим, что 4031AC – это часть хранилища, которое содержит все адреса API-функций, по которым располагаются последние на моей машине. Это хранилище и есть знаменитая IAT или IMPORT ADRESS TABLE, которая является ключом к переносимости, и это означает, что все программы имеют одни и те же косвенные переходы на API-функции, а меняются только значения, находящиеся в хранилище. На разных машинах эти значения могут отличаться.
Но постой, скажет мне кто-то, 4031AC – это адрес в программе, поэтому если на каждой машине такие адреса могут отличаться, то и он может отличаться в разных случаях?
Хе-хе, хороший вопрос (маэстро сам себе плохих вопросов не задаст - прим. пер), сейчас мы рассмотрим, как вся эта система работает. Главная её цель заключается в том, чтобы наполнить IAT правильными адресами.
![]()
Мы знаем, что если сделать VIEW-EJECUTABLE FILE, то можно увидеть, что находится по адресу 4031AC в самом исполняемом файле, находящемся на нашем жёстком диске.
![]()
Видим, что в exe-файле по этому адресу находится значение 60 33, а когда мы находимся на точке входа, то в памяти в этом месте находится адрес API-функции EA 04 D5 77, и это означает, что операционная система, загружая программу, берёт значение 33600, находящееся по СМЕЩЕНИЮ 0FAC, которое соответствует адресу 4031AC, и замещает его реальным адресом API-функции на моей машине.
Магия?
Нет-нет, так работает Windows, для каждого запускающегося исполняемого файла операционная система заполняет IAT правильными для моей машины адресами API-функций, в данном случае в 4031AC помещается адрес API-функции MessageBoxA, а соседние элементы IAT заполняются адресами других функций, пока вся IAT не будет заполнена.
Конечно, система не настолько магическая, как можно подумать, видим, что в 0FAC находится значение 3360, которое указывает на что-то, может быть на то, какую API-функцию надо вызывать?
Если прибавим 3360 к базе образа, то получим 403360, и что мы найдём по этому адресу?
![]()
Вот в чём дело, это указатель на строку MessageBoxA, то есть система смотрит на этот указатель, ищет нужную функцию по имени и, с помощью GetProcAddress, получает её адрес на нашей машине, а затем заносит его в IAT, перезаписывая им 3360. Таким образом, гарантируется, что программа будет работать с любой версией DLL, так как адрес будет найден ещё до того, как выполнение перейдёт к точке входа в программу, а как только оно там окажется, IAT уже будет заполнен адресами API-функций. Если посмотрим IAT на другой машине, то увидим, что содержимое 4031AC отличается, так как адрес функции другой.
JMP [4031AC]
Переход на MessageBoxA, и программисту не нужно думать о тонкостях, так как значение в IAT всегда будет правильным, будучи заполненным правильными адресами при старте программы.
То есть, чтобы система корректно заполнила IAT адресами API-функций, в файле должны находится следующие указатели:
1)В элементе IAT в самом exe-файле должен находится указатель на строку с текстом, который говорит системе, адрес какой API-функции нужно поместить в этот элемент.
2)Конечно, строка с текстом должна быть именем API-функции.
Если эти два пункта выполнены, то можем заключить, что программа запустится, и система заполнит IAT правильными значениями (позже рассмотрим полностью, как всё это работает).
Ок, мы оказываемся на OEP распакованной программы, вопрос заключается в том, когда мы туда добрались и делаем дамп программы, зачем нам нужно реконструировать IAT, упаковщики её, что, ломают? Да, одни больше, другие меньше, смысл в том, что упаковщику не нужен IAT программы, потому что загрузка начинается с процедуры распаковки, которая может прочитать какие API-функции нужны в IAT, узнать их адреса и заполнить ими IAT, строки же, которые задают имена функций, стираются, так как они программе не нужны – когда она будет загружена, IAT уже содержит правильные адреса.
Большинство упаковщиков хранят эти строки в зашифрованном виде или в таком месте, где найти их нам, крэкерам, не так уж просто.
Посмотрим на примере крэкми, запакованного UPX’ом, отличие от нормального исполняемого файла.
![]()
Вот у нас крэкми CrueHead’а, упакованный UPX’ом, который мы уже использовали в прошлых главах.
Смотрим, что находится по адресу 4031AC (в оригинальном крэкми там находится хранилище).
![]()
Ничего, всё стёрто, а строки с именами API-функций, располагавшиеся в 403360, где они?
![]()
Ничего, совсем ничего, доходим до OEP и смотрим, что находится по тем же адресам, так как чтобы программа работала, в IAT должно быть что-то, чтобы был совершён переход на API-функцию.
JMP [4031ac]
Если IAT пуста, то это вызовет ошибку, то есть упаковщик должен сделать работу, которую обычно делает операционная система. Идём в OEP.
![]()
Устанавливаем BP на переход на OEP, делаем RUN и доходим досюда.
Смотрим, что находится в том месте, где располагается IAT.
![]()
Видим, что для обеспечения нормальной работы программы распаковщик заполняет IAT правильными адресами API-функций на моей машине. Если мы сдампим программу в этот момент, то возникнет проблема, так как у получившейся программы не будет достаточно данных, чтобы запуститься.
Строки, которые идентифицировали каждую из API-функций и находились в 403360, сейчас там отсуствуют.
![]()
Сделаем дамп, чтобы посмотреть, что останется, но как мы уже отметили, хотя код программы будет правильный, но запуститься сдампленная программа не запустится, так как система не сможет заполнить IAT из-за отсутствия данных.
Для того, чтобы сделать дамп, будем использовать внешнюю программу, которая называется LORDPE DELUXE. Её можно скачать отсюда.
![]()
Запускаем LORD PE DELUXE, ищем процесс, который нужно сдампить, в нашем случае это crackmeUPX.
![]()
Находим, отмечаем.
![]()
Нажимаем правую кнопку мыши, выбираем INTELLIDUMP, а затем – DUMP FULL.
![]()
Дамп сохраняется под именем DUMPED.exe.
![]()
Si corremos el dumpeado vemos que no arranca tratemos de abrirlo en OLLYDBG
Если попробуем запустить его, то увидим, что происходит ошибка. Открываем дамп в OllyDbg.
Посмотрим, какие ошибки показываются в LOG’е OllyDbg.
![]()
Ошибка происходит в 7c929913 на моей машине. Идём по этому адресу (у вас он может быть другим, поэтому надо смотреть в LOG’е).
![]()
Устанавливаем здесь HARDWARE BREAKPINT ON EXECUTION и BP, что увидеть, что происходит до возникновения ошибки.
Ок, видим, что останова не произошло, но зато случается другая ошибка из-за исключения. Рестартуем:
![]()
Видим, что останавливаемся до прибытия в точку входа из за ошибки.
Раз уж мы оказались здесь, смотрим, что находится в IAT.
![]()
Видим, что дампер сохранил значения, которые были в IAT на момент прибытия в OEP, и они являются правильными на моей мошине, но для того, чтобы exe-файл запускался на любой машине, там должен быть сохранён указатель на строки с именами API-функций, а так как система не может их найти, то и происходит ошибка.
Не нужно быть гением, чтобы понять, что для того, чтобы программа запустилась, нужно восстановить имена API-функций, и указатели на них. Если делать это вручную, то это адская прорва работы: нужно было бы заменить содержимое 4031AC на указатель на строку MessageBoxA, и так со всеми.
Итак, нам брошен вызов: кроме того, что нам нужно было получить правильный дамп, нам ещё нужно починить его, чтобы он запускался.
Так как у нас правильный код, то без всяких сомнений идём в 401000, и смотрим, что там находится.
![]()
Если переместимся в зону переходов на API-функции:
Здесь находится вызов MessageBoxA, уже виденный нами ранее.
![]()
В вызове стоит правильный адрес, переходим на тот же JUMP, что и раньше.
![]()
JUMP’ы являются тем, что может не работать, так как в нормальных условиях система должна заполнить IAT правильными значениями, основываясь на указателях на строки с названиями функций, которые должны быть здесь до старта программы.
Прежде, чем заняться данной областью, посмотрим правильные определения и их расположение в исходной программе с неупаковонными IAT и IT.
Откроем оригинальный CC в OllyDbg.
![]()
Ок, находим нужные нам части, которые должны нормально работать в исходном файле.
В заголовке находим несколько важных указателей.
Идём туда же.
![]()
![]()
Меняем режим SPECIAL-PE HEADER.
![]()
Спускаемся.
![]()
Реальные данные идут с 100, где начинается PE SIGNATURE.
![]()
«Начинается со 100» - это значит, что начинается в 400100.
![]()
Здесь находится указатель на IT, то есть таблицу импорта, которую не надо путать с IAT
- IT = IMPORT TABLE
- IAT = IMPORT ADDRESS TABLE
Как видим, IAT – это хранилище, где система сохраняет после запуска правильные для моей машины адреса. А что такое IT? Идём туда. Как видим, она начинается с 3000 (403000), и её размер 670, то есть заканчивается в 403670. Смотрим, что там.
Поскольку мы ушли из заголовка, выходим из MODE SPECIAL-PE HEADER.
![]()
Это знаменитая IT, она у нас в руках, если мы поймём её, и хотя мы не знаем номер каждого из указателей, мы знаем, чем является каждый из элементов и куда указывает. Тогда мы спасены.
Плохо то, что приходится объяснять это в OllyDbg, так как гораздо удобнее и нагляднее делать это, представив IT в виде 5 колонок.
Знаменитая IT состоит из так называемых IMAGE IMPORT DESCRIPTOR’ов, являющиеся строками из 5 dword-значений, по одному дескриптору на каждую из загружающихся с программой DLL-библиотек.
Смотрим IT, помним, что каждый элемент (IID) занимает 5 колонок.
Рассмотрим пример.
![]()
Это первый IID нашей IT, в котором 5 DWORD’ов, означающих следующее:
Три первых обычно не слишком важны для крэкинга, нас интересуют указатели 4 и 5.
- OriginalFirtsThunks
- TimeDateStamp
- ForwarderChain
- Указатель на имя DLL
- FirtsThunk (Указывает туда, где должен производиться в IAT поиск первого вхождения данной DLL)
![]()
Как видим, 4-ый указывает на имя DLL, которой соответствует данный IID, по адресу 403290 смотрим, что это за DLL.
![]()
Здесь у нас первая DLL, которую будет искать система – это USER32.DLL. 5 dword – это указатель, на вхождения этой DLL в IAT, в данном случае это 403184.
![]()
Вот IAT и её первый элемент, всё внутри IT, которая заканчивается в 403670. Таким образом, каждой DLL в IT соответствует один элемент, называющийся IID или IMAGE IMPORT DESCRIPTOR, а внутри IT находится IAT, то есть хранилище адресов API-функций, всё в компактном виде, готовое для использования системой.
Многие крэкеры, экскспериментировавшие с IAT, говорят, что IAT не всегда находится внутри IT, так как указатель на IID может указывать куда угодно, поэтому тот может располагаться в любом месте программа, куда есть разрешение на запись, чтобы при запуске exe-файла, программа могла сохранить правильные адреса API-функций, но тем не менее, общая последовательность того, что система делает для заполнения IAT, является следующий:
- Ищет IT
- Ищет первый IID и устанавливает, к какой DLL относится 4-ый указатель.
- Затем смотрит первый указатель, чтобы выяснить, где располагается первый элемент IAT
- Там находится указатель на строку с именем API-функции.
- С помощью GetProcAddress ищется адрес и сохраняется в том же элементе.
- Когда в IAT встречается элемент с нулям, это означает, что первая DLL закончилась, и теперь нужно смотреть второй IID, чтобы найти, где располагается информация о второй, и повторить процесс.
Таким образом, важно хорошо понимать, что делает система, и что произойдёт, если она встретит неправильные указатели.
1) Ищем адрес в IT
![]()
2) Идём по этом адресу
![]()
3) В первой IID ищем 4-ый DWORD, который указывает на имя DLL, в данном случае это USER32.DLL.
![]()
Затем смотрим содержимое 5-ого указателя, чтобы найти первый элемент IAT. Он равен 403184. Идём туда.
![]()
![]()
Здесь читаем значение и смотрим, на что оно указывает.
![]()
Это имя первой API-функции, оно располагается в 4032CC, идём туда.
![]()
Видим, что первая API-функция – это KillTimer, её адрес находится с помощью GetProcAddress, мы можем сделать это с помощью OllyDbg.
![]()
И это значение, которое на моей машине равно 77d18c42, сохраняется в тот же элемент IAT, перезаписывая его предыдущее значение.
![]()
То, что мы здесь видим – это первый элемент IAT, и значение, которое оно содержит на моей машине.
Затем переходим к следующему элементу IAT, который легко найти, прибавив 4 к адресу предыдущего, и находим указатель на имя следующей API-функции.
![]()
Конечно, смотрим исполняемый файл и вот оно:
![]()
32d8, то есть 4032d8, смотрим имя следующей API-функции по этому адресу, и так как в этом элементе нет ноля, это означает, что продолжается список функций user32.dll.
![]()
Вторая API-функция – это GetSystemMetrics, её адрес будет найдён и сохранён во второй элемент IAT, и так будут находиться все API-функции библиотеки user32.dll, пока не будет обнаружен элемент, в котором содержутся все нули. Посмотрим, где он находится в исполняемом файле.
![]()
Видим, что поиск в IT user32.dll будет продолжаться, пока не найдётся вот этот элемент с нулями, после чего произойдёт переход к следующему IID.
![]()
Вот 4-ый и 5-ый указатели, 4-ый говорит нам, что в 40329b находится имя 2-ой dll, где будет происходить поиск функций.
![]()
Это kernel32.dll, и первый элемент её IAT находится после элемента с нулями, т.е. начиная с адреса 40321c.
![]()
Здесь видим ноль, который задаёт конец API-функций из user32.dll, и в 40321c начинается IAT Kernel32.dll, где будут искаться указатели на имена функций и которая будет заполнена их реальными адресами.
Думаю, что очень важно понимать этот процесс, я попытался объяснить его и повторить это несколько раза, чтобы вы хорошо поняли его, так как это основа всей темы реконструкции IAT, и хотя мы рассмотрим в следующей части соответствующие инструменты, которые могут нам помочь сделать рутинную работу за нас, очень важно знать, как они работаю, чтобы в случае каких-либо проблем мы не сошли с дистанции и всегда могли понять, почему это произошло.
Ок, на этот раз достаточно, в следующей главе мы приведём IAT в порядок. © Рикардо Нарваха, пер. Aquila
Введение в крэкинг с нуля, используя OllyDbg - Глава 33
Дата публикации 22 сен 2008