Статья Entry Point – дело о пропавшем индексе (часть 1)

Marylin

Marylin

Mod.Assembler
Red Team
05.06.2019
131
325
Термин EntryPoint знаком каждому программисту – это адрес точки входа в программу, т.е. первой инструкции исполняемого кода. Компиляторы прошивают этот адрес в РЕ-заголовок по смещению 28h, а системный "загрузчик образов" потом считывает его от туда, и передаёт на него управление. Такому алгоритму следуют все LDR-загрузчики, начиная с Win98 и до наших дней. Сам загрузчик оформлен в виде функции LdrpInitializeProcess() из библиотеки Ntdll.dll.

В данной статье рассмотрим несколько способов реализации фиктивных точек EntryPoint, что позволит защитить наши программы от взлома и отладки. Например зачем выставлять на показ процедуру проверки введённого пароля, или какой-нибудь авторской техники? До тех пор пока она не станет достоянием общественности, мы сможем использовать её в своих программах снова и снова. Но как-только корабль даст течь, придётся латать дыры, на что нужно время, которого всегда не хватает.

Проблема в том, что если в наших планах написать использующее системные механизмы кроссплатформенное приложение, то мы непременно столкнёмся с трудностями. Связано это с тем, что детали реализации того-же загрузчика отличаются на разных осках, хотя и исповедуют идентичные концепции. Конечно-же можно протестить все версии Win от хрюши до десятки и создать базу фундаментальных особенностей, но это не наши методы – слишком сомнительной будет репутация у такого приложения. Правильным решением будет поиск необходимых сигнатур по ходу дела в реальной обстановке, на что и сделаем ставку.


Места обитания точки-входа

Найти и хакнуть EntryPoint не проблема, гораздо трудней скрыть сам факт подмены. Такие отладчики как WinDbg, Syser, OllyDbg сейчас доступны всем и подняли с горшка не одно поколение начинающих крэкеров. Даже беглый анализ приложения может сразу выявить подвох, что сведёт на-нет все наши планы. К примеру, вот окно утилиты "DiE" для сбора информации об исполняемом файле, где как феникс из пепла восстаёт адрес EP:

Entry Point – дело о пропавшем индексе (часть 1)


Все утилиты подобного рода, в числе которых PeTools, PEiD и прочие, черпают информацию из заголовка дискового файла. Если мы хотим скрыть от них фактическую точку-входа в программу, то не должны трогать поля РЕ заголовка – пусть тулзы и оседлавшие их взломщики думают, что это типичный файл дилетанта, который можно вскрыть одним пальцем. Отличной идеей будет перехват и правка EntryPoint в динамике, непосредственно в процессе загрузки образа в память - это гораздо труднее обнаружить и мы выполним свою задачу(х). Смещения базы и точки-входа в заголовке представлены ниже (указатель на РЕ лежит по смещению 0х3С от начала образа):

Entry Point – дело о пропавшем индексе (часть 1)


Здесь видно, что в поле РЕ.28h зашито значение 0х00002000 – это т.н. Relative_Virtual_Address (RVA). Что будет, если сбросить его в нуль? Тогда точкой-входа получится базовый адрес 0х00400000, поскольку RVA представляет из-себя адрес относительно базы. А что у нас лежит по этому адресу в памяти? Правильно.. тот-же РЕ-заголовок, под который загрузчик имиджей выделяет целую страницу виртуальной памяти так, что первая секция экзешника всегда начинается по смещению Base+1000h (одна страница = 1000h или 4Кб, хвост забивается нулями):

Entry Point – дело о пропавшем индексе (часть 1)


На этом этапе, "горячие головы" ринулись-бы подменять EntryPoint в заголовке находящегося в памяти файла, и.. обломались-бы по полной! Здесь и начинается самое интересное..


Детали системного лоадера LDR

Дело в том, что загрузчик образов считывает значение EntryPoint из РЕ.28h только один раз, на начальном этапе своей работы, и больше к заголовку не обращается вообще. Получив точку-входа, загрузчик кидает её на самое дно стека, а завершающая работу лоадера функция ZwContinue() потом использует это значение как свой аргумент, передавая на неё управление.

Кстати функция Nt/ZwContinue() является подопечной диспетчера исключений – он использует её для возобновления работы потока, после обработки ошибок. На системах х64 вместо неё используется функция RtlRestoreContext(). Вот как выглядит стек в отладчике, когда он только загрузил клиента и не сделал ещё ни одного шага:

