Низкоуровневое программирование для Дzenствующих

Вид материалаДокументы

Содержание


Практический пример: UPX
UPX полностью пересобирает PE
UPX можно воспользоваться хекс-редактором, но лучше - плагином Uncover UPX
Abcdef ->
UPX’а никто больше не умеет определять точные границы этой части. Сигнатура UPX
UPX.EXE (около 327
000Fffff (
UPX. Например, это делает telock
Подобный материал:
1   ...   32   33   34   35   36   37   38   39   ...   42

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


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

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

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

Как программа переводит эти данные в свой формат описано в файле 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 - это контрольные суммы, рассчитанные по алгоритму Марка Адлера (ссылка скрыта). Можете посмотреть исходники данного алгоритма, но они нам не помогут... Предполагается, что у нас нет распакованного варианта экзешника. Значит и подсчитать его контрольную сумму, даже зная алгоритм, мы не можем... На самом деле, обе контрольные суммы не влияют на процесс распаковки, т.е. можно просто отключить проверку данных значений внутри 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. Скачиваем с ссылка скрыта специальные 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 имеет такой странный операнд? Давайте спросим автора HIEWSEN’a. Ответ: «...но такого VA в файле нет, он появится потом, когда UPX память выделит для этого VA, а в файле ничего нет, поэтому hiew просто отсчитывает в глобальных адресах смещение и показывает как есть». Так что это ни в коем случае не баг утилиты. Просто автор очень не хочет включать поддержку многочисленных частных случаев, благодаря чему HIEW был и, пожалуй, так и остается одним из самых быстрых дизассемблеров на сегодяшний день. А уж эта возможность поиска по ассемблерной маске с * и ? – так это вообще фантастика...