Безопасность в Дельфи

       

Проект "АнтиКрэковые Мучения"


Антиотладочные приемы
Раздел "Анти крэковые мучения"
Шифрование кода. Часть I
Часть II
Часть III
Часть VI
Дмитрий Логинов ,
дата публикации 10.08.00

Don't try so hard.

Привет народ,
Последнее предисловие.
Уж потерпите последний раз. Спасибо всем за письма и отзывы про АКМ. Теперь подведу итоги форума. Во-первых:Я не причислял, не причисляю, и не буду причислять себя к хакерам. Не важно верите ли вы тому, что у меня есть опыт. Нет никого повода гордится и хвастаться есть у меня, "послужной" список или нет. При сегодняшнем инструментарии нужно немного сноровки и вы можете сами анализировать тот или иной прием защиты. При этом не обязательно причислять себя к хакерам. А вот если вы не можете что-то сломать, проще говоря, поставить себя на место хакера, то очень трудно написать качественную защиту. И я не являюсь последней инстанцией по этому вопросу. Так уж сложилось, что согласился что-то написать на эту тему и призывал и призываю, чтобы другие писали. Во-вторых:Все когда-нибудь кончается. АКМ тоже, но не сейчас. Сейчас начался очень активный период работы над нашим проектом. Поэтому частота появления АКМ сильно уменьшится. В-третьих:Прошу прощения, что надоедал своими "отвлечениями". Хотел рассказывать об истории появления тех или иных вещей. Чтобы вы могли сами придумывать. Видимо это делал косолапо. Поэтому...

Так как мы все тут крутые профи в том, что пишется и ломается то:

Правило номер 1Я больше ни о чем не буду рассуждать и говорить не в тему. Постараюсь спасти вас от своих "художественных" вставок. Правило номер 2: Больше ничего не буду писать о своем опыте ломки. И уж точно не буду описывать способы ломки. Если кого интересуют эти самые приемы могу отослать к книге замечательного человека Криса Касперски "Техника и философия хакерских атак". Если же нет этой возможности пишите мне лично. Правило номер 3: Не буду описывать основы приемов, которые я буду приводить, ТОЛЬКО сами приемы. Т.к. эти основы можно найти в книгах, хелпах по винде и моих прошлых АКМ. Т.е. никаких описаний систем. Если будут проблемы с поиском инфы пишите мне. Правило номер 4: Т.к. ВСЕ ПРИВОДИМЫЕ МНОЙ ПРИЕМЫ ломаемы и обходимы, то постараюсь вообще не оценивать сложность этих приемов. Т.к. сложность - понятие личного опыта. Т.е. буду приводить все приемы даже САМЫЕ БАНАЛЬНЫЕ и простые. Примерами буду снабжать те приемы, которые без примеров НЕ ПОНЯТНЫ.

Вот вроде бы все. Не буду вас больше раздражать и оставляю вас наедине с сухой теорией. Пока.

ТЕМА "Старые песни о главном"
Я против слишком большого внимания к проблеме защиты программ. Тем более, что это проблема не разработчиков ПО. В обсуждении будущих тем АКМ я встречал то, что мы уже обсуждали. Например, как ограничить кол-во инсталляций, или как ограничить кол-во запусков инсталлятора? Как программные, так и аппаратные способы сделать это имеют один недостаток: ИХ ЛЕГКО ПЕРЕХВАТИТЬ. Т.е. используя лишь программу мониторинга (RegMON, FileMON, PortMON, VXDmon etc), можно свести, без особого изврата, все старания программистов на нет. Наиболее удачным с моей точки зрения является организационное решение. Я его встречал несколько раз. Делается с.о. Инсталлятор не продается, с фирмой заключается договор, приходит человек, устанавливает ПО и уходит. Тоже самое с обновлениями. В крайнем случае они закачиваются по почте, но после предварительного звонка в фирму-поставщик. Понимаю, это не всех устраивает, т.к. требует БОльших действий от БОльшего кол-ва людей. Но это окупается. Вам выбирать. Относительно шаровар. Не стоит в качестве шароварных свойств использовать ограничение запусков, времени использования, disable некоторых пунктов меню. Поверьте, лучше полностью написать прогу, которая действительно не полностью функциональна. Например, как я писал в прошлом АКМ. Да есть еще масса способов. Относительно верности паскалю. Я постараюсь написать все на паскале. Я вас понимаю. Мне тоже нравится паскаль. Красивый процедурный язык высокого уровня. Но как насчет того, чтобы разместить класс в стеке (тоже интересный антиотладочны прием)? Как насчет того, чтобы участок памяти, где лежит класс, декодировался "на лету", при обращении к классу? Как насчет того, чтобы написать процедурку со своими параметрами вызова, а не стандартный паскалевский или сишный? Как разделить выделение памяти под класс и вызов его конструктора? Как сделать автоматическое освобождение памяти класса после выхода из болка BEGIN...END? Вот такой он великий и могучий паскаль. Если вам этого не надо, то, конечно, не стоит думать о таких вещах как С++. Только не стоит воспринимать это как ОБЕСЦЕНИВАНИЕ паскаля. Ничего кроме флейма не получится. Не надо спорить, хорошо? Не буду я вам надоедать этим СИ. И ВООБЩЕ НИЧТО ТАК НЕ УМЕНЬШАЕТ ПОНИМАНИЕ, КАК СТРЕМЛЕНИЕ БЫТЬ ПРАВЫМ. Пусть каждый останется у своего разбитого корыта. Я буду мучиться со своим "супернепонятным" С++. Вы со своим. Вы хорошие парни и я не хочу быть плохим. Просто я люблю С++. И вы молодцы, раз остаетесь такими принципиальными. Самое главное, чтобы это не было вам во вред.
И еще опять всплыла тема аппаратной защиты. Ну что же...
ТЕМА "Аппаратные ключи"
  1. Существуют несколько видов ключей, основанных на различной электронной базе. EEPROM, ASIC и на базе микропроцессоров. Я цитирую терминологию самих производителей. EEPROM - самые дешевые, сильно конфликтующие с ПУ и легко эмулируемые, как аппаратно, так и программно. Единственной защитой для них служит нетривиальный программный интерфейс. ASIC - содержат в себе сложно табулируемую функцию. Иногда это большое кол-во алгоритмов шифрования. Средняя цена, почти невозможно проэмулировать аппаратно. Программная эмуляция справедлива только для одной партии ключей. Микропроцессорные ключи - самые дорогие и просто содержат в себе либо часть алгоритма программы, либо зашифрованные разными алгоритмами данные. Протокол обмена с такими ключами всегда шифрованный. Чаще используется на серверах.
  2. Ничего не имею против аппаратной защиты. Более того, отдаю ей предпочтение. НО! Любую идею можно загадить. Т.е. все сильно зависит от SDK, которое разрабатывает поставщик ключа. Иногда, это служит примером КАК НЕ НАДО РАБОТАТЬ С КЛЮЧОМ. Второе НО. Рассмотрим, например, ASIC ключ производства ALLADIN, HASP версия R3. Защищаемые программы 1С Бухгалтерия 7.0+ и иже, также Компас, RS Банк, БЕСТ и т.д. Кажется все вроде хорошо. Но вот беда, стоит хакеру достаточно подкованному в анализе такого рода защит получить инсталлятор той или иной проги. Он вскроет ее, в смысле напишет эмулятор ключа для этой и только этой партии ключей и положит это на болванку. Маленькая оговорка. Если вы когда-нибудь читали чужие проги, если вам приходилось работать в групповом проекте, то вам должно быть знакомо, как может быть узнаваем стиль программирования. Иногда для этого нужно много времени, иногда меньше. Но это знание приходит. Так вот, также с текстами ломаемых программ. Точно также привыкаешь к манере написания и "антиотладочных фокусов" ALLADIN(ключ HASP) или Rainbow(ключ Santinel). Поэтому если хакер уже ломал что-то от одной фирмы, то он привык к стилю. Ему уже составит меньших усилий проанализировать "новый" код этой фирмы. Итак, он пишет эмулятор и кладет на болванку. И, в общем-то, неважно привязан инсталлятор к этому ключу или нет. Он поставляет инсталлятор вместе с эмулятором. Иногда можно поступить проще. Например, если программа поддерживает младшие версии ключа, то и старые эмуляторы могут подойти. Благо SDK для таких вещей найти можно. Вот так гадко и не хорошо.
    Поймите, неважно, где хранится ключ. Во внешней микросхеме или в голове покупателя. Помните, покупателем может оказаться и хакер. ВСЕГДА ДОСТАТОЧНО ОДНОГО ЭКЗЕМПЛЯРА ИНСТАЛЯТОРА, чтобы начать штамповку болванок и гнать их за 65 рублей. Я, наверно, плохо объясняю? Ну, тогда сами придумайте себе причину, почему АКМ больше не будет писать про АППАРАТНУЮ ЗАЩИТУ.
ТЕМА "Антиотладочные приемы или слышать об этом больше не хочу"
  1. Флаг трассировки. Суть: сброс флага TF в регистре состояний процессора посредством команд ассемблера POPF или IRET. Изжил себя увеличением возможностей процессора к отладке, а также понятием "Виртуальная машина".
  2. Прерывания. Суть: не дать использовать отладчикам прерывания INT 1 и INT 3. Относительно INT 1 смотри предыдущий прием. INT 3 генерится процессором, если в теле программы встречается опкод 0xCC. Распространенные способы:
    • Вешание декодера кода или данных на INT 3.
    • Постоянный подсчет CRC суммы процедуры, чтобы не вставлялся 0xCC.
    • Разброс по программе большого количества 0xCC.
    • Чтение адресов обработчиков прерываний и хранение там каких-нить важных переменных. mov ah, 25h mov al, Int_Number mov dx, offset New_Int_Routine int 21h
    • Чтение IDT и выполнение вышеперечисленных фокусов Типа 'Stone's Win32 Winice Detector': sidt fword ptr pIDT ; Вытащить IDT mov eax, dword ptr [pIDT+2] ; eax -> IDT add eax,8 ; eax -> int 1 "vector" mov ebx, [eax] ; ebx == int 1 "vector" add eax, 16 ; eax -> int 3 "vector" mov eax, [eax] ; eax == int 3 "vector" and eax, 0ffffh ; не использовать селектор and ebx, 0ffffh ; а учитвать только младшее слово sub eax, ebx ; при подсчете разницы cmp eax, 01eh ; и для Siw95 3.0 она равна 1eh Кстати, этот прием был очень распространен, т.к. применим к Ring3. Способы не используются, т.к.
      а) нельзя сделать динамическую расшифровку сегмента кода.
      б) кракеры почти не используют такой метод установки брейка.
      в) АЙС устанавливает ловушку(HOOK) на такие штуки. (bpint или bpm)
    • Блокировка всех аппартных прерываний. Данный метод реализуется несколькими способами:
      а) сброс флага FI.
      б) программирование контроллера прерываний.
      в) программирование порта клавы и др. устройств на запрет аппаратного прерывания.

      Полностью пропали после того, как:

      а) Этот код требует переноса себя в VXD (см. пред.АКМ).
      б) SoftICE начал эмулировать работу с портами (см. пред.АКМ).
    • Сброс отладочных регистров защищенного режима. dr7=0x700 если ICE присутствует. SoftICE перехватывает запись и чтение в/из регистров DR0-DR7. И просто не дает изменить содержимое этих регистров или возвращает 0 или 0х400. Поэтому этот прием не используется, потому что такой код должен быть перенесен в VXD, имеет постоянную сигнатуру и был уже 1.5 года назад предусмотрен в АЙСовых патчах(ловушках).
    Итак - все современные приемы борьбы с отладкой сводятся к обнаружению и уничтожению SoftICE в системе. Сами оценивайте недостатки и преимущества. Да, маленький комент. Когда я буду писать метод устранения, то буду иметь прежде всего обнаружение, т.е. брейк.

    Прием N1: ФАЙЛОВЫЙ.

    а) FindFirst и FindNext-ом ищется loader32 или sivwid.386; Устраняется перехватом этих функций.
    б) Через VWIN32.vxd читаем напрямую сектора с диска или кластеры и исчем там строку "tIce" или "SIWVID" или "WINICE" или "NTICE". Разновидность этого извращенного способа - использование библиотек, реализующих прямой доступ к файлам через FAT. Пример доступа ищи на Torry. Если не найдете - выложу на КОРОЛЕВСТВЕ. Устраняется все той же ловушкой на DeviceIOControl.
    в) Поиск "подозрительных" записей в регистри. В частности: HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Uninstall\SoftICE HKEY_LOCAL_MACHINE\Software\NuMega\SoftICE HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\App Paths\Loader32.Exe Устраняется ловушкой на операции с Registry.
    г) Вариации на предыдущий способ. Работа с регистри через файловые операции или через сектора. Устраняется теми же способами.

    Прием N2: Девайсный.

    а) Этот способ приведен в FrogICE - патче к SoftIce. xor di,di mov es,di mov ax, 1684h mov bx, 0202h ; VxD ID для драйвера "winice" int 2Fh mov ax, es ; ES:DI -> VxD API entry point add ax, di test ax,ax jnz SoftICE_Detected //----------------------------------------- xor di,di mov es,di mov ax, 1684h mov bx, 7a5fh ; VxD ID для драйвера "SIWVID" int 2Fh mov ax, es ; ES:DI -> VxD API entry point add ax, di test ax,ax jnz SoftICE_Detected Я этот способ встречал, но это было под win95(не OSR) и для 3ей версии SoftICE. У меня же установлен 4.0.5(Build 334). Попробовал этот способ на win9x OSR2, но это приводит к синим экранам. Но ID остались прежними, вообще запомните эти цифры. Для любопытных ниже привожу стек "выхода" на обвал синих экранов через вышеуказанные фрагменты кода. Хотя, конечно, никакой это не стек это history:
    - После проверки функций (5300,53001,53002,4001,4002,4021,4022,4023,4026,4027) попадаем на INT 30H;V86MMGR(01)+0658
    -Allocate_PM_Call_Back V86MMGR_Get_Version /////////////////// Simulate_Far_Call Allocate_PM_Call_Back VDD_DO_Phisical_IO /////////////////// Simulate_Far_Call Allocate_PM_Call_Back End_Reentrant_Execution Simulate_IRet Build_Int_Stack_Frame /////////////////// Синий экран б) Способ аналогичный предыдущему, но не для Ring3, а для Ring0. Т.е. для тех кто знает, что такое DDK. //---Ring0 only---------- VMMCall Test_Debug_Installed je not_installed //------ИЛИ---------------- //-----Ring0 only---------- mov eax, Device_ID ; VxD ID -> 202h для SICE или 7a5Fh для SIWVID mov edi, Device_Name ; можно указать символьное имя устройства, ; если же задан ID, то можно пустую строку. VMMCall Get_DDB mov [DDB], ecx ; ecx=DDB или 0 если VxD нету Небольшой комментарий для чистых паскалистов. Эти способы вам не светят. Т.к. для того, чтобы его реализовать нужно установить DDK. Соответственно, хидера для сей и инклудники для асма. Плюс еще некоторые тулзы и библиотеки. А VMMCall это макрос, а не "вызов" библиотечной функции. Он транслируется в INT 20H + маска адреса вызова. (См. DDK). оба способа устранаются bpx Get_DDB if ax==0202 || ax==7a5fh в) MeltICE - это официальное название сл. метода: CreateFile('\\.\SICE',...) //для win32 или _lopen('\\.\SICE',0) //для win16 Естественно, имя может быть либо SICE, либо SIVW, либо NTICE. Если открыть VXD удалось, то значит гад сидит в памяти. Что происходит (см.FrogICE): Фактически, после вызова CreateFileA мы попадаем в Kernel32!ORD_0001, эта затычка эмулирует VxDCall, она нас приводит к функции vxd-шки VWIN32 под названием _VWIN32_ReleaseWin32Mutex и затем мы получаем список DDB и ищем что задано. Она не грузит vxd, а просто посылает им DIOC_OPEN и DIOC_CLOSEHANDLE сообщения и находит как динамически так и не динамически загруженные VXD.
    Отсюда способ устранения:
    BPX CreateFileA if *(esp->4+4)=='SICE' || *(esp->4+4)=='SIWV' || *(esp->4+4)=='NTIC' или BPINT 30 if eax==002A001F && (*edi=='SICE' || *edi=='SIWV') ; очень медленно т.к. обращение к INT 30 и INT 20 происходит очень часто или BPINT 30 if (*edi=='SICE' || *edi=='SIWV') или BPX KERNEL32!ORD_0001 if *edi=='SICE' || *edi=='SIWV' или BPX VMM_GetDDBList if eax->3=='SICE' || eax->3=='SIWV' ;самый быстрый

    Прием N3: Интерфейсные прерывания.

    Так или иначе ICE использует для организации внутреннего API сл. прерывания: int 3 int 41h int 68h. Вообще-то АЙС использует еще и int 1Ah. Но я не буду ничего говорить об этом. Т.к. данное об этом я вытащил, просмотрев SIWVID.386. Это может оказаться не справедливым для других версий АЙСА или для НТ.
    Давайте посмотрим, что написано в FrogICE по поводу 3ех предыдущих прерываний. Кстати, если хотите узнать о прерываниях вообще воспользуйтесь командой SIDT. Итак:

    "ИСПОЛЬЗОВАНИЕ INT 3 ДЛЯ ИНТЕРФЕЙСА С BoundsChecker" (редко используется)

    function IceCheck : boolean; asm push ebp mov ebp, 04243484Bh ; 'BCHK' mov ax, 4 int 3 xor ax, 4 pop ebp end; //.... if IceCheck then ShowMessage('SoftICE detected');

    "ИСПОЛЬЗОВАНИЕ INT 3 ДЛЯ РАБОТЫ С КОМАНДНОЙ СТРОКОЙ SoftICE" (очень часто)

    Впервые было замечено в защите HASPа еще под ДОСкой. Посмотрим описаловку: //функции -AX = 0910h (Display string in SIce windows) -AX = 0911h (Execute SIce commands -command is displayed is ds:dx) -AX = 0912h (Get breakpoint infos) -AX = 0913h (Set Sice breakpoints) -AX = 0914h (Remove SIce breakoints) //обязаловка -SI = 4647h -DI = 4A4Dh INT 3 Например, в FrogICE приводится дамп из HASPINST.EXE старой версии, где выполняются следующий набор команд SoftICE "LDT, IDT, GDT, TSS, RS, HBOOT". Как видите в конце стоит банальная перезагрузка. Так что, достаточно написать волшебные SI,DI,AX=0911,DX=СТРОКА и INT 3. То случится страшное, если конечно за SoftICE-ом сидит лопух.

    "МАГИЧЕСКОЕ ПРЕРЫВАНИЕ"

    Если вызвать INT 41H с функцией AX=4Fh, то в АХ запишется число равное 0F386Н в случае присутсвтия какого-нить системного дебугера. Только не спешите писать следующую функцию: function IceCheck : boolean; asm mov ax,4fh int 41h cmp ax,0F386h jz @@End xor ax,ax @@End: end; Такие простушки АЙСОМ давно мониторятся. Поэтому очень присоветую следующий подход, позволяющий СИЛЬНО ОСЛОЖНИТЬ ЖИЗНЬ хакера при детектировании IceChecker-а. ПРАВИЛЬНЫЙ ПОДХОД:
  3. 1) сохранить старый обработчик INT 41H (умоляю не действуйте напрямую в памяти - только через int 21H или IDT) 2) установить новый обработчик 3) сей обработчик должен делать что-то жизненно-важное для проги и как следствие менять регистры. И в проге появляется масса INT 41H 4) Если ICE загружен, то он постарается не дать поставить новый обработчик. Метод устранения. Поставить bpint или bpx exec_int if ax==41, узнать что делает защита, выйти, запустить какой-нить HexEditor и поставить везде не int 41H, а int 42H, и танцуем джигу на костях защиты.

    "ВТОРОЕ МАГИЧЕСКОЕ ПРЕРЫВАНИЕ"

    Брат близнец предыдущего INT 41H, это INT 68H и тоже число в АХ=0F386H. Все что можно сказать про INT 68H - сказано уже в разделе про INT 41H.

    "ОТСЕБЯТИНА"

    Ну вот вроде все методы "защит" известные ICE. Он их легко обнаруживает и нейтрализует. Работа происходит в "полуавтоматическом" режиме. Можно было бы на этом закончить. Но я приведу один запрещенный прием. НИКОГДА НЕ ИСПОЛЬЗУЙТЕ ЭТОТ МЕТОД. Этот метод был придуман мной, вернее пришел мне в голову самостоятельно. Я не встречал ссылок и данных на подобную методу. Но я очень даже допускаю, что такой метод уже где-то существует и какой-нить параноик использует его. Почему я его НЕ РЕКОМЕНДУЮ ИСПОЛЬЗОВАТЬ? Придется вам, зажав зубы, послушать вступление, кому не в терпеж может сам копаться в сырцах. Для терпеливых. Этот метод не универсален, т.е. он только для win95 и его также можно обнаружить, но обнаружить его будет сложней, чем все другие методы. Значит, если вы не боитесь, что ваша программа станет привязана к win95 и не будет работать на др. системах, то слушайте. Выше адресного пространства процесса (80000000Н) находится код системы, кеш и VXD. Если быть конкретным, то для win95 это адреса с 0С0000000H по 0С0E00000H - тут живут "статичные" VXD. К их числу принадлежит и ICE. Дальше не так интересно, но с 0С0E00000H по 0СF000000H живут динамически подгружаемые VXD. Теперь самое главное надеюсь вы узнаете: struct VxD_Desc_Block { ULONG DDB_Next; /* VMM RESERVED FIELD */ USHORT DDB_SDK_Version; /* INIT RESERVED FIELD */ USHORT DDB_Req_Device_Number; /* INIT */ UCHAR DDB_Dev_Major_Version; /* INIT Major device number */ UCHAR DDB_Dev_Minor_Version; /* INIT Minor device number */ USHORT DDB_Flags; /* INIT for init calls complete */ UCHAR DDB_Name[8]; /* AINIT Device name */ ULONG DDB_Init_Order; /* INIT */ ULONG DDB_Control_Proc; /* Offset of control procedure */ ULONG DDB_V86_API_Proc; /* INIT Offset of API procedure */ ULONG DDB_PM_API_Proc; /* INIT Offset of API procedure */ ULONG DDB_V86_API_CSIP; /* INIT CS:IP of API entry point */ ULONG DDB_PM_API_CSIP; /* INIT CS:IP of API entry point */ ULONG DDB_Reference_Data; /* Reference data from real mode */ ULONG DDB_Service_Table_Ptr; /* INIT Pointer to service table */ ULONG DDB_Service_Table_Size; /* INIT Number of services */ ULONG DDB_Win32_Service_Table; /* INIT Pointer to Win32 services */ ULONG DDB_Prev; /* INIT Ptr to prev 4.0 DDB */ ULONG DDB_Size; /* INIT Reserved */ ULONG DDB_Reserved1; /* INIT Reserved */ ULONG DDB_Reserved2; /* INIT Reserved */ ULONG DDB_Reserved3; /* INIT Reserved */ }; Правильно. Это заголовок VXD. Все что нам нужно знать - это три последних поля и имя. Да, теперь остается только просканировать область VXD и найти "подозрительный". На winNT этот способ доступен только на уровне Ring0, т.е. вам придется освоить Си. Да и еще, вы должны знать на что накалывайте хакера. КОМАНДА BPR - брейк на операцию с участком памяти нельзя поставить на область системного стека и выше, т.е. область VXD. Т.о. BPR нельзя использовать для отлова к обращению к VXD. КОМАНДА BPM - аппаратный брейк на конкретный адрес. Также не может быть установлен на область выше границ процесса, "большой восьмерки". Многие безобидные и обдиные команды АЙСА не работают в этой области памяти. Что может навести на мысль - использовать незанятые места в этих областях. Да и, Единственная, VXD терпимая операция это BPX, но вентили девайсов мы не используем. Поэтому хакеру придется запускать IDAPro и сканить константу 0С0000000H. Но не факт, что мы с нее начнем и не факт, что мы ее будем хранить, а не генерить. Можно не искать резервные байты, можно искать ключевые строчки для АЙСА по области VXD. Да вот, проблема. Не советую пользоваться VirtualQuery. Это раз. Кратность 64К адреса страницы - на это винда сама для себя положила. Поэтому в этом случае придется стряпать SEH на Access Violation, тьфу в смысле try except. Можете импровизировать. Это сильно осложняет жизнь хакеру. Но приходится платить несовместимостью. И одним из способов устранения будет... А вот не скажу! Жадный я стал и неистерпимо болен интриганством. На самом деле, если вы читали прошлые АКМ, то знаете этот способ. Правильный же ответ я сообщу в следующем номере. Все.

    Скачать проект Melt.Zip (4 K)
    Протестирован в D4 и D5. Владельцы младших версий должны будут внести некоторые изменения или написать автору.

    Дмитрий Логинов. 10 августа 2000г.




  4. Содержание  Назад  Вперед