Оригинальный DVD-ROM: eXeL@B DVD !
eXeL@B ВИДЕОКУРС !

ВИДЕОКУРС ВЗЛОМ
выпущен 12 ноября!


УЗНАТЬ БОЛЬШЕ >>
Домой | Статьи | RAR-cтатьи | FAQ | Форум | Скачать | Видеокурс
Новичку | Ссылки | Программирование | Интервью | Архив | Связь

Логгер-эмулятор API Sentinel SuperPro

Обсудить статью на форуме

Хорошая подборка видеоуроков, инструментов крэкера, книг и статей - здесь.

Автор: Dr0x <>

Введение.
Тему донглов в принципе нельзя назвать "неподнятой целиной", материала по вопросу хватает. Но, тем не менее, хватает разработчиков защит, тупо цепляющих их к своему ненаглядному, выстраданному ПО и непонимающих как же это чудо техники сдалось так быстро? Может взломщикам доступна техника послойного сканирования и/или логический анализатор общей стоимостью за 40 000$ ?!? Может, конечно, оно и так :). Но в первую очередь надо винить самих себя. Всегда можно привести такие аргументы, как принципиальная невозможность создать невзламываемую защиту на базе существующих на рынке ключей в случае наличия его у взломщика.
Более того, я убежден, что ключи, секретность которых основывается на неизвестном алгоритме, неспособны противостоять табличной эмуляции. Причина - конечный размер ПО и, как следствие, конечный размер _действительно используемых_ в ПО ответов ключа. Таким образом, построение защищенного ПО сводится к максимальному "запудриванию" работы с ключом как то, динамическая дешифрация ресурсов, данных, кода; недетерминированность трека ключа; ложные запросы к ключу и т.п. И то, насколько эффективно разработчик сможет реализовать подобные приемы и будет характеризовать стойкость ПО к взлому. А самым эффективным, на мой взгляд, подходом к оценке стойкости получившейся защиты будет попытка ее взломать :) И в этом деле логгер с возможностью табличной эмуляции будет большим подспорьем.


Замечание: Прошла информация, что секретная функция реализуемая Sentinel SuperPro больше не секретна. К сожалению, опровергнуть или подтвердить это я не могу.

Введение в архитектуру ключа Sentinel SuperPro и его API (далее просто ключ)
Ключ содержит 64 ячейки памяти, из которых 56 доступны разработчику. Размер каждой ячейки 2 байта. Диапазон внутренних адресов 0 - 3F. Ячейки могут содержать "алгоритмы" или данные. "Алгоритм" - это неизвестная функция, детерминированная своим дескриптором, записанным в пару идущих подряд ячеек. Именно на алгоритмах реализует секретность ключа. Каждая ячейка имеет свой ACCESS CODE, который определяют ее тип.

Возможны следующие варианты значений ACCESS CODE:
0 - Данные
1 - Данные, доступные только для чтения (см. ниже)
2 - Счетчик
3 - Алгоритм
комбинации типов недопустимы.

ACCESS CODE можно читать и\или модифицировать только совместно с чтением и\или модификацией ячейки.

Секретность на уровне данных осуществляется паролями доступа.
Для каждого ключа существуют 2 пароля:
writePassword - двухбайтовый пароль для записи ячеек памяти ключа. Только для ячеек с ACCESS CODE=0
overwritePassword - четырехбайтовый пароль для перезаписи ячеек памяти ключа
Пароль на чтение не существует, но ячейки с ACCESS CODE=3 недоступны для чтения в принципе. Таким образом, обеспечивается секретность дескрипторов алгоритмов.

Алгоритмы могут содержать связанные с ними счетчики и\или пароль активации (activatePassword). Счетчики нужны для деактивации алгоритма, пароли - для активации. Для каждого алгоритма может быть задан свой пароль активации размером 4 байта.

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

Нам предоставляют следующее функции API:

RNBOsproFormatPacket - Инициализация внутренних структур RB_SPRO_APIPACKET для работы API; Неинтересно

RNBOsproInitialize - Инициализация драйвера ключа; Неинтересно

RNBOsproFindFirstUnit - Поиск ключа с подходящим DeveloperID

RNBOsproFindNextUnit - Поиск _следующего_ ключа с подходящим DeveloperID; Неинтересно

