R0 CREW

Борьба с ASProtect - вернуть на доследование (часть 3)

Вместо приветствия…

Добро пожаловать в третью часть, в которой мы продолжим изучение механизмов распаковки и Аспровских техник, призванных нам помешать. Эта часть, скорее всего, будет делать упор на теорию, но без практики в этот раз не получится. К сожалению, пока мы не можем идти дальше, т.к. защитные механизмы даже первой версии аспра нами изучены очень поверхностно. Давайте подведём итог того, что мы можем уже сейчас. А можем мы очень немного:

  1. Бороться с аспровскими переходниками.
  2. Искать ОЕР.

И… всё. Больше я не смог придумать ничего, что бы могло пополнить этот список возможностей. Да и то, мы всё это можем, если нам повезёт! Что это значит? А то, что вы, изучая материал прошлой части, могли заметить, что там нет ни слова про антиотладку аспра. Что же такое антиотладка, как это работает и как этому противостоять? Да, именно с этого мы и начнём третью часть. Пристегните ремни – мы начинаем.

Антиотладка и методы её преодоления

Антиотладка – это набор методов обнаружения и противодействия отладчикам. Целью любой антиотладки является нахождение и использование различий в поведении отлаживаемой и неотлаживаемой программы. Естественно, под понятием отладки не следует понимать отладку только в OllyDebug. Если реверсер использует отладчик уровня ядра, например, Soft-Ice, то это тоже отладка. Естественно, существуют методики противодействия как отладчикам режима пользователя, так и отладчикам режима ядра. Совершенно точно могу вам сказать, что из режима ядра обнаружить, что какой-то процесс отлаживается, гораздо проще, чем из режима пользователя. Как вы помните, аспр является протектором режима пользователя (третьего кольца защиты), что налагает существенные ограничения на его антиотладочные механизмы. Также вы помните, что версия 1.0 вышла в 2000 году, когда Ольга была ещё (была ли вообще?) не особо популярна. В то время популярностью пользовался всё тот же Soft-Ice (далее Айс). Конечно же, Солодовникову приходилось адекватно реагировать на имеющихся тогда инструменты, способные противодействовать его протектору, поэтому если у вас в системе установлен и активен Айс, и нет никаких механизмов его сокрытия, то повторить подвиги, описанные мною во второй части данного цикла статей вам не удалось. Вместо ОЕР и восстановленных переходников вы лицезрели убогое сообщение аспра с непонятным текстом. И в то же время, если Айса у вас нет, и вы, используя Ольгу, решили вообще не ставить себе PhantOm, то были удивлены тем, что Ольга, как ни в чём не бывало, работала. И никакой антиотладки видно не было. Как так?

Для того, чтоб ответить на все ваши вопросы, нам снова нужно вооружиться отладчиком и пойти исследовать антиотладку аспра. С помощью скрипта из второй части проходим на начало ASProtect.dll. Теперь, вспоминая вторую часть, заходим внутрь последнего CALL. Далее – Crtl-F9, и оказываемся вот здесь:

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 00198D98. Теперь здесь нужно немного потрассировать код. Но возникает вполне логичный вопрос – немного, это сколько? Да и как тут его трассировать? По F8 или по F7? Вопрос, может, и логичный, но, по-моему, уже не вполне уместный. Конечно, трассируем по F8, пока не всплывёт ваше любимое окно, которое, типа, сообщит о возникновении внештатной ситуации. Но ведь может так статься, что у вас такой внештатной ситуации и не возникнет, тогда просто послушайте меня, ведь я трассировал этот участок и на F7 тоже, и могу вам сказать, что антиотладка начинается по такому адресу:

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

В случае, если переход выполнится, то протектору удалось сдетектить отладчик. Что же такого происходит внутри CALL 00198D74, что позволяет или не позволяет обнаруживать отладчик? Как это работает?

Зайдя внутрь вышеупомянутого вызова, можем наблюдать следующее:

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

Как видите, весь антиотладочный трюк сводится к вызову CreateFileA, и если вызов завершился неудачей, т.е. функция вернула INVALID_HANDLE_VALUE, то выполняется инкремент регистра EAX. Давайте пока сосредоточимся на вызове CreateFileA. Вызывается она вот с таким параметрами на стеке:

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

