Блог → О выполнении защищённых программ и работы с данными

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

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

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

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

При этом возможны два основных случая:
- файл не содержит внутренних оверлеев;
- файл содержит внутренние оверлеи.

Рассмотрим первый случай. Для обеспечения корректной совместной работы дополняемого модуля (ДМ) и основного модуля (ОМ) необходимо:
- сформировать ДМ в виде .СОМ- или .BIN-файла;
- поместить ДМ в конец .ЕХЕ-файла, к которому вы дополняете заданные функции;
- в заголовке (header) основного модуля исправить поля: (1) длину загружаемой части в 512-байтных страницах, и (2) точки входа CS и IP (чтобы они указывали на ДМ);
- в ДМ обеспечить возврат на старую точку входа ОМ.

Допустим: lo - длина файла ОМ, ld - длина файла ДМ. В заголовке ОМ: hd - длина заголовка ОМ в шестнадцатибайтных параграфах, рс - длина загружаемой части в страницах вместе с заголовком, pr - длина неполной последней страницы в байтах, o_cs - относительный сегмент старого CS, o_ip - смещение старой точки входа. Обозначим также: ncs - относительный сегмент нового CS, n_ip - смещение новой точки входа. Тогда, в оперативную память загружается фрагмент кода длиной



Признаком отсутствия оверлеев служит выполнение соотношения



Следовательно, после дополнения ДМ к ОМ, pc и pr в заголовке ОМ изменятся (где: m/n - целая часть от деления m на n, а m%т - остаток от деления m на n):



При этом, также изменится и точка входа:



Для удобства можно установить n_ip = 0, соответственно выровняв в файле начало ДМ на границу параграфа. Далее необходимо обеспечить возврат к старой точке входа в конце ДМ. Этого можно добиться, используя команду IRET:

pushf
mov ax,cs ;o_cs — относительный сегмент старого CS
sub ax,n_cs-o_cs
push ax

;o_ip — смещение старой точки входа
push ах
mov ax,o_ip
iret


Поясню данный фрагмент немного подробнее. Пусть код загруженной в оперативную память программы начинается с сегмента SEG. Учитывая, что в заголовке .ЕХЕ-файла указан относительный сегмент, получим реальный сегмент точки входа (то есть сегмент в оперативной памяти): SEG + o_cs, смещение o_ip. После присоединения ДМ сегмент точки входа изменится и станет равным: SEG + n_cs, смещение n ip. Тогда для возврата к старой точке входа необходимо:
- взять содержимое регистра CS (реальный сегмент);
- вычесть: CS - (n_cs - o_cs) = SEG + n_cs - n_cs + o_cs = SEG + o_cs;
- получим реальный сегмент старой точки входа, таким образом, будет обеспечен возврат к точке входа ОМ (старой точке входа).

.MODEL TINY
.CODE
org l00h
s:

; Сохранение регистров
pushf
push es
push bp
push si
push di
push ds

; Установление ds=cs
push cs
pop ds

; Вывод на экран сообщения
mov dx,offset
mes

; Это нужно только из-за org 100h
; при использовании org 0 не нужно
sub dx,100h
mov ah,09h
int 21h

; Восстановление регистров
pop ds
pop di
pop si
pop bp
pop es

mov ax,cs
; Установление точки возврата
; должно быть заранее занесено n_sc-o_cs
sub ах,0
push ах

; должно быть заранее занесено старое содержание IP (o_ip)
mov ах,0
push ах
iret

mes db "Добрый день!$"

end s


После компиляции, для которой необходимо дать команды (мы получим ДМ, представляющий из себя COM-файл, длиной 52 байта):



Его содержание:



Для присоединения ДМ к ЕХЕ-файлам я предлагаю воспользоваться следующей программой:

// SETDM.C
#include <stdio.h>
#include <io.h>
main (int argc,char *argv[]) {
FILE * out;

// Сигнатура .ЕХЕ-файла
unsigned int sign;

// Длина загружаемой части в страницах
unsigned int h_l;

// Новое значение IP
unsigned int r_ip;

// Старое значение IP
unsigned int o_ip;

// Новое значение CS
unsigned int r_cs;

// Старое значение CS
unsigned int o_cs;

// Размер заголовка в параграфах
unsigned int h_d;

// Число байт в последней странице
unsigned int PartPag;

// l - длина дополнения, li - длина защищаемого файла
long l,li,lp;
int i,seg,ofs;

// Содержание ДМ в шестнадцатиричном виде
static unsigned int dop[26]={
0x9c90,0x5506,0x5756,0x0ele,Oxbalf,0x0124,0xea81,0x0100,
0x09b4, 0x21cd,0x5fIf,0x5d5e,0x8c07,0x2dc8,0x0000,0xb850,
0x0000,0xcf50, 0x2020,0xae84,OxeOal,0xa9eb,0xa420,0xada5,
0x20ec,0x2421 };

int ost;
char pr=0;

if(argc!=2}{
printf ("Формат: SETDM[.EXE] имя .ЕХЕ-файла");
printf ("Пример: SETDM[.EXE] NDD.EXE");
return(0);
}

out=fopen(argv[l],"r+b");
if(out==NULL){
printf ("He найдено файла с именем %s.",argv[1]);
return(0);
}

li=filelength(fileno(out));
fseek(out,01,0);
fread(&sign,sizeof(int),1,out); if(sign!=0x5A4D){
printf ("Указанный файл не является .ЕХЕ-файлом");
return(0);
}

fseek(out,0x41,0);
fread(&h_l,sizeof(int),l,out);
fseek(out,0x81,0);
fread(&h_d,sizeof(int),1,out);
fseek(out,0x141,0);
fread(&o_ip,sizeof(int),1,out);
fseek(out,0x161,0);
fread(&o_cs,sizeof(int),1,out);

// Длина ДМ
l=521;
ost=0;

// Длина программы без заголовка
lp=li-16*h_d;

// Вычисление нового сегмента
seg=lp/0xl0;

// Вычисление нового смещения
ofs=lp%0xl0;

// Нахождение разницы до длины целого параграфа
if(ofs!=0){
ost=16-ofs;
seg++;
ofs=0;
}

// Новая длина в страницах
h_l=(li+1)/512+1;

// Новая длина последней страницы
PartPag=(li+l)%512;
r_ip=ofs;
r_cs=seg;

// Относительный сегмент n_cs - o_cs
seg=seg-o_cs;
fseek(out,0x21,0);

fwrite (ScPartPag,sizeof(int),1,out);
fseek(out,0x41,0);
fwrite(&h_l,sizeof(int),1,out);
fseek(out,0x141,0);
fwrite(&r_ip,sizeof(int),l,out);
fseek(out,0x161,0);
fwrite(&r_cs,sizeof(int),1,out);

// Подготовка к записи в конец файла ОМ
fseek(out,li,0);

// Установка точки возврата в основную программу
dop[14]=seg;
dop[16]=o_ip;

// Выравнивание в файле на границу параграфа
if(ost!=0) {
for(i=0;icost;i++) }

fwrite(&pr,sizeof(char),1,out);
fwrite(dop,sizeof(int),26,out);
fclose(out);
return(0);


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



При этом возможны два варианта, когда:
- оверлейная программа целиком может быть загружена в оперативную память (lo < свободного объема оперативной памяти);
- оверлейная программа превосходит емкость памяти (lo > 640 килобайт).

В первом случае можно применить тот же подход, как если бы программа не была оверлейной. Правда в этом случае теряется смысл "оверлейности". Кроме того, итоговый модуль может не поместиться в оперативную память. Таким образом работают системы защиты, которые "могут защищать оверлейные модули" - это такие программы, как: Super Guard, Protect-Sweet, Zond- 3.0.

Во втором случае, когда программа больше объёма оперативной памяти, можно попытаться "расширить" программу на длину ДМ в месте окончания загружаемой части ОМ. Дополненная таким образом программа будет работать, если считывание оверлея происходит от конца файла или от конца загружаемой части. В ином случае, итоговый модуль не будет работоспособен. Необходимо отметить, что для оверлеев стандарта Borland такой подход иногда не проходит!