Воробьёвы

(-:

Volodya/HI-TECH, NEOx/UINC.RU

Об упаковщиках в последний раз

(часть 2.2)

(продолжение)

“That people seeking education should have the opportunity to find it.”
Nick Parlante
“Binary Trees”

Рецензент:
Dr.Golova/UINC.RU

Сорецензенты:
Four-F/HI-TECH, Quantum, Sten, Fixer

А запакован ли файл?

написано совместно с Fixer

В виду того, что эта статья позиционируется для взломщика с некоторым опытом, кажется немного наивным поднимать здесь такой вопрос. И в самом деле, для того, чтобы определить, а запакован ли файл, нужен только опыт. Дизассемблируется мусор, редакторы ресурсов отказываются работать и т.д., и т.п. Словом, все симптомы налицо и не о чем тут говорить. Это так. Возражений нет. Однако в рамках этой главы нам бы хотелось показать довольно оригинальный прием программной детекции упакованного файла. Для этого определим понятие "энтропия".

Энторпия
Боже, как только это слово в термодинамике не обругали. И мера порядка системы, и мера рассеивания энергии, уж чего только там не было! Без сомнения, настоящего физика от нашего определения покоробит, а настоящий математик искренне возмутится. Тем не менее, по дилетантски определим слово "энтропия" как некоторую меру эффективности хранения информации. Для иллюстрирования понятия продемонстрируем следующий пример. Условимся считать файл всего лишь строкой байт, конец которой определяется каким-то загадочным образом самой операционной системой.

Положим, у нас есть строка:

abcdcdaad

Подсчитаем количество вхождений каждого байта. Вот так:

        a = 3
        b = 1
        c = 2
        d = 3
        ------
        9 total

 

Таким образом, мы можем сказать, что частота (т.е. вероятность - мы лезем в статистику) появления данного байта в этой конкретной последовательности составляет:

        a = 3/9 = 0.33...
        b = 1/9 = 0.11...
        c = 2/9 = 0.22...
        d = 3/9 = 0.33...,

где 9 - общая длина последовательности.
Определим теперь энтропию каждого символа по формуле:

entropy = |log2(frequency_of_given_byte)|,
        

где log2 - логарифм по основанию 2.
Таким образом, имеем:

        a: |log2(3/9)| = 1,5849625007211561814537389439478
        b: |log2(1/9)| = 3,1699250014423123629074778878956
        c: |log2(2/9)| = 2,1699250014423123629074778878956
        d: |log2(3/9)| = 1,5849625007211561814537389439478,
      

Просуммировав сумму всех энтропий, получаем:

        1,5849625007211561814537389439478 A
        1,5849625007211561814537389439478 A
        1,5849625007211561814537389439478 A
        3,1699250014423123629074778878956 B
        2,1699250014423123629074778878956 C
        2,1699250014423123629074778878956 C
        1,5849625007211561814537389439478 D
        1,5849625007211561814537389439478 D
        1,5849625007211561814537389439478 D
        ----------------------------------------------
        17,019550008653874177444867327367 бит информации
        

Теоретически, это означает, что данную строку мы могли бы хранить в компьютерной памяти, используя лишь 17 бит информации. Реально же используется 72 - т.е. символов у нас 9, каждый символ - это байт, а байт - 8 бит. Итого 72 = 8*9. Остается сделать последний штрих - подсчитать остаток от деления общего количества бит на "энтропийные" биты (72/17 = 4,23). Выполнив его, мы увидим, что эффективность хранения информации невысока - фактически, имеем разницу в ЧЕТЫРЕ раза.

Отношение к упаковке это имеет самое прямое. Когда секция кода файла сжата, то суммарная энтропия при подсчете возрастет, т.е. приблизится к стандартной сумме всех бит - это будет говорить о повышенной эффективности хранения информации, т.е. возможном сжатии секции кода (остаток от деления уменьшится).

Алгоритм может выглядеть так:

    long *ArrFreq;
    double *aEntropy;
    double msgEntropy=0.0;
    ArrFreq=new long[256];
    aEntropy=new double[256];
    ZeroMemory(ArrFreq,256*sizeof(long));
    ZeroMemory(aEntropy,256*sizeof(double));
    BYTE *pBuff=(BYTE*)Offset;
    long Max=0;
    DWORD i = 0;
    // подсчитаем каждый байт в сегменте (кода, данных и т.п.)
    for (;i<pSegment->Size;i++)
    {
     ArrFreq[pBuff[i]]++;
    }
    BYTE OpCode=0xff;

    for (i=0;i<255;i++)
    {
     if (ArrFreq[i]>Max)
     {
      Max = ArrFreq[i];
      OpCode=(BYTE)i;
     }
     // хранит вероятность появления символа
     double prob=(double)ArrFreq[i]/(double)pSegment->Size;
     if (prob)
     {
      //подсчитаем энтропию для i-го байта
      aEntropy[i]=(-log(prob)/log(2))*(double)ArrFreq[i];
      // и в общую сумму!
      msgEntropy+=aEntropy[i];
     }
    }

    // теперь в битах
    double DataSize=(double)pSegment->Size*8.0;

    // теперь делим, для вычисления остатка
    double CompressionRatio=DataSize/msgEntropy;

Данный кусочек кода был любезно предоставлен Manuel Jimenez - автором BDASM (www.bdasm.com) - очень перспективного и быстрого дизассемблера, из которого в будущем может получится достойный соперник IDA! Разумеется, код нельзя просто скомпилировать, однако общий подход он даст.

А особо любознательным расскажем, что идея эта, естественно, отнюдь не нова. Давным-давно криптографы определили понятие "гаммирование" - т.е. наложение какой-либо последовательности байт на текст, чтобы исказить его до неудобочитаемости. Шифр Вернама, перестановки Цезаря - все это призвано было защитить файл от чужих глаз. Однако криптоаналитики придумали подход, который можно назвать "частотным анализом". Т.е., делается предположение о том, что зашифрованный файл содержит в себе осмысленные предложения из такого-то или такого-то языка. И зная вероятности появления символов алфавита в тексте (кстати, как вы думаете, а какая самая часто встречающаяся буква в русском алфавите?), можно попытаться примерно таким же алгоритмом угадать, а что же спрятано за маской? Доказано, что гаммирование принципиально нельзя сломать при условии равенства (и достаточно скрупулезного подбора!) длины последовательности (гаммы), длине шифруемой последовательности. Но даже если это и не так, то, ответьте, что мне мешает заархивировать файл, а уж потом наложить гамму, пусть и неустойчивую? Архивация полностью уничтожит вероятностные распределения букв под маской, делая дешифровку невозможной. Так? А вот и не так! Популярных архиваторов не так уж и много! Опытный криптоаналитик, увидев подобную белиберду, первым делом попробует сжать файл. Как, не сжимается? Ах так! Ну мы тогда...

Впрочем, кажется, мы увлеклись. Итак, определение того, что файл упакован, не займет много времени. Теперь зададим вопрос: "А ЧЕМ упакован файл?". Ответ на подобный вопрос нужен не одним нам с вами. Мировым стандартом (не побоимся этой фразы) считается составление сигнатуры и поиска этой сигнатуры в файле. С точки зрения алгоритмики имеем поиск подстроки в строке.

Приготовление сигнатуры – вещь не сложная и во многом должна определяться квалификацией того человека, который эту сигнатуру составляет. В Pe Tools для этой цели разработана утилита SignMan, следующая версия которой будет основана на очень простом принципе: побайтовом сравнении файлов, запакованных ОДНИМ И ТЕМ ЖЕ упаковщиком с разными опциями:

    SetFilePointer(на оффсет, введенный пользователем,
                 т.к. отсчитывать от точки входа – неверное решение!);

     ... //отвести буферы и т.д., и т.п.

    for(int i = 0; i < до какого-то значения, заданного пользователем; i++)
    {
      
    if(byFromFile1[i] == byFromFile2[i])
       {
     /*хорошо – в отчет*/
       }
    }

Сигнатуры хранятся в текстовой форме в файле Sign.txt. Практика показала, что это решение удачно. У многих людей, заинтересованных в судьбе утилиты, нашлось свободное время, чтобы отправить баг-репорт с правками сигнатур и, можно заявить, что файл, на сегодняшний день является достаточно тщательно проверенным не только авторами утилиты, но и многочисленными пользователями. Хотя, разумеется, это не означает, что багов там нет...

Словом, примем сигнатуру для поиска достаточно надежным средством. Осталось определится с тем, как ее искать. Мы трактуем файл как последовательность байт. PE Sniffer, по версию 1.5.х. включительно, является пока еще утилитой-ребенком и от него, ни в коем случае, пока нельзя ожидать многого. Поэтому утилита должна быть переписана с учетом быстрого и в достаточной степени надежного поиска. Итак, поиск может быть разделен на две категории:

1) Точный поиск подстроки в строке

Байт-в-байт в точке входа – тривиальное strcmp с минимальными трюками по пропуску плавающих байт. Медленно, наивно, не всегда работает. Скажем, ничто не мешает переписать точку входа PE-файла на свой, достаточно безобидный код, который, скажем, делает не больше, чем xchg eax,eax – и этого хватит, чтобы сигнатура не была опознана. Учтите, что автоматический алгоритм детекции секции кода может быть неверным (причины см. в первой части), поэтому надежнее сначала просто спросить у пользователя, какую секцию PE он считает секцией кода. Что мы имеем здесь в более продвинутом плане? Первое - поиск по принципу бинарного дерева – реализован в IDA во FLIRT-алгоритме. Более подробно об этом можно почитать в статье Ильфака Гильфанова о FLIRT. Второе - поиск Бойера-Мура в файле. Рекомендуется прочесть некоторые документы с http://algolist.manual.ru/ или ознакомиться со статьей http://www.rsdn.ru/article/alg/textsearch.xml на RSDN. Хочется сказать спасибо автору статьи - Андрею Боровскому – за такие объяснения, какими они должны быть. Понять алгоритм можно только посидев с карандашом над ним, разрисовав палочки и черточки. Эта статья относится именно к такому классу. Приведенные в ней алгоритмы в несколько более эффективном варианте, переписанные на С, будут использованы в PE Sniffer. В этом случае с файлом работают при помощи MMF-функций.