RNBOsproRead - Чтение word’a из указанной ячейки памяти ключа

RNBOsproExtendedRead - Чтение word’a из указанной ячейки памяти ключа + получение accessCode

RNBOsproWrite - Запись word’a + accessCode в указанную ячейку памяти ключа; Необходим writePassword; Только для ячеек с accessCode=0

RNBOsproOverwrite - Запись word’a + accessCode в указанную ячейку памяти ключа; Необходим writePassword и OverwritePassword; Перезапись любых ячеек с адресов >7

RNBOsproDecrement - Уменьшает указанную ячейку на 1. В случае нуля возвращает ошибку. Может использоваться для деактивации алгоритмов. Необходим writePassword;

RNBOsproActivate - Активирует неактивный алгоритм по указанному адресу.Необходим writePassword+activatePassword

RNBOsproQuery - Реализует "секретную" функцию. Преобразование над входными данными проводит алгоритм по заданному адресу. Ответ: преобразованные алгоритмом данные той же длинны, что и входные + дополнительно отдельно последние 4 байта ответа.

RNBOsproGetFullStatus - Расширенная информация - только для поддержки; Неинтересно

RNBOsproGetVersion - Версия и тип драйвера; Неинтересно

Отбросим сервисные функции, выполнение которых не зависит от наличия ключа:
RNBOsproFormatPacket,RNBOsproInitialize,RNBOsproGetFullStatus,RNBOsproGetVersion. Также отбросим RNBOsproFindNextUnit, т.к эмулируя ключ мы просто не дойдем до её исполнения.

Теперь, определившись с набором функций требующих эмуляции, подумаем немного о том, как это можно было бы осуществить.
При статической линковке код функций API ключа будет включён в тело программы. Таким образом, первая проблема - это локализация кода API функций ключа и установка соответствия кода функций их API прототипам.
Возможные пути решения:
1) Изучение работы программы под отладчиком с ключом и без. В случае отсутствия ключа программа "сломается". Посмотрев "аварийную" ветку кода вы вскоре должны будете обнаружить функцию, послужившею причиной - это, скорее всего, будет RNBOsproFindFirstUnit. Соответственно по соседству будет располагаться код остальных API функций ключа, т.к. линкер _обычно_ размещает функции, принадлежащие одной библиотеке рядом друг с другом.
Известно, что структура RB_SPRO_APIPACKET, необходимая для функционирования _всех_ API функций ключа, начинается с сигнатуры "Br" (4272h). API функции ключа проверяют вначале своей работы корректность структуры B_SPRO_APIPACKET, таким образом их можно локализовать прямым поиском в дизассемблере последовательности "Br" (4272h) или более сложных конструкций типа:

 mov         w,[ebx],07242 ;"rB"
 cmp         w,[ebx],07242 ;"rB"
 cmp         w,[esi],07242 ;"rB"
 cmp         w,[eax],07242 ;"rB"
и тому подобное.

Определить принадлежность кода функций их API прототипам можно анализом работы программы с установленными точками останова на подозрительных функциях. Зная типы входных параметров, их количество и контекст применения достаточно просто установить соответствие между именами API функций и местоположением их точек входа в коде.
2) Технология "Flirt" дизассемблера IDA позволяет полностью автоматизировать этот процесс. Файлы сигнатур для SentinelSuperPro можно взять с Wasm или сделать самим (статья с Tin).

Перейдём непосредственно к практике. На сайте Rainbow есть пример приложения, работающего с ключом SentinelSuperPro (url). Его мы и используем в качестве подопытного кролика. Распаковав архив соберем пример на Delphi, расположенный в папке OTHERS. А теперь забудем о том что у нас есть исходники этого примера. :)

Итак, мы имеем исполнимый файл sproeval.exe, который использует ключ защиты SentinelSuperPro посредством статически прилинкованной библиотеки. На данном этапе для нас совершенно неважно, что это _пример_ работы с ключом, а не защищённое приложение. Технология работы с ключом отличаться не будет.

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

Итак приступим к локализации функций API ключа.
Поищем строку "Br" в IDA в режиме Hex. Первое интересное вхождение будет

 00454997                 mov     word ptr [ebx], 7242h