Entry Point – дело о пропавшем индексе (часть 1)


Судя по рисунку, алгоритм работы загрузчиков XP и семёрки отличается. В частности на семке маячат две точки-входа, в то время-как хрюше достаточно одной.

Но в любом случае, если мы получим указатель на дно стека, то двигаясь вверх (назад) можно найти адрес EntryPoint'a перебором всех значений, предварительно сформировав для этого сигнатуру поиска – в данном случае это значение 0х00402000. Такой финт будет работать на любой системе и получим кросплатформенность. Указатель на дно стека система прописывает в структуре ТЕВ по смещению 04h, что демонстрирует отладчик WinDbg:

Entry Point – дело о пропавшем индексе (часть 1)


Рассмотрим такой код..
Позже мы оформим его в виде процедуры, а пока это просто демонстрация того, как можно получить сигнатуру поиска EntryPoint в стеке,. Здесь следует обратить внимание на механизм ASLR – Address Space Layout Randomization, который ввели начиная с Win-7. Если в двух словах, то ASLR присутствует в системе как часть загрузчика и призвана рандомизировать ImageBase в памяти. При этом текущая база в PE.34h уже игнорируется, и в вместо неё подставляется рандомное значение.

В результате, запустили мы файл в первый раз – получили базу, например, 0х00500000. Запустили второй раз тот-же файл – получили уже совсем другую базу типа 0х00700000 и т.д. Механизм ASLR позволил "выкурить" из операционной системы целый класс вирусов и червей, для которых база в РЕ-заголовке была центром вселенной.


Практика – сбор информации

Таким образом, с приходом ASLR поле PE.34h на вещдок уже не тянет – нам нужна реальная база, вытащить которую можно из структуры PEB.08h. Рrocess Еnvironment Вlock – это системная структура и левых значений в ней быть не может. Поскольку мы стремимся к дзену, в исходники предусмотрена эта фишка и адрес ImageBase парсится напрямую из структуры РЕВ, а не из файлового заголовка РЕ.34h. Код содержит комментарии, так-что повторяться не буду:

C-подобный:
format   pe console
include 'win32ax.inc'
entry    start
;//------
.data
capt     db   13,10,' Fix EntryPoint file --> %s'
         db   13,10,' ******************************************',0
info     db   13,10,' Stack Base in TEB.04h.......: %08X'
         db   13,10,' Image Base in PEB.08h.......: %08X'
         db   13,10,' EPoint RVA in  PE.28h.......: %08X'
         db   13,10,' ------------------------------------------'
         db   13,10,' Signature = PEB.08h + PE.28h: %08X',0
stAddr   db   13,10,' EntryPoint stack address....: %08X',0
frmt     db   '%s',0
fName    db   0
;//------
.code
start:    invoke  GetModuleFileName,0,fName,128   ;// сбрасываем своё имя в буфер
         cinvoke  printf,capt,fName               ;// выводим шапку на экран

;//=== Получаем сигнатуру поиска в виде ImageBase + ePoint RVA
;//=============================================================
         mov      eax,[fs:4]          ;// EAX = TEB.04 StackBase
         push     eax                 ;// запомнить

         mov      ebx,[fs:30h]        ;// EBX = указатель на PEB
         mov      ebx,[ebx+8]         ;// EBX = PEB.08 ImageBase

         mov      ecx,[ebx+3Ch]       ;// ECX = Base.3Ch = RVA на РЕ-заголовок
         add      ecx,ebx             ;// ECX = виртуальный адрес РЕ-заголовка
         mov      ecx,[ecx+28h]       ;// ECX = PE.28h EntryPoint RVA

         mov      edx,ebx             ;// EDX = PEB.08h ImageBase
         add      edx,ecx             ;// EDX = ImageBase + EntryPoint RVA
         push     edx                 ;// запомнить сигнатуру!

        cinvoke   printf,info,eax,ebx,ecx,edx

;//=== Кроссплатформенный поиск адреса ePoint в стеке, по его сигнатуре
;//====================================================================
         pop      ebx esi             ;// EBX = сигнатура (маска для поиска), ESI = StackBase
         std                          ;// SetDirection - флаг процессора DF=1, обратное чтение из ESI