2) Неточный поиск подстроки в строке

Поиск Бойера-Мура достаточно быстр за счет построения таблицы смещений и делает меньше сравнений, чем тривиальное strcmp. Однако он подходит лишь для точного поиска образца (или, в улучшенных вариантах, допускает лишь минимальные отклонения, согласно простейшим регулярным выражениям). Здесь же мы ни в коей мере не можем быть уверены, что подстрока (сигнатура) не будет самым злостным образом искажена. Давайте рассмотрим пару примеров и на их основе попытаемся сформулировать ряд правил для написания движка.

Пример: libc.lib – стандартная библиотека языка С. Известно, что код программы начинается не с main, а с *mainCRTStartup (одной из четырех). Поэтому ничто не препятствует поменять код процедуры с такого, например:

    posvi = (OSVERSIONINFOA *)_alloca(sizeof(OSVERSIONINFOA));
    posvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
    (void)GetVersionExA(posvi);
    _osplatform = posvi->dwPlatformId;
    _winmajor = posvi->dwMajorVersion;
    _winminor = posvi->dwMinorVersion;

на вот такой:

    posvi = (OSVERSIONINFOA *)_alloca(sizeof(OSVERSIONINFOA));
    goto here;

    now_here:
    _osplatform = posvi->dwPlatformId;
    _winmajor = posvi->dwMajorVersion;
    _winminor = posvi->dwMinorVersion;
    goto keep_on;

    here:
    posvi->dwOSVersionInfoSize = sizeof(OSVERSIONINFOA);
    (void)GetVersionExA(posvi);
    goto now_here;

    keep_on:
     ...

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

Реально же имеем следующее:

1) Для каждого пакера существует устойчивая кодовая последовательность именуемая в дальнейшем сигнатурой для которой действуют следующие правила (здесь под элементом сигнатуры подразумевается байт):

a) Элементы сигнатуры не могут меняться местами
b) Элементы сигнатуры не могут быть заменены на другие

2) Между отдельными элементами может присутствовать "шумовой" код (если мы для сигнатуры выберем 1, 5, 10 байты сгенерированные пакером, для уменьшения количества ложных срабатываний, необходимо учесть минимальное расстояние на котором могут встретиться эти элементы сигнатуры друг от друга), а между некоторыми такой код появиться не может (двух и более байтные команды).

Что-то наподобие такого и будет реализовано в новой, уже не совсем детской версии PE Sniffer. Хотя и здесь решение для общего случая, скорее всего, НЕ существует. Уж слишком злопакостно можно исказить сигнатуру при желании, и ни CRC, ни xor-сумма строки, ни побайтовое сравнение по хитрым правилам не помогут. Скажем, пункты 1.a/1.b явно дискуссионны, чего только стоят т.н. stolen bytes в Asprotect. Поэтому если имеете собственное суждение – не стесняйтесь его высказать.

Возможно, самые отчаянные захотят обсудить экзотические методы детекции – применение нейронных сетей, генетические алгоритмы, fuzzy logic и т.п. – это прекрасно. Дерзайте! Учтите, что и OEP (см. главу об OEP) можно искать в памяти по такому же принципу – поиском сигнатур компилятора. Так, к примеру, поступает PEiD в своем genoep.dll – просто-напросто в дампе программы выполняется поиск компиляторных сигнатур, при этом, кстати, опять таки, не учитывается понятие «украденные байты» - stolen bytes – защитный прием некоторых пакеров (Asprotect), при котором куски кода из OEP нагло утаскиваются.

В заключение было бы интерестно рассмотреть – а как же действуют профессиональные антивирусы? Ведь как-то же они предполагают наличие вируса, хотя, зачастую и неверно, давая ложные срабатывания или просто не узнавая его. Однако мир антивирусов практически закрыт от конечного пользователя. И кто-то еще ругает Windows за закрытость кода?

Как правило, в открытую печать не попадает сколь-нибудь ценной технической информации... Что тут говорить. Тем не менее, мы предложим пару линков:

  1. «Heuristic Techniques in AV Solutions: An Overview»
    http://www.securityfocus.com/infocus/1542 - в качестве вступления смотрися неплохо.
    2. «Stripping Down An Av Engine»
  2. http://www.nai.com/common/media/vil/pdf/imuttik_VB_%20conf_2000.pdf – в качестве вступления к следующему линку тоже ничего.
  3. http://clamav.elektrapro.com/ - попытка антивируса с открытым исходым кодом (язык С). Функция эвристики (пока только ребенок) находится в файле matcher.c и использует обобщение алгоритма Кнута-Морриса-Пратта в поиске в односвязном списке – функция cl_scanbuff. Есть и еще несколько подобных антивирусов, но у их основателей хватило ума писать их на Java… Что тут сказать... И это там, где важна скорость и низкоуровневые трюки...
  4. http://www.peterszor.com/ - на удивление приличные статьи о детекции метаморфов, пара статей о Win32 высокого класса. Это достаточно хороший уровень. Очень рекомендуется!

Тонкости PE-формата

Вы решили потратить деньги на приобретение пакера. ОК, выбор ваш. Положим, вы не уверены, что способны написать хорошую защиту, просто нет времени или еще что-то. Тогда проверьте пакер! Хороший криптор должен не только перезагружать Windows если кто-то подошел к монитору, он должен и корректно обрабатывать многие тонкости и сложности PE-формата.

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

    __declspec(thread ) int i = 1;
    __declspec(thread ) int m = 0;

    void main(void)
    {
    /*для нас совершенно неважно, какой именно код тут используется,
    он написан просто так, чтобы что-то написать;
    что действительно важно, так это объявления переменных.
     Статические объявления заставляют компилятор
    создать секцию .tls в результирующем файле*/
    printf(“%ld\n”, i*m);
    }

Основной смысл такого упражнения – в создании секции .tls в результирующем PE-файле (заметьте, мы говорим о статическом tls – с tls можно работать и динамически – подробнее – Джеффери Рихтер). .tls-секция (если она существует) обрабатывается лоадером при загрузке – вызываются callback-функции и т.п. Все это достаточно подробно описано Питреком в

http://www.microsoft.com/msj/0999/hood/hood0999.aspx

и облегчать жизнь писателям пакеров у нас желания нет. Однако факт остается фактом – многие коммерческие упаковщики не учитывают инициализацию tls-цепочек лоадером, в результате чего запакованный файл падает. Среди таковых и Aspack 2.12, который такой файл даже обработать не может! А люди еще за это и деньги платят...

Более того, можно сделать еще веселее! Положим, мы имеем дело с dll (tls-цепочки используются, в основном, именно в dll) – как прикажете обрабатывать секцию tls, которая подвержена перемещению, т.е. появляются fixup-элементы? В случае exe-файла аналогичного результата можно добиться опцией MS-линкера /FIXED:NO. Так это вообще фантастика! UPX гарантировано обрабатывает такие вещи, а вот некоторые остальные, не будем показывать пальцами...

Вот такими и является большинство упаковщиков. С миру по нитке накопировано антиотладочных приемов (зачастую, беззастенчиво спертых из кода вирусов или толковых статей кракеров), все это достаточно неумело отлажено, кое-как обрабатывается PE-файл, при этом совершенно не учитываются многие нюансы и тонкости формата и за это просят деньги, утверждая, что пакер работает превосходно и надежно спасает от недалеких кракеров.

OEP и иже с ним

Для этой главы неплохо было бы выбрать самый простой из всех возможных упаковщик, на котором и проиллюстрировать некоторые закономерности работы. Такой, к счастью, есть. Называется PE Deminisher и доступен для закачки с wasm.ru.

Итак, пакуем наш старый добрый calc.exe и что мы имеем:

      Name   VirtSize   RVA    PhysSize  Offset    Flag
    .text    000124EE 00001000 0000782B 00000600 E0000020
    .data    000010C0 00014000 000003E1 00008000 C0000040
    .rsrc    00002B98 00016000 00002C00 00008400 40000040
    .teraphy 00001000 00019000 0000041A 0000B000 C0000040

Очевидно, три секции являются нормальными, четвертая принадлежит упаковщику. Так же очевидно, что, в этом случае, остальные секции (за исключением .rsrc) являются сжатыми по какому-либо алгоритму (в данном случае это apLib, но нам это не важно). Что находится в четвертой секции и почему она расположена именно четвертой? Вот слегка обрезанный отчет HIEW о данном файле:

    Name        RVA      Size
    Import       00019391 00000089  ;импорт перенаправлен
    Resource     00016000 00002B98  ;ресурсы оставлены
    Debug        00001210 0000001C  ;старый трюк – см. первую часть
    Import Table 00001000 0000020C  
    ;недоработка данного упаковщика – IID
    ;перенаправлена в секцию .teraphy, IAT оставлена, но не валидна

Использование директории отладки является старым добрым антиотладочным приемом, мы это уже описывали и повторяться неинтерестно. Ресуры тоже уже мало кого удивляют. Любопытнее выглядит изменение RVA директории импорта в секцию пакера. Если глянуть на заголовок (Optional Header), то можно видеть, что и точка входа переориентирована в новую секцию, и количество секций, соответственно, увеличено. Разумеется, изменилось поле SizeOfImage, иначе файл валидным не будет. Кому интерестно видеть все мелочи – воспользуйтесь функцией Compare из Pe Tools. Нам же интерестнее ответить на вопрос: так почему же секция идет четвертой. Если чуточку подумать над проблемкой, то ответ ясен – так легче. Положим, можно поставить и третьей, если не лень пересчитывать ресурсы как директорию и как секцию. А вот первой – ни-ни. Так как слишком уж это дело будет хлопотное... Но если кому-то не лень, что тогда? Смотрите – теоретически невозможно поставить секцию пакера первой – это потребует коррекции ссылок в секции кода и коррекции ссылок между секциями кода и данных и т.п., да и не только. Откуда получить такую информацию? Если файл содержит IMAGE_DIRECTORY_ENTRY_BASERELOC, то тогда, используя информацию оттуда, такое дело возможно, однако методика для общего случая работать не будет.

Что в этом плане можем извлечь мы. Да очень простой, старый и почти безотказно работающий трюк. Только давайте сначала четко определимся с терминами. Итак: OEP – original entry point – это не та самая точка входа которая записана в заголовке PE файла (OptionalHeader.AddressOfEntryPoint). OEP - это VA, куда упаковщик передаст управление после полной распаковки файла. Т.е. это - оригинальная точка входа которая была в заголовке PE файла до упаковки. А точка входа в запакованном файле называется EP (Entry Point). Так вот, нетрудно заметить, что прыжок после распаковки всегда будет происходить из области больших адресов в область меньших адресов. На этом механизме и были построены многие OEP-трейсеры – revirgin и icedump в их числе. Очевидно, что если кто-нибудь (гм, например, мы) не поленится написать драйвер, который даже будет не сколько трассировать приложение, сколько просто смотреть за EIP, когда тот будет выходить за пределы секции (секций) упаковщика. Положим, протектор сможет делать ложные прыжки, положив оные в конструкцию try/catch (см. ниже) – но отчет утилиты покажет это все человеку, а уж человек элементарно разберется – какой прыжок ложный, а какой нет. С другой стороны, пакер вполне может применять засечки количество тактов процессора – rdtsc и, чуть что не так, начинать орать. Словом, тут есть над чем подумать...

Работать с dll примерно так же просто. Достаточно давно разработана методика Break & Enter. Смысл ее состоит во влеплении опкода СС (о самом опкоде см. ниже) прямо в EP программы. Известно, что Soft-Ice Symbol Loader часто просто проскакивает мимо EP. Поэтому LordPE и PE Tools лепят СС-байт прямо в EP, предварительно запоминая оригинальный. Все, что остается пользователю – ввести bpint 3 и восстановить старый байт после всплытия Soft-Ice. Скоро будет написан плагин под PE Tools в виде лоадера dll, т.к. dll, с нашей с вами точки зрения, мало чем отличается от exe.

Что до директории импорта – тоже достаточно просто понять, что оригинальная директория импорта остается нетронутой лоадером (он ее просто не видит). Вместо этого пакер сам, после расшифровки содержимого файла, находит эту директорию и в цикле, перед передачей управления на OEP, с помощью GetProcAddress, наполняет ее валидными для данной системы адресами и производит перерасчет RVA на VA (см практический пример с Aspack). Обязательно следует заметить, что и тут прогресс ушел далеко вперед. Современные крипторы уже не используют GetProcAddress. Уж слишком легко нам поставить на нее брейкпоинт и разобраться в логике пакера (см. практические примеры).

bpx на функции API

Команда bpx Soft-Ice использует зарезервированный Intel опкод CC. Интеловские талмуды говорят нам следующее: «The INT 3 instruction generates a special one byte opcode (CC) that is intended for calling the debug exception handler. (This one byte form is valuable because it can be used to replace the first byte of any instruction with a breakpoint, including other one byte instructions, without over-writing other code).». Не путайте также опкод CD 03 с CC – они используются в разных случаях – «Note that the “normal” 2-byte opcode for INT 3 (CD03) does not have these special features. Intel and Microsoft assemblers will not generate the CD03 opcode from any mnemonic, but this opcode can be created by direct numeric code definition or by self-modifying code.». Поставить точку останова на API и получить результат – это прекрасно для нас с вами, но не слишком хорошо для авторов упаковщиков. Вот пример немного наивного кода для детекции bpx в самом начале LoadLibraryA:

mov  eax,[KernelBase]
push offset LoadLibraryA
push eax
call GetProcAddress
cmp byte ptr [eax],0cch
je Found_Hook

Код прост, но хорошо отражает суть. Его можно выразить и чуть иначе, например, так:

mov edi, offset на собственную IAT – сканируем первый байт всех адресов 
mov al, 0CCh
repnz scasb ;код скорее, схематичен, ;чем реален, но в сети есть настоящие примеры

а можно и еще парой десятков вариаций. Для его обхода опытные люди используют трюк с bpx API-name + x, где х – число, которое приходится на начало другой инструкции. Важно заметить, что + х может быть не абы каким числом. Это обязательно должно быть поле операнда, иначе исключения #DB не произойдет, произойдет другое исключение :) Скажем, часто предлагается ставить нечто вроде bpx GetProcAddress + 3. В этом случае Soft-Ice превратит символьное имя GetProcAddress в VA (гм, а может и не в VA, бог его знает, что там внутри Soft-Ice происходит – ведь большинство не задумывается ПОЧЕМУ команда bpx на какую-нибудь API-функцию типа MessageBox срабатывает в любом адресном пространстве, т.е. контекстно-неспецифична – под Windows для первых двух гигабайт это попахивает черной магией!), добавит к этому адресу 3 и воткнет туда СС. Будьте готовы – авторам упаковщиков элементарно проверить каждую вызываемую функцию на наличие CC, а тогда ... Тогда можно еще использовать bpm или уходить на уровень NativeAPI. К примеру, для GetProcAddress цепочка выглядит так:

GetProcAddress -> LdrGetProcedureAddress -> LdrpGetProcedureAddress 

Последняя основательно прокомментирована в статье о DLL-лоадере, рассмотренной нами в первой части.

Вместо этого используется более изощренная технология – прямое сканирование директории экспорта целевых dll. Более подробно об этом можно почитать в статье «Win32 Assembly Components» написанной LSD Team. Статью можно скачать с wasm.ru или с сайта команды – http://lsd-pl.net/projects.html#windowsassembly. Что мы можем сказать по этому поводу? М-м-м... Опять таки, если время сканирования экспорта опирается на rdtsc – тут тяжелее. В общем случае – почему бы, наконец, не раскачать Sten’a на написание нормальной bpr-команды, которую, к сожалению, убрали из Soft-Ice... Присутствие такой bpr-команды позволит ставить точки останова на большие диапазоны памяти – например, на диапазон директории экспорта, и отслеживать обращающиеся к этому диапазону инструкции. Команда должна быть контекстно-специфичной.

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

Как ни странно, ответ на вопрос – когда нужно дампить программу, часто заключается в самой запакованной программе. Все зависит от того, насколько разумно она проектировалась программистом. Как известно, язык С позволяет создавать статические и глобальные переменные. Оставим спор по поводу необходимости их применения в стороне, а сами зададим вопрос: «а чем от этого плохо нам?». Рассмотрим следующий пример:

    #include "windows.h"
    static void* h_heap = 0; /*вот в этом вся и соль*/
    void main(void)
    {
      if (!h_heap)
        h_heap=HeapCreate(0,0x1000,0);
     
      HeapAlloc(h_heap, HEAP_ZERO_MEMORY, 0x500);

      MessageBox(0, "I said NOW!", "Dump me NOW!", MB_OK);

      HeapDestroy(h_heap);
    }

Программа запускается и работает абсолютно нормально, но стоит сдампить ее как раз на MessageBox – последствия не заставят себя ждать. Причина в том, что статические и глобальные переменные инициализируются КОМПИЛЯТОРОМ! Следовательно, при нормальном развитии событий переменная имеет свой нолик еще в секции PE-файла и проверка проходит нормально. В сдампленной программе переменная уже заполнена функцией HeapCreate и, следовательно, при следующем запуске будет нам с вами радость.

Так что, в общем случае, рекомендация должна звучать примерно так: секции данных лучше дампить сразу после раскриптовки/распаковки, ибо протектор может их подпортить еще до ОЕР. И это подводит нас к очень важному выводу: не надо полагаться на ImpRec, OEP-finder’ы, TRW с его makepe или что-либо еще – с каждым пакером пока приходится работать индивидуально, т.к. общее решение проблемы пока не разработано.

Дампер процессов

Основой получения списка процессов является функция NtQuerySystemInformation. Читатель может заворчать – вот, опять Native API. Да, именно так. Только учитывайте, что бывают РАЗНЫЕ уровени недокументированности! Скажем, весьма и весьма вероятно, что реализация некоторых Mi*-функций, рассмотренная нами ранее, может и, скорее всего, будет, варьироваться от ОС к ОС и даже от SP к SP, в то время как некоторые Native API функции весьма стабильны и едва ли изменятся в будущем. Во всяком случае, прототип, название и смысл должны сохранится. А смысл этой функции настолько велик, что Шрайбер назвал ее «кладезем» информации о системе. Также Гарри Неббет (Gary Nebbet) неплохо осветил эту тему в 1 главе своей книги. Кое-что из этой информации мы повторим здесь. Приступим:

    NTSYSAPI NTSTATUS NTAPI NtQuerySystemInformation (
        IN SYSTEM_INFORMATION_CLASS SystemInformationClass,
        OUT PVOID SystemInformation,
        IN ULONG SystemInformationLength,
        OUT PULONG ReturnLength OPTIONAL
        );


    typedef enum _SYSTEM_INFORMATION_CLASS {
        SystemBasicInformation,
        SystemProcessorInformation,
        SystemTimeOfDayInformation,
        SystemPathInformation,
        SystemProcessInformation,
        SystemCallCountInformation,
        SystemDeviceInformation,
        SystemProcessorPerformanceInformation,
        SystemFlagsInformation,
        SystemCallTimeInformation,
        SystemModuleInformation,
        SystemLocksInformation,
        SystemStackTraceInformation,
        SystemPagedPoolInformation,
        SystemNonPagedPoolInformation,
        SystemHandleInformation,
        SystemObjectInformation,
        SystemPageFileInformation,
        SystemVdmInstemulInformation,
        SystemVdmBopInformation,
        SystemFileCacheInformation,
        SystemPoolTagInformation,
        SystemInterruptInformation,
        SystemDpcBehaviorInformation,
        SystemFullMemoryInformation,

       SystemLoadGdiDriverInformation,
        SystemUnloadGdiDriverInformation,
        SystemTimeAdjustmentInformation,
        SystemSummaryMemoryInformation,
        SystemNextEventIdInformation,
        SystemEventIdsInformation,
        SystemCrashDumpInformation,
        SystemExceptionInformation,
        SystemCrashDumpStateInformation,
        SystemKernelDebuggerInformation,
        SystemContextSwitchInformation,
        SystemRegistryQuotaInformation,
        SystemExtendServiceTableInformation,
        SystemPrioritySeperation,
        SystemPlugPlayBusInformation,
        SystemDockInformation,
        SystemPowerInformation,
        SystemProcessorSpeedInformation,
        SystemCurrentTimeZoneInformation,
        SystemLookasideInformation
    } SYSTEM_INFORMATION_CLASS;

Где, SystemInformationClass – тип требуемой информации, нас интересует только информация о процессах, т.е. SystemInformationClass = 5, SystemInformation – указатель на буфер данных, SystemInformationLength – размер буфера данных, ReturnLength – размер записанных в буфер данных. Если выделенного буфера недостаточно, то в параметр ReturnLength будет возвращён требуемый размер буфера. Рекомендуемый размер = sizeof(SYSTEM_PROCESS_INFORMATION) * 1024, так как вряд ли у кого то будет запущенно 1024 процесса.

Далее если не произошло ошибок, в буфер SystemInformation будет передана структура SYSTEM_PROCESS_INFORMATION, описывающая отдельный процесс в списке. Первым членом структуры (NextEntryOffset) будет смещение на следующий процесс в списке, если же оно равно NULL, значит это последний процесс в списке.

    typedef struct _SYSTEM_PROCESS_INFORMATION {
        ULONG NextEntryOffset;
        ULONG NumberOfThreads;
        LARGE_INTEGER SpareLi1;
        LARGE_INTEGER SpareLi2;
        LARGE_INTEGER SpareLi3;
        LARGE_INTEGER CreateTime;
        LARGE_INTEGER UserTime;
        LARGE_INTEGER KernelTime;
        UNICODE_STRING ImageName;
        KPRIORITY BasePriority;
        HANDLE UniqueProcessId; // PID
        HANDLE InheritedFromUniqueProcessId;
        ULONG HandleCount;
        ULONG SpareUl2;
        ULONG SpareUl3;
        ULONG PeakVirtualSize;
        ULONG VirtualSize;
        ULONG PageFaultCount;
        ULONG PeakWorkingSetSize;
        ULONG WorkingSetSize;
        ULONG QuotaPeakPagedPoolUsage;
        ULONG QuotaPagedPoolUsage;
        ULONG QuotaPeakNonPagedPoolUsage;
        ULONG QuotaNonPagedPoolUsage;
        ULONG PagefileUsage;
        ULONG PeakPagefileUsage;

       ULONG PrivatePageCount;
        SYSTEM_THREAD_INFORMATION TH[1];
    } SYSTEM_PROCESS_INFORMATION, *PSYSTEM_PROCESS_INFORMATION;

Простой пример использования функции, подразумевается, что буфер выделен, и NtQuerySystemInformation вызвалась без ошибок:

    SYSTEM_PROCESS_INFORMATION *pSysInfo;
     ...

    NtQuerySystemInformation(…);

    while(1)
    {
     // Здесь уже можно использовать структуру
     ...

     // Проверям – последний это процесс в списке? Если да, то
     // выходим из цикла
    if(!pSysInfo->NextEntryOffset)
     break;

    // Переходим к следующему процессу
    pSysInfo = (SYSTEM_PROCESS_INFORMATION)((PVOID)pSysInfo + pSysInfo->NextEntryOffset);
    }

Внимание: важный момент, перед вызовом NtQuerySystemInformation рекомедуется установить своему приложению привилегию отладки программ. Это нужно для получения списка системных процессов (System Idle process). Но это возможно только под правами Администратора. Всё это проделывает функция EnableDebugPrivilege. Чтобы установить привилегию, первый и единственный параметр должен быть равен TRUE, чтобы убрать FALSE.

    BOOL EnableDebugPrivilege(BOOL bEnable)
    {
     HANDLE hToken;
     BOOL   bOk = FALSE;
     if(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken))
     {
      TOKEN_PRIVILEGES tp;
      tp.PrivilegeCount = 1;
      LookupPrivilegeValue(NULL, SE_DEBUG_NAME, &tp.Privileges[0].Luid);
      tp.Privileges[0].Attributes = bEnable ? SE_PRIVILEGE_ENABLED : 0;
      AdjustTokenPrivileges(hToken, FALSE, &tp, sizeof(tp), NULL, NULL);
      bOk = (GetLastError() == ERROR_SUCCESS);
      CloseHandle(hToken);
     }
     return bOk;
    }

Получение списка загруженных процессом модулей

Для получения списка модулей служит функция RtlQueryProcessDebugInformation.

    RtlQueryProcessDebugInformation(HANDLE hPID, DWORD dwInfo, PVOID pRtlBuff);

где,

  • hPID – идентификатор процесса (PID),
  • dwInfo – код запроса требуемой информации,
  • pRtlBuff – буфер куда будет передана информация о модулях процесса. Ниже приведена структура этого буфера:
    typedef struct _DEBUGMODULEINFO
    {
     DWORD ImageBase;
     DWORD ImageSize;
     DWORD unknown1;
     USHORT DllSequenceNum
     USHORT NumDlls;
     DWORD GrantedAccess;
     CHAR Name[MAX_PATH];
     DWORD unknown;
    } DEBUGMODULEINFO,PDEBUGMODULEINFO;

Для работы с функцией RtlQueryProcessDebugInformation необходимо создать (RtlCreateQueryDebugBuffer) рабочий буфер для хранения информации о модулях. И после окончания работы, вызвать функцию RtlDestroyQueryDebugBuffer, чтобы освободить этот самый буфер.

    PDWORD RtlCreateQueryDebugBuffer(DWORD, DWORD);
    DWORD RtlDestroyQueryDebugBuffer(PDWORD);

Пример:

    QUERYDEBUGBUFFER *pModuleInfo; // Информация о модулях
    DWORD dwNtStatus;              // Код возврата функции
                                   // RtlQueryProcessDebugInformation
    DWORD dwPID;                  // PID процесса
    // Выделяем буфер
    DWORD *pRtlBuffer = RtlCreateQueryDebugBuffer(NULL, NULL);

    if(!pRtlBuffer)
    {
     // Error!
    }
    // Запрашиваем информацию о модулях
    dwNtStatus = RtlQueryProcessDebugInformation((HANDLE *)dwPID, 0x01, pRtlBuffer);

    if(!dwNtStatus)
    {
     pModuleInfo = (QUERYDEBUGBUFFER*)pRtlBuffer;

     // Производим перечисление модулей процесса
    for(int i = 0; i < pModuleInfo->dwNumNames; i++)
    {
     printf(“ImageBase: 0x%0.8Xl”, pModuleInfo[i]->ImageBase);
     printf(“ImageSize: 0x%0.8Xl”, pModuleInfo[i]->ImageSize);
     ...
    }
    }
    else if(dwNtStatus == DEBUG_ACCESS_DENIED)
    {
    // Error
    }

    // Освобождаем рабочий буфер
    RtlDestroyQueryDebugBuffer(pModuleInfo);

Определение ImageBase и ImageSize

ImageBase и ImageSize определяются достаточно легко. Нужно сделать перечисление модулей того процесса, для которого определяются эти значения. И в СПИСКЕ МОДУЛЕЙ найти имя нужного процесса, как правило оно идёт первым. Далее взять значения из структуры DEBUGMODULEINFO:

    QUERYDEBUGBUFFER *pModuleInfo;
    ...
    pModuleInfo->ImageBase;
    pModuleInfo->ImageSize;

