Вместо приветствия…
Добро пожаловать в третью часть, в которой мы продолжим изучение механизмов распаковки и Аспровских техник, призванных нам помешать. Эта часть, скорее всего, будет делать упор на теорию, но без практики в этот раз не получится. К сожалению, пока мы не можем идти дальше, т.к. защитные механизмы даже первой версии аспра нами изучены очень поверхностно. Давайте подведём итог того, что мы можем уже сейчас. А можем мы очень немного:
1. Бороться с аспровскими переходниками.
2. Искать ОЕР.
И… всё. Больше я не смог придумать ничего, что бы могло пополнить этот список возможностей. Да и то, мы всё это можем, если нам повезёт! Что это значит? А то, что вы, изучая материал прошлой части, могли заметить, что там нет ни слова про антиотладку аспра. Что же такое антиотладка, как это работает и как этому противостоять? Да, именно с этого мы и начнём третью часть. Пристегните ремни – мы начинаем.
Антиотладка и методы её преодоления
Антиотладка – это набор методов обнаружения и противодействия отладчикам. Целью любой антиотладки является нахождение и использование различий в поведении отлаживаемой и неотлаживаемой программы. Естественно, под понятием отладки не следует понимать отладку только в OllyDebug. Если реверсер использует отладчик уровня ядра, например, Soft-Ice, то это тоже отладка. Естественно, существуют методики противодействия как отладчикам режима пользователя, так и отладчикам режима ядра. Совершенно точно могу вам сказать, что из режима ядра обнаружить, что какой-то процесс отлаживается, гораздо проще, чем из режима пользователя. Как вы помните, аспр является протектором режима пользователя (третьего кольца защиты), что налагает существенные ограничения на его антиотладочные механизмы. Также вы помните, что версия 1.0 вышла в 2000 году, когда Ольга была ещё (была ли вообще?) не особо популярна. В то время популярностью пользовался всё тот же Soft-Ice (далее Айс). Конечно же, Солодовникову приходилось адекватно реагировать на имеющихся тогда инструменты, способные противодействовать его протектору, поэтому если у вас в системе установлен и активен Айс, и нет никаких механизмов его сокрытия, то повторить подвиги, описанные мною во второй части данного цикла статей вам не удалось. Вместо ОЕР и восстановленных переходников вы лицезрели убогое сообщение аспра с непонятным текстом. И в то же время, если Айса у вас нет, и вы, используя Ольгу, решили вообще не ставить себе PhantOm, то были удивлены тем, что Ольга, как ни в чём не бывало, работала. И никакой антиотладки видно не было. Как так?
Для того, чтоб ответить на все ваши вопросы, нам снова нужно вооружиться отладчиком и пойти исследовать антиотладку аспра. С помощью скрипта из второй части проходим на начало ASProtect.dll. Теперь, вспоминая вторую часть, заходим внутрь последнего CALL. Далее – Crtl-F9, и оказываемся вот здесь:
Заходим внутрь CALL 00198D98. Теперь здесь нужно немного потрассировать код. Но возникает вполне логичный вопрос – немного, это сколько? Да и как тут его трассировать? По F8 или по F7? Вопрос, может, и логичный, но, по-моему, уже не вполне уместный. Конечно, трассируем по F8, пока не всплывёт ваше любимое окно, которое, типа, сообщит о возникновении внештатной ситуации. Но ведь может так статься, что у вас такой внештатной ситуации и не возникнет, тогда просто послушайте меня, ведь я трассировал этот участок и на F7 тоже, и могу вам сказать, что антиотладка начинается по такому адресу:Code:00199354 55 PUSH EBP 00199355 8BEC MOV EBP,ESP 00199357 53 PUSH EBX 00199358 56 PUSH ESI 00199359 57 PUSH EDI 0019935A 33C0 XOR EAX,EAX 0019935C 55 PUSH EBP 0019935D 68 7A931900 PUSH 19937A 00199362 64:FF30 PUSH DWORD PTR FS:[EAX] 00199365 64:8920 MOV DWORD PTR FS:[EAX],ESP 00199368 8B45 08 MOV EAX,DWORD PTR SS:[EBP+8] 0019936B E8 28FAFFFF CALL 00198D98 00199370 33C0 XOR EAX,EAX
В случае, если переход выполнится, то протектору удалось сдетектить отладчик. Что же такого происходит внутри CALL 00198D74, что позволяет или не позволяет обнаруживать отладчик? Как это работает?Code:00198E77 A1 F8EE1900 MOV EAX,DWORD PTR DS:[19EEF8] 00198E7C E8 F3FEFFFF CALL 00198D74 00198E81 84C0 TEST AL,AL 00198E83 75 3A JNZ SHORT 00198EBF
Зайдя внутрь вышеупомянутого вызова, можем наблюдать следующее:
Как видите, весь антиотладочный трюк сводится к вызову CreateFileA, и если вызов завершился неудачей, т.е. функция вернула INVALID_HANDLE_VALUE, то выполняется инкремент регистра EAX. Давайте пока сосредоточимся на вызове CreateFileA. Вызывается она вот с таким параметрами на стеке:Code:00198D74 53 PUSH EBX 00198D75 8BD8 MOV EBX,EAX 00198D77 6A 00 PUSH 0 00198D79 68 80000000 PUSH 80 00198D7E 6A 03 PUSH 3 00198D80 6A 00 PUSH 0 00198D82 6A 01 PUSH 1 00198D84 68 00000080 PUSH 80000000 00198D89 53 PUSH EBX 00198D8A E8 ED1FFFFF CALL 0018AD7C ; JMP to kernel32.CreateFileA 00198D8F 40 INC EAX 00198D90 0F95C0 SETNE AL 00198D93 5B POP EBX 00198D94 C3 RETN
Т.е. в принципе, тут всё понятно со всеми параметрами, кроме первого. На имя файла это не похоже, тогда что это? Дело в том, что когда отладчик уровня ядра начинает работу, то при загрузке его драйвера происходит создание объекта-устройства с некоторым именем. В документации это именуется мелкомягкими как Device Object Name. И именно его и ищет защита. В данном случае, таким образом детектится наличие в системе Айса. Но вот что хорошо в этом методе – отладчик удастся обнаружить только в том случае, если он активен, а не просто установлен. Соответственно, если никакими Айсами мы отроду не пользовались, то CreateFileA вернёт нам 0xFFFFFFFF - INVALID_HANDLE_VALUE, что можно узнать всё в том же MSDN, о котором речь шла во второй части.Code:0012FE0C 00198D64 |FileName = "\\.\SIWDEBUG" 0012FE10 80000000 |Access = GENERIC_READ 0012FE14 00000001 |ShareMode = FILE_SHARE_READ 0012FE18 00000000 |pSecurity = NULL 0012FE1C 00000003 |Mode = OPEN_EXISTING 0012FE20 00000080 |Attributes = NORMAL 0012FE24 00000000 \hTemplateFile = NULL
Хочется обратить ваше внимание на один красивый трюк с ассемблерными командами. К антиотладке он не относится, но нужно воздать должное Борландовскому компилятору в запутывании нас с вами. После вызова CreateFileA мы видим такой код (он уже приводился выше, но продублируем):
Казалось бы, INC EAX должен увеличить INVALID_HANDLE_VALUE на 1, тогда в ЕАХ окажется 0. Ну, это, разумеется, в выгодном для нас с вами случае. Но не всё так просто! Посмотрите в мануалы интела, там в описании инструкции INC говорится следующее:Code:00198D8F 40 INC EAX 00198D90 0F95C0 SETNE AL 00198D93 5B POP EBX 00198D94 C3 RETN
Т.е. помимо увеличения непосредственного операнда, команда воздействует на флаги. Но «в зависимости от результата» меня не устроило, как пояснение, поэтому я решил всё в тех же мануалах посмотреть, что же это за зависимости. В Table B-1. EFLAGS Condition Codes можно найти ответ на вопрос, в результате каких условий происходит изменение вышеупомянутых флагов. Кстати, это – первый том интеловских мануалов. Также следует упомянуть, что сравнения производятся над операндом, указанным в команде, т.е. в нашем случае, над значением регистра ЕАХ. Следующая за инкрементом команда SETNE AL обращается только к флагу ZF. Т.е. если он (флаг) взведен, AL будет равен нулю, в противном случае станет равным единице. Таким образом, если протектор не сдетектил Айс, то в AL будет 0. Во как всё хитро работает.AFlags Affected
The CF flag is not affected. The OF, SF, ZF , AF, and PF flags are set according to the result.
Но вернёмся к антиотладке.
Здесь прот считывает код последней ошибки в случае, если CreateFileA завершилась неудачно, и если последней ошибкой, которая возникла в ходе неудачного вызова CreateFileA стала не ERROR_FILE_NOT_FOUND (00000002), то снова прот ставит нам палки в колёса. Далее трюки весьма однообразны – через всё тот же CreateFileA ищутся устройства:Code:00198E85 E8 3A1FFFFF CALL 0018ADC4 ; JMP to ntdll.RtlGetLastWin32Error 00198E8A 83F8 02 CMP EAX,2 00198E8D 75 30 JNZ SHORT 00198EBF
\\.\SICE
\\.\NTICE
После их ненахождения проверяются коды ошибки, как это уже было описано выше. Следующий трюк вообще очень прост и неэффективен:
На INT3 произойдёт исключение, и если отладчика нет, управление передастся на обработчик. А если есть – то отладчик, якобы, должен «поглотить» исключение, и выполнение программы продолжится JMP SHORT 00194129. Но Оля на такое не покупается, поэтому смело идём на адрес обработчика. Его найти просто даже вручную: смотрим, чему равна база селектора, загруженного в fs, у меня 7ffdf000. Далее по этому адресу находим указатель на цепочку фреймов SEH:Code:00194124 CC INT3 00194125 EB 02 JMP SHORT 00194129
А по этому указателю – саму цепочку, и переходим на адрес первого обработчика:Code:7FFDF000 0012FA70 (Pointer to SEH chain)
Далее это всё приводит нас к тому, что переход по адресу 00194528 ведёт нас на уже известный из второй части обработчик (вы ведь нашли ОЕР самостоятельно, правда?):Code:0012FE30 0012FE3C Pointer to next SEH record 0012FE34 00194528 SE handler
После обработки исключения мы возвращаемся на адрес:
И всё – антиотладка на этом закончилась. Да, вот такая убогость техник наблюдается в нашем любимом протекторе. Конечно, в последующих версиях добавятся и другие трюки, но особо ситуация не изменится, поэтому зачастую авторы программ добавляют собственные трюки, но об этом мы тоже поговорим. К статье я прикладываю описание различных антиотладочных техник и методов их обхода, чтоб вы могли ознакомиться с информацией, которая, возможно, пригодится при распаковке других протекторов.Code:0019452D E8 AA07FFFF CALL 00184CDC
Но, вернёмся к нашему асприку – как в нём обойти антиотладку, если всё-таки происходит детект? Сразу определимся, что все эти приёмы – вообще не приёмы, если у нас в системе неактивен Айс. Но что, если Айс активен и используется в настоящий момент для отладки какого-то вполне легального приложения, а мы захотели запустить программулину и снять с неё аспр? Как тогда быть?
Вообще, при обходе антиотладочных трюков есть два основных (и наиболее простых в реализации) способа. Первый основан на патче ОС, а второй – на патче кода протектора. Т.е. если мы используем первый способ для сокрытия, к примеру, того же Айса, то мы можем перехватить сплайсингом CreateFileA, можем перехватить в ntdll.dll сервис ZwCreateFile, а можем перехватить IoCreateFile в ядре. На этом способы перехвата, конечно, не заканчиваются. Я их привёл здесь, скорее, для примера. Но суть вы уловили – можно патчить всё, кроме кода протектора.
Второй же принцип – полная противоположность первому. Т.е., как вы уже догадались, нужно патчить именно код прота и ничего больше. К счастью, аспр предоставляет нам большой подарок для использования именно второго способа. Помните, когда во второй части мы готовили поделку, которую позже начали распаковывать, то в аспре можно было выбирать опции защиты. Тогда мы выбрали защиту от отладки и защиту путём проверки контрольной суммы. Так вот, теперь смотрите – чуть выше начала антиотладочного кода видим некий условный переход:
И если этот переход не выполняется, то начинаются поиски отладчика, а если выполняется? А если выполняется, то мы как раз проходим стороной всю антиотладку. А ниже – начинается код, выполняющий проверку контрольной суммы, а перед ним есть такой же переход. Естественно, что если и он не выполняется, то не будет никакой проверки контрольной суммы. Вот так всё легко и непринуждённо. Но как же так? Получается, что мы исправили всего два перехода с условных на безусловные, и в результате прошли и антиотладку, и проверку контрольной суммы. Но как Солодовников допустил такое? А очень просто – уже известная нам ASProtect.dll не пересобирается протектором каждый раз, т.е. её код постоянен. Меняются лишь данные, которые отвечают за опции защиты. Т.е. говоря проще – когда протектор упаковывает программу, он учитывает опции защиты. Эти опции выбирает пользователь, а протектор в точности выполняет лишь те проверки и защитные трюки, «заказанные» пользователем. Но ему (аспру) нужно как-то узнать, следует ли выполнять пересчёт контрольной суммы и искать отладчик. Для этого он добавляет некоторые данные, которые в последствии обрабатываются кодом ASProtect.dll, и уже она решает, как защищать приложение. Она решает, а мы – поможем.Code:0018F8CC 8945 F4 MOV DWORD PTR SS:[EBP-C],EAX 0018F8CF 837D F4 00 CMP DWORD PTR SS:[EBP-C],0 0018F8D3 0F84 F1000000 JE 0018F9CA
Как вы могли заметить, генерация переходников происходит после антиотладки, что вполне логично. Поэтому нам следует дополнить скрипт для прохода на переходники, чтоб он по пути пропатчил антиотладку. Но я решил его переписать полностью, потому что по неизвестным мне причинам первый скрипт работал нестабильно. Скрипт, как обычно, приложен к статье.
Дамп, секции и прочие страшные термины
Теперь настало время для знакомства с всепоглощающей теорией распаковки. До этого мы, можно сказать, тупо искали всякие ОЕР, восстанавливали таблицы и боролись с антиотладкой. Но теперь пора заняться подготовкой распакованного файла. Всякий распакованный файл – это исправленный и дополненный дамп, но дамп лежит в основе распаковки. Что же такое дамп и чем он отличается от обычного РЕ-файла? Как вы знаете, РЕ-файл загружается в память и выполняется оттуда. Но после проецирования в память перед выполнением точки входа происходит инициализация важных элементов. Сейчас пока не будем вдаваться в подробности, какие именно элементы инициализируются, но суть в том, что в памяти они уже инициализированы. Дамп – это копия памяти выбранного процесса, сохранённая на диске. В дампе обычно нужно восстановить таблицу импорта, перемещаемые элементы, правильно выставить значение ЕР. В принципе, это – необходимый минимум. Как вы уже знаете из «Об упаковщиках в последний раз», ехе-файл может прекрасно существовать и без таблицы перемещаемых элементов. Но без таблицы импорта и нового значение ЕР распакованная программа не заработает никогда. Логично предположить, что раз необходимо исправить значение ЕР, то его следует исправить в заголовке, который тоже может дампиться из памяти, а может копироваться непосредственно с диска. Как вы понимаете, копировать с диска – надёжнее, но если протектор не портит заголовок в памяти, то, по сути, нет никакой разницы. Теперь подытожим сказанное.
Дамп – это копия памяти выбранного процесса, начиная с ImageBase, и заканчивая ImageBase+ImageSize. Дамп сохраняется на диске для последующего редактирования, дополнения и исправления. Сразу возникает вопрос – а что там редактировать? А редактировать там как раз есть что. Вот вы, например, слышали, что такое DumpFixer? Так вот это страшное слово означает приравнивание Physical Size и Physical Offset значению Virtual Size и Virtual Offset. Зачем это нужно? Дело в том, что после упаковки протектором секции сжимаются, т.е. уменьшаются в размере, потому что проты и пакеры используют различные алгоритмы сжатия. Далее код самого пакера обычно добавляется в виде дополнительных секций в конец файла. Т.е. таким образом, упакованный файл может весить больше неупакованного, если пакер добавляет много своего кода к защищаемому файлу. Но что-то мы отвлеклись.
Когда секции разжимаются, мы, стоя на ОЕР, дампим как раз разжатые секции, и, вроде бы, всё ок. Но когда мы вставляем старый заголовок из упакованного приложения, то оказывается, что инициализированных данных (их размер - Physical Size) в заголовке объявлено гораздо меньше, чем есть на самом деле. Таким образом часть дампа просто не грузится с диска в память, и в памяти у нас – обрубок. Но это – только в теории, а на практике, как правило, мы получаем надпись о невалидности РЕ-файла и идём на опушку леса.
С другой стороны, если мы укажем, что у нас в дампе слишком много инициализированных данных, это приведёт к тому, что часть неинициализированных станет частью инициализированных. Что это значит? Вы знаете, что неинициализированные данные – это, те, память под которые выделяется лишь по ходу загрузки программы на исполнение. Их начальное значение равно нулю (обычно это так), следовательно, хранить эти нули на диске совсем необязательно. Но приходится, ведь точно определить, где заканчиваются инициализированные и начинаются неинициализированные никак нельзя. Хотя, с большой степенью точности это делают всякие Rebuild’еры, которые находят в конце секций место, где начинаются нули, и обрезают секцию по этому значению.
Респект тому, кто прочитал последние пару абзацев, но согласитесь – получилось как-то туманно, а главное – непонятно, зачем нам всё это знать. Теперь – простой практический вопрос. Представьте, вот вы сделали дамп, собрались прикручивать импорт, ресурсы восстанавливать и всё такое, но понимаете, что для уменьшения размера дампа неплохо было бы сделать Rebuild. Вопрос: когда именно нужно его делать? Вот вы пока над этим вопросом подумайте, а мы идём дальше.
Теперь уясним, что простейший дампер выглядит вообще без фанатизма. Ему нужно открыть процесс, т.е. получить его описатель, а потом считать память из адресного пространства только что открытого процесса и сохранить её на диск. Обычно (некоторые пакеры заслуживают отдельного разговора) всё сводится к использованию вполне документированных API. С более изощрёнными техниками мы можете начать ознакомление вот по этой ссылке:
http://www.wasm.ru/article.php?article=dumping
Я не оговорился – именно начать, поскольку список техник ядерного дампа на этом не заканчивается. Но давайте вернёмся к нашему минимальному дамперу. По-хорошему, мы должны считать с файла на диске, т.е. из его заголовка значение ImageSize, далее создать процесс через CreateProcess и определить базу его загрузки, тогда у нас будет информация обо всех потоках во вновь созданном процессе. Это даст нам возможность получать значение регистров процессора, которые являются контекстно-специфичными для каждого отдельно взятого потока, т.е. попросту говоря, различаются для различных потоков. Тогда в нужный момент при определённом состоянии всё тех же регистров и ещё некоторых значений в памяти мы и можем снимать дамп. Существует и другой, вполне хорошо документированный и не менее хорошо описанный всё в тех же «Об упаковщиках в последний раз» способ. Но мне хотелось бы сейчас обратить ваше внимание на иной метод, основанный на разборе структуры РЕВ (Process environment block). Достаточно подробное описание структур для Windows XP SP1 и SP2 я прилагаю к данной части статьи. Но теперь давайте ближе к сути вопроса.
Как РЕВ может помочь сдампить файл? Всё очень просто – именно в РЕВ, а точнее в PEB_LDR_DATA есть информация обо всех загруженных в процесс модулях. Т.е. из этой структуры можно извлечь LDR_MODULE, которая и содержит нужную нам информацию о ImageBase и ImageSize. Но давайте – по порядку.
Определить адрес начала РЕВ в чужом процессе можно с помощью Native API ZwQueryInformationProcess. Её прототип выглядит вот так:
Если ProcessInformationClass равен нулю, то на выходе функция возвращает заполненную структуру:Code:DWORD __stdcall ZwQueryInformationProcess( IN DWORD ProcessHandle, IN DWORD ProcessInformationClass, OUT PVOID ProcessInformation, IN DWORD ProcessInformationLength, OUT PDWORD ReturnLength);
Для работы ZwQueryInformationProcess нам нужно предварительно получить описатель процесса. Для этого мы должны открыть его с помощью вполне документированной функции OpenProcess. Тем не менее, не любой процесс удаётся открыть без соответствующих привелегий. Статья, описывающия базовые понятия привилегий, лежит вот здесь:Code:typedef struct _PROCESS_BASIC_INFORMATION { DWORD ExitStatus; PVOID PebBaseAddress; DWORD AffinityMask; DWORD BasePriority; DWORD UniqueProcessId; DWORD InheritedFromUniqueProcessId; } PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;
http://www.rsdn.ru/article/baseserv/privileges.xml
В принципе, вопрос о привилегия после прочтения вышеупомянутой статьи должен отпасть, от себя добавлю, что существует гораздо более приятный способ включения привилегий - с помощью функции RtlAdjustPrivilege. Как и все Native API, она также экспортируется из ntdll.dll. Её прототип:
О параметрах, которые нужно ей передавать, можно почитать вот здесь:Code:NTSTATUS RtlAdjustPrivilege ( ULONG Privilege, BOOLEAN Enable, BOOLEAN CurrentThread, PBOOLEAN Enabled )
http://forum.sysinternals.com/forum_posts.asp?TID=15745
Теперь, как вы уже знаете из «Об упаковщиках …», нам нужны отладочные привилегии. Вот и используйте вышеупомянутые ссылки для получения последних.
Так вот – теперь поговорим о PEB_LDR_DATA. Указатель на эту структуру храниться по смещению 0х0С от начала РЕВ. Вот её объявление:
Нас с вами будет интересовать разбор InLoadOrderModuleList. А ну-ка, можете ли вы сами вычислить, какое смещение нужно прибавить к началу PEB_LDR_DATA, чтоб оказаться на этом самом InLoadOrderModuleLis? Т.е. теперь, надеюсь, вам понятно, что мы можем пройтись по некоему двусвязому списку и получить информацию обо всех загруженных в процесс модулях. Ведь, в принципе, нас с вами может интересовать не только распаковка ЕХЕ, на и распаковка динамической библиотеки, но об этом – позже. LIST_ENTRY представляет из себя следующую структуру:Code:typedef struct _PEB_LDR_DATA{ DWORD Length; DWORD Initialized; PVOID SsHandle; LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; } PEB_LDR_DATA, *PPEB_LDR_DATA;
Так вот, Flink в InLoadOrderModuleList как раз указывает на параметры ЕХЕ. Точнее, на сруктуру LDR_MODULE, описывающую наш ехе-файл. LDR_MODULE, в свою очередь, объявлен так:Code:typedef struct _LIST_ENTRY { struct _LIST_ENTRY *Flink; struct _LIST_ENTRY *Blink; } LIST_ENTRY, *PLIST_ENTRY;
Очевидно, что нас интересует значение BaseAddress и SizeOfImage. Вот и подумайте, как их получить, а я прилагаю рабочий MiniDumper.exe и его исходники, если хотите посмотреть, как это всё можно сделать. Надеюсь, вопрос о непосредственном чтении памяти и сохранении на диск у вас не возникнет, но если всё-таки он появится, то MiniDumper вам в этом поможет.Code:typedef struct _LDR_MODULE{ LIST_ENTRY InLoadOrderModuleList; LIST_ENTRY InMemoryOrderModuleList; LIST_ENTRY InInitializationOrderModuleList; PVOID BaseAddress; PVOID EntryPoint; DWORD SizeOfImage; UNICODE_STRING FullDllName; UNICODE_STRING BaseDllName; DWORD Flags; WORD LoadCount; WORD TlsIndex; LIST_ENTRY HashTableEntry; DWORD TimeDateStamp; } LDR_MODULE, *PLDR_MODULE;
Сразу оговорюсь, что MiniDumper является практически первым моим приложением, написанным на С, поэтому вполне вероятно, что качество исходных кодов оставляет желать лучшего. Я, вообще-то, привык кодить на Delphi, но т.к. многие этот язык не знают и не любят, я любезно согласился хотя бы примеры к циклу статей писать на любимом языке мелкомягких.
Но у метода, основанного на парсинге структуры РЕВ, есть одно существенное ограничение. Дело в том, что эта структура не выглядит одинаково для различных версий ОС и даже для различных Service Pack’ов одной и той же оси. Таким образом, о полной поддержке всей линейки NT говорить достаточно трудно, но, как вариант, метод иногда может сгодиться.
Тем не менее, зачем изобретать велосипед, когда уже существует Pe-Tools?
http://www.cracklab.ru/download.php?action=get&n=MTU1
Этот чудесный дампер позволяет применять все вышеописанные методики дампинга легко и непринуждённо. Стоя в отладчике на ОЕР, мы в Pe-Tools можем задать опции, позволяющие:
1. Сделать дамп и сохранить его на диск.
2. Вставить в него заголовок из файла на диске.
3. Сделать заголовку DumpFixer.
4. Сделать дампу Rebuild.
С четвёртым пунктом мы пока повременим, а вот остальные можно смело указывать в опциях настройки программы.
Вот теперь, стоя на ОЕР, можно получить нормальный дамп и работать с ним дальше. Дамп можно получить и с помощью MiniDumper’а, но тогда просто сделайте ему DumpFixer – Pe-Tools это умеет.
Тогда возникает вопрос – если всё это и так достаточно неплохо расписано в других статьях, зачем нам снова к этому возвращаться? Терпение, друзья мои – очень скоро вы увидите эти техники в новом свете, но пока вы, как говориться, должны хорошо усвоить мат. часть.
Пару слов о правке дампа
Ну вот, наконец, наш дамп готов, но использовать его – признак плохого воспитания и нехватки знаний. Помните – нам важно не только получить распакованный дамп, который можно использовать для анализа, но и вполне полноценную программу, которую можно использовать для локализаций, упаковки другими пакерами (для уменьшения её размера), да просто для того, чтоб она на диске занимала меньше места. Для этого код аспра должен быть безжалостно удалён. НО! Аспр добавляет к защищаемому файлу две секции, которые идут последними.
И часть важных таблиц (TLS, релоки, ресурсы) он перемещает внутрь этих секций. Если мы их отрежем – мы лишимся информации, которая в них содержится. Следовательно, наш дамп станет бесполезным. Значит мы должны эти структуры переместить. Но куда? По сути – на их родное место. Но где их родное место? А вот это сказать сложно. Очевидно одно – где-то НЕ в секциях, добавленных аспром. А вот где именно – сказать сложно. Но нам это и не нужно, нам нужно переместить всякую важную для нас инфу куда-нибудь за пределы новодобавленных аспром секций, чтоб работало. Вот такая стоит перед нами задача. Теперь давайте подумаем, что нужно переносить. Переносить нужно все таблицы, IMAGE_DATA_DIRECTORY.VirtualAddress которых указывает внутрь аспровых секций. Думаю, сейчас вы легко можете определить, что именно нужно переносить, а что – нет.
Сага о Thread Local Storage…
Про TLS сказано не слишком-то и много, но этот факт можно объяснить относительной незамысловатостью данной структуры. В общем-то, цель как динамической, так и статической TLS – сделать некоторые глобальные переменные доступными одному потоку, и недоступными другому. Да, кстати, базовую информацию про статическую и динамическую TLS можно найти здесь:
http://www.wasm.ru/article.php?article=tls
Также там описывается, как TLS можно использовать для антиотладки. Но аспр (возможно, пока) не пользуется этим механизмом нам во вред. Т.е. мы имеем дело с обычной TLS, только расположенной не там, где ей следует быть. Но давайте немного отвлечёмся…
Иногда в некоторых источниках информации можно встретить совет «занулять» некоторые структуры IMAGE_DATA_DIRECTORY, и, якобы, всё продолжает работать и без них. Давайте сейчас всё проверим на практике - будет ли работать наш распакованный файл, если мы запишем нули вместо TLS Directory?
Результат вообще превзошёл все ожидания. Программа не перестала работать, но при выборе некоторых пунктов меню просто закрылась без страха и упрёка!
Отсюда вывод – TLS где-то как-то используется программой. Но как? Честно говоря, я не знаю, как авторы программ, написанных на Delphi, используют статическую TLS в своих творениях. Именно поэтому я решил поисследовать использование TLS в нашем конкретном примере, но исследования не дали ничего вразумительного.
Обращение к TLS происходит через механизм APC (в нашем конкретном случае). При создании потока ядро помещает в очередь APC некоторые функции, которые будут выполнены. В ядре доставка происходит через функцию KiDeliverApc. Далее KiInitializeUserApc занимается отправкой АРС в режим пользователя. Внутри этой функции встречается такая строка:
А значение _KeUserApcDispatcher соответствует адресу KiUserApcDispatcher, с которой и начинается обработка АРС режима пользователя. Соответствие _KeUserApcDispatcher и KiUserApcDispatcher:Code:mov ecx, ds:_KeUserApcDispatcher
В KiUserApcDispatcher вызывается LdrInitializeThunk. Далее мы можем проследить выполнение цепочки:Code:push offset _KeUserApcDispatcher push offset aKiuserapcdisp ; " KiUserApcDispatcher " call _PspLookupSystemDllEntryPoint@8 ; PspLookupSystemDllEntryPoint(x,x)
_LdrpTlsList – какая-то структура, в составе которой с 0х08 смещения располагается наша TLS Directory. Далее вызывается RtlAllocateHeap, и в выделенную таким образом память копируется содержимое от начала до конца блока данных потока.Code:_LdrpInitializeThread -> _LdrpAllocateTls -> _LdrpTlsList
Собственно, больше никакая работа с TLS не выполняется. Если кто-то в курсе, зачем выполняются такие операции – пишите, с удовольствием прочтём, запомним и учтём. Но на момент написания этой части цикла статей данная информация не была мне доступна. Хотя, конечно, определённые догадки на этот счёт имеются, и не вижу смысла их скрывать. Поскольку обращение к TLS происходит исключительно через АРС и при создании нового потока, то можно предположить, что некоторые компоненты Runtime библиотеки Delphi создают дополнительные потоки и требуют наличия TLS для своей работы.
Теперь о восстановлении. Давайте пока опустим тему флагов секций – об этом мы поговорим позже. Сейчас сосредоточимся исключительно на переносе данных в другую секцию. Тут задача вообще упрощается и сводится к следующему алгоритму:
1. Считываем содержимое TLS Directory.
2. Ищем свободное место в секциях дампа, начиная с первой секции и заканчивая четвёртой с конца. (Т.к. две последние добавляет аспр, и их нужно удалить, а третья с конца – секция ресурсов, и её придётся перестроить).
3. Переписываем на свободное место содержимое TLS Directory.
4. Меняем в заголовке адрес TLS Directory.
Восстановление перемещаемых элементов
Надеюсь, что дочитав до этого места, вы уже неплохо представляете себе, что такое релоки, или fixup’ы, или как их там ещё называют. Знаете даже, что их иногда восстанавливают, а иногда – нет. В этой части нашего цикла мы не будем ломать себе голову над восстановлением этих самых перемещаемых элементов, а вместо этого я расскажу, почему в ехе-файле они не используются. Также расскажу, что же с ними происходит после упаковки программы аспром, а потом повествование закончится тем, что вы узнаете, что же нужно делать в подобных ситуациях, когда релоки преднамеренно испорчены протектором.
Итак, начнём. Я сказал, что релоки испорчены? Это как? Сейчас поясню, что я имел ввиду. Нет, я не имел ввиду какую-то хитроумную защиту этого элемента РЕ-формата, правильнее было бы сказать, что значительная их часть просто удалена из файла.
Вот так изначально выглядела директория перемещаемых элементов в незапакованной аспром программе. А что стало с ней после упаковки:
Очевидно, что теперь загрузка образа по адресу, отличному от ImageBase, становиться невозможной. А раз так, значит эти самые релоки нам просто не нужны, и нет никакого смысла тянуть в новый дамп эти жалкие объедки директории перемещаемых элементов. Ну а если мы захотим восстановить их первоначальное значение, которое было до упаковки программы аспром? А вот это уже – никак. Помните, в первой части я говорил, что на 100% идентичный файл после распаковки получить удается не всегда? Вот это как раз тот случай – релоки для нас утеряны навсегда. Поэтому нам остаётся просто стереть информацию о релоках из нашего файла с помощью Pe Tools и чудесной опции Wipe Relocations, а директорию перемещаемых элементов в заголовке обнулить.
Это, конечно, далеко не всё, что следует знать о релоках, но более подробно мы вернёмся к этой теме при рассмотрении распаковки динамической библиотеки, где релоки нельзя просто удалить из бинарника. Но на данном этапе это всё, что я хотел сказать о перемещаемых элементах.
Восстановление ресурсов
Сейчас можно было бы расписать саму структуру директории ресурсов, описать метод порчи этой структуры аспром, а закончить всё алгоритмом по её восстановлению. И мы обязательно это сделаем, но в более поздних частях нашего цикла. Сейчас я хотел бы сосредоточить ваше внимание на самых базовых понятиях, связанных с ресурсами вообще, и с аспром в частности.
Первый, и, наверное, любимый вопрос – зачем восстанавливать директорию ресурсов, ведь и так всё работает? С этим мы уже определились, но для танкистов я повторю:
1. Чтоб отрезать секции протектора и сделать файл меньше.
2. Чтоб появилась возможность редактирования ресурсов.
Если первое, по мнению некоторых, относится к выпендрёжу (люди, имеющие такое мнение, считают, что секции протектора занимают мало места, и нет никакого смысла так пыхтеть, чтоб их удалить), то второе – действительно важно, т.к. иногда есть необходимость перевести программу на другой язык, сменить диалоговые окна и логотипы и т.д. Поэтому сформулируем следующие требования к директории ресурсов:
1. Она должна находиться в ОДНОЙ секции файла с именем .rsrc.
2. Эта секция должна идти последней для возможности её увеличения.
В принципе, оба требования должны быть понятны, поэтому на этом я не останавливаюсь. Теперь о том, как нам этого добиться. Для этого можно, конечно, использовать программу Resource Binder, но мне хотелось бы показать некоторым несведущим людям, что ResFixer by seeQ, на самом деле, может справляться с этой задачей не хуже. Сам ResFixer и прилагаю в архиве к статье.
Задача предельно ясна – приступаем. Нам понадобятся две копии одного и того же дампа. Как вы помните – сейчас у нас есть дамп с выполненным DumpFixer’ом. Делаем его копию и продолжаем. Теперь переносим директорию TLS из секций аспра – уже обсуждалось. Притом это действие нужно провести только в одной из копий дампа – вторую мы держим исключительно для извлечения оттуда ресурсов. Теперь отрезаем три последние секции у дампа с перенесённой TLS. Второй же дамп мы грузим в ResFixer. Как видим, некоторые смещения указывают за пределы секции ресурсов – в секции аспра. Это нам и придётся устранить. К нашему дампу, от которого мы отрезали секции, мы сейчас добавим перестроенную секцию ресурсов. Для этого мы выбираем в настройках Method 2 (Full reconstruct), а новую секцию ресурсов настраиваем на новый RVA, который можно вычислить, как RVA последней секции в дампе + Virtual Size этой же последней секции.
Из рисунка следует, что RVA новой секции ресурсов будет равно 0x000C4000 + 0x0000A000 = 0x000CE000. ResFixer обычно высчитывает это значение самостоятельно, но бывают случаи, когда ему необходимо помочь.
После этого остаётся только нажать Rebuild и сохранить новую секцию на диск. Теперь с помощью Pe Tools делаем Load Section from disk. Потом остаётся лишь подредактировать заголовок секции – изменить имя секции и RVA. Также подправим RVA директории ресурсов (в данном случае – не требуется, но вообще следить за этим нужно). На этом восстановление директории ресурсов можно было бы считать закрытой темой, но постойте, а как же восстановление импорта? Ведь вполне может быть, что при восстановлении импорта нам придётся добавить ещё одну секцию в наш дамп, но ведь секция ресурсов должна быть последней. Значит восстановление импорта следует выполнять до восстановления ресурсов. Как именно? Думаю, теперь вы сможете уверенно ответить на этот вопрос.
Подведение итогов
Знаете, всё время не могу отделаться от ощущения, что эта часть получилась излишне абстрактной. И правда – ни алгоритмов, ни глубокого разбора РЕ-формата. Наверное, всё потому, что эти вещи уже делались в статьях «Об упаковщиках в последний раз». Но мне тоже не хотелось вот так всё бросать. Я решил осветить ещё один интересный момент. Часто бывает так, что после отрезания секций в Pe Tools дамп напрочь отказывается работать. Загрузчик вообще выдаёт сообщение, что файл не является представителем РЕ-формата. Но в том же Pe Tools есть чудесная опция Validate PE, которая всё возвращает на круги своя. Так как же она работает? Здесь я привёл результат своих собственных исследований:
Как видите, всё вполне очевидно. Мне кажутся излишними даже какие-то комментарии. Но обратите внимание, что эта функция экспортируется из модуля RebPE32.dll. Этот модуль – настоящий клад для авторов распаковщиков. И хотя он не является Open Source, зато его работоспособность многократно проверена и вселяет уверенность. Настоятельно рекомендую вам поисследовать этот модуль – там ещё много чего интересного экспортируется.Code:ValidatePe BOOLEAN __stdcall ValidatePE (PVOID ImageBase,PVOID ImageSize) push [ebp+Base] ; Base call ImageNtHeader mov [ebp+var_2C], eax ; eax = начало структуры PE Header cmp eax, edi ; edi = 0 jz loc_10004C41 ; если всё ок, то никаких переходов movzx ecx, word ptr [eax+14h] ; есх = NT Header Size lea ecx, [ecx+eax+18h] ; в есх – адрес начала первой секции mov [ebp+var_28], ecx mov [ebp+var_1C], ecx ; сохраняем смещение начала первой секции в локальных переменных or [ebp+var_20], 0FFFFFFFFh ; запихиваем в эту локальную переменную -1 mov [ebp+var_30], edi ; а в эту – ноль loc_10004BA8: movzx edx, word ptr [eax+6] ; получили Num of Objects (NumberOfSections) в edx cmp [ebp+var_30], edx ; сравнивает NumberOfSections с нулём jnb short loc_10004BCC ; переход не выполняется mov edx, [ebp+var_1C] mov edx, [edx+14h] ; считывается значение PointerToRawData для первой секции cmp edx, edi ; значение PointerToRawData сравнивается с нулём jz short loc_10004BC3 ; для первой секции не выполняется cmp edx, [ebp+var_20] ; значение PointerToRawData сравнивается с -1 без учёта знака jnb short loc_10004BC3 ; переход не выполняется mov [ebp+var_20], edx ; сохраняется новое PointerToRawData loc_10004BC3: add [ebp+var_1C], 28h ; перемещает указатель на следующую секцию inc [ebp+var_30] ; увеличивает счётчик секций на 1 jmp short loc_10004BA8 ; возвращает на вышерассмотренный код Таким образом, ищется самый маленький PointerToRawData. Когда цикл доходит до последней секции, выполняется следующий код: loc_10004BCC: mov edx, 1000h cmp [ebp+var_20], edx ; самый маленький PointerToRawData сравнивается с 1000h jbe short loc_10004BD9 ; прыжок выполняется Если самый маленький PointerToRawData больше 1000h, то в [ebp+var_20] записывается 1000h loc_10004BD9: mov edx, [ebp+var_20] ; edx = 1000h или меньшему PointerToRawData mov [eax+54h], edx ; Header Size = 1000h или меньшему PointerToRawData mov [ebp+var_1C], ecx mov [ebp+var_24], ecx ; в локальные переменные записалось начало первой секции (смещение начала) lea edx, [ecx+28h] ; в edx – начало второй секции mov [ebp+var_24], edx ; начало секции сохраняется mov [ebp+var_30], edi ; обнуляется локальная переменная, бывшая счётчиком loc_10004BEE: movzx esi, word ptr [eax+6] ;– считывается NumberOfSections dec esi ;уменьшается на 1 cmp [ebp+var_30], esi ; сравнивается со счётчиком jnb short loc_10004C12 ; когда станет больше либо равно – прыжок mov esi, [edx+0Ch] ; esi = Section RVA (второй секции при первом проходе) sub esi, [ecx+0Ch] ; esi = esi – Section RVA первой секции mov [ecx+8], esi ; Virtual Size = Section RVA второй секции – то же первой секции add ecx, 28h ; переход на следующую секцию mov [ebp+var_1C], ecx add edx, 28h mov [ebp+var_24], edx inc [ebp+var_30] jmp short loc_10004BEE loc_10004C12: mov [ebp+var_34], ecx ; записывается смещение последней секции mov edx, [ebp+arg_4] ; edx = ImageSize sub edx, [ecx+14h] ; edx = ImageSize – PointerToRawData последней секции mov [ecx+10h], edx ; SizeOfRawData последней секции = edx mov esi, [ecx+8] ; esi = VirtualSize последней секции cmp esi, edi ; esi сравнивается с нулём mov ecx, [ecx+0Ch] ; ecx = PointerToRawData последней секции jz short loc_10004C2 ; не выполняется add ecx, esi ; VirtualSize складывается с PointerToRawData (для последней секции) jmp short loc_10004C2E loc_10004C2E: mov [eax+50h], ecx ; записывается новый ImageSize or [ebp+var_4], 0FFFFFFFFh push 1 pop eax ; eax = TRUE
Теперь – про перспективы и результаты. Вот и подходит к концу третья часть из цикла про распаковку ASProtect. Кое-что уже сделано, но ещё очень многое предстоит сделать. Касательно четвёртой части могу сказать, что изначально я планировал описать там работу ImpRec’а, восстановление импорта вообще и уделить внимание проблемам, которые могут при этом возникнуть, но сейчас, оглядываясь на третью часть, опасаюсь ещё одной абстракции. Поэтому я принял решение в этом цикле чередовать абстрактные статьи с более практичными. Так что следующий выпуск будет всецело посвящён уже известной нам библиотеке ASProtect.dll. К вопросам импорта мы вернёмся, но пока не буду обещать вам, как скоро это произойдёт.
Говоря про теорию, я не имею ввиду, что в скором времени мы с вами вместе начнём изучать букварь. Очень многое остаётся за кадром, и этот материал вам придётся освоить самостоятельно. Во-первых, я предполагаю, что вы сносно (как и я) знаете С, во-вторых – знаете хоть что-то про устройство Windows, а также читаете прилагаемые мною ссылки. Пока этого будет достаточно. Хотя, если кто-то чувствует, что позабыл инфу из букваря, то советую её немедленно освежить в памяти, но самостоятельно.
На этом я с вами прощаюсь, до новых встреч в четвёртой части…
Архив к третьей части
ARCHANGEL © AHTeam, r0 Crew



Reply With Quote
Thanks
