Данная защита появилась достаточно недавно, поэтому найти программу стороннего разработчика для исследования мне не удалось.
Да и зачем, учитывая, что исполняемый файл защищен этой же защитой. Лично я думаю, что нигде нельзя увидеть защиту во всей красе, кроме как на программном продукте этого же разработчика.

Tools: Soft-Ice, HIEW, Lord PE, Import Reconstructor.

Осмотр

Obsidium использует вот такие приколы для затруднения анализа:

  1. Независимо от того, сколько секций было до этого, но после обработки их становиться 5 с затертыми именами (не
    затёрто только название секции .rsrc).
  2. Запакованные ресурсы.
  3. Своеобразные переходники в импорте.
  4. Управление на функции из внешних dll передается обычно на 3-ю от начала инструкцию.
  5. Вызовы GetTickCount для замера времени выполнения некоторых участков.
  6. Определяет наличие отладчика 3 уровня, чтобы он трогал Soft-Ice я не заметил.
  7. Обнаружение драйвера revirgin’а: CreateFileA(“\\.\RvtracerDevice0”).
  8. Relocation образа исполняемого файла, а не только
    dll.
  9. Динамическая распаковка.
  10. Собственные API.
  11. Вставка в некоторые места программы ложных инструкций, с последующей обработкой этих исключений.З
  12. абирает к себе несколько начальных байт из
    OEP.
  13. Большое количество мусорных инструкций.

Кстати, хочу заметить, что работу с dll данная защита пока не поддерживает.

Небольшое введение

Практически все производимые операции obsidium реализует на основе нескольких функций, которые записаны вот в такую структуру:

Смешение  Описание
+0  Базовый адрес, по которому находится новый образ исполняемого файла.
+20  Вызывает процедуру, CRC имени которой ей передана в качестве параметра.
+24  Определяет длину инструкции, адрес на которую передан в качестве параметра.
+30  Считает контрольную сумму строки переданной в параметре.
+39  Старый базовый адрес исполняемого файла(до обработки защитой)
+40  Ищет адрес функции, по CRC имени.
+44  Ищет адрес начала образа библиотеки.
+48  Базовый адрес библиотеки kernel32.dll
+4C  Базовый адрес библиотеки user32.dll
+50  Размер образа исполняемого файла.
+68  Используя релоки, производит настройку кода на новый базовый адрес.
+С8  Полная распаковка необходимого участка.

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

Небольшое примечание: немного забегу вперед, но вот что я заметил
— Obsidium несколько видоизменяет релоки таким образом, что он использует их не только для настройки на новый базовый адрес, но и для дописывания некоторого кода. Например, у меня при динамической расшифровке один кусок расшифровывался неправильно без релоков, а с релоками в программе появились специально вставленные инструкции, вызывающие ошибки, которые обрабатывает сама защита.

Снятие дампа

Итак, при распаковке происходят следующие вещи:

  1. Выделяется 1-й раз место под переходники для импорта и другую необходимую информацию для защиты.
  2. Выделяется 2-й раз место для хранения промежуточных данных.
  3. Выделяется 3-й раз место для образа исполняемого файла.
  4. Производиться конечная распаковка файла.
  5. Настройка кода исполняемого файла с учетом нового базового адреса.
  6. Выделяется 4-й раз место (для чего я так и не понял).
  7. Удаляется выделенное на 2 этапе место.

Также хочу заметить, что все место, выделяемое защитой для работы исполняемого файла легко выделить, т.к. на этих участках стоит атрибут
Read+Write+Execute. 

Итак, ставим бряк на VirtualAlloc (bpx VirtualAlloc+3) и ждем, когда вызов произойдет в 3 раз, потом выходим и топаем до тех пор, пока новый выделенный участок не заполниться данными (у меня он почти всегда находился по адресу 009С0000). Если попутно вы будете обходить все вызовы [ebx+68], то получите данные без использования релоков. Вызов функции [ebx+68] полезен тем, что он дает возможность узнать какие бывшие секции, куда “проецируются”. Узнать это можно посмотрев на передаваемые ей параметры, один из них — это смещение относительно базового адреса и размер. В итоге получаем:

Смещение Размер
1 1000 C1000
2 C2000 C200
3 D3000 200

Поиск OEP и спертых байт

Найти предположительную OEP можно, если после дампа забить все байтом СС и в SI поставить бряк на int 3(bpint 3). Но это только предположительно, т.к. есть еще те байты, которые защита забрал к себе и до них
придется топать самостоятельно. Желательно предварительно поставить бряк на VirtualFree (bpx VirtualFree+3). Выход к OEP выглядит следующим образом:

seg000:007F5C08 mov byte ptr [ebp+eax+40C228h], 0E9h ; ‘щ’
seg000:007F5C16 mov edx, [ebp+40C334h]
seg000:007F5C21 add edx, [esi] 
seg000:007F5C26 lea ecx, [ebp+eax+40C22Dh]
seg000:007F5C32 sub edx, ecx
seg000:007F5C38 mov [ebp+eax+40C229h], edx
seg000:007F5C44 popa
seg000:007F5C4A popf
//
—————————Далее идут украденные байты
—————————
//
seg000:007F5C53 mov eax, ds:0A8208Bh
seg000:007F5C5B shl eax, 2
seg000:007F5C64 mov ds:0A8208Fh, eax
seg000:007F5C6C push edx
seg000:007F5C71 push 0

seg000:007F5C79 jmp near ptr 9C144Ah ;
дальнейшее выполнение программы
(Для хорошей читабельности мусор и постоянные jmp удалены).

Импорт

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

seg000:007F3E60 pusha
seg000:007F3E61 mov ax, 0 ;
смещение в этом Thunck’е
seg000:007F3E65 mov dl, 0 ;
Thunck
seg000:007F3E67 jmp loc_7F314C

Потом выполняется функция, которая и выполняет все работу. Эта функция берет из специально таблички атрибут данной функции и в зависимости от него решает, что дальше делать с этой функцией. Всего возможно 4 варианта:

  1. Это API Obsidium’а.
  2. В табличке храниться CRC имени функции.
  3. В табличке храниться по XOR’нный адрес функции.
  4. Адрес функции храниться не в этой табличке и функция вызывается несколько по-другому.
  5. Функции, значение которых не меняется на протяжении всего времени выполнения программы.

Интересный момент, что Obsidium заполняет в IAT на 1 больше ячеек, таким образом если IAT нескольких библиотек шли подряд, то после обработки их Obsidium’ом ImpRec распознает только одну большую IAT. Так же интересным моментом является то, что защита использует нечто вроде «кэширования», т.е. в связи с тем, что операция поиска адреса функции по CRC её имени может занимать достаточно много времени, то чтобы не вызывать эту функцию несколько раз он берет и записывает адрес искомой функции вместо адреса переходника. Я функции, относящиеся ко 2 и 3-му типу, восстанавливал путем написания плагина к ImpRec’ку, который просто создавал приблизительно такую конструкцию

nop
call reducer
nop
ret

где reducer – это текущий переходник, потом вызывал её и после её обработки получал, вместо reducer относительный адрес системной функции.

Конечно, пришлось несколько подредактировать процедуру, находящуюся по адресу 7F314C, т.к. она проверяла, принадлежит ли вызывающий код исполняемому файлу.

seg000:007F314C call $+5
seg000:007F3155 pop ebx
seg000:007F315C mov ebp, ebx
seg000:007F3161 sub ebp, 411BD2h
seg000:007F316D mov ebx, [ebx-0E9h]
seg000:007F317E call $+5
seg000:007F3187 add dword ptr [esp], 20h ; ‘ ‘
seg000:007F318B mov ecx, [ebp+411BC9h]
seg000:007F3194 call ecx ;
последняя инструкция функции AddAtom
seg000:007F319C retn

seg000:007F31A3 mov esi, [ebx+28h]
seg000:007F31A6 movzx edx, dl
seg000:007F31A9 movzx eax, ax
seg000:007F31AC shl edx, 3
seg000:007F31AF mov ecx, edx
seg000:007F31B1 shl edx, 1
seg000:007F31B3 add ecx, edx
seg000:007F31B5 lea edi, [esi+ecx+4]
seg000:007F31B9 cmp dword ptr [edi], 0
seg000:007F31BC jz loc_7F334E
seg000:007F31C2 add esi, [edi+10h]
seg000:007F31C5 lea esi, [esi+eax*8]
seg000:007F31C8 movzx eax, word ptr [esi]
seg000:007F31CB cmp eax, 40h ;
API Obsiduim’а ?
seg000:007F31CE jz loc_7F3264
seg000:007F31D4 cmp eax, 4 ;
по xor’ная функция?
seg000:007F31D7 jz short loc_7F3238
seg000:007F31D9 cmp eax, 1
seg000:007F31DC jz short loc_7F320D
seg000:007F31DE movzx eax, word ptr [esi+2] ;
в таблице храниться CRC

А по адресу 457А55 идет вызов функции, которая производит запись реального адреса функции в call вызвавший наш переходник.

00457F55 50 push eax
00457F56 51 push ecx
00457А57 FFD2 call edx ;
вызов приведенной ниже функции