@find_Jocker: 
         lodsd                        ;// EAX = dword из ESI (esi авто/уменьшается на 4)
         cmp      eax,ebx             ;// сравнить с маской
         jne      @find_Jocker         ;// повторить, если не равно..

         cld                          ;// иначе: ClearDirection - сбросить флаг обратного шага
         add      esi,4               ;// коррекция указателя ESI
        cinvoke   printf,stAddr,esi   ;// выводим его на экран!

        cinvoke   scanf,frmt,capt     ;// ждём нажатие клавиши..
         invoke  exit,0               ;

;//=== Секция импорта =======================
data     import
library  msvcrt, 'msvcrt.dll',kernel,'kernel32.dll'
import   msvcrt, printf,'printf',scanf,'scanf',exit,'exit'
import   kernel, GetModuleFileName,'GetModuleFileNameA'
end      data
Entry Point – дело о пропавшем индексе (часть 1)


Как видим, код с более-чем скромным функционалом позволил найти адрес в стеке, куда загрузчик LDR сбрасывает точку-входа для функции ZwContinue(). Причём поиск по такому алго даст положительный результат на любой системе Win, от хр и до десятки. Статическая база адресов тут не преемлема из-за ASLR, зато приведённый выше динамический скан – осечки уже не даст.

Теперь, правка значения по указанному адресу 0х0006FFF8 приведёт к тому, что ZwContinue() снимет со-стека не реальный, а фиктивный EntryPoint и мы можем стартануть основной поток программы с любого адреса. На этот адрес можно повесить, например, распаковщик основного тела или что-то ещё. До недавнего времени, за такой трюк антивирусы сожгли-бы нас на костре, а сейчас – это легальный способ большинства протекторов типа Armadillo, ORiEN и прочих. Так-что если антивирус и горит желанием порубить нашу прожку на мелкие куски, то не сможет этого сделать из эстетических соображений – сразу откажет целый класс программ, чего бизнес позволить себе не может.

И тут мы сталкиваемся с основной проблемой!
Нам нужно подменить значение в стеке задолго до того, как загрузчик закончит свою работу и передаст управление на первую инструкцию секции-кода. Другими словами, нам нужно вмешаться в работу загрузчика образов LDR. Как это сделать? На первый взгляд – миссия не выполнима, т.к. наш код ещё не получил управления. Но посмотрим на рисунок ниже..

Entry Point – дело о пропавшем индексе (часть 1)


Важными для нас тут являются пункты 3 и 4 – это предварительная загрузка статически прилинкованных к нашему приложению библиотек импорта, после чего загрузчик вызывает цепочку Callback-процедур из секции TLS (если таковая имеется). Причём анализ импорта опережает цепочку коллбэков, т.к. в каталоге секций РЕ-заголовка импорт лежит под номером(1), а TLS – аж под номером(9). С деталями этой кухни можно ознакомится в предыдущей статье TLS-Callback. По этой причине, подмену точки-входа лучше осуществлять внутри Callback-процедуры. - на этот момент импортируемые нашим приложением библиотеки DLL уже находятся в памяти, и мы можем вызывать из коллбэка, API-функции.

Но если душа желает хардкора, нично не мешает осуществить перехват EP и внутри статической библиотеки. Правила оформления кода DLL требуют вполне документированной функции инициализации под названием DllEntryPoint() из Ntdll.dll (точка-входа в библиотеку). Она позволяет вызывать свои экспортируемые функции по четырём внешним факторам:

• Dll_Process_Attach – вызов функции при подключении библиотеки к процессу (наш клиент).
• Dll_Process_Detach – вызов функции экспорта при завершении родительского процесса.
• Dll_Thread_Attach – родитель создаёт новый поток (на основной поток не распространяется).
• Dll_Thread_Detach – поток отработал и завершается.

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


Заключение

Тема манипуляции с EntryPoint требует понимания общих принципов функционирования системного загрузчика образов LDR. В этой части и так уже сказано многое, поэтому поставим здесь точку, чтобы всё/это переварить. Во-второй части попробуем реализовать сказанное на практике, создав небольшой crackme. Мы окунёмся в шифрование и полиморфизм, что сносит башню не только отладчикам, но и дизассемблерам. До скорого..
 
Последнее редактирование:
T-Rex

T-Rex

Happy New Year
05.10.2019
20
35
Никогда ранее не интересовался реверсом, но такая подача материала даже меня впечатлила.
 
Aleks Binakril

Aleks Binakril

Happy New Year
04.12.2019
25
4
жду с нетерпением третьей части
 
Мы в соцсетях: