ZX-Ревю 1994 №2 1993 г.

Возвращаясь к напечатанному - защита программ от несанкционированного копирования.


защита программ от несанкционированного копирования

© Луппов Г.Б., г. Киров.

В "ZX-РЕВЮ" № 7-8 за 1993 год я уже поднимал вопросы защиты программ, и так как тема заинтересовала многих читателей, то сейчас я предлагаю Вашему вниманию более взвешенный и обдуманный материал, отражающий мой взгляд на данную проблему. Материал дан в общеописательном виде. Рассматриваются идеи, а не конкретные реализации. Кто захочет - сможет сам реализовать эти идеи в своих программах.

Защита программ - привилегия не только компьютеров фирмы IBM. Эта задача актуальна для многих БПЭВМ

- там, где затрагиваются коммерческие интересы. Автор программы должен получить за свой труд. Это понятно. Иначе поток новых разработок потихоньку пропадет. Кто должен оплатить труд автора? Видимо, коммерческая фирма, специализирующаяся на продаже программного обеспечения. Это удобно и автору, т.к. позволяет не отвлекаться на многочисленные мелкие хлопоты, а также гарантирует гонорар за программу. Фирма, тиражирующая авторскую разработку, должна получить прибыль, которая зависит от стоимости и количества проданных копий. Число проданных копий зависит от степени защищенности разработки от дублирования, а также от "честности" покупателей. Но, так как на честность в наше время и в нашей стране полагаться было бы по меньшей мере неразумно, то особое внимание нужно обратить на защиту от копирования. Но защитить программу - дело достаточно сложное, хотя и возможное. Особенно проблематично защитить кассетную версию программы. Даже, если Вам это удалось, то не забудьте о качестве советской аудиотехники, о старении пленки, о возможных дефектах носителя, об определенном неудобстве для пользователя. Гораздо удобнее защита с использованием ключевой дискеты. Именно она в настоящее время широко используется IBM. Вероятно и на "Спектруме" возможно создание систем защиты с созданием ключевых дискет (имеются ввиду программные комплексы типа "Cerberus" на IBM).

Прежде, чем перейти дальше, определим направление защиты. Защищать можно:

1) данную копию программы, т.е. дискету или кассету. Программа будет работать только, если запущена с данного носителя независимо от пользователя и особенностей его компьютера;

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

3) гибридный вариант - защищенная копия программы + "электронный ключ".

1. Внутренняя защита программ.

Если у Вашей программы можно в любой момент перехватить управление, сделать снимок "памяти", посмотреть, как и что работает, а затем снова продолжить выполнение программы, то никакая внешняя защита Вам уже не поможет. Нет никакого смысла заниматься защитой в этом случае. Для дисковых Синклер-компьютеров технология снятия защиты через Magic-key и получения затем рабочей кассетной версии программы давно разработана и отлажена. Учтите это при разработке защиты.

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

При разработке внутренней защиты в первую очередь уделите внимание существующим программным и аппаратным методам снятия памяти. Здесь нужно выяснить, что нарушается или теряется при использовании этих методов, и строить систему защиты на основе этих нюансов. Например, может портиться содержимое некоторых ячеек ОЗУ. Тогда имеет прямой смысл разместить в этих ячейках особо важные для работы программы данные, любое изменение которых блокирует выполнение программы. Может портиться или теряться содержимое некоторых портов

- храните данные в этих портах или контролируйте их содержимое. Могут изменяться регистры микропроцессора -тем лучше! Используйте этот момент. Если портиться содержимое счетчика команд, то у взломщика встанет проблема определения адреса запуска вспомогательной программы. Если портится содержимое регистра стека или же самого стека, то можно подвязаться по этим значениям. При попытке взлома может измениться состояние прерываний запрещены/разрешены. Кстати, видимо из-за этого некоторые программы после Magic-key не работают.

Маловероятно, чтобы при "снятии" памяти, ничего не нарушалось. Но если это так, то защитить кассетную версию программы практически нельзя. Можно попытаться затруднить взлом, если на каждом уровне подгружать новый участок программы. Но взломщик может снять программу столько раз, сколько имеется уровней (хотя это и не очень удобно). Можно вместе с программой поставлять блочок с ПЗУ, подключаемый через разъем для принтера. Такая система защиты называется "электронный ключ". Вместо ПЗУ, содержимое которого легко скопировать, в таком ключе лучше использовать ПЛМ. Но тут тоже есть свои проблемы: технологичность, стоимость, степень защищенности ключа и др. Можно, наконец, поставлять инсталляционную версию программы. Эта версия (изначально нерабочая) будет после запуска подвязываться к каким-либо особенностям конкретного компьютера, настраиваться на него и выгружать на ленту (диск) рабочую версию, которая будет работать только на данном компьютере, и ни на каком больше. Это довольно просто сделать на IBM, но можно ли это сделать на "Спектруме"?

Не знаю.

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

А теперь разберемся, как сделать программу трудночитаемой для постороннего глаза.

Первый момент состоит в том, что программа в ОЗУ должна находиться в частично закодированном виде. То есть все модули (подпрограммы) в программе делятся на активные и пассивные. Активные модули работают как обычно, пассивные модули закодированы и могут находиться как в ОЗУ, так и где-то еще (диск, ПЗУ, дополнительное ОЗУ и т.д.). Перед работой пассивного модуля он загружается и декодируется с использованием каких-то ключей. После работы он или стирается из памяти или шифруется. Если взломщик "снимет" ОЗУ во время работы Вашей программы, то при дизассемблировании он будет постоянно наталкиваться на зашифрованные участки, которые сможет декодировать, только если знает ключи шифрования. Как и где хранить ключи, мы уже рассмотрели выше.

Аналогично, можно использовать неявные обращения к подпрограммам. Зачем нам нужно простое и ясное "CALL", если можно вычислить адрес обращения с помощью хитрых манипуляций, в том числе используя ключи дешифрации. Можно вообще выделить одну подпрограмму для вычисления адресов обращений. На вход такой подпрограммы будет поступать код, на основе которого подпрограмма вычислит адрес обращения, обратится по вычисленному адресу и вернет управление в точку вызова. Сочетание такого подхода с шифрацией/дешифрацией пассивных подпрограмм способна серьезно затруднить дизассемблирование программы.

Следующий момент касается контроля над целостностью программы. Как правило, при взломе "пират" старается идти легким путем - что-то изменил, запустил, посмотрел. Или же запускает программу под отладчиком. Автором должны быть приняты меры противодействия этому. Сделайте перекрестный контроль между наиболее важными участками программы. Проверять можно целостность участка программы (путем подсчета КС, сравнения с образцом, использования части команд как данных и т.д.) или же "следы" работы этого участка (наличия каких-то ключей, меток, флагов, массива данных сгенерированного участка кодов и т.д.).

С отладчиком бороться гораздо труднее, чем с дизассемблером. Но и здесь есть несколько нюансов, за которые можно зацепиться.

1. Отладчик занимает часть ОЗУ. Значит, если осуществлять контроль за памятью, можно помешать использовать отладчик. Контроль возможен в следующих формах:

- подсчет КС

- использование этой памяти под хранение данных (в статичной и динамичной формах). Необязательно хранить данные в этой области подряд. Можно - через 3, 5, 7 ... 17 и т.д. байт или же с использованием RND.

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

Стек назначается по адресу "реальный конец участка программы + глубина стека". При работе процессор последовательно выполняет команды программы, двигаясь от младших адресов к старшим. При работе в стек постоянно откладываются данные и не извлекаются оттуда. Эти данные представляют собой самогенерируемый участок программы. При этом указатель стека движется от старших адресов к младшим. В какой-то момент оба указателя встречаются, и программа плавно переходит на только что созданный участок команд. Выражение "реальный конец программы" подчеркивает, что далее могут располагаться любые данные, в том числе и просто для отвлечения внимания. Если кроме того, использовать содержимое стека как набор данных для шифрации/дешифрации или по другому назначению, то малейшее изменение области стека повлечет за собой нарушение процесса выполнения программы. (Этот способ широко используется в системах защиты на IBM.) Нужно только учесть взаимодействие с прерываниями по 38Н адресу, так как при этом прерывании меняется содержимое области стека.

