R0 CREW

Нестандартный PE

Нулевая точка вода в приложениях Win32.

Нестандартный PE заголовок может спасти наше приложение от инструментов анализа приложений, которые проверяют корректность заголовка. Как известно точка входа — это адрес, куда загрузчик, после загрузки приложения в память, передаёт управление. Точка входа расчитывается как сумма базы загрузки и смещения точки входа. В заголовке файла поле ImageBase обозначает именно базу загрузки приложения, а поле AddressOfEntryPoint — смещение точки входа.
База загрузки начинается с DOS-заголовка, который с свою очередь начинается с MZ-сигнатуры «MZ», в байтовом представлении она выглядит как 4D 5A.

DOS-заголовок под отладчиком

Если смещение точки входа равно нулю, следовательно исполнение программы начнётся с байтов 4D 5A, в IA32 эти байты трактуются как:

DECEBP
POPEDX
Следовательно, чтобы восстаносить конекст исполнения следует увеличить EBP на единицу и восстановить исходное состояние стека. Для этого сразу после этих значений добавим:

INCEBP
PUSHEDX

И прыжок на оригинальную точку входа.
Получилось что-то вроде этого:

Модифицированный DOS-заголовок

Осталось только поправить поле AddressOfEntryPoint:

Модификация AddresOfEntryPoint

Теперь можете проверить, если вы работаете под вайном, то ничерта не получится, потому что вайн отказывается запускать подобные приложения. Так что, если вы пользователь Linux, то вам, как и мне, придётся запускать всё под виртуальной машиной :wink:
Кстати, Ollydbg запущенная под Linux не работает с файлом, однако, она же, запущенная под Windows, начала отлаживать программу — чудеса XD

Пустая таблица импорта

Есть ещё один трюк, он довольно популярный и очень эффективный в борьбе с поведенческим анализом программы — это нативная подгрузка библиотек и вызов функций из них. Как можно обойтись без функций вовсе? Это небольшой хак. В адресное пространство программы всегда подгружена библиотека kernel32.dll, даже если она не указана в таблице импорта, её адрес можно найти просканировав память. Сделать это очень просто:

search_kernel: ; eax = Адрес внутри kernel32
deceax ; Уменьшение адреса
movbx, word[eax] ; Помещение 2-ух байтов в bx
cmpbx, ‘MZ’ ; Сравнение с MZ-сигнатурой
jnzsearch_kernel ; Если в bx не сигнатура, то повтор

Для того, чтобы сократить колличество итераций, можно заранее обнулить AL либо весь AX целиком. Дело в том, что байтов, состовляющих сигнатуру MZ не встречается более нигде в kernel32.dll. А да, кстати, это делать нужно на точке входа, либо сохранять состояние EAX на точке входа и класть его перед этим кодом, так как на точке входа в EAX уже лежит адрес, который распологается где-то внутри kernel32.dll, ведь CreateProcess вызывается для создания вашего приложения и тем самым получается невольно подгруженным в ваше адресное пространство.
Если у вас, каким-то образом не получается сохранить начальное значение EAX, либо сразу исполнить этот код на точке входа, можно взять адрес из стека, подходят ведь все адреса, лежащие внутри kernel32, а в стеке, в начале исполнения программы как раз лежит адрес выхода из программы, перейдя по которому мы попадём внутрь kernel32. Поэтому передав адрес, лежащий в стеке, мы получим тот же самый результат.
Дальше — больше, чтобы вызвать нужную функцию в библиотеке, нужно найти её виртуальный адрес. Он хранится в таблице директории экспорта в таблице адресов, которая параллельна таблице имён функций и таблице ординалов. То есть, другими словами, чтобы найти адрес функции в памяти нужно:

  1. Прибавить к базе считанное значение по адресу [Base_PE+3Ch]
  2. Прибавить к получившемуся адресу значение [PE_Header+78h]