seg000:007F5214 mov edx, [ebx]
seg000:007F521B cmp eax, edx
seg000:007F5220 jb loc_7F5354 ;
забиваем прыжок
seg000:007F522A add edx, [ebx+50h]
seg000:007F5232 cmp eax, edx
seg000:007F5239 jnb loc_7F5354 ;
забиваем прыжок
seg000:007F5248 cmp word ptr [eax-6], 15FFh
seg000:007F5251 jnz short loc_7F52BF

seg000:007F52C2 cmp byte ptr [eax-5], 0E8h
seg000:007F52CB jnz loc_7F5354
seg000:007F52D7 mov edx, eax
seg000:007F52DC add edx, [eax-4]
seg000:007F52E8 mov ecx, [esi+172h]
seg000:007F52F3 add ecx, [ebx]
seg000:007F52F8 cmp edx, ecx
seg000:007F52FF jb short loc_7F5354 ;
забиваем прыжок
seg000:007F5305 add ecx, [esi+176h]
seg000:007F530B cmp edx, ecx
seg000:007F5311 jnb short loc_7F5354 ;
забиваем прыжок
seg000:007F531D cmp word ptr [edx], 25FFh
seg000:007F5328 jnz short loc_7F5354 ;
забиваем прыжок
seg000:007F5333 mov edx, [ebp+0Ch]
seg000:007F5339 mov byte ptr [eax-5], 0E8h
seg000:007F5343 sub edx, eax
seg000:007F5348 mov [eax-4], edx
seg000:007F5357 pop ebx
seg000:007F5358 pop esi
seg000:007F5359 leave
seg000:007F535A retn 8

Теперь пару слов о функциях, значение которых не меняется на протяжении всего времени выполнения программы
(GetVersion, GetCommandLineA, GetCurrentProcessId). Они имеют переходники вида:

mov eax,some_value
ret

Для их восстановления достаточно просто по очереди вызвать эти функций и проверить на равенство результата значению, помещаемому в
eax. Таким образом, мы восстановили почти всю таблицу импорта, кроме функции VirtualQuery, т.к. она единственная обрабатывается 4 способом.
Получаем восстановленный импорт(API Obsidium’а пока оставим в покое) и переходим к сборке исполняемого файла с нормальным PE-заголовком.

Сборка exe

Я взял PE-заголовок у старого exe’шника и стал добавлять секции. Размеры и RVA могут отличаться от тех, которые мы получили выше, потому что я ставил для себя цель получить для начала работающий исполняемый файл, из-за этого я специально увеличил размер дабы исключить появления дыр между секциями, т.к. разные версии Windows на это по разному реагируют.

1. Первая у нас будет кодовая секция.
Имя: .text
RVA = 1000
Size = C1000

2. Вторая это секция с данными:
Имя: .data
RVA = C2000
Size = E00

3. Так как программа написана на Borland C++ Builder, то третья это tls секция:
Имя: .tls
RVA = D000
Size = 5000

Так же не забудьте перенести из старого исполняемого файла TLS структуру настроив её предварительно на новое местоположение.

4. Четвёртая секция будет с полученным ранее импортом.

5. Пятая секция с ресурсами. В связи с тем, что ресурсы зашифрованы, то берем
и дампим запущенный процесс, и применяем к нему утилиту Resource Rebuilder by Dr.Golova, чтобы получить новую секцию с расшифрованными ресурсами, перестроенными под новый базовый адрес.

6. Необязательная шестая секция с релоками. Релоки в первоначальном виде можно найти в участке памяти, который защита выделяет в первый раз (обычно в районе адреса 7E0000) по характерным байтам: 00 00 10 00 00 10 02 00 00

Динамическое шифрование

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

009C20AE push 00000517
009C20B3 call dword ptr [004010A9] ;
расшифровка

мусор

009C25D0 push 00000517
009C25D5 call dword ptr [004010AD] ;
обратно зашифровываем

Хочу заметить, что между этими двумя вызовами как раз 517h байт. Теперь просто делаем дамп этого участка после расшифровки и вставляем в наш дамп. Вызовы функция для шифрования теперь можно забить nop’ами.
Таких участков я нашел 7 штук. Вот они:

1. 9C20B9 — 517
2. 9C38FE — 30B
3. 9C54AB — 17
4. 9C5FB6 — D5
5. 9C668E — 8E
6. 9C6ACB — 10
7. 9E874B — 5E

Эмуляция API

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

  1. getRegInfo – возвращает в зависимости от 1-го параметра, на кого зарегистрирована программа.
  2. isRegistered – возвращает true, если программа зарегистрирована.
  3. CheckRegCode(в документации я её не нашел) – проверяет регистрационный код(вызывается только 1 раз при вводе регистрационного кода).

Послесловие

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

Оставить мнение

Check Also

Windows 10 против шифровальщиков. Как устроена защита в обновленной Windows 10

Этой осенью Windows 10 обновилась до версии 1709 с кодовым названием Fall Creators Update …