Т.е. в принципе, тут всё понятно со всеми параметрами, кроме первого. На имя файла это не похоже, тогда что это? Дело в том, что когда отладчик уровня ядра начинает работу, то при загрузке его драйвера происходит создание объекта-устройства с некоторым именем. В документации это именуется мелкомягкими как Device Object Name. И именно его и ищет защита. В данном случае, таким образом детектится наличие в системе Айса. Но вот что хорошо в этом методе – отладчик удастся обнаружить только в том случае, если он активен, а не просто установлен. Соответственно, если никакими Айсами мы отроду не пользовались, то CreateFileA вернёт нам 0xFFFFFFFF - INVALID_HANDLE_VALUE, что можно узнать всё в том же MSDN, о котором речь шла во второй части.

Хочется обратить ваше внимание на один красивый трюк с ассемблерными командами. К антиотладке он не относится, но нужно воздать должное Борландовскому компилятору в запутывании нас с вами. После вызова CreateFileA мы видим такой код (он уже приводился выше, но продублируем):

00198D8F    40              INC EAX
00198D90    0F95C0          SETNE AL
00198D93    5B              POP EBX
00198D94    C3              RETN

Казалось бы, INC EAX должен увеличить INVALID_HANDLE_VALUE на 1, тогда в ЕАХ окажется 0. Ну, это, разумеется, в выгодном для нас с вами случае. Но не всё так просто! Посмотрите в мануалы интела, там в описании инструкции INC говорится следующее:

Т.е. помимо увеличения непосредственного операнда, команда воздействует на флаги. Но «в зависимости от результата» меня не устроило, как пояснение, поэтому я решил всё в тех же мануалах посмотреть, что же это за зависимости. В Table B-1. EFLAGS Condition Codes можно найти ответ на вопрос, в результате каких условий происходит изменение вышеупомянутых флагов. Кстати, это – первый том интеловских мануалов. Также следует упомянуть, что сравнения производятся над операндом, указанным в команде, т.е. в нашем случае, над значением регистра ЕАХ. Следующая за инкрементом команда SETNE AL обращается только к флагу ZF. Т.е. если он (флаг) взведен, AL будет равен нулю, в противном случае станет равным единице. Таким образом, если протектор не сдетектил Айс, то в AL будет 0. Во как всё хитро работает.

Но вернёмся к антиотладке.

00198E85    E8 3A1FFFFF     CALL 0018ADC4                            ; JMP to ntdll.RtlGetLastWin32Error
00198E8A    83F8 02         CMP EAX,2
00198E8D    75 30           JNZ SHORT 00198EBF

Здесь прот считывает код последней ошибки в случае, если CreateFileA завершилась неудачно, и если последней ошибкой, которая возникла в ходе неудачного вызова CreateFileA стала не ERROR_FILE_NOT_FOUND (00000002), то снова прот ставит нам палки в колёса. Далее трюки весьма однообразны – через всё тот же CreateFileA ищутся устройства:

\.\SICE
\.\NTICE

После их ненахождения проверяются коды ошибки, как это уже было описано выше. Следующий трюк вообще очень прост и неэффективен:

00194124    CC              INT3
00194125    EB 02           JMP SHORT 00194129

На INT3 произойдёт исключение, и если отладчика нет, управление передастся на обработчик. А если есть – то отладчик, якобы, должен «поглотить» исключение, и выполнение программы продолжится JMP SHORT 00194129. Но Оля на такое не покупается, поэтому смело идём на адрес обработчика. Его найти просто даже вручную: смотрим, чему равна база селектора, загруженного в fs, у меня 7ffdf000. Далее по этому адресу находим указатель на цепочку фреймов SEH:

7FFDF000  0012FA70  (Pointer to SEH chain)

А по этому указателю – саму цепочку, и переходим на адрес первого обработчика:

0012FE30  0012FE3C  Pointer to next SEH record
0012FE34  00194528  SE handler

Далее это всё приводит нас к тому, что переход по адресу 00194528 ведёт нас на уже известный из второй части обработчик (вы ведь нашли ОЕР самостоятельно, правда?):

После обработки исключения мы возвращаемся на адрес:

0019452D    E8 AA07FFFF     CALL 00184CDC

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

Но, вернёмся к нашему асприку – как в нём обойти антиотладку, если всё-таки происходит детект? Сразу определимся, что все эти приёмы – вообще не приёмы, если у нас в системе неактивен Айс. Но что, если Айс активен и используется в настоящий момент для отладки какого-то вполне легального приложения, а мы захотели запустить программулину и снять с неё аспр? Как тогда быть?
Вообще, при обходе антиотладочных трюков есть два основных (и наиболее простых в реализации) способа. Первый основан на патче ОС, а второй – на патче кода протектора. Т.е. если мы используем первый способ для сокрытия, к примеру, того же Айса, то мы можем перехватить сплайсингом CreateFileA, можем перехватить в ntdll.dll сервис ZwCreateFile, а можем перехватить IoCreateFile в ядре. На этом способы перехвата, конечно, не заканчиваются. Я их привёл здесь, скорее, для примера. Но суть вы уловили – можно патчить всё, кроме кода протектора.

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

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