НО! Если процесс использует антидамповые приёмы (один из которых можно посмотреть во введении – нечего было его проскакивать!), то, как правило, ImageSize имеет неправильное значение. Поэтому рекомендуется его считать из PE заголовка процесса, но можно и из файла на диске (последнее даже надежнее). Ниже приведён пример функции считывающей ImageSize из заголовка:

    //Первый параметр - это PID процесса, второй  - ImageBase.

    DWORD GetRealSizeOfImage(DWORD dwPID, PVOID pModBase)
    {
     IMAGE_DOS_HEADER pDosh = {0};
     IMAGE_NT_HEADERS pNT = {0};

     HANDLE hProcess = OpenProcess(PROCESS_VM_READ, FALSE, dwPID);
     if(hProcess)
     {
      ReadProcessMemory(hProcess,
                             pModBase,
                             &pDosh,
                             sizeof(IMAGE_DOS_HEADER),
                             NULL);
      if(IMAGE_DOS_SIGNATURE == pDosh.e_magic)
       ReadProcessMemory(hProcess,
                                (PBYTE)pModBase + pDosh.e_lfanew,
                                 &pNT,
                                sizeof(IMAGE_NT_HEADERS), NULL);
      CloseHandle(hProcess);
      if(pNT.Signature == IMAGE_NT_SIGNATURE)
       return pNT.OptionalHeader.SizeOfImage; 
     }
     return NULL;
    }

Однако даже наличие таких методик в PE Tools все равно не спасет от защиты-драйвера (например, Extreme-Protector). Поэтому, видно, пришло время и PE Tools переходить на драйверный движок, который мало в чем будет опираться на структуры кольца-3. Так правильнее.

Практический пример: UPX

написано совместно с Quantum

UPX является практически единственным исключением в своем роде - полностью открыт исходный код. Это обстоятельство пытаются использовать многие авторы упаковщиков, и распаковщиков тоже. Часть просто молча ворует исходники (GPL-лицензия таки налагает некоторые забавные ограничения, хоть исходный код и открыт), другая часть пытается использовать это знание нам во вред, забывая - что OpenSource - это палка о двух концах. В данной главе принципиально не будет приведено ни единой строчки кода дизассемблера. Зачем? Все есть в кодах UPX. Итак, скачивайте кода с http://upx.sourceforge.net и в директории stub находите файл l_w32pe.asm. Не забудьте заглянуть и в stub.asm. В самом начале данного файла есть забавное предупреждение - 5 минут смеха обеспечены! Тем не менее, ассемблерный листинг стаба проще читается под отладчиком, так как исходники набиты всякими директивами препроцессору С (невидимыми ассемблеру), которые явно мешают уловить суть алгоритма.

UPX полностью пересобирает PE-файл, меняя все, что можно, за исключением ресурсов. К ним UPX относится достаточно бережно. Все остальное программа переводит в свой внутренний формат и сжимает по алгоритму UCL (кстати, вовсе не обязательно именно UCL, есть еще прогрессивный NRV) http://www.oberhumer.com/opensource/ucl.

Как программа переводит эти данные в свой формат описано в файле p_w32pe.cpp (для каждого поддерживаемого формата - свой cpp-файл со своими методами). Масса полезной информации находится также в файлах packer.cpp, packhead.cpp и compress.ch (очень занимательное чтиво). Однако нас это интересует мало, тем более не всякий открытый исходный код - панацея, т.к. за спасибо можно получить только исходники компрессора UCL, а последняя бета-версия UPX использует компрессор NRV (не GPL, однако). Получается, что основная часть упаковщика остаётся за кадром...

В результирующем файле (сколько бы секций ни было в оригинальном) всегда будут только три секции (для версий, отличных от 1.24-1.90, правило, возможно, соблюдаться не будет – кода мы не изучали) -

  1. UPX0 - кладутся tls
  2. UPX1 - fixup-элементы, импорт, экспорт, код и т.п.
  3. UPX2 - ресурсы, однако в действительности этой секции, как правило, нет, т.к. автор утилиты прекрасно знал о том, что имя секции .rsrc очень много значит.

Код стаба - l_w32pe.asm - разжимает секции и обрабатывает директории импорта и fixup-элементов. Заметьте, повторим еще раз - идет обработка директории импорта! Часто встречается утверждение, что, мол, UPX переводит все в свой внутренний формат и, посему, лучше использовать ImpRec и иже с ним. Переводить-то, утилита переводит, да только потом разжать-то ведь надо, и перевести назад – в валидный для ОС формат. Внимательно рассмотрим код l_w32pe.asm по обработке импортов:

    /*код стаба построен достаточно хитро - он насыщен инструкциями вроде %endif;
     __PEMAIN01__, где невидима для ассемблера,
      но прекрасно видима препроцессором Си
      - т.о. этот код может (и будет!) различаться для dll/exe и т.п.*/
                    pushad
                    mov     esi, 'ESI0'                ; VA секции UPX1
                    lea     edi, [esi + 'EDI0']        ; VA секции UPX0
     ...
                    push    edi
     ...
                    pop     esi
     ...
                    lea     edi, [esi + 'BIMP']        ;распакованные имена функций
                                                       ;во внутреннем формате UPX

    next_dll:
    ; проверить на конец массива имен – DWORD = 0
                    mov     eax, [edi]
                    or      eax, eax
    ; хоть метка и называется imports_done, правильнее - names_done
                    jz      imports_done
                    mov     ebx, [edi+4]
    ; имена dll во внутреннем формате UPX
                    lea     eax, [eax + esi + 'IMPS']
                    add     ebx, esi
    ; как мы постоянно упоминаем – имена лежат во внутреннем формате и edi
    ; показывает на два хитрых DWORD’a, 
    ; на основании второго из них ([edi+4]) вычисляется VA,
    ; куда GetProcAddress будет класть полученные адреса
                    push    eax    
                    add     edi, 8
                    call    [esi + 'LOAD'] ; LoadLibraryA
                    xchg    eax, ebp  ; ebp - хендл
    next_func:
                    ; имя функции
                    mov     al, [edi]
    ; как мы уже упоминали – формат внутренний,
    ; строки разделены нулями
                    inc     edi
                    or      al, al
                    jz      next_dll
                    mov     ecx, edi        

                    push    edi            ; имя функции из dll
                    dec     eax            ; 0 - разделитель
                    repne
                    scasb                  ; встать на начало следующего имени

                    push    ebp   ; хендл
                    call    [esi + 'GETP'] ; GetProcAddress
                    or      eax, eax
                    jz      imp_failed
                    mov     [ebx], eax     ; начинаем готовить массив адресов
                    add     ebx, 4         ; увеличить VA на sizeof(DWORD)
                    jmps    next_func
    imp_failed:
      ; к ExitProcess
    imports_done:
      ; к OEP

То, что UPX, фактически, распространяется с открытым кодом, да ещё и для различных форматов исполнимых файлов (Win16, Win32, Posix, MS-DOS и т.д.) серьёзно ограничивает его антиотладочные возможности... Цель UPX заключается в максимальном сжатии файла, но не в противостоянии хакерским усилиям по его распаковке. UPX.EXE поддерживает модификатор -d в командной строке для распаковки своих же файлов и почти всех предыдущих версий UPX включительно. На вопрос "как распаковать UPX?" можно лениво ответить, что мол "UPX -d packed.exe" (зря вы дампер приготовили). Так даже не интересно...

Стоп! Мы забываем про утилиты для защиты UPX, так называемые скрамблеры (scramblers). Скрамблеры пытаются немного замаскировать запакованные файлы, чтобы обмануть UPX. Но мы-то знаем, что перед нами файл, запакованный UPX'ом, потому что нам об этом сообщил идентификатор (sniffer) файлов или в дизассемблере "на глаз" был подмечен стаб UPX'а.

Здесь мы НЕ будем учиться распаковывать UPX. Существуют утилиты, которые прекрасно справляются с этой задачей, без особого участия со стороны пользователя. Здесь мы попытаемся понять принципы защиты UPX от распаковки, которые активно используются скрамблерами, вроде UPX-SCRAMBLER и HidePX. Данные утилиты призваны уберечь поднаготную запакованного экзешника от посторонних глаз, но делают они это не очень эффективно. Скачайте себе любую из них, или обе, или какой-нибудь другой скрамблер и пропустите через него ваш calc.exe. Что, уже не получается распаковать через upx -d? Как уже упоминалось раньше, существуют мощные распаковщики, которым скрамблеры погоды не строят, но наша цель - преодолеть защиту скрамблеров своими руками.

Не поленитесь сравнить calc.exe до и после прохождения через скрамблер. Можете воспользоваться WinHex / File Manager / Compare или другой подобной утилитой (PE Tools и LordPE умеют сравнивать и поля PE-формата – опция – “Compare”). Внимательно изучите листинг расхождений в обоих файлах и вы скоро поймёте, что первостепенные различия связаны с именами секций и сигнатурой UPX. UPX-SCRAMBLER заменяет UPX0 на code и UPX1 на text. HidePX затирает имя UPX0 и заменяет UPX1 на .rdata.

Если бы всё дело было только в именах секций, то для восстановления calc.exe можно было бы просто восстановить имена секций в любом PE-редакторе. Имена секций восстановлены, но там ещё и с сигнатурой что-то не так... Что такое сигнатура в данном случае? Здесь есть два понятия, которые необходимо различать. Под сигнатурой, с точки зрения последовательности байт кода, сама UPX понимает следующее:

    /*код взят из файла p_w32pe.cpp метод canUnpack класса PackE32Pe*/
    /*этот метод очень важен – именно тут UPX делает проверки
     на количество секций и их имена, проверятся байтовая сигнатура и,
     если что-то не так, бросается исключение с надписью
     «file is modified/hacked/protected; take care!!!»*/

    bool PackW32Pe::canUnpack()
    {
     ...
          static const unsigned char magic[] = "\x8b\x1e\x83\xee\xfc\x11\xdb";
          // mov ebx, [esi];    sub esi, -4;    adc ebx,ebx

    }

