Главная
страница 1
Отчёт: Анализ программных комплексов для защиты ПО от нелицензионного использования и копирования
Автор: ViNCE [AHTeam]

Постановка задачи:

- Рассмотреть теорию .NET защит

- Привести практические примеры

- Произвести обзор текущих вариантов .NET-защит со следующими критериями:

- механизм активации через сервер (с возможностью разворачивать у себя

сервер авторизации и заносить дополнительную информацию в

активирующие ключи (например, о кол-ве пользователей или доступной

функциональности)),

- привязка к оборудованию,

- сложность обхода

- стоимостная и лицензионная политика

- Согласно 3-му пункту привести примеры

- Выбрать оптимальный вариант
Вводная часть

В настоящее время разработчикам программного обеспечения доступны три основных варианта защиты ПО:

- Использование защиты, основанной на общедоступных примерах.

- Использование навесных защит

- Использование “обфускаторов”
А теперь подробнее об каждом пункте.

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


If length(eUserName.Text)>3 then

Begin


If eRegNum=’546547-338596824-45823948’ then

Begin


MessageDlg(‘Регистрация успешно завершена!’+#10#13

+’Спасибо за поддержку!’,mtConfirmation,[mbOk],0);

Const_Reg_ok:=true;

fMain.Show();

fTrial.Close();

End else MessageDlg(‘Неправильный регистриционный номер!’,mtError,[mbOk],0);

End;
Подобного вида защиты являются безусловно ошибочными, т.к. злоумышленник совершенно свободно, вооружившись всего лишь отладчиком, может прочитать серийный номер. Пример этого же кода, взятого из ассемблерного листинга:
00453971 CALL Project1.00432A24 ; получаем длину имени

00453976 MOV EAX,DWORD PTR SS:[EBP-4] ; загружаем имя

00453979 CALL Project1.0040410C ; опять получаем длину имени

0045397E CMP EAX,3 ; сравниваем длину с 3

00453981 JLE SHORT Project1.004539E2 ; если равно, продолжаем

00453983 LEA EDX,DWORD PTR SS:[EBP-8]

00453986 MOV EAX,DWORD PTR DS:[EBX+2FC]

0045398C CALL Project1.00432A24 ; получаем длину введённого кода

00453991 MOV EAX,DWORD PTR SS:[EBP-8] ; загружаем введённый код

00453994 MOV EDX,Project1.00453A14 ; ASCII "546547-338596824-45823948"

00453999 CALL Project1.00404258 ; сравниваем

0045399E JNZ SHORT Project1.004539CD ; если неравны, то переходим к ошибке


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

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


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


  • AsProtect

  • Armadillo

  • tElock

  • PEGuard

  • EXECrypter

  • Obsidium

  • OriEN

Является ли такой метод защиты ПО надёжным? Конечно же нет. В случае начинающих “кодокопателей” он поможет и защитит ПО от взлома. Но профессионального взломщика данные защиты лишь задержат на некоторое время от достигаемой цели.


В добавок к вышесказанному, к большей части навесных защит существуют программы-распаковщики.
- AsProtect -> AsProtect Stripper

- Armadillo -> DilloDumper

- tElock -> unTELOCK

- PEGuard -> PEGuard Deprotector


К EXECrypter, Obsidium и ORIEN нет автораспаковщиков, но в интернете достаточно ресурсов, где “на пальцах” объясняется процесс обезвреживания данных защит.
Среди представленных вариантов самым надёжным и самым крепким является Armadillo. К сожалению, на данном этапе развития программных защит во всех программных комплексах отсуствует поддержка .NET-приложений. Безусловно, встречаются случаи когда к .NET-приложению можно подключить навесную защиту, но в тоже время никто не гарантирует работоспособность защищённой программы на компьютерах с разными аппаратными характеристиками.
В поставленной задаче важным пунктом является – поддержка .NET-приложений. Соответственно, рассмотрим представителей защит из сферы.NET-приложений.
Теоретическая часть. Технология .NET
Со времён появления Java перед разработчиками встала проблема обеспечения защиты своих алгоритмов. В то время появилось понятие “обфускация” программного кода, т.е. приведение программы к виду, затрудняющему восприятие и модификацию.

В настоящее время разработано около десятка обфускаторов (в большей степени для Java, но сейчас уже и для .NET-приложений). В 2000 году вышла в свет технология .Net от Microsoft. Данная технология оперирует ключевыми понятиями: Assembly и MSIL.


Assembly – это набор ресурсов и типов, а также метаданные, описывающие типы и метода, реализованные в сборке. Получается что сборка (assembly) – это самоописанный компонент.
MSIL – это процессоронезависимый промежуточный язык, разработанный Microsoft. MSIL язык более высокого уровня, чем большинство машинных языков. Этот язык понимает типа объектов и имеет инструкции для создания и инициализации объектов, вызова виртуальных методов и непосредственной манипуляции элементами массива. Особо стоит заметить: Microsoft вредоставляет ассемблер и дизассемблер для MSIL.
Логично, что любой код на который Вы потратите определённое количество времени, написанный на C#, VB.Net, Managed C++ или J# сможет посмотреть любой человек более или менее знакомый с компьютером. Например, программой .Net Reflector.

Ощущается некоторая беспомощность программиста и его продукта. К счастью, существует несколько путей решения данной проблемы:



  • Обфускация:

Именно этот метод мы и возьмём за основу при выборе надёжного и безопасного ПО для защиты .Net приложений. В самых сложных и действенных обфускаторах встроены функции оптимизирующего компилятора.

  • Оптимизирующий компилятор:

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

  1. ngen - утилита, превращающая Вашу сборку в native

  2. DotFuscator - обфускатор, поставляемый вместе с Visual Studio .Net 2003

Процесс обфускации

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

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


  1. Время, затраченое на понимание кода злоумышленником превышает время, в течение которого актуальность алгоритма остается значимой.

  2. Стоимость деобфускации превышает стоимость самого продукта.

Методы обфускации

Рассмотрим самые распространённые методы обфускации, на которых базируются поколения обфускаторов.

Самым простейшим методом является – символьная обфускация. Соответственно, обфускаторы, основанные на данном методе относятся к первому поколению, опять же – обфускаторов.

Характерные особенности данного метода:



  • Переименование методов, переменных и т.д. в набор бессмысленных символов. Например: преобразование метода класса CheckLicense() в Ajfu83Ajsndas(). Метод стал непонятным, но есть одна проблема: некоторые декомпиляторы преобразуют такую несуразицу в вполне сносные имена, например: meth_1, meth_2. Работа обфускатора в таком случае сводится к нулю.

  • Переименование и краткие имена. Некоторые обфускаторы в цикле проходят по всем классам, методам, параметрам и заменяют их имена на порядковые номера. Например, был GetTrialDays(), а стало 5().

  • Использование для имён нечитаемых символов. Ряд обфускаторов вставляют в названия нечитаемые символы, например, из японского языка.

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

  • Использование имён, меняющих смысл. Довольно интересный способ, запутывающий злоумышленника. Например, был класс MainLicense, а стал GetDirectory.

Вторым методом идёт обфускация символьных данных. Конечно же, помимо имен классов, переменных, методов, сборка содержит в себе множество другой "полезной" символьной информации, которая будет помогать взломщику в понимании кода. Помимо этого, некоторые программисты хранят в строковых константах очень конфиденциальную информацию, как то: строки соединения с базой данных, приватные ключи и т. п. Помимо этого сборка хранит в себе имена других пакетов используемых в этом классе (объявленные в директивах "import", "using") и прочее. Обфускация подобных строк заключается в их кодировании (string encryption). Таким образом, либо при обращении к закодированной строке во время выполнения программы производится вызов алгоритма декодирования строки, который представлен в виде отдельного метода данного класса, либо при запуске программы сначала выполняется декодирование всех ее закодированных строк (алгоритм декодирования помещается в отдельный класс), и только потом выполняется программа.

Пример обфускации кода, на примере простейшего приложения типа “Hello World”:

class HelloWorld

{

public static void Main()



{

System.Console.WriteLine("Hello World");

}

}
После компиляции Вы можете использовать утилиту ildasm из .NET Framework для просмотра ассемблерного листинга:


.method public hidebysig static void Main() cil managed

{

.entrypoint



.maxstack 1

IL_0000: ldstr "Hello World"

IL_0005: call void [mscorlib]System.Console::WriteLine(string)

IL_000a: ret

}
Шифрование строк:

Итак, у нас есть пример со строкой “Hello World”. Обфускатор ищет инструкцию ldstr и заменяет строки зашифрованными значениями. Для полноты картины рассмотрим следующий алгоритм шифрования строк:


public static string Encrypt(string inputStr)

