Небольшая заметка о создании специфичного PE файла
Может быть она будет кому-то полезной ;)
Нулевая точка вода в приложениях Win32.
Нестандартный PE заголовок может спасти наше приложение от инструментов анализа приложений, которые проверяют корректность заголовка. Как известно точка входа — это адрес, куда загрузчик, после загрузки приложения в память, передаёт управление. Точка входа расчитывается как сумма базы загрузки и смещения точки входа. В заголовке файла поле ImageBase обозначает именно базу загрузки приложения, а поле AddressOfEntryPoint — смещение точки входа.
База загрузки начинается с DOS-заголовка, который с свою очередь начинается с MZ-сигнатуры «MZ», в байтовом представлении она выглядит как 4D 5A.
DOS-заголовок под отладчиком
Если смещение точки входа равно нулю, следовательно исполнение программы начнётся с байтов 4D 5A, в IA32 эти байты трактуются как:
DEC EBP
POP EDX
Следовательно, чтобы восстаносить конекст исполнения следует увеличить EBP на единицу и восстановить исходное состояние стека. Для этого сразу после этих значений добавим:
INC EBP
PUSH EDX
И прыжок на оригинальную точку входа.
Получилось что-то вроде этого:
![]()
Модифицированный DOS-заголовок
Осталось только поправить поле AddressOfEntryPoint:
![]()
Модификация AddresOfEntryPoint
Теперь можете проверить, если вы работаете под вайном, то ничерта не получится, потому что вайн отказывается запускать подобные приложения. Так что, если вы пользователь Linux, то вам, как и мне, придётся запускать всё под виртуальной машиной ;)
Кстати, Ollydbg запущенная под Linux не работает с файлом, однако, она же, запущенная под Windows, начала отлаживать программу — чудеса XD
Пустая таблица импорта
Есть ещё один трюк, он довольно популярный и очень эффективный в борьбе с поведенческим анализом программы — это нативная подгрузка библиотек и вызов функций из них. Как можно обойтись без функций вовсе? Это небольшой хак. В адресное пространство программы всегда подгружена библиотека kernel32.dll, даже если она не указана в таблице импорта, её адрес можно найти просканировав память. Сделать это очень просто:
search_kernel: ; eax = Адрес внутри kernel32
dec eax ; Уменьшение адреса
mov bx, word[eax] ; Помещение 2-ух байтов в bx
cmp bx, 'MZ' ; Сравнение с MZ-сигнатурой
jnz search_kernel ; Если в bx не сигнатура, то повтор
Для того, чтобы сократить колличество итераций, можно заранее обнулить AL либо весь AX целиком. Дело в том, что байтов, состовляющих сигнатуру MZ не встречается более нигде в kernel32.dll. А да, кстати, это делать нужно на точке входа, либо сохранять состояние EAX на точке входа и класть его перед этим кодом, так как на точке входа в EAX уже лежит адрес, который распологается где-то внутри kernel32.dll, ведь CreateProcess вызывается для создания вашего приложения и тем самым получается невольно подгруженным в ваше адресное пространство.
Если у вас, каким-то образом не получается сохранить начальное значение EAX, либо сразу исполнить этот код на точке входа, можно взять адрес из стека, подходят ведь все адреса, лежащие внутри kernel32, а в стеке, в начале исполнения программы как раз лежит адрес выхода из программы, перейдя по которому мы попадём внутрь kernel32. Поэтому передав адрес, лежащий в стеке, мы получим тот же самый результат.
Дальше — больше, чтобы вызвать нужную функцию в библиотеке, нужно найти её виртуальный адрес. Он хранится в таблице директории экспорта в таблице адресов, которая параллельна таблице имён функций и таблице ординалов. То есть, другими словами, чтобы найти адрес функции в памяти нужно:
- Прибавить к базе считанное значение по адресу [Base_PE+3Ch]
- Прибавить к получившемуся адресу значение [PE_Header+78h]
Теперь мы находимся в директории экспорта, чтобы найти нужную функцию, нужно считать две таблицы — таблицу адресов и таблицу имён. Указатель на первую находится по смещению 1Ch, на вторую — 20h.
Однако, некоторые экспортируемые функции могут не иметь имени, а их адреса могут быть в таблице адресов, это следует учитывать. Исходный код примера Слишком велик, для публикации в этой статье. Вызвать функцию из библиотеки, которая не загружена в память нельзя, для этого необходимо загрузить её в память, использую LoadLibrary. Чтобы бастро найти функцию по её имени, можно использовать функцию GetProcAddress. Так же, вместо использования имени функции, можно использовать хеш от имени, это дасть б`ольшую защищённость.




Reply With Quote
Thanks



