Рассылка закрыта
При закрытии подписчики были переданы в рассылку "О карьере и профессиональном развитии IT-специалистов" на которую и рекомендуем вам подписаться.
Вы можете найти рассылки сходной тематики в Каталоге рассылок.
← Ноябрь 2003 → | ||||||
2
|
||||||
---|---|---|---|---|---|---|
3
|
4
|
5
|
6
|
7
|
8
|
9
|
10
|
11
|
12
|
13
|
14
|
15
|
16
|
17
|
18
|
19
|
20
|
21
|
22
|
23
|
24
|
25
|
26
|
27
|
28
|
29
|
30
|
Статистика
+11 за неделю
Низкоуровневое программирование для дZeнствующих #0045 -2
Информационный Канал Subscribe.Ru |
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 4. А запакован ли файл?
|
Энторпия
|
Боже, как только это слово в термодинамике не обругали.
И мера порядка системы, и мера рассеивания энергии, уж чего только
там не было! Без сомнения, настоящего физика от нашего определения
покоробит, а настоящий математик искренне возмутится. Тем не менее,
по дилетантски определим слово "энтропия" как некоторую
меру эффективности хранения информации. Для иллюстрирования понятия
продемонстрируем следующий пример. Условимся считать файл всего лишь
строкой байт, конец которой определяется каким-то загадочным образом
самой операционной системой.
Положим, у нас есть строка: 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 за закрытость кода?
Как правило, в открытую печать не попадает сколь-нибудь ценной технической информации... Что тут говорить. Тем не менее, мы предложим пару линков:
- «Heuristic Techniques in AV Solutions: An Overview»
http://www.securityfocus.com/infocus/1542 - в качестве вступления смотрися неплохо.
2. «Stripping Down An Av Engine» - http://www.nai.com/common/media/vil/pdf/imuttik_VB_%20conf_2000.pdf – в качестве вступления к следующему линку тоже ничего.
- http://clamav.elektrapro.com/ - попытка антивируса с открытым исходым кодом (язык С). Функция эвристики (пока только ребенок) находится в файле matcher.c и использует обобщение алгоритма Кнута-Морриса-Пратта в поиске в односвязном списке – функция cl_scanbuff. Есть и еще несколько подобных антивирусов, но у их основателей хватило ума писать их на Java… Что тут сказать... И это там, где важна скорость и низкоуровневые трюки...
- 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] Код прост, но хорошо отражает суть. Его можно выразить и чуть иначе, например, так: mov edi, offset на собственную IAT – сканируем первый байт всех адресов а можно и еще парой десятков вариаций. Для его обхода
опытные люди используют трюк с 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
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, правило, возможно, соблюдаться не будет – кода мы не изучали) -
- UPX0 - кладутся tls
- UPX1 - fixup-элементы, импорт, экспорт, код и т.п.
- 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Н – структурных исключениях, что и рассматривается в следующей главе.
(продолжение следует...)
###########################################################################
всТупление
| Edmond / HI-TECH | |
------------- |
###########################################################################
Рассылка составлена HI-TECH GROUP 01 ноября 2003 года. |
(c) HI-TECH 2000 - 2003
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||