Это код принадлежит функции sproFormatPacket, т.к. только она
устанавливает байты заголовка RB_SPRO_APIPACKET, остальные функции только проверяют их.
Функция берет 3 аргумента, следовательно, эта функция как минимум подфункция sproFormatPacket. Пройдем по XRef и посмотрим, кто её вызывает:

 00459FBA                 call    sub_454990
У вызывающей функции 2 параметра, как и у определения sproFormatPacket. Посмотрим по XRef на один из её вызовов:

 004544F3                 push    404h
 004544F8                 push    ebx
 004544F9                 call    00459F78
Параметр 404h соответствует thePacketSize, т.е. оставляем рабочую гипотезу, что это функция по адресу 00459F78 и есть sproFormatPacket.

Продолжим наши изыскания. Следующий результат поиска:

 00459AA2                 cmp     word ptr [ebx], 7242h
Хмм интересно… Но посмотрим на начало фрагмента кода, к которому принадлежит эта функция. Он начинается по адресу 00459A7E и никаких ссылок на него мы не видим. Есть небольшой шанс, что ИДА ошиблась, но пока примем рабочую версию, что этот фрагмент кода не используется.

По этим же соображениям отбрасываем:

 ...
 00459AA2                 cmp     word ptr [ebx], 7242h
 ...
 00459B2E                 cmp     word ptr [ebx], 7242h
 ...
 00459BC6                 cmp     word ptr [ebx], 7242h
 ...
Следующее вхождение уже интереснее:

 00459FFF                 cmp     word ptr [ebx], 7242h 
Заметим, что проверка происходит _почти_ в самом начале функции. Это типично
для API функций ключа. Вначале всегда идёт проверка корректности заголовка RB_SPRO_APIPACKET. Посмотрим на вызов этой функции(sub_459FE0). Заметим, что у нее только один аргумент.

 
 004544F0 sub_4544F0      proc near               
 004544F0                 push    ebx
 004544F1                 mov     ebx, eax
 004544F3                 push    1028
 004544F8                 push    ebx
 004544F9                 call    sub_459F78 // мы определили, как sproFormatPacket
 004544FE                 test    ax, ax
 00454501                 jnz     short loc_454509
 00454503                 push    ebx
 00454504                 call    sub_459FE0
 00454509 
 00454509 loc_454509:                             
 00454509                 pop     ebx
 0045450A                 retn
 0045450A sub_4544F0      endp
 
Как мы видим, вызов sub_459FE0 предваряется вызовом sproFormatPacket. И единственная функция, которую имеет смысл вызывать сразу после sproFormatPacket это sproInitialize. Кроме того, она принимает только один
аргумент, как и sproInitialize, что косвенно подтверждает наше предположение.
Всё это мини-исследование имеет для нас чисто “академический” интерес.
Посмотрим внимательно на список API функций, вызовы которых мы собрались перехватывать. Среди них нет функций, принимающих только _один_ аргумент. Т.е. мы могли её почти сразу отбросить. В дальнейшем так и будем поступать.

Следующее вхождение:

 0045A0D6                 cmp     word ptr [esi], 7242h
Функция sub_45A0B4, которой принадлежит этот код, принимает пять параметров. Таким образом это может быть sproGetVersion или sproActivate или sproWrite
Посмотрим на её вызов:

 ………..
 0045451E                 lea     edx, [ebp+var_4] 
 00454521                 push    edx // пятый аргумент - указатель на var_4
 00454522                 lea     edx, [ebp+var_3] 
 00454525                 push    edx //четвёртый аргумент - указатель на var_3
 00454526                 lea     edx, [ebp+var_2] 
 00454529                 push    edx // третий аргумент - указатель на var_2
 0045452A                 lea     edx, [ebp+var_1]  
 0045452D                 push    edx // второй аргумент - указатель на var_1
 0045452E                 push    eax // первый аргумент  
 0045452F                 call    sub_45A0B4
 00454534                 mov     dl, [ebp+var_1] // берём значение из Var_1
 00454537                 mov     [ebx], dl
 00454539                 mov     dl, [ebp+var_2] // берём значение из Var_2
 0045453C                 mov     [esi], dl
 0045453E                 mov     edx, [ebp+arg_4]
 00454541                 mov     cl, [ebp+var_3] // берём значение из Var_3
 00454544                 mov     [edx], cl
 00454546                 mov     edx, [ebp+arg_0]
 00454549                 mov     cl, [ebp+var_4] // берём значение из Var_4
 0045454C                 mov     [edx], cl
 ……………
Как мы видим, функции sub_45A0B4 передаются _указатели_. А в прототипе sproGetVersion как раз используются четыре указателя:

 FUNCTION RNBOsproGetVersion( ApiPacket  : RB_SPRO_APIPACKET_PTR;
                              majVer     : POINTER;
                              minVer     : POINTER;
                              rev        : POINTER;
                              osDrvrType : POINTER ) :WORD; STDCALL; EXTERNAL;
 
Следующее вхождение:

 0045A1AB                 cmp     word ptr [esi], 7242h
Функция sub_45A190 может быть только sproFindFirstUnit, т.к. она единственная из всех API принимает _два_ аргумента.

Следующее вхождение:

 0045A249                 cmp     word ptr [esi], 7242h
Этот код принадлежит функции sub_45A224, которая является подфункцией функции sub_45A274. У sub_45A274 только один аргумент - она нам не интересна.

Следующее вхождение:

 0045A336                 cmp     word ptr [ebx], 7242h
Функция sub_45A318 принимает три параметра, т.е. это может быть sproDecrement или sproRead
Посмотрим на её вызов:

 …………
 0045456C                 push    esp  // Третий аргумент
 0045456D                 push    edx 
 0045456E                 push    eax 
 0045456F                 call    sub_45A318
 00454574                 mov     dx, [esp] // берём  word по адресу 3-го
 							  // аргумента
 00454578                 mov     [ebx], dx
 0045457B                 pop     edx
 0045457C                 pop     ebx
 …………
Как видим, 3-й аргумент это указатель, по которому возвращается WORD. Очевидно, это функция sub_45A318 это sproRead.

Следующее вхождение:

 0045A3D5                 cmp     word ptr [ebx], 7242h
Функция sub_45A3B0 может быть только sproExtendedRead, т.к. она единственная из всех API принимает _четыре_ аргумента

Следующее вхождение:

 0045A472                 cmp     word ptr [ebx], 7242h
Функция sub_45A458 принимает пять параметров, и это не функция GetVersion т.к. она уже найдена. Выбираем между sproActivate или sproWrite.
Посмотрим на её вызов:

 ………
 004545B4                 mov     bl, [ebp+arg_0] // байт!
 004545B7                 push    ebx // пятый аргумент
 004545B8                 mov     bx, [ebp+arg_4]
 004545BC                 push    ebx
 004545BD                 push    ecx
 004545BE                 push    edx
 004545BF                 push    eax
 004545C0                 call    sub_45A458
 ………
как видим, последний аргумент функции Byte, следовательно, функция sub_45A458 это sproWrite.

Следующее вхождение:

 0045A501                 cmp     word ptr [ebx], 7242h
Функция sub_45A4E4 может быть только sproOverWrite, т.к. она единственная из всех API принимает _семь_ аргументов.

Следующее вхождение:

 0045A59A                 cmp     word ptr [ebx], 7242h
Функция sub_45A580 принимает три аргумента, и это не функция sproRead
т.к. она уже найдена. Следовательно, это функция sproDecrement.

Следующее вхождение:

 0045A616                 cmp     word ptr [ebx], 7242h
Функция sub_45A5FC принимает пять аргументов, и это не функции GetVersion или sproWrite т.к. они уже найдены. Следовательно, это функция sproActivate.

Следующее вхождение:

 0045A6AD                 cmp     word ptr [ebx], 7242h
Функция sub_45A688 может быть только sproQuery, т.к. она единственная из всех API принимает _шесть_ аргументов.

Следующее вхождение:

 0045A79F                 cmp     word ptr [eax], 7242h
Функция sub_45A788 принимает только _один_ аргумент. Отбрасываем её.


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

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