{

int len = inputStr.Length;



int sum = 10;

char[] encryptedChars = new char[len];

for (int i=0; i

{

encryptedChars[i] = (char)((int)inputStr[i] + sum);



}

char[] newNameStr = new char[len];

for (int i=0; i

{ // reverse the name

newNameStr[i] = encryptedChars[len-1-i];

}
for (int i=0; i

{ // reverse the name

encryptedChars[i] = newNameStr[i];

}

return String.Intern(new String(encryptedChars));



}

После шифрования, строки будет сложно распознать и обычно они представляются в качестве байт-массива. Фраза “Hellow world” будет выглядеть следующим образом:

bytearray(6E 00 76 00 7C 00 79 00 61 00 2A 00 79 00 76 00 76 00 6F 00 52 00)

Итак, программа будет иметь следующий вид после шифрования:

.method public hidebysig static void Main() cil managed

{

.entrypoint .maxstack 1



IL_0000: ldstr bytearray(6E 00 76 00 7C 00 79 00 61 00 2A 00 79 00 76 00 76 00 6F 00 52 00) // "Hello World"

IL_0005: call void [mscorlib]System.Console::WriteLine(string)

IL_000a: ret

}

Расшифровка строк:



Итак, строка “hello world” зашифрована, но в момент работы программы нам требуется расшифрованный вариант.

Приведём пример кода дешифрации строк:

public static string Decrypt(string inputStr)

{

int len = inputStr.Length;



char[] decryptedChars = new char[len];

for (int i=0; i

{

decryptedChars[i] = (char)((int)inputStr[i] - 10);



}
char[] newNameStr = new char[len];

for (int i=0; i

{ // reverse the name

newNameStr[i] = decryptedChars[len-1-i];

}
for (int i=0; i

{ // reverse the name

decryptedChars[i] = newNameStr[i];

}
return String.Intern(new String(decryptedChars));

}

Обфускатор используется функцию дешифровки отдельно для каждой строки. Сначала он внедряет свой код дешифратора перед “загрузкой” строк и походу выполнения программы вызывает его.



В итоге мы получим следующий код (после обработки обфускатором):

.method public hidebysig static string Decrypt(string inputStr) cil managed

{

// Code size 119 (0x77)



.maxstack 5

.locals init (int32 V_0,

char[] V_1,

int32 V_2,

char[] V_3,

int32 V_4,

int32 V_5,

string V_6)

IL_0000: ldarg.0

IL_0001: callvirt instance int32 [mscorlib]System.String::get_Length()

IL_0006: stloc.0

IL_0007: ldloc.0

IL_0008: conv.ovf.u4

IL_0009: newarr [mscorlib]System.Char

IL_000e: stloc.1

IL_000f: ldc.i4.0

IL_0010: stloc.2

IL_0011: br.s IL_0025

IL_0013: ldloc.1

IL_0014: ldloc.2

IL_0015: ldarg.0

IL_0016: ldloc.2

IL_0017: callvirt instance char [mscorlib]System.String::get_Chars(int32)

IL_001c: ldc.i4.s 10

IL_001e: sub

IL_001f: conv.u2

IL_0020: stelem.i2

IL_0021: ldloc.2

IL_0022: ldc.i4.1

IL_0023: add

IL_0024: stloc.2

IL_0025: ldloc.2

IL_0026: ldloc.0

IL_0027: blt.s IL_0013

IL_0029: ldloc.0

IL_002a: conv.ovf.u4

IL_002b: newarr [mscorlib]System.Char

IL_0030: stloc.3

IL_0031: ldc.i4.0

IL_0032: stloc.s V_4

IL_0034: br.s IL_0048

IL_0036: ldloc.3

IL_0037: ldloc.s V_4

IL_0039: ldloc.1

IL_003a: ldloc.0

IL_003b: ldc.i4.1

IL_003c: sub

IL_003d: ldloc.s V_4

IL_003f: sub

IL_0040: ldelem.u2

IL_0041: stelem.i2

IL_0042: ldloc.s V_4

IL_0044: ldc.i4.1

IL_0045: add

IL_0046: stloc.s V_4

IL_0048: ldloc.s V_4

IL_004a: ldloc.0

IL_004b: blt.s IL_0036

IL_004d: ldc.i4.0

IL_004e: stloc.s V_5

IL_0050: br.s IL_0060

IL_0052: ldloc.1

IL_0053: ldloc.s V_5

IL_0055: ldloc.3

IL_0056: ldloc.s V_5

IL_0058: ldelem.u2

IL_0059: stelem.i2

IL_005a: ldloc.s V_5

IL_005c: ldc.i4.1

IL_005d: add

IL_005e: stloc.s V_5

IL_0060: ldloc.s V_5

IL_0062: ldloc.0

IL_0063: blt.s IL_0052

IL_0065: ldloc.1

IL_0066: newobj instance void [mscorlib]System.String::.ctor(char[])

IL_006b: call string [mscorlib]System.String::Intern(string)

IL_0070: stloc.s V_6

IL_0072: br.s IL_0074

IL_0074: ldloc.s V_6

IL_0076: ret

} // end of method 'Global Functions'::Decrypt


.method public hidebysig static void Main() cil managed

{

.entrypoint .maxstack 1



IL_0000: ldstr bytearray(6E 00 76 00 7C 00 79 00 61 00 2A 00 79 00 76 00 76 00 6F 00 52 00) // "Hello World"

IL_0005: call string Decrypt(string)

IL_000a: call void [mscorlib]System.Console::WriteLine(string)

IL_000f: ret

}
Мы рассмотрели простейший метод защиты строковых констант. Подобные защиты с лёгкостью обезвреживаются с помощью Salamander Decompiler.

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

Обфускация данных производится следующими методами:


  • Изменение местоположения данных (объединение нескольких сущностей в одну или наоборот)

  • Кодирующие преобразования (динамическая декодировка данных во время загрузки программы)

  • Добавление неиспользуемых данных (на основе неиспользуемых данных формируются ложные условия)

  • Изменение способа хранения данных (некоторые открытые члены делаются закрытыми и наоборот)

  • Выполнение финализации (finalization): защита данных от переопределения в классах потомков, созданных от класса, которому принадледит данный член.

Обфускация графа потока управления.

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

Основные методы обфускации графа потока управления:


  • Преобразование вычислений (вставка в алгоритмы ложных условий)

  • Удаление или добавление абстракций кода (например, обфускатор может заменить вызов какой-нибудь функции непосредственно телом функции или наоборот).

  • Перемешивание случайным образом линейных участков.

Подготовка сборки к обфускации


Прежде чем подвергнут свою сборку обфускации можно немного облегчить работу обфускатора - подготовить сборку. Это позволит также достичь более высоких результатов обфускации. Например, с целью защиты алгоритмов, реализованных в Вашей программе можно вынести все интересные, с точки зрения взломщика, реализации из публичных методов в отдельный, например internal класс с protected методами. Этот класс может быть в дальнейшем подвергнут обфускации, что позволит скрыть детали реализации от любопытных глаз. Также, если Ваш новый класс поддеживает сериализацию могут возникнуть проблемы с десериализацией после обфускации класса. Вообще существует множество способов запутать свой код, сделать его более "obfuscation friendly", более подробно эти методы описаны в [2].

Группы .Net обфускаторов

Все существующие программы-обфускаторы можно поделить на несколько групп, в зависимости от их подхода к решению задачи обфускации:



  • Парсеры: Представители данной группы обфускаторов работают с IL-кодом, который они предварительно получают используя ildasm. В самом конце обфускатор получает защищённую сборку с помощью ilasm. Минусы такого рода обфускаторов: зависимость от ildasm и ilasm.

  • Обфускаторы, знающие формат PE-файла и метаданных в нём. Такие обфускаторы анализируют метаданные. Анализ производится либо вручную, либо с помощью внешних интерфейсов вроде MSCORE.DLL.

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

  • Защищающие только exe-сборки. Обфускаторы данного типа формируют exe-сборку, куда включают в виде managed resource зашифрованные сборки и ресурсы. При запуске программа расшифровывает сборки, загружает в свой домен (AppDomain) и передает управление "родной" Main функции. Также шифруются и обфусцируются ресурсы сборок.
    Также, возможны случаи, когда программа забирает TypeResolve, AssemblyResolve, ResourceResolve события, генерируемые доменом при возникающих затруднениях с типами, сборками, ресурсами, и переназначает их. По мере необходимости, программа динамически подгружает данные. К сожалению, скорость инициализации подобных обфускаторов достаточно мала, также могут возникнуть проблемы с безопасностью, некоторые виды сборок, например managed C++ сборки не смогут быть загружены Assembly.Load(byte[]) путем.

Обзор .Net обфускаторов

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


Demeanor .Net


На первое место, среди всех представленных на сегодняшний день на рынке обфускаторов, по степени предоставляемой защиты можно поставить "Demeanor .Net" от компании Wise Owl. Ее руководитель - Brent Rector, опубликовал свою книгу - "Introducing Longhorn for Developers" при содействии Microsoft. Авторитет данной програмы достаточно велик, как, в принципе, и ее цена. За одну лицензию Вам придется заплатить 799 долларов. При этом, чтобы получить триальную версию программы, Вам необходимо написать письмо в компанию, которая рассмотрев причины Вашего интереса с их продукту, примет решение об отправке вам триальной версии программы. Мне в этом плане повезло. Господин Brent Rector с легкостью прислал мне версию "Demeanor .Net" для изучения. К сожалению, триальная версия программы является по сути своей консольным приложением, что определенно является большим минусом при ее изучении. Данный обфускатор предоставляет довольно широкие возможности для защиты Вашей .Net сборки. "Demeanor .Net" удаляет ненужную информацию из метаданных, позволяет шифровать строковые переменные, а также работает с мультимодульными сборками. Также из описания следует, что он интегрируется с Visual Studio .Net.

Итак, от краткого описания к обзору данного продукта…

Demeanor – это программный продукт, который позволяет защитить приложения, разработанные с помощью .NET-технологии с использованием обфускации. Для установки Demeanor .NET Edition необходимо наличие установленного .NET Framework. Установка проходит довольно быстро и просто. После установки Demeanor можно запускать как отдельное приложение или из пакета Visual Studio .NET. Для запуска Demeanor из VS .NET потребуется дополнение Demeanor add-in.

Интерфейс:

Интерфейс очень простой и лёгок в использовании. Левая панель показывает все приложения или DLL которые будут в последствии подвержены процессу обфускации. Если Вы выбрали один из пунктов, на правой панели сразу же отобразятся свойства объекта. Свойства можно изменять, в зависимости от потребностей.

Demeanor поддерживает работу с файлами проектов. Здесь понятие проекта заменено на – предустановку (preset). Предустановка после сохранения записывается в xml формат. Это очень удобно тем, что если Вы работаете с рядом программных продуктов и защищаете их всех с одними и теми же опциями обфускатора, то можно воспользоваться созданным проектом. В одну предустановку можно загружать более одного приложения и DLL.

Испытательный период (Trial)

Для примера создадим приложение в Microsoft Visual C# .NET для того чтобы наглядно посмотреть как Demeanor произведёт обфускацию приложения. С опциями “по умолчанию” программист может попасть в просак, т.к. Demeanor не произведёт процесс обфускации. Использую декомпилятор, мы можем получить наш код. Ошибка была в том, что в настройках Demeanor’а следует включить опцию Application (включить её в режим True).

Рассмотрим код приложения: оригинальный; после декомпилятора и после обработки обфускатором:

Оригинальный код:

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;

namespace DecompileMe

{

        ///



        ///Zusammenfassung f|r Form1.

        ///

        publicclass Form1 : System.Windows.Forms.Form

        {

            ///

            ///

            privateSystem.ComponentModel.Container components = null;

            publicForm1()

            {

               InitializeComponent();

             }

       ///

      ///


       protected overridevoid Dispose( booldisposing )

        {

               if( disposing )

               {

                      if (components != null)

                    {

                       components.Dispose();

                  }

                  }

              base.Dispose( disposing );

        }
  #region Vom Windows Form-Designer generierter Code

       ///

       ///

       private voidInitializeComponent()

        {

             //

              //Form1

              //

              this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

             this.ClientSize = new System.Drawing.Size(292, 266);

             this.Name = "Form1";

              this.Text = "Form1";

              this.Load += new System.EventHandler(this.Form1_Load);

        }

        #endregion

       ///

       

       ///

        [STAThread]

       static voidMain()

        {

           Application.Run(new Form1());

        }

        private void Form1_Load(object sender, System.EventArgs e)

        {

           //Can you decompile me ???

           MessageBox.Show("You decompiled me !!!");

        }

    }


}

После того как мы установим свойства:

Force -> True

Names -> Numeric

Мы получим следующий код (после декомпилятора):

public class _1 : Form

{

   privateContainer _1 = null;



   public_1()

   {


        _1();

    }


    protected overridevoid Dispose(booldisposing)

   {


        if (disposing && _1 != null)

        {

            _1.Dispose();

        }

        base.Dispose(disposing);

    }


   privatevoid _1()

   {


        AutoScaleBaseSize = new Size(5, 13);

        base.ClientSize = new Size(292,266);

        base.Name = "Form1";

        Text = "Form1";

        base.Load += new EventHandler(this._1);

   }


   [STAThreadAttribute()]

   private static void _2()

   {

        Application.Run(new _1());



    }

   privatevoid _1(object , EventArgs )

   {

        MessageBox.Show("You decompiled me !!!");



   }

}

Как мы видим Demeanor преобразовал имена в порядковые значения. Ещё одной возможностью Demeanor является – удаление всей неиспользуемой информации из кода приложения. После обфускации с данной опцией размер приложения может уменьшится от 10 и более процентов.



В заключении стоит отметить, что Demeanor является отличным обфускатором с широким спектром возможностей, но запредельная цена данного продукта отталкивает потенциальных покупателей (около 800 долларов США).

Согласно поставленной задаче: поддержка активации через удалённый сервер, данный продукт не является одним из претендентов на выбор в качестве защиты для .NET-приложений.


RemoteSoft Salamander Obfuscator


RemoteSoft Salamander Obfuscator - это продукт, с очень хорошо проработнным интерфейсом и позволяющий просматривать не только метаданные, но и структуру PE-файла, анализировать ресурсы, бинарные данные. Символьный обфускатор применяет максимально короткие и часто используемые названия для членов сборки. Также, данный продукт содержит Protector, который преобразует методы в "native embedded code" - не поддаются дизассемблированию, но зависимы от текущей версии .Net Framework.

Минусы: Выделенная в тексте особенность делает данный обфускатор довольно бесполезным, т.к. вряд ли разработчики ПО будут прикладывать .Net Framework со своим продуктом. Для больших продуктов это может быть актуальным, а для небольших – неприемлимо. Согласно поставленной задаче: поддержка активации через удалённый сервер, данный продукт не является одним из претендентов на выбор в качестве защиты для .NET-приложений..


Preemptive Solution DotFuscator


Preemptive Solution DotFuscator - этот продукт широко известен разработчикам .Net, так как он входит в стандартный пакет Visual Studio .Net 2003. Данный обфускатор является одним из немногих, позволяющих обфусцировать поток управления. Однако бедный интерфейс и, порой, не понятные настройки делают его сложным для работы. Одним из его плюсов является интеграция с Visual Studio .Net.

Минус: Редкая возможность обфусцировать потоки управления не может не радовать, но бедный интерфейс и небольшой набор возможностей отталкивает от данного продукта. Согласно поставленной задаче: поддержка активации через удалённый сервер, данный продукт не является одним из претендентов на выбор в качестве защиты для .NET-приложений..

9Rays .Net ILObfuscator (ver 4.1.3)


9Rays .Net ILObfuscator (ver 4.1.3) - более известный как Spices .Net, это хороший продукт, с проработанным интерфейсом. Данный обфускатор имеет большое количество различных настроек, как то коллекция замен и исключений, варианты именования обфусцированных классов, функция оптимизации сборки после обфускации (для managed code), генерация карт обфускации и т.д.

С точки зрения процесса обфускации Spices .Net предоставляет следующие возможности:



  1. Защита от дизассемблеров ILDASM, Anakrino, Reflector, Remotesoft Salamander Decompiler

  2. Два режима обработки строк : скрытие строк в "обертках" и шифрование строк.

  3. Поддержка работы с satellite сборками

  4. Несколько режимов переименования классов, методов, и т.п.

  5. Несколько режимов переименования namespace

  6. Использование технологии генерации наиболее коротких имен для замены.

  7. Реализация cross-obfuscation - работа с набором сборок.

Надо сказать, что Spices .Net - это один из немногих обфускаторов, проверяющих сборку после обфускации. Также Spices .Net выполняет функции оптимизатора и позволяет защитить сборку от несанкционированного распространения (цифровая подпись).

Также большим плюсом является то, что Spices .Net поставляется в обфусцированном формате, что говорит о том, что разработчики доверяют своему продукту.

Новая версия 2.5 разительно отличается от предыдущей 2.0. Очень много качественных изменений. Во-первых появился GUI. Что многим облегчает задачу работы со сборками. ILOGUI - это и навигатор по сборкам, дизассемблер, система работы с проектом обфускации, в ней можно просматривать и экстрагировать ресурсы, содержащиеся в сборке. Чем отличается от других? Managed code - С# и managed С++, работа с набором сборок, а значит и кросс-обфускация, позволяющая достичь максимального уровня обфускации, неплохой и понятный GUI c VS IDE-like MDI интерфейсом, с ILO поставляется SDK, позволяющим вам создать свою собственную систему обфускации, удобная работа со свойствами проекта, есть коллекции замен и исключений, гибкие возможности по запутыванию - скажем фича по запутыванию namespaces (можно выбрать один из варантов), также предлагаются на выбор варианты наименования обфусцированных классов.

Есть функция оптимизации сборки после обфускации, правда только для managed сборок, без embedded native code. ILO самообфусцирован - вы можете убедиться в качестве его обфускации. Из заявленных на следующую версию фичей - поддержка solutions  - т.е наборов проектов, генерация карт обфускации и импорт их при обфускации проектов, эти фичи уже позволят фиксировать результат обфускации и использовать его в следующей обфускации проекта и уменьшать время тестирования сборки после обфускации и максимально обфусцировать проект. Кроме этого обещано функцию оптимизации дополнить фичей удаления неиспользуемых членов сборок и вставить функцию анализа проекта для выявления оных, дополнить навигацию закладками и history. 



Согласно поставленной задаче: поддержка активации через удалённый сервер, что напрочь отсутствует в ILObfuscator, данный продукт не является одним из претендентов на выбор в качестве защиты для .NET-приложений..

Lesser Software LSW IL-Obfuscator


Lesser Software LSW IL-Obfuscator - это достаточно удобная, с точки зрения интерфейса и работы программа, предоставляющая весьма не плохие средства для обфускации.

Из основных достоинств данной программы можно выделить поддержку ANSI и UNICODE и поддержку различных форматов файлов, содержащих MSIL (exe, dll, Netmodule).

С точки зрения процесса обфускации данный продукт позволяет:


  1. Удалить неиспользуемые данные

  2. Применять различные способы именования

  3. Перемещать структуры данных

В целом простая и быстрая прога, которая для многих подойдет. Немного настроек, все понятно и просто. Берет как Il-файлы, так и скомпилированные сборки. Правда не все - иногда выдает какую-то странную ошибку и скопилированную сборку не подымает. Такое впечатление что он ее сначал дизассемблирует. Не работает с наборами сборок, но поддерживает замены и исключения из обфускации. Native code. Но за $40.

Минусы:


  • Нестабильность

  • Скромные возможности

  • Отсутствие поддержки работы с серверами активации

Thinstall.Net


Thinstall.Net - данный продукт попадает в ту категорию программ-обфускаторов, которые работают только с exe-сборками. Он позволяет создавать самоустанавливающуюся exe - программу, закрывая от дизассемблера сборки. К сожалению, данный обфускатор обладает весьма скудным интерфейсом, что не позволяет делать гибкие настройки процесса обфускации.

Работает только с exe -файлами. Создает самоустанавливающийся exe-файл, со всеми файлами на борту, закрывая от дизассемблирования сборки. Такой файл не без усилий но вскрывается, не стоит обманываться. Как признаются сами производители - лучше всего использовать вместе с обфускатором.

С точки зрения обфускации данный продукт позволяет:


  1. Шифровать IL байт код, строки и метаданные
    При запуске программы байт код будет дешифрован во временный буфер, где он будет конвертирован в native код ассемблером от Microsoft

  2. Уменьшить размер программы.

Минусы:
- Для обфускации необходимо соединение с сервером разработчиков (!!!)
- Ограниченный диапазон применения (только exe)
- Скудный интерфейс
- Отсутствие гибкости в настройке
- Отсутствие поддержки сервером активации

И наконец, рассмотрим один из самых лучших, по мнению общественности, обфускаторов.


XenoCode 2005


Самым лучшим обфускатором, с моей точки зрения, является обфускатор "XenoCode 2005". По оценкам специалистов, данный обфускатор не на много уступает по качеству "Demeanor .Net", однако гораздо дешевле. Помимо того, что это, опять же с моей точки зрения, единственный обфускатор, обладающий действительно доступным, и простым в использовании интерфейсом, он также обладает рядом возможностей, которыми не обладают остальные обфускаторы.

Применяемые методы обфускации


Каким образом "XenoCode 2005" позволяет защитить Вашу программу от просмотра:

  1. обработка символьной информации
    "XenoCode 2005" переименовывает классы, методы, параметры и т. д. в бессмысленные наборы символов

  2. обфускация управляющей логики
    "XenoCode 2005" манипулирует управляющей логикой путем добавления новых ложных веток в графе управления, тем самым запутывая последовательность инструкций

  3. Шифрование строковых данных
    "XenoCode 2005" шифрует строковые данные, используемые в коде программы.

  4. Защита от ildasm
    "XenoCode 2005" добавляет в метаданные заведомо неверные данные, которые приводят к краху ildasm.

  5. Удаление debug информации
    "XenoCode 2005" удаляет всю debug информацию из сборки

Помимо непосредственно защиты от просмотра, "XenoCode 2005" обладает широким возможностями по оптимизации сборок, защиты от несанкционированного распространения программы (водяные знаки) и т. п.

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

Так с чего же начать?

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

Не стоит путать проектирование с программированием. Какие факторы необходимо учитывать. Обфускатор не Господь Бог, всего не может, поэтому стоит пойти его возможностям навстречу. Типичной обфускацией является символьная (учитывая все плюсы и минусы упомянутые выше)– когда обфускатор только и всего изменяет названия типов, полей, методов, свойств и событий на бессмысленные. Скажем тип Obfuscator переименовывается в 0, а его метод Run() – тоже в 0, а параметры методов просто перенумеруются – 0,1,2,3,4. После подобной обфускации теряется логическая связь между классами, дизассемблированной код – трудночитаем.

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

К слову сказать кроме символьной обфускации есть еще обфускация алгоритмов методов – когда простейшее умножение I*3 может быть представлено более сложным алгоритмом, например – I*((1+1/2)*2) или запутаны while и for. Но мы будем говорить только о символьной обфускации, применительно к обфускации .Net сборок.

Редко можно встретить полностью замкнутые, автономные системы, поэтому типичным случаем будет являться частичная обфускация. Хотя идеальной задачей для более качественной обфускации будет являться как раз написание максимальной замкнутой системы сборок. Вот вам и первый фактор.



Рекомендации по подготовке проекта(продукта) к обфускации:

  1. Для усложнения дизассемблирования вы можете использовать подмену типов (которую не всегда возможно реализовать из-за sealed типов) Имеется ввиду такой приемчик – некий системный тип SomeType наследуется в новом типе AnotherType, который подходит для обфускации (скажем, он лежит в пределах видимости вашей сборки, его можно назначить как internal) и вы его спокойно обфусцируете. На выходе мы получаем испоьзование SomeNamespace.0 вместо известного System.SomeType. Конечно можно установить что AnotherType – это простой наследник от SomeType, но на это уйдет время, не так ли? А нам и нужно тратить время злоумышленника покусившегося на ваш код.

  2. Если вы планируете использовать некоторые типы как публичные но хотели бы их максимально защитить, стоит их поместить "защитную скорлупу" наследования. Некий тип public SomeType можно превратить в два класса : internal _SomeType (underground class), который несет всю реализацию класса, кроме публичных свойств, необходимых для сериализации и для использования во внешнем для сборки коде, и public SomeType, который будет наследоваться от _ SomeType (front class), но нести внешнюю нагрузку – иметь публичные свойства, необходимые для сериализации, конверсии, использования во внешнем для сборки коде. Таким образом у вас будет еще возможность воспользоваться приемчиком 1. для порождения класса SomeType- подобных типов но наследуемых от SomeType.

  3. Внимательнее с атрибутами. Не стоит забывать об использовании атрибутов в вашем коде. Многие из них достаточно тесно помогают взаимодействовать среде .Net с вашими классами. Например атрибут TypeConverterAttribute – им вы привязываете к вашему классу класс конвертера SomeConverter. Не каждый обфускатор «знает» об этом – и поэтому стоит уберечь класс конвертера от обфускации или проверить как обфускатор работает с атрибутами. Иначе связь, установленная между двумя классами посредством атрибута может быть разрушена.

  4. Если ваш класс идет под нож обфускации, стоит также задуматься о механизме его сериализации. Скажем класс- наследник Form сериализует себя таким образом что если обфускатор изменил имя его типа SomeForm на 0, то возникнет проблема при инициализации десериализации такого класса – он просто не сможет найти ресурс 0.resources, так как сериализовался в в SomeForm.resources.

  5. Используйте static string обьявления вместо const string – это затруднит поиск инициализации этого поля (в метаданных оба обьявления будут представлены как поля).

  6. Если вы имеете список строк, которые у вас представлены как список строковых констант, лучше список строк обьявите/ опишите как строковый массив, а в константах храните индекс к необходимой строке в строковом массиве.

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

  8. Пользуйтесь такими фичами как nested types, это не повлияет на производительность, но повлияет на «трудноизучаемость» вашего кода.

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

  10. Инициализируйте класс формы без использования .resx и .resources файлов - к ним лучше обращаться по индексу во избежание проблем при обфускации ресурсов.

После обфускации:

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

  2. После обфускации, обязательно проверьте сборку утилитой peverify, которая идет с .Net Framework SDK – эта утилита проверяет метаданные вашей сборки на корректность. Если ваша сборка помечена как CLS-compliant – это тестирование обязательно. Есть халявщики, которые некорректно обфусцированную сборку прикрывают специальным разрешением из PermissionSet.SkipVerification для избежания встречи результирующей сборки с ее верификацией. Проверьте – не появилось ли такое разрешение в вашей сборке после обфускации, понятно на что намекаю. Правда managed C++ сборки имеют изначально такое разрешение, даже если вы о нем не упоминали в вашем коде, это тоже намек :) ).

  3. Немного о качестве обфускации – не поленитесь, посмотрите как обфусцирован сам обфускатор. Если обфускатор написан в native code – возможно авторы сомневаются в качестве своей обфускации?

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

  5. И самое главное - при проблемах, контактируйте с авторами, присылайте сборки, пишите какой цели вам необходимо достичь – специалист подобен флюсу, при работе со своим продуктом глаз у него «замылен», ему надо подкидывать материал для тестирования – чем его больше, тем выше качество обфускации.

Так что же является наилучшим для защиты .Net сборок? На текущий момент сочетание протектора (software protection имеется ввиду) и обфускатора и полная обфускация проекта дают наилучшую защиту.

Практическая часть

1. Простейшие основы

Необходимое ПО для исследований: Remotesoft .NET Explorer (www.remotesoft.com)

Чтобы не быть голословными возьмём тестовый пример. Создадим в Delphi 2005 .NET приложение:

Листинг кода:

using System;

using System.Drawing;

using System.Collections;

using System.ComponentModel;

using System.Windows.Forms;

using System.Data;


namespace Project2

{

///



/// Summary description for WinForm.

///

public class WinForm : System.Windows.Forms.Form

{

///



/// Required designer variable.

///

private System.ComponentModel.Container components = null;

private System.Windows.Forms.TextBox textBox1;

private System.Windows.Forms.Label label1;

private System.Windows.Forms.Button button1;


public WinForm()

{

//



// Required for Windows Form Designer support

//

InitializeComponent();


//

// TODO: Add any constructor code after InitializeComponent call

//

}
///



/// Clean up any resources being used.

///

protected override void Dispose(bool disposing)

{

if (disposing)



{

if (components != null)

{

components.Dispose();



}

}

base.Dispose(disposing);



}
#region Windows Form Designer generated code

///

/// Required method for Designer support - do not modify

/// the contents of this method with the code editor.

///

private void InitializeComponent()

{

this.textBox1 = new System.Windows.Forms.TextBox();



this.label1 = new System.Windows.Forms.Label();

this.button1 = new System.Windows.Forms.Button();

this.SuspendLayout();

//


// textBox1

//


this.textBox1.Location = new System.Drawing.Point(8, 24);

this.textBox1.Name = "textBox1";

this.textBox1.TabIndex = 0;

this.textBox1.Text = "";

//

// label1



//

this.label1.Location = new System.Drawing.Point(8, 8);

this.label1.Name = "label1";

this.label1.Size = new System.Drawing.Size(100, 16);

this.label1.TabIndex = 1;

this.label1.Text = "Serial number:";

//

// button1



//

this.button1.Location = new System.Drawing.Point(16, 48);

this.button1.Name = "button1";

this.button1.TabIndex = 2;

this.button1.Text = "check";

this.button1.Click += new System.EventHandler(this.button1_Click);

//

// WinForm



//

this.AutoScaleBaseSize = new System.Drawing.Size(5, 13);

this.ClientSize = new System.Drawing.Size(120, 85);

this.Controls.Add(this.button1);

this.Controls.Add(this.label1);

this.Controls.Add(this.textBox1);

this.Name = "WinForm";

this.Text = ".NET Example";

this.ResumeLayout(false);

}

#endregion


///

/// The main entry point for the application.

///

[STAThread]

static void Main()

{

Application.Run(new WinForm());



}

private void button1_Click(object sender, System.EventArgs e)

{

if (textBox1.Text == "6458-438534-6895634") {



MessageBox.Show("Reg ok!","Message");

} else {


MessageBox.Show("Reg err!","Message");

}

}



}

}

Приложение – наглядные пример простейшей регистрации с использованием серийного номера. Допустим, программист включил для данной сборки программы корректный серийный номер:


6458-438534-6895634

Действительно, такой номер запросто не подберёшь. Тогда остаётся его подсмотреть. Берём инструмент: Remotesoft .NET Explorer. Откроем в программе нашу “жертву” и видим следующую картину:



Слева структура нашего проекта. Нас интересует сам проект, а разные объявления нам на данном этапе не нужны.

Предположим мы взломщик, который пытается найти верный серийный номер приложения. По рисунку мы видим что программа состоит из одной формы (WinForm). На форме есть одна кнопка (button1), текстовое поле (textBox1) и метка (label1). Ниже идут функции и процедуры. В нашем премере мы вводим номер и нажимаем на кнопку. Логично рассуждать, что дальше идёт событие нажатия на кнопку и какой-то код обрабатывает наш ввод. Естественно это событие: button1_Click.

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

.method private hidebysig instance void button1_Click(object sender, class [mscorlib]System.EventArgs e) cil managed

{

.maxstack 2



IL_0000: ldarg.0

IL_0001: ldfld class [System.Windows.Forms]System.Windows.Forms.TextBox Project2.WinForm::textBox1

IL_0006: callvirt instance string [System.Windows.Forms]System.Windows.Forms.Control::get_Text()

IL_000b: ldstr "6458-438534-6895634"

IL_0010: call bool [mscorlib]System.String::op_Equality(string, string)

IL_0015: brfalse.s IL_0029

IL_0017: ldstr "Reg ok!"

IL_001c: ldstr "Message"

IL_0021: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string, string)

IL_0026: pop

IL_0027: br.s IL_0039

IL_0029: ldstr "Reg err!"

IL_002e: ldstr "Message"

IL_0033: call valuetype [System.Windows.Forms]System.Windows.Forms.DialogResult [System.Windows.Forms]System.Windows.Forms.MessageBox::Show(string, string)

IL_0038: pop

IL_0039: ret

}
“Интересные” моменты я выделил цветом. Как мы видим то что поидее должно было скрыто от глаз пользователя перед нами. Корректный номер. Можно запросто скопировать его и ввести. Программа проглотит его без проблем. Ниже я выделил цветом ключевой переход, который отвечает за логику процесса регистрации.

brfalse – является командой ветвления в программе. Если результат после вызова функции op_Equality(string, string) будет “отрицательным” (false), то осуществится переход на метку IL_0029.


3. Анализ защиты XHEO

С оффициального сайта производителя можно скачать Trial-версию программы. Версия имеет следующие ограничения:

- Все созданные проекты будут иметь “срок жизни” 3 дня.

- 30 дней использования продукта.


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


Зададимся целью защитить наш пример (project2) от злоумышленников. XHEO любезно предоставляет два варианта создания нового проекта:

  • Пустой проект

  • Проект по шаблону

Выберем пункт по шаблону (New From Template) и нам откроется неплохой выбор заготовок:

Здесь располагаются 4 закладки:

  • General – здесь находится три базовых проекта

  • Limit Examples – примеры проектов с различными ограничениями (лимиты на кол-во запущенных копий
    - Ограничение на кол-во запущенных копий
    - Привязка к определённой аппаратной конфигурации
    - Ограничение на кол-во пользователей
    , использующих программу
    - Периодические напоминания о регистрации
    - Ограничение по времени работы программы после её запуска
    - Проверка лицензии с удалённого сервера
    - Привязка к
    IP адресу
    - Привязка к домену

  • Unlock by Serial – регистрация с помощью серийного номера

  • Activation – разные способы активации ПО

Выбираем пример “Trial” из раздела “Limit Examples”.

Появится следующее окно:

После нажатия на пункт “trial” мы видим снизу рабочего окна свойства trial. Здесь представлены параметры:

  • Duration – в течение какого времени будет продолжаться trial-период

  • Ends – когда закончится испытательный срок

  • InfoURL – ссылка на информационный ресурс.

  • LogoResource - Файл-изображения для логотипа

  • PurchaseURL - Ссылка для покупки продукта

  • ShowRegisterIfAvailable – дать возможность регистрации прямо из программы

  • Started – когда начинается испытательный период

  • Terms – сообщение о триальности программы

  • UseGUI – показывать диалоговое окно

  • UsePurchaseGUI – диалоговое окно для раздела покупки программы

Заключение


В заключение мне хотелось бы подвести черту под всем выше изложенным.

Несомненно, представленная выше информация демонстрирует довольно большие плюсы обфускации:



  1. Полученная сборка с трудом поддается дизассемблированию (зачастую не поддается вообще)

  2. Запутывание графа управления позволяет затруднить работу дизассемблера

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

  4. Процесс обфускации уменьшает размер программы

К сожалению, существуют и некоторые негативные стороны обфускации:

  1. Процесс обфускации может привести программу к неработоспособному состоянию

  2. Запутывание графа потока управления приводит к замедлению работы программы

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

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

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





Смотрите также:
Анализ программных комплексов для защиты по от нелицензионного использования и копирования
310.43kb.
1 стр.
Система защиты от несанкционированного копирования
361.57kb.
1 стр.
Н. Н. Блавацкая программы защиты от несанкционированного копирования. ( Л екция №8 для студентов ) ( Время 2 часа) Форма обучения: дневная Лекция
199.38kb.
1 стр.
Анализ способов защиты приложений…
78.18kb.
1 стр.
Анализ средств защиты приложений от несанкционированного копирования
83.03kb.
1 стр.
Лабораторная работа №7 Изучение аналитических моделей надежности программного обеспечения Цель и задачи работы
78.58kb.
1 стр.
Лабораторная работа №7 по дисциплине «эксплуатацияэвми систем» Изучение аналитических моделей надежности программного обеспечения
42.81kb.
1 стр.
Программа дисциплины "информационная безопасность и защита информации" Рекомендуется Министерством образования РФ для направления подготовки
167.34kb.
1 стр.
Унификация процесса разработки программных комплексов
30.78kb.
1 стр.
Программа Системный инженер (специалист по эксплуатации аппаратно-программных комплексов персональных компьютеров и сетей на их основе)
69.2kb.
1 стр.
Современные тенденции в разработке средств визуализации программного обеспечения параллельных вычислений
34.32kb.
1 стр.
Контрольные вопросы для самопроверки Литература Программа по курсу «Информационные технологии в системе Федерального Казначейства»
1670.52kb.
6 стр.