Теперь мы находимся в директории экспорта, чтобы найти нужную функцию, нужно считать две таблицы — таблицу адресов и таблицу имён. Указатель на первую находится по смещению 1Ch, на вторую — 20h.
Однако, некоторые экспортируемые функции могут не иметь имени, а их адреса могут быть в таблице адресов, это следует учитывать. Исходный код примера Слишком велик, для публикации в этой статье. Вызвать функцию из библиотеки, которая не загружена в память нельзя, для этого необходимо загрузить её в память, использую LoadLibrary. Чтобы бастро найти функцию по её имени, можно использовать функцию GetProcAddress. Так же, вместо использования имени функции, можно использовать хеш от имени, это дасть б`ольшую защищённость.

Ну … есть немного замечаний

Во-первых помоему трюк с NULL-EP работать не будет на системах с DEP, так как секция заголовков загружается в память с правами readonly. Потом что косается борьбы с поведенческим анализом не понятно как вообще это повлияет на него, если имеется в виду защита данных импорта от антивируса, то это скорее относится к эвристике. Алгоритм поиска Kernel32 ужасен, утверждение:

неверно, обычно вытащить адрес любой библиотеки достаточно просто через PEB\TEB и распарсив LDR_MODULE. Даже если брать этот алгоритм за основу он как минимум должен учитывать то что сигнатура MZ расположена в начале страницы, тоесть выравнивание 0x1000.

Если я не ошибаюсь то таблица виртуальных адресов не параллельна таблицам имён и ординалов, ординал это индекс в таблице адресов.

Внесу свои 5 копеек )
После создания процесса на стеке лежит адрес возврата в кернел32, поэтому
pop esi <---- адрес где то в кернеле
выравниваем esi на 64к значение и сверяем с MZ
если нет то отнимаем от esi 64к и снова сверяем

Все бинарники грузятся в винде с выравниванием на 64к поэтому нет смысла побайтово парсить память на MZ

JKornev
Видел семпл, который использовал память под свои заголовки, по ходу исполнения впихивая туда куски кода и исполняя их, поэтому думаю, что использовать заголовки для исполнения норм.
С выравниваением - да, согласен, спасибо, возьму на заметку. С PEB/TEB получается морока с версиями NT и вообще не стабильно получается, а вот просто сканить память - всё норм.
Импорт на поведенческий анализ не влияет, не то написал, импорт влияет на статический анализ.

Когда исполняемый код помещается в секцию заголовков, секцие выделяются права RWE, в случае вашего трюка секции права на RWE не выделяются, т.к. по стандарту загрузчик проецирует заголовок с правами R. Для того чтобы ваш трюк работал везде, нужно либо использовать TLS callback и из него давать права RWE, либо возможно есть какие-то хаки для загрузчика, которых может и не существовать вообще. Я сделал так как у вас написано у меня не работает смотрите аттач.

Код парсинга PEB\TEB работает как на XP так и на 8.1, в чём именно может выражатся нестабильность или неприменимость?

Ваще не вопрос, лень кодить пример, но FSG код распаковщика в РЕ заголовок пихает.[ATTACH]159[/ATTACH]

У меня всё работает, см - http://rghost.net/54963960
Про Нахождение kernel32.dll. Через PEB в XP находится kernelbase.dll, а в смёрке kernel32.dll, вот вам и проблема.

Hide

чойта у мну аттач не работает ошибается на файлы :frowning:

Я же писал выше что оно не будет работать именно с DEP

http://msdn.microsoft.com/en-us/library/windows/desktop/ms680339(v=vs.85).aspx

В вашем примере этот флаг не установлен, поэтому оно и исполняет код в секции заголовка

Наоборот в 7ке первым в списке идёт kernelbase а в XP kernel32, т.к. если я не ошибаюсь в XP не используется kernelbase(покрайней мере я пока не встречал такого). Но это не проблема, LDR_MODULE это двусвязный список, вчём проблема понему пройтись?

Вот исходный код из моей статьи про шеллкоды, который демонстрирует обход списка LDR_MODULE и изьятия из него базового адреса любого модуля

Ну… может быть, никогда не сталкивался с DEP, отсюда и неопытность в этом вопросе :slight_smile:

P.S. в моем примере структура называется не LDR_MODULE а LDR_DATA_TABLE_ENTRY

Да уж, неплохо. Разобрался, чото наглупил :slight_smile:

Инфа по сабжу:

http://code.google.com/p/corkami/wiki/PE?show=content

Кто там про адрес говорил ?

push eax          // EAX = BaseThreadInitThunk
sub eax , 0x4DE0A // EAX = EAX - BaseThreadInitThunk
sub eax , 0x1000  // EAX = DOS
mov ecx , eax
pop eax
Hide

или проще, на 7 x32 норм работает

:smiley:

это непереносимый код

Я знаю ), но хоть какой то