Теперь собственно, к библиотеке:
Наша библиотека будет экспортировать одну единственную функцию AttachHook, которая и будет осуществлять всю основную работу по настройке перехвата API ключа.

 {***********************************************************************************}
 procedure AttachHook(ModName_:dword);stdcall;
 var  ImageBase:dword;
      i,len:integer;
      pfilename:array[0..max_path-1] of char;
      temp,ModName:string;
 begin
 GetModuleFileName(GetModuleHandle(Pchar(ModName_)), @pFileName, SizeOf(pFileName));
 temp_:=pfileName;
 path:=extractfilepath(temp_);
 Modname:=extractfilename(temp_);
 ImageBase:=GetModuleHandle(Pchar(ModName_));
 
 ParseModule(Modname,ImageBase);
 AddSeparator;
 AddToLog(Modname+’ trapping activated successfully, IMAGEBASE=’+inttohex(ImageBase,8));
 end;
 {***********************************************************************************}
 
Предполагается, что эта функция будет вызываться в контексте исследуемого процесса.
Единственный её аргумент – указатель на имя исследуемого модуля, который мы будем модифицировать. Он становится нужен, в случае если код работы с ключом содержится DLL.
Итак, мы получили ImageBase и полный путь. После этого вступает в дело функция ParseModule.

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

 type funcRec=record
      MyFuncAddr:pointer; // VA нашей “API функции”
      funcVA:dword; // VA  оригинальной API функции
      stolenBytes:array [0..30] of byte;// Буфер с оригинальным началом функции
      patchBytes:array [0..30] of byte; // Буфер с  _нашим_ началом функции
 end
 
Эта вспомогательная структура содержит информацию модуле в целом.

 type sproFunc=record
      ModuleName:string; // Имя модуля
      ImageBase:dword; // Его База
      fRVA:dword;// RVA оригинальной функции sproFormatPacket 
      funcArr:array [1..FuncCount] of funcRec; // Каждый элемент этого массива –    
 // смещение определённой API функции относительно fRVA		                                       
 end
 
Для удобства определим константы, соответствующие API функциям:

 const       
 _SproQuery=1;_SproRead=2;_SproFindFirstUnit=3;_SproExtendedRead=4;
 _SproActivate=5;_SproDecrement=6;_sproWrite=7;_sproOverWrite=8;
 
Теперь проведём несложные математические вычисления и заполним массив fType смещениями API функций относительно sproFormatPacket:
Например: SproQuery у нас имеет индекс 1, следовательно запишем туда
(VA SproQuery - VA sproFormatPacket)= (45A688h-459F78h)=710h
И так для всех интересующих нас API функций. В результате должно получится:
fType:array [1..8] of dword= ($710,$3A0,$218,$438,$684,$608,$4E0,$56C));

Заполним частично нашу таблицу параметров модуля:
var sproModuls:array [1.. ModulsCount] of sproFunc=
(
(ModuleName: ’sproeval.exe’; fRVA: $59F78)
);
где fRVA – это RVA sproFormatPacket; ModulsCount – количество модулей, с которые мы _можем_ перехватывать. У нас ModulsCount=1, т.к. модуль только один.

Перейдем к функции ParseModule:

 {***********************************************************************************}
 procedure ParseModule(Modname:string;ImageBase:dword);
 var i,ind:integer;
 {************************************************************}
 function GetModulIndex(Modname:string):integer;
 var i:integer;
 begin
 result:=-1;
 for i:=1 to ModulsCount do
   begin
    if UpperCase(sproModuls[ i].ModuleName)=UpperCase(Modname) then begin
     result:=i;
     exit;
     end;
   end;
 end;
 {************************************************************}
 procedure PatchFunctEnty(inModuleAddr:dword;funcAddr:pointer);
 type bytes=array [0..30] of byte;
 var  b,a:^bytes;
      addr_:dword;
 begin
 b:=pointer(inModuleAddr);
 addr_:=cardinal(@RetAddr);
 a:=@addr_;
 b^[0]:=$50;// push eax
 b^[1]:=$8B;b^[2]:=$44;b^[3]:=$24;b^[4]:=$04;   // mov eax,[esp+00004]
 b^[9]:=a^[3];b^[8]:=a^[2];b^[7]:=a^[1];b^[6]:=a^[0];b^[5]:=$A3;// mov [RetAddr],eax
 
 b:=pointer(inModuleAddr+9);{+1}
 addr_:=cardinal(@CallAddr);
 a:=@addr_;
 b^[1]:=$E8;b^[2]:=$0;b^[3]:=$0;b^[4]:=$0;b^[5]:=$0; //call $+5
 b^[6]:=$58;// pop eax
 b^[11]:=a^[3];b^[10]:=a^[2];b^[9]:=a^[1];b^[8]:=a^[0];b^[7]:=$A3;// mov [CallAddr],eax
 b^[12]:=$58; // pop eax
 
 b:=pointer(inModuleAddr+13+9);
 addr_:=cardinal(funcAddr)-(cardinal(b)+5);
 a:=@addr_;
 b^[4]:=a^[3];b^[3]:=a^[2];b^[2]:=a^[1];b^[1]:=a^[0];b^[0]:=$E9;// JMP MyFuncEntry
 end;
 {************************************************************}
 
 begin
  ind:=GetModulIndex(Modname);
  sproModuls[ind].ImageBase:=ImageBase;
 for i:=1 to FuncCount do begin
 sproModuls[ind].funcArr[ i].funcVA:=sproModuls[ind].fRVA+fType[ i]+ImageBase;
 CopyMemory(@sproModuls[ind].funcArr[ i].stolenBytes[0],
 Pointer(sproModuls[ind].funcArr[ i].funcVA),30);
 end;
  sproModuls[ind].funcArr[_SproQuery].MyFuncAddr:=@SproQueryMy_;
  sproModuls[ind].funcArr[_SproRead].MyFuncAddr:=@SproReadMy_;
  sproModuls[ind].funcArr[_SproFindFirstUnit].MyFuncAddr:=@sproFindFirstUnitMy_;
  sproModuls[ind].funcArr[_sproExtendedRead].MyFuncAddr:=@sproExtendedReadMy_;
  sproModuls[ind].funcArr[_sproActivate].MyFuncAddr:=@sproActivateMy_;
  sproModuls[ind].funcArr[_SproDecrement].MyFuncAddr:=@SproDecrementMy_;
  sproModuls[ind].funcArr[_sproWrite].MyFuncAddr:=@SproWriteMy_;
  sproModuls[ind].funcArr[_sproOverWrite].MyFuncAddr:=@SproOverWriteMy_;
 
 
  PatchFunctEnty(sproModuls[ind].funcArr[_SproQuery].funcVA,
 sproModuls[ind].funcArr[_SproQuery].MyFuncAddr);
  PatchFunctEnty(sproModuls[ind].funcArr[_SproRead].funcVA,
 sproModuls[ind].funcArr[_SproRead].MyFuncAddr);
  PatchFunctEnty(sproModuls[ind].funcArr[_SproFindFirstUnit].funcVA,
 sproModuls[ind].funcArr[_SproFindFirstUnit].MyFuncAddr);
  PatchFunctEnty(sproModuls[ind].funcArr[_sproExtendedRead].funcVA,
 sproModuls[ind].funcArr[_sproExtendedRead].MyFuncAddr);
  PatchFunctEnty(sproModuls[ind].funcArr[_sproActivate].funcVA,
 sproModuls[ind].funcArr[_sproActivate].MyFuncAddr);
  PatchFunctEnty(sproModuls[ind].funcArr[_SproDecrement].funcVA,
 sproModuls[ind].funcArr[_SproDecrement].MyFuncAddr);
  PatchFunctEnty(sproModuls[ind].funcArr[_sproWrite].funcVA,
 sproModuls[ind].funcArr[_sproWrite].MyFuncAddr);
  PatchFunctEnty(sproModuls[ind].funcArr[_sproOverWrite].funcVA,
 sproModuls[ind].funcArr[_sproOverWrite].MyFuncAddr);
 end;
 {***********************************************************************************}
 
Несколько слов о том, как она работает. Вначале выясняем, какой собственно модуль мы будем модифицировать, т.е. пытаемся в sproModuls найти индекс, для которого имя модуля, переданное нам в вызове, будет совпадать с записанным в структуре. Это и делает функция GetModulIndex. У нас только один модуль, который мы должны модифицировать и, в нашем случае, эта функция не нужна.



Дальше нам нужно сохранить начала всех оригинальных API функций. Что мы и делаем, циклически проходя по всем индексам funcArr.
Теперь нам надо заполнить поля MyFuncAddr адресами _наших_ API функций. Заметим, что их объявление должно в точности повторять объявление оригинальных API.
После этого мы можем спокойно модифицировать каждую из них последовательно вызывая
PatchFunctEnty для каждой API фукции.

Код, который мы будем врезать на место первых байт функций API, выглядит так:

 push   eax // сохраним eax 
 mov    eax,[esp+00004] // возьмём из стека адрес возврата
 mov    [RetAddr],eax // и поместим его по адресу RetAddr  глобальной переменной нашей библиотеки 
 call   $+5 // положим в стек адрес следующей команды (delta value) 
 pop    eax // достанем его
 mov    [CallAddr],eax // и поместим его по адресу CallAddr глобальной переменной нашей библиотеки 
 pop    eax // восстановим eax
 JMP    MyFuncEntry // перейдем на начало нашей API функции 
Он не претендует на оптимальность, он просто работает.

Функция PatchFunctEnty для каждой API функции настраивает адреса RetAddr, CallAddr и MyFuncEntry, после чего они указывают соответственно на глобальные переменные RetAddr, CallAddr (dword’ы) и соответствующую _нашу_ API функцию.

Теперь немного о том, как должна выглядеть наша реализация API функции. Возьмем, например sproFindFirstUnit. В таком случае наша реализация может выглядеть так:

 {***********************************************************************************}
 function sproFindFirstUnitMy_(ApiPacket: RB_SPRO_APIPACKET_PTR;devleoperID: WORD ): WORD; STDCALL;
 var    status:word;
        CallAddr_:dword;
        RetAddr_:dword;
 begin
 CallAddr_:=CallAddr-15;
 RetAddr_:=RetAddr;
 
 AddSeparator;
 AddToLog(GetModuleName(CallAddr_)+’: RNBOsproFindFirstUnit at EIP: ’ +inttohex(CallAddr_,8)+’;Ret EIP: ’+inttohex(RetAddr_,8));
 AddToLog(’           IN:’);
 AddToLog(’  developerID: ’+inttohex(devleoperID,2));
 RestoreOriginalBytes(CallAddr_);
 sproFindFirstUnit:=RNBOsproFindFirstUnit(CallAddr_);
 status:=sproFindFirstUnit( ApiPacket,devleoperID);
 RestorePatchBytes(CallAddr_);
 AddToLog(’          OUT:’);
 AddToLog(’       RESULT: ’+StatusDesc(status));
 
 result:=0;//result:=status;
 end;
 {***********************************************************************************}
 
Как видите её определение в точности совпадает с оригинальной sproFindFirstUnit. Первым делом, получив управление,нам желательно получить VA оригинальной sproFindFirstUnit. Его, как и адрес возврата уже любезно подготовил наш “врезанный” фрагмент кода. Заберём его в наши локальные переменные,не забыв скорректировать адрес sproFindFirstUnit на 15.
Функция AddToLog просто добавляет переданную ей строку в лог-файл, AddSeparator – добавляет разделитель.
Теперь добавим в лог информацию о имени модуля, в котором мы перехватили вызов, адрес точки входа функции FindFirstUnit и адрес возврата из неё.

На этапе иницализации мы заполнили структуры SproModuls. Теперь остаётся только их перебрать и вытащить из нужной имя модуля. Это делает функция GetModuleName:

 function GetModuleName(EIP_:dword):string;
 var i,j:integer;
 begin
 for j:=1 to ModulsCount do begin
 for i:=1 to FuncCount do begin
  if sproModuls[j].funcArr[ i].funcVA=EIP_ then begin
      result:=sproModuls[j].ModuleName;
      end;
  end;
 end;
 end;
 
Теперь добавим в лог информацию о входных аргументах sproFindFirstUnit. После этого
вызовем оригинальную функцию sproFindFirstUnit. Для этого сначала восстановим оригинальные байты на её точке входа вызовом RestoreOriginalBytes(CallAddr_).

 procedure RestoreOriginalBytes(EIP_:dword);
 var i,j:integer;
 begin
 for j:=1 to ModulsCount do begin
 for i:=1 to FuncCount do begin
  if sproModuls[j].funcArr[ i].funcVA=EIP_ then begin
      CopyMemory(@sproModuls[j].funcArr[ i].patchBytes[0],
 Pointer(sproModuls[j].funcArr[ i].funcVA),30); //Save Patch
      CopyMemory(Pointer(sproModuls[j].funcArr[ i].funcVA),
 @sproModuls[j].funcArr[ i].stolenBytes[0],30);// Put stolen
      exit;
      end;
  end;
 end;
 end;
 
