Опубликовав свой первый проект в Steam «любовался» достаточно неплохим количеством скачиваний. Только какой в этом толк, если вся эта движуха происходила на торрент-трекерах...
Поэтому всерьез задумался о защите своих коммерческих проектов от пиратов.
Конечно, универсального способа защиты от пиратов не существует и тема защиты от пиратства является как-никак актуальной: темой постоянных дискуссий и споров.
В рамках данной статьи рассмотрим вариант дополнительной защиты Unity-проекта (под Windows) с использованием библиотеки kernel32.dll. А использовать данный способ защиты в своем проекте, либо не использовать – решать вам. И так приступим.
На нулевой нашей сцене создадим объект с названием SecurityManager и повесим на него скрипт с названием Security.
Подключаем необходимые библиотеки:
using UnityEngine;
using System;
using System.Text;
using System.IO;
Объявим необходимые переменные.
private string sn = "";
public string folder = "/Data";
public string fileName = "Settings.dat";
public string code = "AG7XpyPfmwN28193";
Значение переменной code придумываем любое. Желательно с помощью генератора паролей.
Создаем функцию с именем DebugSave() и пишем следующий код.
private void DebugSave()
{
try //обработка исключений
{
sn = code;
//кодируем в двоичный код
byte[] buf = Encoding.UTF8.GetBytes(sn);
StringBuilder sb = new StringBuilder(buf.Length * 8);
foreach (byte b in buf)
{
sb.Append(Convert.ToString(b, 2).PadLeft(8, '0'));
}
string binaryStr = sb.ToString();
//создаем папку в директории проекта
Directory.CreateDirectory(Application.dataPath + folder);
//сохраняем в файл
using (var stream = File.Open(Application.dataPath + folder + "/" + fileName, FileMode.Create))
{
using (var writer = new BinaryWriter(stream, Encoding.UTF8, false))
{
writer.Write(binaryStr);
writer.Close();
}
}
}
catch
{
Debug.Log(message: "Ошибка чтения в файл"); //выводим сообщение об ошибке
}
}
И вызовем ее при старте сцены:
void Start()
{
DebugSave();
}
При выполнении функции DebugSave() в директории проекта создается папка с именем Data. В папке Data создается бинарный файл с именем Settings.dat. Значение переменной code кодируется в двоичный код и записывается в файл Settings.dat. Кодируем в двоичный код для того, чтобы обычный юзер не смог прочитать значение в файле с помощью блокнота
Теперь приступим к реализации непосредственно самой защиты. Код работы с библиотекой kernel32.dll подсмотрел ТУТ.
Конечно, данную защиту можно реализовать с помощью стандартной юнитовской команды SystemInfo.deviceUniqueIdentifier, но в случае апгрейда ПК (замены процессора, прошивки BIOS) наш проект будет ругаться о пиратстве. Нас такой расклад не устраивает.
Подключаем следующую библиотеку:
using System.Runtime.InteropServices;
И объявляем необходимые переменные:
[DllImport("kernel32.dll")]
private static extern long GetVolumeInformation(
string PathName,
StringBuilder VolumeNameBuffer,
UInt32 VolumeNameSize,
ref UInt32 VolumeSerialNumber,
ref UInt32 MaximumComponentLength,
ref UInt32 FileSystemFlags,
StringBuilder FileSystemNameBuffer,
UInt32 FileSystemNameSize);
public string disk; // задать в инспекторе "C:\"
Для переменной disk в инспекторе указываем "C:\". В самом скрипте не получается – ругается IDE.
Создаем функцию с именем Getvolumeinformation() и пишем следующий код:
private void Getvolumeinformation() //считываем системную информацию
{
string drive_letter = disk;
drive_letter = drive_letter.Substring(0, 1) + ":\\";
uint serial_number = 0;
uint max_component_length = 0;
StringBuilder sb_volume_name = new StringBuilder(256);
UInt32 file_system_flags = new UInt32();
StringBuilder sb_file_system_name = new StringBuilder(256);
if (GetVolumeInformation(drive_letter, sb_volume_name,
(UInt32)sb_volume_name.Capacity, ref serial_number,
ref max_component_length, ref file_system_flags,
sb_file_system_name,
(UInt32)sb_file_system_name.Capacity) == 0)
{
Debug.Log(message: "Error getting volume information.");
}
else
{
sn = serial_number.ToString(); //серийный номер
Debug.Log(message: sn);
}
}
Сразу добавим функцию, отвечающую за вывод системных сообщений:
public static class NativeWinAlert
{
[System.Runtime.InteropServices.DllImport("user32.dll")]
private static extern System.IntPtr GetActiveWindow();
public static System.IntPtr GetWindowHandle()
{
return GetActiveWindow();
}
[DllImport("user32.dll", SetLastError = true)]
static extern int MessageBox(IntPtr hwnd, String lpText, String lpCaption, uint uType);
/// <summary>
/// Shows Error alert box with OK button.
/// </summary>
/// <param name="text">Main alert text / content.</param>
/// <param name="caption">Message box title.</param>
public static void Error(string text, string caption)
{
try
{
MessageBox(GetWindowHandle(), text, caption, (uint)(0x00000000L | 0x00000010L));
Debug.Log("Игра закрылась");
Application.Quit(); // закрыть приложение
}
catch (Exception ex) { }
}
}
Код функции, отвечающую за вывод системных сообщений, позаимствовал ТУТ.
Создаем функцию с именем Save() и пишем следующий код:
private void Save()
{
try //обработка исключений
{
//кодируем в двоичный код
byte[] buf = Encoding.UTF8.GetBytes(sn);
StringBuilder sb = new StringBuilder(buf.Length * 8);
foreach (byte b in buf)
{
sb.Append(Convert.ToString(b, 2).PadLeft(8, '0'));
}
string binaryStr = sb.ToString();
//сохраняем в файл
using (var stream = File.Open(Application.dataPath + folder + "/" + fileName, FileMode.Create))
{
using (var writer = new BinaryWriter(stream, Encoding.UTF8, false))
{
writer.Write(binaryStr);
writer.Close(); //закрываем файл
}
}
}
catch
{
Debug.Log(message: "Ошибка чтения в файл"); //выводим сообщение об ошибке
}
}
Функция перевода двоичного когда в текст:
public static string BinaryToString(string data)
{
List<Byte> byteList = new List<Byte>();
for (int i = 0; i < data.Length; i += 8)
{
byteList.Add(Convert.ToByte(data.Substring(i, 8), 2));
}
return Encoding.ASCII.GetString(byteList.ToArray());
}
Создаем функцию с именем Set() и пишем следующий код:
private void Set()
{
if (File.Exists(Application.dataPath + folder + "/" + fileName)) //проверяем наличие файла, если его нет выводим сообщение о приатстве.
{
using (var stream = File.Open(Application.dataPath + folder + "/" + fileName, FileMode.Open))
{
using (var reader = new BinaryReader(stream, Encoding.UTF8, false))
{
string binaryStr = reader.ReadString();
reader.Close(); //закрываем файл
//двоичный код переобразовываем в строку
string resultText = BinaryToString(binaryStr);
Getvolumeinformation(); //читаем серийный номер диска
if ((resultText == code) || (resultText == sn))
{
if (resultText == code)
{
Save();
}
}
else
{
Debug.Log(message: "Пират!"); //выводим сообщение об ошибке
NativeWinAlert.Error("This copy of game is not genuine.", "Error");
}
}
}
}
else
{
Debug.Log(message: "Пират!"); //выводим сообщение об ошибке
NativeWinAlert.Error("This copy of game is not genuine.", "Error");
}
}
При выполнении функции Set() вначале проверяем наличие файла Settings.dat, если его нет, то выводим сообщение о пиратстве (вызываем функцию NativeWinAlert). Если файл Settings.dat существует, читаем его, двоичный код преобразовываем в текст, считываем значение серийного номера тома. И выполняем очередную проверку, если значение не равняется значению переменной code или значению серийному номеру тома С, то выводим сообщение о пиратстве (вызываем функцию NativeWinAlert), в противном случае в файл Settings.dat записываем значение серийного номера тома С.
В функции Start() закоментируем вызов функции DebugSave() (она нужна только нам) и добавим вызов функции Set().
void Start()
{
Set();
//DebugSave(); //записываем в файл значение переменной code
}
Теперь при первом запуске нашего проекта, значение в файле Settings.dat перезаписывается на значение серийного номера тома С. После данной процедуры копию проекта нельзя будет запустить на другой машине.
Исходный файл проекта располагается на сервисе GitHub по следующей ссылке.
Комментарии (24)
BugM
30.01.2022 04:59+14Это так наивно. Ваша защита ломается даже интересующимися студентом за часы. Не говоря уже о людях с опытом.
horror_x
30.01.2022 06:14+18Мне наивным больше кажется очередной пересчёт скачиваний торрента в упущенную прибыль…
Dreablin
30.01.2022 06:44+2Ну какой-то убыток вполне может быть от этого.
Я бы, если игра хорошая получилась, сам бы сделал демку на 10-20% геймлея и первый залил на популярные трекеры прямо в момент релиза. С возможностью переноса сейвов, конечно.
napa3um
30.01.2022 09:23+7Насколько увеличился доход от продаж после внедрения вами этой защиты в свой продукт?
uncle_doc
30.01.2022 10:10+7Вместо этой защиты, как мне кажется, больший результат принесли бы другие действия, например: Показ сообщения что вы типа пират и это не хорошо, купите игру со скидкой например или задонатьте для отключения этого сообщения ну или что-то в этом роде. В игру играть, я бы на вашем месте разрешил, даже в пиратскую.
Akuma
30.01.2022 11:33По мне так гуманнее будет сделать внутриигровую авторизацию через стимовский аккаунт. Удобнее и надежнее
qw1
30.01.2022 12:00Против этого есть стандартные эмуляторы стим, которые подсунут игре аккаунт с любым заранее настроенным именем (например, CODEX :) ) и скажут игре, что она куплена на этом аккаунте.
Akuma
30.01.2022 14:24+2Так против всего есть методы. Денува ломается так же как и все остальное.
Просто то, что в статье - какой-то гемор для пользователя.
qw1
30.01.2022 11:59+6То есть, пират может легко выложить свою копию игры на трекер, если после установки из steam он ни разу не запускал игру, и файл settings.dat ещё не перезаписан серийным номером диска. Даже код ковырять не надо.
igor_alt
31.01.2022 02:22Вспомнил, как в начале карьеры не получил ни копей прибыли от готового продукта, так как сильно боялся, что его будут воровать, а сообразить должную защиту я не смог :)
Alexsey
Все это бесполезно если не использовать IL2CPP. Две минуты и ваша защита уже отломана.
Да и возникает вопрос вполне справедливых наездов пользователей из-за ложных срабатываний - переустановка или клонирование винды (а иногда и даже банальный апгрейд на новую версию десятки) практически наверняка закончится изменением серийника тома и отваливанием защиты.
FranzDev Автор
Насчёт шифрования это понятно. Меня вот мучает другой вопрос, как с помощью Unity выдрать серийный номер самого жёсткого диска? Т.к. с библиотекой System.Management Unity не умеет работать... :(
horror_x
Вряд ли поможет, но под админом можно спросить у диска напрямую:
FranzDev Автор
Получилось решить данную задачу чуть-чуть проще: написанием отдельной библиотеки dll и подключением ее к Unity
https://github.com/FranzDevBy/Serial-Number-Disk-in-Unity
qw1
Ну, такое себе. У меня WMI-запрос «Select * from Win32_DiskDrive» первым выдал съёмный диск PhysicalDrive7 (и вообще порядок дисков разве чем-гарантируется?). А вы берёте первый диск из выдачи и привязываетесь к его серийнику. Это может кончится тем, что при вставке новой флешки игра начнёт обвинять легального пользователя в пиратстве )))
FranzDev Автор
Гарантируется латинским алфавитом.
qw1
Вы не читали, что я выше написал? У меня первым выпал PhysicalDrive7, на котором назначена буква диска E:
FranzDev Автор
А PhysicalDrive7 в какой разъём SATA (порядковый номер) на материнской плате приходит? Или вообще в разъем M.2?
qw1
Если вам интересно (хотя к проблеме не имеет никакого отношения), системный M2 SSD (диск C:) Windows 10 видит как PhysicalDrive5, а WMI в списке дисков его показывает последним. Тут полный рандом. Есть диск, подключенный через SATA-контроллер в PCI-e слоте, его номер PhysicalDrive4. И до него по нумерации, и после него, есть диски из штатных SATA-линий с материнки.
При чём здесь SATA? Я втыкаю флешку — появляется PhysicalDrive8, и в списке «Select * from Win32_DiskDrive» он где-то в середине (а мог бы и первым встать, и попасть вашей «защите» на проверку серийника — гарантий никаких нет, что так не будет).
FranzDev Автор
Ок, спасибо