3. Любой отладчик позволяет использовать два различных режима отладки: запуск программы с возвратом в отладчик по RET и установку точки прерывания. В первом случае текст программы не меняется. Отладчик запоминает в стеке адрес возврата в себя и передает управление подпрограмме. Если подпрограмма изменит адрес стека или же просто проконтролирует "а не выходит ли адрес возврата за пределы рабочей области программы", то отладчик будет блокирован. Здесь также можно определиться со стеком: стек отладчика или стек пользователя? В первом случае можно подвязаться под адрес стека, во втором - под содержимое стека. Использование обоих вариантов сразу серьезно затруднит работу с отладчиком (учтите, что это верно, если подпрограмма достаточно сложна для визуального исследования).

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

Самым шиком в борьбе с работой отладчика является "стелс-технология". Вы в курсе, что половина всего компьютерного мира (я имею в виду пользователей) трепещет перед страшными стелс-вирусами? Они знамениты тем, что после заражения очередной программы тест вируса меняется. В двух программах, зараженных одним стелс-вирусом, текст вируса абсолютно различен, хотя функциональное назначение остается прежним. Это несколько осложняет работу антивирусов, так как нельзя использовать привязку к строке стандартных для данного вируса байтов. Этот же принцип можно использовать для борьбы с отладчиком. Представьте себе подпрограмму, которая в цикле многократно изменяет саму себя, в том числе и точку выхода из подпрограммы, в том числе и адрес выхода в программу, не нарушая при этом своей работы! Здесь нельзя уже использовать отладчик в легких вариантах (выполнение подпрограммы с возвратом в отладчик и установку точек прерывания при выходе в программу), так как точки прерывания будут затерты (или вообще не будут использованы), а адрес возврата изменится.

Остается только пошаговое выполнение подпрограммы. Ну, а если цикл повторяется 200, 500, 1000 раз? И ведь нельзя этот процесс автоматизировать, так как отладчик сам не сможет догадаться, когда управление будет передано в подпрограммы. Кроме того, нужно следить, чтобы отладчик не был затерт и обнаружен. Естественно, придумать такую подпрограмму труд не из легких. Но, если Вы хотите получить нераскалываемый участок программы, то нужно поработать. Казалось бы - очень сильно закручено, больше нельзя. Но нет, можно еще более усилить защиту.

Представим ситуацию критическую: или у пирата хватило терпения пройти все циклы нашего стелс-участка, или же появился супер-отладчик, который, используя дополнительное ОЗУ, дисковод и т.д., эмулирует все доступное микропроцессору ОЗУ (64 Кб), программу выполняет в режиме интерпретации, то есть выступает в роли микропроцессора, контролирует операции с памятью и стеком (выход за определенные границы, обращения к каким-то участкам памяти и др.) и т.д. Казалось бы все, не защититься. Но, не падайте духом, автор защиты! Режим интерпретации (да и пошаговое выполнение программы) очень замедляет работу программы. Значит, контролируя время выполнения определенных подпрограмм или модулей, можно сказать, есть кто-то наверху или нет. Основной вопрос философии, что первично - сознание или материя, заменяется на основной вопрос программной защиты, что первично - наша программа или кто-то сверху, дергающий за веревочки.

Проконтролировать время достаточно просто, и на "Спектруме" это можно реализовать тремя способами:

1) музыкальный процессор имеет три независимых 16-х счетчика. Перед выполнением подпрограммы, зависящей от времени, запишите в счетчик (счетчики) какое-либо число. Счетчик тут же начнет вычитать из этого числа по единичке с определенной тактовой частотой. В конце работы программы она читает состояние счетчика и сравнивает с заданным, либо использует его косвенным образом. Более крутой вариант предполагает чтение "на лету" счетчика во время работы подпрограммы и использования этих данных каким-либо образом (для той же дешифрации, например);

2) если нет музыкального процессора, то всегда есть регистр регенерации R, состояние которого также циклически изменяется. Использовать его можно способом, аналогичным вышеописанному;

3) прерывание по адресу 38Н. Оно возникает с твердо заданной частотой и очень подходит на роль таймера. Идеально было бы иметь по этому адресу произвольную подпрограмму пользователя, но, так как в "Спектруме" по эти адресам сидит ПЗУ, то их можно использовать только косвенным образом. При прерывании происходят следующие действия:

- процессор довыполняет текущую команду! (то есть время между прерываниями равное, а вот между передачей управления по 38Н адресу зависит от времени выполнения текущей команды)