Эта функция перебирает SproModuls и ищет совпадение в массиве funcArr с переданным на её вход адресом. В случае совпадения мы сохраняем в соответствующем поле структуры текущее изменённое начало функции и, затем, меняем его на оригинальное.

Теперь ничто не мешает вызвать оригинальную функцию sproFindFirstUnit.
Для удобства я определил в начале модуля типы, соответствующие нужным нам API функциям:

Type RNBOSproFindFirstUnit=function( ApiPacket:RB_SPRO_APIPACKET_PTR;developerID:WORD) : WORD;stdcall;

И определил глобальную переменную:
Var SproFindFirstUnit:RNBOSproFindFirstUnit;

Теперь достаточно сделать следующее:
sproFindFirstUnit:=RNBOsproFindFirstUnit(CallAddr_);

И вызвать sproFindFirstUnit с необходимыми параметрами. После вызова
восстановим перехват, возвратив байты на её начало функцией RestorePatchBytes
и добавим в лог возвращаемые значения. В нашем случае это только код завершения функции.

Установим Result в то, что нам вернула оригинальная sproFindFirstUnit и покинем функцию.
Я не стану приводить код обработчиков остальных API функций, т.к. нет никаких трудностей в самостоятельном его написании.

Эмуляция API.
Эмуляцию API функций достаточно просто реализовать следующим образом:
1) Для SproFindFirstUnit достаточно просто возвращать SUCCESS.
2) Для функций, работающих НЕ с ячейками алгоритмов, таких как SproRead, SproExtendedRead, SproDecrement, sproOverWrite, sproWrite достаточно один раз прочитать все 56 байт ключа, а потом осуществлять все манипуляции уже в сохраненной памяти ключа.
3) Необходимость эмулировать SproActivate и SproDecrement(для алгоритмов) возникает только в случае, если защита _специально_ пытается деактивировать алгоритм и после этого вызвать его с целью проверки кода ошибки. В противном случае достаточно просто возвращать SUCCESS.
4) SproQuery поддается только табличной эмуляции; его ответы придётся снимать с работающей под ключом программы, а в процессе эмуляции находить в таблице правильные ответы.

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


Обсуждение статьи: Логгер-эмулятор API Sentinel SuperPro >>>


Комментарии к статье: Логгер-эмулятор API Sentinel SuperPro

nice 16.02.2005 20:50:17
Отлично!
Полезно было почитать.
---
Dr0x 16.02.2005 00:43:38
Спасибо :)
---
Sh[AHT] 17.02.2005 19:00:47
Неплохо, весьма неплохо.
---
Dr0x 16.02.2005 23:41:19
Обсуждение переехало сюда :)
http://cracklab.ru/f/index.php?action=vthread&forum=1&topic=1587
---
Dr0x 01.03.2005 15:08:12
Забыл. Собственно ссылка:
ftp://www.rtcs.ru/pub/drm/sx/6.0/interfaces/bxdel32.zip
---
MeteO 03.03.2005 14:49:22
Зачем так через жопу извращаться? При анализе кода достаточно воспользоваться сигнатурами библиотек SSpro, а для эмуляции донгла не заниматься херней по патченью кода, а написать эмулятор vxd/sys, благо все необходимое уже давно есть в сетке.

А все функции Сентинеля расписаны в файле api.txt. Потом по поводу неинтересности FindNext: я знаю как минимум две тяжеловесных софтины, в которых используется два ключа с одним Devid
---
Dr0x 03.03.2005 14:58:12
Цель этой статьи - показаь _один_ из подходов к проблеме, что и было сделано.
По поводу всего остального - смотри ветку форума
---
Anatolii 03.03.2005 17:19:27
Зачем так через жопу извращаться?
sory for Russian_English all Soft working
cherez OS budet Cracket
Potomu Chto OS otkrita for prog and Crack it
---

Материалы находятся на сайте https://exelab.ru



Оригинальный DVD-ROM: eXeL@B DVD !


Вы находитесь на EXELAB.rU
Проект ReactOS