Очевидно, изменив код этого метода, несложно добится того, что для программы перестанет иметь значение имя секции (количество секций лучше не трогать), об отсутствии или неверном offset’е сигнатуры она станет лишь предупреждать, а не бросать исключение, но это лишь малая часть айсберга, т.к. существует ВТОРАЯ сигнатура! Под второй сигнатурой понимается структура, начинающаяся с “UPX!” (см. таблицу), которую UPX помещает перед сжатой частью файла. Помните, мы говорили, что UPX полностью перестраивает формат файла и граница старого сжатого файла начинается со второй сигнатуры. И, если испорчена она (что и делают скрамблеры), то тогда UPX просто слетит с внутренним исключением. Очевидно, снятие ВТОРОЙ сигнатуры и есть самое главное препятствие. Препятствие ли?

Для нахождения/просмотра/восстановления сигнатуры UPX можно воспользоваться хекс-редактором, но лучше - плагином Uncover UPX для PE Tools. Плагин прилагается к данной главе, так что можете сразу копировать его в каталог Plugins и PE Tools автоматически поместит его в соответствующее меню.

Основная черта данной утилиты заключается в автоматическом пересчёте контрольной суммы (поле CRC) при изменении остальных полей сигнатуры. Плагин также умеет частично или даже полностью восстанавливать сигнатуру после применения скрамблера. В случае с HidePX вам потребуется ввести (исправить) некоторые значения. Итак, что там за поля такие в сигнатуре? Учтите, что все значения отображаются в обратном порядке байт, т.е. в формате little endian, например: 12345678 -> 78563412, ABCDEF -> EFCDAB, ABCD -> CDAB, AB -> AB.

Поле
Размер
(в байтах)
Значение
Magic 4 Последовательность ASCII-символов 'UPX!’
Version 1 Версия упаковщика, например: 0C значит 1.24, 0D - это последняя на данный момент бета 1.90. Если вы пользуетесь UPX v1.90 и подопытный экзешник не очень старый (после 2001), можете спокойно прописать сюда 0D.
Format 1 Для интересующих нас экзешников в формате PE32, это поле всегда равно 09.
Method 1 Наиболее распространённые методы сжатия - это NRV и UCL. Обоим соответствует значение 02.
Level 1 Степень сжатия.
U_adler 4 Контрольная сумма части экзешника в распакованном виде*.
C_adler 4 Контрольная сумма части экзешника в запакованном виде*.
U_len 4 Размер части экзешника в распакованном виде*.
C_len 4 Размер части экзешника в запакованном виде*.
U_file_size 4 Размер распакованного экзешника.
Filter 2 Об этом чуть позже!
CRC 1 Контрольная сумма сигнатуры. Плагин показывает её в виде 16-битного значения, потому что перед CRC идёт дополнительный байт выравнивания.

* Под частью экзешника подразумевается та часть, которая подлежит сжатию/разжатию. В общем, кроме самого UPX’а никто больше не умеет определять точные границы этой части.

Сигнатура UPX для старых NE, линуксовых ELF и т.д. представлена иначе, но нам интересен только формат PE32.

Uncover UPX самостоятельно восстанавливает сигнатуру после UPX-SCRAMBLER, так как данный скрамблер уничтожает сигнатуру частично, но что делать если сигнатура утеряна полностью, как в случае с HidePX? Тогда Uncover UPX заполнит её значениями по умолчанию и нам придётся немного ему помочь, если в том возникнет нужда.

Поле magic, понятное дело, менять не стоит. Поле version обычно оставляется как есть (0C или 0D). Format оставьте со значением 09. Method в 99% случаев равен 02. В level можете поместить любое отличное от нуля значение, так-как распаковщик не обращает внимания на уровень сжатия. Тоже самое относится к полю u_file_size.

Значения в u_adler и c_adler - это контрольные суммы, рассчитанные по алгоритму Марка Адлера (http://www.cdrom.com/pub/infozip/zlib/). Можете посмотреть исходники данного алгоритма, но они нам не помогут... Предполагается, что у нас нет распакованного варианта экзешника. Значит и подсчитать его контрольную сумму, даже зная алгоритм, мы не можем... На самом деле, обе контрольные суммы не влияют на процесс распаковки, т.е. можно просто отключить проверку данных значений внутри UPX. Вы уже скачали исходники UPX и UCL? Кстати, в исходниках UCL есть пример реализации алгоритма М. Адлера, но не будем отвлекаться. В исходниках UPX, в файле packer.cpp есть код следующего содержания:

    void Packer::decompress(const upx_bytep in, upx_bytep out,
                            bool verify_checksum)
    {
        // verify_checksum = true, т.е. этот код всегда выполняется
        if (verify_checksum)
        {
            unsigned adler = upx_adler32(in,ph.c_len);
            if (adler != ph.c_adler)
                throwChecksumError();
        }

        // Тут происходит вызов 'настоящего' распаковщика
        unsigned new_len = ph.u_len;
        int r = upx_decompress(in,ph.c_len,out,&new_len, ph.method);
        if (r != UPX_E_OK || new_len != ph.u_len)
            throwCompressedDataViolation();

        // опять эти адлеры...
        if (verify_checksum)
        {
            unsigned adler = upx_adler32(out,ph.u_len);
            if (adler != ph.u_adler)
                throwChecksumError();
        }
    }

Если отключить обе проверки (до и после распаковки), про адлеры можно будет забыть. Следующий код заодно отключает проверку c_len и u_len, т.е. гонимся за четырьмя зайцами и успешно их ловим:

    void Packer::decompress(const upx_bytep in, upx_bytep out,
                            bool verify_checksum)
    {
        // Тут происходит вызов 'настоящего' распаковщика
        int r = upx_decompress(in,ph.c_len,out,&ph.u_len,ph.method);
    }

Правда, даже если вы сможете перекомпилировать этот код, вы получите версию UPX без поддержки NRV. Было бы куда лучше внести эти исправления в последнюю версию UPX (1.90 на данный момент), которая поддерживает сразу UCL и NRV. Где наш дизассемблер? Стоп, перед устранением этого бага в UPX, не забудьте его распаковать (он сам собой и запакован)

Распакованный UPX.EXE (около 327 Кб) грузим в HIEW и задаём поиск 56578B7C241484DB8BF1. Кстати, данная последовательность применительна и к предыдущей версии UPX. Узнаёте следующий код?

    53         push  ebx          
    8A5C2410   mov   bl,[esp][10] 
    55         push  ebp          
    56         push  esi
    57         push  edi
    8B7C2414   mov   edi,[esp][14]
    84DB       test  bl,bl
    8BF1       mov   esi,ecx
    74XX       je    XXX  ; это тот if (verify_checksum)

Меняем 74 на EB и первая проверка адлеров решена! Чуть дальше вы встретите вторую проверку контрольной суммы:

    7405       je    XXX
    E8XXXXFFFF call  XXX
    84DB       test  bl,bl
    74XX       je    XXX  ; это второй if (verify_checksum)

Исправляем на безусловный переход, как в первом случае и вторая проверка тоже решена! Осталось исправить throwCompressedDataViolation(). Для этого следуем за первым исправленным переходом и вскоре видим вызов функции с пятью параметрами - это upx_decompress:

    8B561C     mov   edx,[esi][1C]  ; это ph.u_len
    8B4614     mov   eax,[esi][14]
    8B6C2418   mov   ebp,[esp][18]
    8D4C241C   lea   ecx,[esp][1C]  ; а это new_len
    8954241C   mov   [esp][1C],edx
    8B5620     mov   edx,[esi][20]
    50         push  eax            ; ph.method
    51         push  ecx            ; &new_len
    55         push  ebp            ; out
    52         push  edx            ; ph.c_len
    57         push  edi            ; in
    E8XXXXXXXX call  XXX            ; upx_decompress

Функции вместо new_len нужно подсунуть адрес ph.u_len. Это можно организовать заменив 8D4C241C на 8D4C261C. Сразу за этим вызовом видим код примерно следующего содержания:

    85C0       test  eax,eax
    75XX       jne   XXX      ; если r == UPX_E_OK
    8B44XXXX   mov   eax,XXX
    8B4EXX     mov   ecx,XXX
    3BC1       cmp   eax,ecx
    74XX       je    XXX      ; если new_len == ph.u_len

Исправьте второй переход с условного на безусловный. Всё, теперь нам море по колено! Можете вписывать в поля u_adler и c_adler всё, что хотите (хоть FFFFFFFF, чтобы не путаться с little endian) Для полного отключения проверки валидности файла можете ещё удалить сравнение имени первой секции. Это сравнение очень просто найти в... Нет уж, ищите сами! В противном случае, будете и дальше править имена секций в PE Editor.

Что до u_len и c_len, то проверку валидности данных полей вы уже отключили но, в отличии от адлеров, выбор значений u_len и c_len налагает некоторую ответственность. Дело в том, что UPX резервирует два буфера в памяти: один размером с u_len для временного хранения распакованного файла, другой размером с c_len для чтения запакованного содержимого файла. Понятно, что если задать слишком маленькое значение для u_len, то распакованный файл просто не поместится в буфер. С другой стороны, слишком большое значение заставит UPX потреблять больше динамической памяти. Для u_len вполне подойдёт значение 000FFFFF (FFFF0F00 в little endian) для большинства упакованных экзешников.

С c_len чуть сложнее. Опять же, слишком маленькое значение вызовет конфликтную ситуацию с динамической памятью, но слишком большое, кроме излишнего расхода памяти, отрицательно воздействует на сам процесс распаковки. Иначе говоря, фокус с 000FFFFF не пройдёт. Надо подобрать более близкое значение. К счастью, в версии 1.24 выдаётся одно сообщение об ошибке, когда значение меньше правильного и другое - когда больше. В версии 1.90 эта фича отсутствует, но на данный момент HidePX не поддерживает 1.90, а UPX-SCRAMBLER не портит значение c_len. В версии 1.24 даже не нужно задавать абсолютно точное значение c_len - небольшая погрешность спокойно поглощается распаковщиком.

В заключение стоит упомянуть поле filter. В нём обычно хранится значение 260X, где X может быть 0, реже – 1, а еще реже – что-нибудь другое, например, 6, для старых версий UPX. Данное поле заслуживает особого внимания, так как неправильное значение фильтра приводит к неправильной распаковке экзешника, т.е. файл распаковывается, но не запускается! В общем, зря скрамблеры пренебрегают этой записью. Немного подправив значение фильтра можно защитить файл куда эффективнее, хотя от дамперов это всё равно не поможет, но всё-таки...

Теперь давайте проведем два практических примера. Наконец-то!

Итак, UPX-SCRAMBLER. Скачиваем с wasm специальные upx by Quantum/Volodya. Результат:

    C:\Downloads\PE\upx>upx1_24.exe -d s.calc.exe 
    распакованно мгновенно!

Теперь HidePX.

    C:\Downloads\PE\upx>upx1_24.exe -d p.calc.exe
    upx1_24: p.calc.exe: CantUnpackException: fillPackHeader: Seems like HidePX...

Поможем нашему UPX. Загружаем PE Tools, запускаем Quantum’овский плагин. Жмем одну-единственную кнопочку – Fix. Повтор:

    C:\Downloads\PE\upx>upx1_24.exe -d p.calc.exe 
    распакованно мгновенно!

На тот маловероятный случай, что что-то пойдет не так... Хм, а зачем мы вам столько писали, а?

В заключение статьи надо сказать, что некоторые упаковщики довольно бездарно пытаются замаскироваться под UPX. Например, это делает telock. Только вот все они не учитывают одной маленькой тонкости - UPX 1.24+ создает ТОЛЬКО ДВЕ секции - UPX1 и UPX0 - независимо от того, сколько на самом деле секций в файле. А telock этого попросту не учитывает, создавая в некоторых случаях несколько UPX-секций, что сразу бросается в глаза. Гораздо более точным критерием в данном случае можно считать присутствие характерного стаба, ведь он действительно нужен для того, что бы экзешник мог сам себя распаковать в памяти.

В самое заключение главы. Помните, в самом начале мы говорили, что здесь не будет ни строчки дизассемблированного листинга? Так вот, мы соврали. Имеет смысл разобрать один скользкий момент с HIEW. Передача управления на OEP в UPX отображается HIEW (по 6.85 включительно) так:

    .0101AFF1: 8903                         mov         [ebx],eax
    .0101AFF3: 83C304                       add         ebx,004 ;"¦"
    .0101AFF6: EBE1                         jmps       .00101AFD9  -----^ (3)
    .0101AFF8: FF9690BC0100                 call        d,[esi][0001BC90]
    .0101AFFE: 61                           popad
    .0101AFFF: E91C74FFFF                   jmp         0FFFFE820 ;на OEP

Почему же jmp по адресу 0x101AFFF имеет такой странный операнд? Давайте спросим автора HIEW – SEN’a. Ответ: «...но такого VA в файле нет, он появится потом, когда UPX память выделит для этого VA, а в файле ничего нет, поэтому hiew просто отсчитывает в глобальных адресах смещение и показывает как есть». Так что это ни в коем случае не баг утилиты. Просто автор очень не хочет включать поддержку многочисленных частных случаев, благодаря чему HIEW был и, пожалуй, так и остается одним из самых быстрых дизассемблеров на сегодяшний день. А уж эта возможность поиска по ассемблерной маске с * и ? – так это вообще фантастика...

Практический пример: Aspack

Что делает новичок, увидев программу, запакованную Aspack? Ну, берется Soft-Ice, берется дампер процессов и, обязательно, Imprec. Потом над всем этим инструментарием начинают интенсивно издеваться – зацикливать на OEP, дампить целиком, посекционно или еще как, немедленно запускать Imprec и вставлять полученный дамп директории импорта в файл. Ну что ж. Можно и так. Работает. Только давайте усложним задание. Положим, дампер процессов не имеет никакого движка по перестройке импорта, а Imprec и иже с ним у нас просто нет. Что тогда?

А вот тут-то и надо рассматривать сам алгоритм работы упаковщика. К счастью, есть и такие статьи. Например: «Исследование алгоритма работы упаковщика ASPack v1.08.03» - довольно толковая статья (когда будете читать – хм, вы уже знаете, что такое KTEB). Несмотря на то, что рассматривается старая версия, в новой (2.12) не так уж и много изменений с нашей точки зрения. Для кросс-проверки и некоторого дополнения сведений можно также проглядеть статью «ASPack 'Spelunking'».

В связи с тем, что есть такие великолепные материалы, мы не станем рассматривать код ASPack подробно. Для начала учтите – что это не криптор, это самый обычный упаковщик. Здесь нет ни антиотладки, ни сколь-нибудь сложных приемов противостояния дизассемблерам. Лишь в самом начале есть нечто, робко напоминающее полиморфный код:

    ;версия 2.12
    .aspack:01019001                 pusha 
    ;КРЕПКО запомните эту инструкцию! Проникнитесь! Она нам ох как пригодится!
    ;да, меж прочим, дизассемблирована эта команда неверно!
    ;В 32-битном режиме это pushad.
    .aspack:01019002                 call    loc_101900A
    .aspack:01019002 ; ------------------------------------------------------------
    .aspack:01019007                 db 0E9h ; щ
    .aspack:01019008 ; ------------------------------------------------------------
    .aspack:01019008                 jmp     short loc_101900E
    .aspack:0101900A ; ------------------------------------------------------------
    .aspack:0101900A 
    .aspack:0101900A loc_101900A:                        ; CODE XREF: start+1p
    .aspack:0101900A                 pop     ebp
    .aspack:0101900B                 inc     ebp
    ;++ebp = eip – хороший пример позиционно-независимого кода – PIC
    .aspack:0101900B start           endp
    .aspack:0101900B 
    .aspack:0101900C                 push    ebp
    .aspack:0101900D

    .aspack:0101900D locret_101900D:                     ; CODE XREF: start+7u
    .aspack:0101900D                 retn
    ;учтите – это не антиотладка, это лишь достаточно красивый
    ;пример вывертов с ассемблером – смотреть приятно!
    .aspack:0101900E ; -------------------------------------------------------------
    .aspack:0101900E 
    .aspack:0101900E loc_101900E:                        ; CODE XREF: start+7j
    .aspack:0101900E                 call    loc_1019014
    .aspack:01019014 loc_1019014:                        ; CODE XREF: .aspack:0101900Ep
    .aspack:01019014                 pop     ebp
    ;вопрос на засыпку – на что показывает ebp?
    ;здесь очень удобна возможность IDA переходить по G “+”
    ; – просто ставьте курсор на нужное смещение и вперед.

Итак, ясно, что ebp показывает на начало кода/данных. В связи с этим весь остальной код теперь достаточно четок и ясен. Полагаем, понять, что делает этот код, после фокуса с ebp, уже предельно просто:

    .aspack:01019035     lea     eax, [ebp+42Eh]       ;offset на строку kernel32.dll
    .aspack:0101903B     push    eax
    .aspack:0101903C     call    dword ptr [ebp+0F4Dh] ;вызывается GetModuleHandleA
    ; явное получение хендла нужно для вызова некоторых
    ; дополнительных функций – VirtualAlloc/VirtualFree и т.п.

Фокус с

    .aspack:0101906C                 lea     eax, [ebp+77h]
    .aspack:0101906F                 jmp     eax

уже тоже сбить не должен. Куда передаст управление регистровый jmp, вы уже можете найти и без отладчика.

Теперь давайте посмотрим, как правильно надо сбрасывать дамп из программ, запакованных Aspack. Здесь предельно четко нужно понимать, что нам нужен файл ДО обработки его самим упаковщиков, т.е. СРАЗУ ЖЕ после разжатия. Такой момент существует не для всех упаковщиков, однако для Aspack он проявляется предельно наглядно.

Итак, Aspack выполняет разжимание секций по LZ+Хаффман-подобному алгоритму с использованием VirualAlloc для отведения памяти под временный буфер и VirtualFree для высвобождения этого буфера. В буфер вбрасывается разжатое содержимое, которое копируется двойными словами (хорошая оптимизация!) (и докопируется побайтно при необходимости) на то место, что принадлежит ему по праву – на оригинальный RVA секции. После полного разжатия и копирования Aspack принимается обрабатывать директорию импорта (см. ниже), директорию перемещаемых элементов, вычисляет и предает управление на OEP. Наша задача – сбросить дамп ДО обработки импорта, fixup-элементов и т.п. Сделаем мы это как раз здесь:

    .aspack:0101916D        mov     ecx, eax        ; счетчик
    .aspack:0101916F        mov     edi, [esi]
    .aspack:01019171        add     edi, [ebp+422h] ; ImageBase + section RVA
    .aspack:01019177        mov     esi, [ebp+152h] ; внутренние таблицы упаковщика
    .aspack:0101917D        sar     ecx, 2
              ; копирование двойнми словами – edi - приемник
    .aspack:01019180        rep movsd
    .aspack:01019182        mov     ecx, eax
    .aspack:01019184        and     ecx, 3
    .aspack:01019187        rep movsb               ; докопировать хвостик
    .aspack:01019189        pop     esi
    .aspack:0101918A        push    MEM_RELEASE     ; DWORD dwFreeType
    .aspack:0101918F        push    0               ; в точности после VirtualAllloc
    .aspack:01019191        push    dword ptr [ebp+152h] ; LPVOID lpAddress
    .aspack:01019197        call    dword ptr [ebp+551h] ; VirtualFree
    .aspack:0101919D        add     esi, 8
    .aspack:010191A0        cmp     dword ptr [esi], 0
    .aspack:010191A3        jnz  
      unpack_loop ; для каждой секции
    .aspack:010191A9        push    MEM_RELEASE ;---> это и есть наша цель
    .aspack:010191AE        push    0
    .aspack:010191B0        push    dword ptr [ebp+156h]
    .aspack:010191B6        call    dword ptr [ebp+551h] ; VirtualFree

Адрес 0х010191A9 здесь как раз и является «заветным». Зацикливая программу на этом адресе мы можем быть уверены, что дамп является полностью рабочим и не подвергнут ни обработке импорта, ни чему-либо еще. Ввести программу в бесконечный цикл можно по-разному, к примеру, введите ассемблерную команду jmp eip, воспользуйтесь командой !dump в IceExt, выберите SuspendThread, словом, вам и карты в руки. Мы же здесь, разумеется, объясним как использовать PE Tools.

Итак, после того как jnz не сработает – все секции распакованы и можно приступать. Полный дамп файла с опциями Full Dump: fix header, Full Dump: rebuild image. DumpFix, ValidatePE, RebuildPE. В дампе ручками в PE Editor: Optional Header > пересчитываем: SizeOfImage, SizeOfHeaders, Checksum (при помощи кнопки "?"). Уменьшаем размер. Делаем RebuildPE, с опциями: DumpFix, ValidatePE, RebuildPE.

Файл ПОЧТИ валиден. Осталась мелочь – параметры директории импорта и правка EP. Нам более не нужен код пакера, поэтому EP должна быть переориентирована назад – на OEP и директория импорта должна быть поправлена. Для этого залезем в алгоритм Aspack еще раз. Вспоминаем первую часть и действие функции LdrpSnapIAT, что в ntdll.dll. Все пакеры должны эмулировать ее действие, заключающееся (не только) в превращении RVA полей директории импорта в VA – взгляните на код:

    .aspack:01019278       mov     esi, 12A40h     ; RVA на директорию импорта
    ;обратите внимание – вычисляется упаковщиком
    ;при обработке файла – гляньте на RVA в нормальном файле
    ;остается в файле в открытом виде, что очень удобно для поиска по константе
    .aspack:0101927D       mov     edx, [ebp+422h] ; ImageBase
    .aspack:01019283       add     esi, edx    ; в VA (VA = RVA + ImageBase)
    .aspack:01019285 
    .aspack:01019285 process_IID:              ; CODE XREF: .aspack:01019395j
    .aspack:01019285       mov     eax, [esi+IMAGE_IMPORT_DESCRIPTOR.Name]
    .aspack:01019288       test    eax, eax
    .aspack:0101928A       jz      finish      ; конец директории импорта?
    .aspack:01019290       add     eax, edx    ; RVA на имя dll -> в VA на имя dll
    .aspack:01019292       mov     ebx, eax
    .aspack:01019294       push    eax
    .aspack:01019295
          call    dword ptr [ebp+0F4Dh] ; GetModuleHandle
    .aspack:0101929B       test    eax, eax
    .aspack:0101929D       jnz     short module_already_loaded
    .aspack:0101929F       push    ebx
    .aspack:010192A0       call    dword ptr [ebp+0F51h]  ; LoadLibraryA
    ...
    .aspack:010192F5       push    ebx              ; или имя, или ординал
    .aspack:010192F6       push    dword ptr [ebp+545h]    ; handle
    .aspack:010192FC       call    dword ptr [ebp+0F49h]   ; GetProcAddress
    .aspack:01019302       test    eax, eax
    .aspack:01019304       pop     ebx           ; *PIMAGE_THUNK_DATA32
    .aspack:01019305       jnz     short addr_ok ; функция вернула валидный адрес
    ...
    .aspack:01019376
    addr_ok:                ; CODE XREF: .aspack:01019305j
    .aspack:01019376                         ; .aspack:0101935Ej
    .aspack:01019376       mov     [edi+IMAGE_THUNK_DATA.u1], eax
                                   ; +sizeof(IMAGE_THUNK_DATA32)
    .aspack:01019378       add     dword ptr [ebp+549h], 4
    .aspack:0101937F       jmp     loc_10192B6
    ; после заполнения IMAGE_THUNK_DATA реальными
    ; адресами функций и замене RVA на VA 
    ; директория теряет смысл для нас

Итак, RVA директории импорта ясен, осталось узнать размер. Делаем так:

    :g 1019278             ;встать на mov esi, 12A40h
    :d esi+edx             ;видим начало IID и визуально определяем
                           ; конец
    001B:01012A40 C0 2B 01 00 FF FF FF FF-FF FF FF FF E6 2C 01 00  .+..........ж,..
    001B:01012A50 F4 10 00 00 60 2B 01 00-FF FF FF FF FF FF FF FF  ....+..........
    001B:01012A60 08 2E 01 00 94 10 00 00-CC 2A 01 00 FF FF FF FF  ....”....*......
    ;он выглядит так:
    :d 010134D0
    0023:010134D0 65 01 47 65 74 57 69 6E-64 6F 77 54 65 78 74 57  e.GetWindowTextW
    0023:010134E0 00 00 55 53 45 52 33 32-2E 64 6C 6C 00 00 00 00  ..USER32.dll....
    0023:010134F0 00 00 00 00 00 00 00 00-00 00 00 00 00 00 00 00  ................
    :? 10134f0-1012a40     ;вычисляем длину
    <ulong> = 0xAB0, 2736, "\n|"

Параметры директории должны выглядеть так: RVA – 0x12A40, Size – 0xAB0. Теперь OEP:

    .aspack:0101939A     mov     eax, 12420h     ; OEP RVA –
                                                 ; и OEP хранится в чистом виде
    .aspack:0101939F     push    eax
    .aspack:010193A0     add     eax, [ebp+422h] ; OEP VA = OEP RVA + ImageBase
    .aspack:010193A6     pop     ecx
    .aspack:010193A7     or      ecx, ecx
    .aspack:010193A9     mov     [ebp+3A8h], eax
    ;заметьте, в коде aspack много подобных инструкций –
    ;это его отличительная черта, сбросьте флажок Writable у 
    ;секции aspack в вашем файле и немедленно увидите результат
    .aspack:010193AF     popa
    .aspack:010193B0     jnz     short loc_10193BA
    .aspack:010193B2     mov     eax, 1
    .aspack:010193B7     retn    0Ch              ; оригинальная dll не имела
                                                  ; точки входа
    .aspack:010193BA ; --------------------------------------------------------------
    .aspack:010193BA 
    .aspack:010193BA loc_10193BA:              ; CODE XREF: .aspack:010193B0j
    .aspack:010193BA     push    0   
    ;операнд инструкции push заполняется динамически,
    ;инструкцией по адресу 010193A9
    .aspack:010193BF     retn                  ;на OEP

Меняем RVA OEP на 0х12420. Все. Дамп рабочий и полностью готов. В качестве домашнего задания – попытайтесь удалить секции Aspack. Если с тем, что здесь написано, есть внутренние сомнения – почитайте дополнительно очень хорошую статью: «unpacking files with a .aspr section», а мы, тем временем, рассмотрим один оригинальный, хотя и не новый, прием. Надеемся, вы еще помните, что мы советовали обратить внимание на pusha в начале этой главы. Вы уже знаете, что дампить программу нужно не на OEP, а, желательно, задолго до него. В случае Aspack, где в открытом виде остается директория импорта, перемещаемых элементов, распаковываются секции, до OEP добираться нет нужды. Однако, возможно представить ситуации, когда времени просто нет и нужно добежать. Тогда используйте трюк с bpm esp-4. Немедленно после pushad ставьте эту точку останова. Действие pushad можно глянуть в интеловских талмудах в виде псевдокода. Если лень глядеть, вот:

    Temp ? (ESP);
    Push(EAX);  // 0x1C
    Push(ECX);  // 0x18 
    Push(EDX);  // 0x14 
    Push(EBX);  // 0x10 
    Push(Temp);  // 0x0C 
    Push(EBP);  // 0x08 
    Push(ESI);  // 0x04 
    Push(EDI);  // 0x00

Точка останова сработает поблизости от OEP – на popad – при вытаскивании регистров назад. Вот здесь:

    .aspack:010193AF    popa   ;-> точка останова сработает здесь
    .aspack:010193B0    jnz     short loc_10193BA
    .aspack:010193B2    mov     eax, 1
    .aspack:010193B7    retn    0Ch    ; оригинальная dll не имела точки входа
    .aspack:010193BA ; ---------------------------------------------------------
    .aspack:010193BA 
    .aspack:010193BA loc_10193BA: ; CODE XREF: .aspack:010193B0j
    .aspack:010193BA    push    0   
    .aspack:010193BF    retn      ;на OEP

Заметим напоследок, что трюк с bpm esp-4 сейчас работает не так уж и часто. Многие крипторы специально осложняют нам жизнь. В некоторых случаях это можно обойти, к примеру, поставив точку останова еще ниже, но с некоторыми крипторами не помогает и это. К примеру, вполне возможно поставить в блок try/catch пример на исчерпание стека в бесконечном цикле, а обработчик исключения должен это дело разобрать. Для успешного понимания таких защит необходимо четко разбираться в SEН – структурных исключениях, что и рассматривается в следующей главе.

(продолжение следует...)