- затем он сохраняет в стеке адрес следующей команды!

- передает управление по 38Н адресу!

- подпрограмма прерываний сохраняет состояние регистров в стеке!

- затем выполняет свою задачу!

- восстанавливает регистры!

- передает управление в точку вызова прерывания.

Пункты, отмеченные "!", можно использовать для косвенной проверки результата прерываний. Вспомним вышеописанный пример дешифрации, когда указатель стека и указатель команд двигались друг другу навстречу. В какой-то момент происходит прерывание. Содержимое области стека заменяется содержимым регистров микропроцессора и адрес возврата из подпрограммы обработки прерываний! Если данные в стеке использовались для шифрации/дешифрации, то мы получим смену ключей дешифрации, причем эти ключи зависят от времени. Естественно, любой жесткий контроль со стороны отладчика приведет к замедлению работы программы и нарушению процесса ее выполнения.

Кроме всего прочего для запутывания потенциального взломщика можно использовать следующие трюки:

1. Использование редких, недокументированных команд или команд, результат которых неочевиден.

2. Использование многобайтных команд для создания вложенных друг в друга участков подпрограмм. Например, команда 8000 JMP мл.байт ст.байт с 8000Н адреса будет выглядеть как JMP, а с 8001Н адреса уже по другому. То есть при достаточно тонком программировании мы получим с 8000Н адреса одну подпрограмму с каким-то определенным назначением, а с адреса 8001Н - другую подпрограмму с другим целевым назначением, хотя они обе используют одну и ту же область памяти.

3. Аналогично, можно таким образом подобрать набор текста/данных, чтобы внутри можно было спрятать функционирующий участок кодов. Не беда, что при этом будут использоваться команды-пустышки, то есть не имеющие отношения к выполняемой задаче и не влияющие на это выполнение. Таким образом, можно защитить текстовую рекламу автора или его фирмы от коррекции.

Еще несколько слов о ключах. Ключи - это данные, которые используются, для шифрации/дешифрации, выбора управления и т.д. Они могут располагаться в ячейках ОЗУ, видео-ОЗУ, в портах (ВИ55), в регистрах микропроцессора, то есть в тех местах, которые портятся при попытке взлома или за которыми трудно проследить. Ключи нужно использовать неявно. Например, подсчитав КС, не надо сравнивать ее с образцом, лучше на основе ее значения вычислите адрес перехода. А еще лучше сохранить ее на время в какой-нибудь ячейке и проконтролировать позднее. Не задавайте явный адрес ключа, так как по нему очень легко отследить все обращения к данному ключу. Лучше адрес ключа получить серией арифметико-логических операций. Ключам, кстати, необязательно быть неизменными. Они вполне могут динамично изменяться в ходе выполнения программы. Это дополнительно усложнит задачу взломщика. Самым лучшим на данный момент ключом является дистрибутивная дискета. Но об этом далее.

2. Внешняя защита программ

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

Для использования внешней защиты достаточно поменять способ записи на МЛ: изменить кодирование бита, изменить кодирование байта, поменять формат записи на МЛ и т.д. Естественно, чтобы загрузить закодированную таким образом программу, нужно запустить ЗАГРУЗЧИК, который "знает", как и что загрузить. Естественно, сам ЗАГРУЗЧИК должен быть в стандартном для данной ПЭВМ формате записи. Чтобы по содержимому ЗАГРУЗЧИКА нельзя было узнать о формате, в котором записана основная программа, его лучше закодировать, а впереди поместить ДЕКОДЕР. ДЕКОДЕР и ЗАГРУЗЧИК могут быть совмещены. Для того, чтобы взломщик не вскрыл ДЕКОДЕР, используйте все методы, описанные в разделе 1. Если взломщик не вскроет ДЕКОДЕР с ЗАГРУЗЧИКОМ, он не сумеет снять защиту. Итак, структура защиты: ЗАГРУЗЧИК+ПРОГРАММА в нестандартном формате; ЗАГРУЗЧИК=ДЕКОДЕР+БЛОК ЗАГРУЗКИ.

Для затруднения взлома можно использовать следующие трюки:

1) скользящая смена формата записи на МГ: загружается порция информации, которая корректирует ЗАГРУЗЧИК или перехватывает его управление и настаивается на другой формат, отличный от первоначального.

2) при работе модуля загрузки должны устанавливаться определенные параметры - "следы" работы ЗАГРУЗЧИКА, которые можно проконтролировать в дальнейшем.

3) передача управления загруженной программе должна быть нестандартной и неочевидной.

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

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

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

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

3. Ключевые дискеты (чисто обзорно).

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

Рассмотрим несколько методов создания ключевой дискеты:

1. Запись информации в нестандартные участки на дискете: в служебные сектора или на технологические цилиндры (с номером за 80).

Легко обходится полным копированием всей дискеты.

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

3. Можно применять на некоторых дорожках нестандартный формат. Например, вместо 16-ти секторов по 256 байтов можно отформатировать данную дорожку на 3 сектора по 1024 байта, 2 по 512 байтов и один - 256 байтов. Можно записать нестандартный сектор с нестандартной длиной или номером. Можно играть с чередованием секторов, с межсекторной длиной, с количеством пробелов в конце дорожки и т.д.

4. Очень интересную защиту можно реализовать простой иголкой. Возьмите иголку и проткните ею дискету. Если Вы попали в сектор, то у Вас получится плохой нечитаемый сектор. Место дефекта можно определить с точностью до байта. Записав эту информацию куда-нибудь в защищенную программу, Вы сможете реализовать последующую проверку "истинности" дискеты. Программа копирования (даже плата побитового копирования на IBM) сможет повторить все, вплоть до нестандартного формата, но не сможет, грубо говоря, "ткнуть иголкой в то же место".

5. Самым шиком на Западе сейчас считается защита "плавающими битами". Она реализуется специальными аппаратными методами и в домашних условиях ее использование проблематично. Суть защиты в том, что создается бит с уровнем сигнала не 0 и не 1, а где-то между. При этом он может семь раз из десяти считаться как 1, а три раза -как 0. Таким образом, реализуется случайный статистический закон распределения, и скопировать его один к одному достаточно сложно.

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

4. Другие методы защиты.

1. К программе прилагается руководство или описание в объеме и виде, сложном для ксерокопирования. Программа же периодически запрашивает: "введите N-ое слово в J-ой строке на I-ой странице Вашего описания". Конечно, тут без руководства не поработать. Но защита эта хороша до тех пор, пока какой-нибудь упорный обладатель руководства не пройдет всю программу и не составит небольшой списочек-файл кодовых слов, который затем пойдет гулять по рукам.

Дополнительно усилить защиту можно, применив случайный запрос ключевого слова из достаточно большого справочника ключевых слов. Тогда один проход программы не сможет гарантировать знание всего списка кодов. Но с другой стороны, а почему бы не набрать все руководство?

2. Бухгалтерские программы можно подвязывать к названию организаций, в которые они проданы. Название организации присутствует в отчетной документации, и для другой фирмы такой документ не подходит. Попытка же изменить название приводит к зависанию программы.

3. Юридический метод. Оформите авторское свидетельство на свою программу. Тогда Вы сможете пригрозить судом нелегальным продавцам Вашей программы. Правда, только если эти продавцы достаточно крупные фирмы или магазины. Ребятам с толкучки Вы сделать ничего не сможете.

4. Метод предоплаты. Разошлите рекламу на свою программу с просьбой прислать определенную сумму для получения копии, но с условием, что высылать будете, только если наберется N-ое количество заказов, иначе - деньги будут возвращены. После этого все зависит от Вашей рекламы и действия ее на потенциальных покупателей.

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

* * *




СОДЕРЖАНИЕ:


  Оставте Ваш отзыв:

  НИК/ИМЯ
  ПОЧТА (шифруется)
  КОД



Темы: Игры, Программное обеспечение, Пресса, Аппаратное обеспечение, Сеть, Демосцена, Люди, Программирование

Похожие статьи:
Юмор - Как мы дружили с Федорчуками.
История - история ASCII арта (часть 1).
Ха-хакер - "Высказывания" отдельных представителей постоянно вымирающего класса - военных.
Новости - Оказывается CDOS-device может эмулировать Sound Drive.
Иной мир - Radeon X700: недорогая основа для геймерских карт.

В этот день...   3 декабря