И если этот переход не выполняется, то начинаются поиски отладчика, а если выполняется? А если выполняется, то мы как раз проходим стороной всю антиотладку. А ниже – начинается код, выполняющий проверку контрольной суммы, а перед ним есть такой же переход. Естественно, что если и он не выполняется, то не будет никакой проверки контрольной суммы. Вот так всё легко и непринуждённо. Но как же так? Получается, что мы исправили всего два перехода с условных на безусловные, и в результате прошли и антиотладку, и проверку контрольной суммы. Но как Солодовников допустил такое? А очень просто – уже известная нам ASProtect.dll не пересобирается протектором каждый раз, т.е. её код постоянен. Меняются лишь данные, которые отвечают за опции защиты. Т.е. говоря проще – когда протектор упаковывает программу, он учитывает опции защиты. Эти опции выбирает пользователь, а протектор в точности выполняет лишь те проверки и защитные трюки, «заказанные» пользователем. Но ему (аспру) нужно как-то узнать, следует ли выполнять пересчёт контрольной суммы и искать отладчик. Для этого он добавляет некоторые данные, которые в последствии обрабатываются кодом ASProtect.dll, и уже она решает, как защищать приложение. Она решает, а мы – поможем.

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

Дамп, секции и прочие страшные термины

Теперь настало время для знакомства с всепоглощающей теорией распаковки. До этого мы, можно сказать, тупо искали всякие ОЕР, восстанавливали таблицы и боролись с антиотладкой. Но теперь пора заняться подготовкой распакованного файла. Всякий распакованный файл – это исправленный и дополненный дамп, но дамп лежит в основе распаковки. Что же такое дамп и чем он отличается от обычного РЕ-файла? Как вы знаете, РЕ-файл загружается в память и выполняется оттуда. Но после проецирования в память перед выполнением точки входа происходит инициализация важных элементов. Сейчас пока не будем вдаваться в подробности, какие именно элементы инициализируются, но суть в том, что в памяти они уже инициализированы. Дамп – это копия памяти выбранного процесса, сохранённая на диске. В дампе обычно нужно восстановить таблицу импорта, перемещаемые элементы, правильно выставить значение ЕР. В принципе, это – необходимый минимум. Как вы уже знаете из «Об упаковщиках в последний раз», ехе-файл может прекрасно существовать и без таблицы перемещаемых элементов. Но без таблицы импорта и нового значение ЕР распакованная программа не заработает никогда. Логично предположить, что раз необходимо исправить значение ЕР, то его следует исправить в заголовке, который тоже может дампиться из памяти, а может копироваться непосредственно с диска. Как вы понимаете, копировать с диска – надёжнее, но если протектор не портит заголовок в памяти, то, по сути, нет никакой разницы. Теперь подытожим сказанное.

Дамп – это копия памяти выбранного процесса, начиная с 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. Её прототип выглядит вот так:

DWORD __stdcall ZwQueryInformationProcess(
    IN DWORD ProcessHandle,
    IN DWORD ProcessInformationClass,
    OUT PVOID ProcessInformation,
    IN DWORD ProcessInformationLength,
    OUT PDWORD ReturnLength);

Если ProcessInformationClass равен нулю, то на выходе функция возвращает заполненную структуру:

typedef struct _PROCESS_BASIC_INFORMATION { 
DWORD ExitStatus;
PVOID PebBaseAddress;
DWORD AffinityMask;
DWORD BasePriority;
DWORD UniqueProcessId;
DWORD InheritedFromUniqueProcessId;
} PROCESS_BASIC_INFORMATION, *PPROCESS_BASIC_INFORMATION;

Для работы ZwQueryInformationProcess нам нужно предварительно получить описатель процесса. Для этого мы должны открыть его с помощью вполне документированной функции OpenProcess. Тем не менее, не любой процесс удаётся открыть без соответствующих привелегий. Статья, описывающия базовые понятия привилегий, лежит вот здесь:

http://www.rsdn.ru/article/baseserv/privileges.xml

В принципе, вопрос о привилегия после прочтения вышеупомянутой статьи должен отпасть, от себя добавлю, что существует гораздо более приятный способ включения привилегий - с помощью функции RtlAdjustPrivilege. Как и все Native API, она также экспортируется из ntdll.dll. Её прототип:

NTSTATUS RtlAdjustPrivilege
 (
  ULONG    Privilege,
  BOOLEAN  Enable,
  BOOLEAN  CurrentThread,
  PBOOLEAN Enabled
 )

О параметрах, которые нужно ей передавать, можно почитать вот здесь:

http://forum.sysinternals.com/forum_posts.asp?TID=15745

Теперь, как вы уже знаете из «Об упаковщиках …», нам нужны отладочные привилегии. Вот и используйте вышеупомянутые ссылки для получения последних.

Так вот – теперь поговорим о PEB_LDR_DATA. Указатель на эту структуру храниться по смещению 0х0С от начала РЕВ. Вот её объявление:

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;

Нас с вами будет интересовать разбор InLoadOrderModuleList. А ну-ка, можете ли вы сами вычислить, какое смещение нужно прибавить к началу PEB_LDR_DATA, чтоб оказаться на этом самом InLoadOrderModuleLis? Т.е. теперь, надеюсь, вам понятно, что мы можем пройтись по некоему двусвязому списку и получить информацию обо всех загруженных в процесс модулях. Ведь, в принципе, нас с вами может интересовать не только распаковка ЕХЕ, на и распаковка динамической библиотеки, но об этом – позже. LIST_ENTRY представляет из себя следующую структуру:

typedef struct _LIST_ENTRY {
  struct _LIST_ENTRY *Flink;
  struct _LIST_ENTRY *Blink;
} LIST_ENTRY, *PLIST_ENTRY;

Так вот, Flink в InLoadOrderModuleList как раз указывает на параметры ЕХЕ. Точнее, на сруктуру LDR_MODULE, описывающую наш ехе-файл. LDR_MODULE, в свою очередь, объявлен так:

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;

Очевидно, что нас интересует значение BaseAddress и SizeOfImage. Вот и подумайте, как их получить, а я прилагаю рабочий MiniDumper.exe и его исходники, если хотите посмотреть, как это всё можно сделать. Надеюсь, вопрос о непосредственном чтении памяти и сохранении на диск у вас не возникнет, но если всё-таки он появится, то MiniDumper вам в этом поможет.

Сразу оговорюсь, что 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 занимается отправкой АРС в режим пользователя. Внутри этой функции встречается такая строка:

mov     ecx, ds:_KeUserApcDispatcher

А значение _KeUserApcDispatcher соответствует адресу KiUserApcDispatcher, с которой и начинается обработка АРС режима пользователя. Соответствие _KeUserApcDispatcher и KiUserApcDispatcher:

push    offset _KeUserApcDispatcher
push    offset aKiuserapcdisp ; " KiUserApcDispatcher "
call    _PspLookupSystemDllEntryPoint@8 ; PspLookupSystemDllEntryPoint(x,x)

В KiUserApcDispatcher вызывается LdrInitializeThunk. Далее мы можем проследить выполнение цепочки:

_LdrpInitializeThread -> _LdrpAllocateTls -> _LdrpTlsList

_LdrpTlsList – какая-то структура, в составе которой с 0х08 смещения располагается наша TLS Directory. Далее вызывается RtlAllocateHeap, и в выделенную таким образом память копируется содержимое от начала до конца блока данных потока.

Собственно, больше никакая работа с 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, которая всё возвращает на круги своя. Так как же она работает? Здесь я привёл результат своих собственных исследований:

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

Как видите, всё вполне очевидно. Мне кажутся излишними даже какие-то комментарии. Но обратите внимание, что эта функция экспортируется из модуля RebPE32.dll. Этот модуль – настоящий клад для авторов распаковщиков. И хотя он не является Open Source, зато его работоспособность многократно проверена и вселяет уверенность. Настоятельно рекомендую вам поисследовать этот модуль – там ещё много чего интересного экспортируется.

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

Говоря про теорию, я не имею ввиду, что в скором времени мы с вами вместе начнём изучать букварь. Очень многое остаётся за кадром, и этот материал вам придётся освоить самостоятельно. Во-первых, я предполагаю, что вы сносно (как и я) знаете С, во-вторых – знаете хоть что-то про устройство Windows, а также читаете прилагаемые мною ссылки. Пока этого будет достаточно. Хотя, если кто-то чувствует, что позабыл инфу из букваря, то советую её немедленно освежить в памяти, но самостоятельно.

На этом я с вами прощаюсь, до новых встреч в четвёртой части…

Архив к третьей части

ARCHANGEL © AHTeam, r0 Crew

good

Ждем продолжения :slight_smile: