如何保護軟件版權,最常用的辦法就是設計一套license驗證框架。
1、我們的常規需求如下:
1.可以限制軟件只能在一臺機器上使用;
目前很多軟件都是一機一碼的銷售,軟件換一臺機器則不能使用,想要幾臺機器使用就得購買幾個license;
2.可以設置一個使用期限;
試用版軟件一般有幾十天的免費使用期,銷售時也可以分爲一年版、終生版等;
3.可以設置能使用的權限;
試用版軟件對處理能力有限制,比如短信發送軟件設置發送條數限制,抽獎軟件設置總人數限制,打印軟件試用版插一個軟件廣告等等;
進一步分析如下:
試用版:無需License,安裝後的默認版本;有使用期限;有功能限制或插入廣告等;
有限期限版:需要License;有使用期限;無功能限制;
終身免費版:需要License;無限制;
一般破解的辦法有以下幾種:
1.試用版到期後修改系統時間;
2.試用版到期後找到license文件並修改或刪除;
3.試用版到期後卸載軟件,重新安裝;
4.黑客直接反編譯軟件,屏蔽掉驗證License的邏輯;
2、License結構設計
針對以上需求,我們來對應設計License的結構如下:
using System;
namespace LicenseDemo
{
/// <summary>
/// License信息
/// </summary>
[Serializable]
public class LicenseModel
{
//客戶機器唯一識別碼,由客戶端生成
public string CustomMachineCode { get; set; }
//最後使用時間
public DateTime LastUseTime { get; set; }
//過期時間expire
public DateTime ExpireTime { get; set; }
//權限類型(如可分爲 0: 15天試用版 1:1年版 2:終身版)
public RoleType CustomRole { get; set; }
}
/// <summary>
/// 幾種角色類型
/// </summary>
[Serializable]
public enum RoleType
{
/// <summary>
/// 試用版
/// </summary>
Trial=0,
/// <summary>
/// 有期限版
/// </summary>
Expiration=1,
/// <summary>
/// 終身免費版
/// </summary>
Free=2
}
}
結構說明:
爲什麼這樣設計就可以基本達到要求呢?首先一機一碼就要包含客戶機器的唯一標識,可以通過獲取機器硬件CPU、主板、Bios、Mac地址、顯卡、聲卡等的ID來生成;然後需要有個會員類型來區分是試用版、有限期限版還是永久免費版;過期時間是用來限制使用時間的,就不用多說;最後使用時間這個字段是爲了防止用戶通過修改系統時間,簡單的跨過試用期限;當然我們業務層還可以加一下其他功能限制或廣告來繼續促成用戶使用正版;
用戶購買License後,這個license如何保存,試用版本的License如何保證即使用戶卸載了軟件重裝,也依然不能改變試用時間。這就要保存好License,可以放到隱藏系統文件裏面、註冊表裏面、遠程服務器端,安全係數會依次提高;
具體採用什麼方式也跟你的軟件被什麼客戶羣使用有關係,比如你的軟件主要用於上市公司,那麼你都不用擔心盜版問題,上市公司自己會找你買正版,他得規避法律風險;你的license就可以放在明處,比如著名的圖像算法處理軟件Haclon,就是每個月發佈一次試用版的License,你自己去放到自己的軟件文件夾下面;如果你刪掉他的license,那麼軟件直接打不開。
如果你的客戶是C端個人用戶,那麼就得考慮法律罪責比較難的問題了,只能從加強自己的軟件驗證水平,防破解了;
然後設計一下License分發和驗證的流程:
試用版的客戶端,是不需要license的,軟件第一次啓動時先找一下本地是否有License,如果沒有則默認生成一個試用版License,下次直接讀取到的就是試用版License。後續用戶購買正版License後可以在軟件中重新激活正版License。
3、會使用到的一些工具類
生成客戶機器碼的工具類:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Management;
using System.Security.Cryptography;
namespace LicenseDemo
{
/// <summary>
/// 硬件碼生成器
/// 作者博客:https://www.cnblogs.com/tuyile006/
/// </summary>
public class HardwareInfo
{
private static string myMachineCode = "";
/// <summary>
/// 生成一個16字節的機器唯一碼
/// 如: 4876-8DB5-EE85-69D3-FE52-8CF7-395D-2EA9
/// </summary>
/// <returns></returns>
public static string GetMachineCode()
{
if (string.IsNullOrEmpty(myMachineCode))
{
string omsg = " CPU >> " + CpuId() + " BIOS >> " +
BiosId() + " BASE >> " + BaseId();
// + " DISK >> " + DiskId() + " VIDEO >> " +
//VideoId() + " MAC >> " + MacId();
myMachineCode = MD5(omsg);
}
return myMachineCode;
}
/// <summary>
/// MD5哈希加密
/// </summary>
/// <param name="scr">原始string數據</param>
/// <returns>加密後的數據</returns>
private static string MD5(string scr)
{
MD5 md5 = new MD5CryptoServiceProvider();
byte[] palindata = Encoding.Default.GetBytes(scr);//將要加密的字符串轉換爲字節數組
byte[] encryptdata = md5.ComputeHash(palindata);//將字符串加密後也轉換爲字符數組
return GetHexString(encryptdata);//將加密後的字節數組轉換爲加密字符串
}
/// <summary>
/// byte[]轉換成十六進制
/// </summary>
/// <param name="bt"></param>
/// <returns></returns>
private static string GetHexString(byte[] bt)
{
string s = string.Empty;
for (int i = 0; i < bt.Length; i++)
{
byte b = bt[i];
int n, n1, n2;
n = (int)b;
n1 = n & 15;
n2 = (n >> 4) & 15;
if (n2 > 9)
s += ((char)(n2 - 10 + (int)'A')).ToString();
else
s += n2.ToString();
if (n1 > 9)
s += ((char)(n1 - 10 + (int)'A')).ToString();
else
s += n1.ToString();
if ((i + 1) != bt.Length && (i + 1) % 2 == 0) s += "-";
}
return s;
}
public static string CpuId()
{
//Uses first CPU identifier available in order of preference
//Don't get all identifiers, as it is very time consuming
string retVal = identifier("Win32_Processor", "UniqueId");
if (retVal == "") //If no UniqueID, use ProcessorID
{
retVal = identifier("Win32_Processor", "ProcessorId");
if (retVal == "") //If no ProcessorId, use Name
{
retVal = identifier("Win32_Processor", "Name");
if (retVal == "") //If no Name, use Manufacturer
{
retVal = identifier("Win32_Processor", "Manufacturer");
}
//Add clock speed for extra security
retVal += identifier("Win32_Processor", "MaxClockSpeed");
}
}
return retVal;
}
//BIOS Identifier
public static string BiosId()
{
return identifier("Win32_BIOS", "Manufacturer")
+ identifier("Win32_BIOS", "SMBIOSBIOSVersion")
+ identifier("Win32_BIOS", "IdentificationCode")
+ identifier("Win32_BIOS", "SerialNumber")
+ identifier("Win32_BIOS", "ReleaseDate")
+ identifier("Win32_BIOS", "Version");
}
//Main physical hard drive ID
public static string DiskId()
{
return identifier("Win32_DiskDrive", "Model")
+ identifier("Win32_DiskDrive", "Manufacturer")
+ identifier("Win32_DiskDrive", "Signature")
+ identifier("Win32_DiskDrive", "TotalHeads");
}
//Motherboard ID
public static string BaseId()
{
return identifier("Win32_BaseBoard", "Model")
+ identifier("Win32_BaseBoard", "Manufacturer")
+ identifier("Win32_BaseBoard", "Name")
+ identifier("Win32_BaseBoard", "SerialNumber");
}
//Primary video controller ID
public static string VideoId()
{
return identifier("Win32_VideoController", "DriverVersion")
+ identifier("Win32_VideoController", "Name");
}
//First enabled network card ID
public static string MacId()
{
return identifier("Win32_NetworkAdapterConfiguration", "MACAddress", "IPEnabled");
}
//Return a hardware identifier
private static string identifier(string wmiClass, string wmiProperty, string wmiMustBeTrue)
{
string result = "";
ManagementClass mc = new ManagementClass(wmiClass);
ManagementObjectCollection moc = mc.GetInstances();
foreach (ManagementObject mo in moc)
{
if (mo[wmiMustBeTrue].ToString() == "True")
{
//Only get the first one
if (result == "")
{
try
{
result = mo[wmiProperty].ToString();
break;
}
catch
{
}
}
}
}
return result;
}
//Return a hardware identifier
private static string identifier(string wmiClass, string wmiProperty)
{
string result = "";
ManagementClass mc = new ManagementClass(wmiClass);
ManagementObjectCollection moc = mc.GetInstances();
foreach (ManagementObject mo in moc)
{
//Only get the first one
if (result == "")
{
try
{
result = mo[wmiProperty].ToString();
break;
}
catch
{
}
}
}
return result;
}
}
}
說明:上面的HardwareInfo類就是幫助生成機器唯一信息的。實際運用中,mac地址、聲卡網卡等容易變動,可以不加到信息裏面。
對象序列化幫助工具:
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
namespace LicenseDemo
{
/// <summary>
/// 序列化工具類
/// 作者博客:https://www.cnblogs.com/tuyile006/
/// </summary>
public class SerializeHelper
{
/// <summary>
/// 將對象序列化爲二進制數據
/// </summary>
/// <param name="obj"></param>
/// <returns></returns>
public static byte[] SerializeToBinary(object obj)
{
using (MemoryStream stream = new MemoryStream())
{
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(stream, obj);
byte[] data = stream.ToArray();
stream.Close();
return data;
}
}
/// <summary>
/// 將二進制數據反序列化
/// </summary>
/// <param name="data"></param>
/// <returns></returns>
public static object DeserializeWithBinary(byte[] data)
{
using (MemoryStream stream = new MemoryStream())
{
stream.Write(data, 0, data.Length);
stream.Position = 0;
BinaryFormatter bf = new BinaryFormatter();
object obj = bf.Deserialize(stream);
stream.Close();
return obj;
}
}
/// <summary>
/// 將二進制數據反序列化爲指定類型對象
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="data"></param>
/// <returns></returns>
public static T DeserializeWithBinary<T>(byte[] data)
{
return (T)DeserializeWithBinary(data);
}
}
}
以及加解密工具:EncodeHelper 源碼見我的另一篇文章:
註冊表操作類:
using Microsoft.Win32;
namespace LicenseDemo
{
/// <summary>
/// 註冊表工件類
/// 作者博客:https://www.cnblogs.com/tuyile006/
/// </summary>
public class RegistryHelper
{
//用於存儲你軟件信息的註冊表菜單名
public static string YourSoftName = "YourSoftName";
/// <summary>
/// 獲取你軟件下對應註冊表鍵的值
/// </summary>
/// <param name="keyname">鍵名</param>
/// <returns></returns>
public static string GetRegistData(string keyname)
{
if (!IsYourSoftkeyExit()) return string.Empty;
string registData;
RegistryKey aimdir = Registry.LocalMachine.OpenSubKey("SOFTWARE\\"+ YourSoftName, RegistryKeyPermissionCheck.ReadWriteSubTree, System.Security.AccessControl.RegistryRights.FullControl);
registData = aimdir.GetValue(keyname).ToString();
return registData;
}
/// <summary>
/// 向你的軟件註冊表菜單下添加鍵值
/// </summary>
/// <param name="keyname">鍵名</param>
/// <param name="keyvalue">值</param>
public static void WriteRegedit(string keyname, string keyvalue)
{
RegistryKey software = Registry.LocalMachine.OpenSubKey("SOFTWARE", RegistryKeyPermissionCheck.ReadWriteSubTree, System.Security.AccessControl.RegistryRights.FullControl);
RegistryKey aimdir ;
if (!IsYourSoftkeyExit()) //不存在則創建
{
aimdir = software.CreateSubKey(YourSoftName);
}
else //存在則open
{
aimdir = software.OpenSubKey(YourSoftName, true);
}
aimdir.SetValue(keyname, keyvalue,RegistryValueKind.String);
aimdir.Close();
}
/// <summary>
/// 刪除你軟件註冊表菜單下的鍵值
/// </summary>
/// <param name="keyname">鍵名</param>
public static void DeleteRegist(string keyname)
{
if (!IsYourSoftkeyExit()) return;
string[] aimnames;
RegistryKey aimdir = Registry.LocalMachine.OpenSubKey("SOFTWARE\\" + YourSoftName, RegistryKeyPermissionCheck.ReadWriteSubTree, System.Security.AccessControl.RegistryRights.FullControl);
aimnames = aimdir.GetValueNames();
foreach (string aimKey in aimnames)
{
if (aimKey == keyname)
aimdir.DeleteValue(keyname);
}
aimdir.Close();
}
/// <summary>
/// 判斷你軟件註冊表菜單下鍵是否存在
/// </summary>
/// <param name="keyname">鍵名</param>
/// <returns></returns>
public static bool IsRegeditExit(string keyname)
{
if (!IsYourSoftkeyExit()) return false;
string[] subkeyNames;
RegistryKey aimdir = Registry.LocalMachine.OpenSubKey("SOFTWARE\\"+ YourSoftName, RegistryKeyPermissionCheck.ReadWriteSubTree, System.Security.AccessControl.RegistryRights.FullControl);
subkeyNames = aimdir.GetValueNames();// GetSubKeyNames();
foreach (string kn in subkeyNames)
{
if (kn == keyname)
{
Registry.LocalMachine.Close();
return true;
}
}
return false;
}
/// <summary>
/// 刪除你軟件的註冊表項
/// </summary>
public static void DeleteYourSoftKey()
{
Registry.LocalMachine.DeleteSubKeyTree("SOFTWARE\\" + YourSoftName);
Registry.LocalMachine.Close();
}
/// <summary>
/// 判斷你軟件的鍵是否存在
/// </summary>
/// <returns></returns>
private static bool IsYourSoftkeyExit()
{
using (RegistryKey yourSoftkey = Registry.LocalMachine.OpenSubKey("SOFTWARE\\" + YourSoftName, RegistryKeyPermissionCheck.ReadWriteSubTree, System.Security.AccessControl.RegistryRights.FullControl))
{
return yourSoftkey != null;
}
}
}
}
註冊表操作需要用到管理員權限,否則會提示無權限操作註冊表。解決辦法是在項目中添加“app.manifest"文件
並修改manifest中的如下部分。
其實存到註冊表依然不是好辦法,最好還是將用戶 license保存到服務端,用從服務端請求的方式。或者兩者結合,有網絡的時候進行網絡驗證。
最後的License管理器:
using System;
namespace LicenseDemo
{
/// <summary>
/// License管理器
/// 作者博客:https://www.cnblogs.com/tuyile006/
/// </summary>
public class LicenseManage
{
/// <summary>
/// 當前程序的license 業務層需配合控制權限
/// </summary>
public static LicenseModel ApplicationLicense = null;
/// <summary>
/// 提取客戶機器信息,返回編碼
/// </summary>
/// <returns></returns>
public static string GetMachineCode()
{
return HardwareInfo.GetMachineCode();
}
private const string regeditkey = "lic";//註冊表鍵名
private const string aeskey = "小y加;&tu@"; //密鑰
/// <summary>
/// 服務端生成License文本 可授權給客戶
/// </summary>
/// <param name="lic">LicenseModel對象,由客戶提供機器碼,並由商業提供期限和權限角色</param>
/// <returns></returns>
public static string CreateLicenseString(LicenseModel lic)
{
byte[] licByte = SerializeHelper.SerializeToBinary(lic);
return EncodeHelper.AES(Convert.ToBase64String(licByte), aeskey);
}
/// <summary>
/// 客戶端獲取本地的license 根據自己設計的存儲介質,可以是從文件中取、也可以是註冊表或遠程服務器上取。
/// </summary>
/// <returns></returns>
public static LicenseModel GetLicense()
{
if (LicenseManage.ApplicationLicense != null) return LicenseManage.ApplicationLicense;
try
{
//如果以前裝過,則從註冊表取值 這裏可以改成從數據庫、文件、或服務端
//未取到鍵則建一個
if (!RegistryHelper.IsRegeditExit(regeditkey))
{
//第一次使用默認是試用版
LicenseModel license = new LicenseModel()
{
CustomMachineCode = GetMachineCode(),
CustomRole = RoleType.Trial,
LastUseTime=DateTime.Now,
ExpireTime = DateTime.Now.AddDays(30)
};
RegistryHelper.WriteRegedit(regeditkey, CreateLicenseString(license));
LicenseManage.ApplicationLicense = license;
}
else
{
string licFromReg = RegistryHelper.GetRegistData(regeditkey);
try
{
string strlic = EncodeHelper.AESDecrypt(licFromReg, aeskey);
byte[] licbyte = Convert.FromBase64String(strlic);
LicenseModel lm = SerializeHelper.DeserializeWithBinary<LicenseModel>(licbyte);
//取到的值還原license並返回
LicenseManage.ApplicationLicense = lm;
}
catch(Exception ex1)
{
//_log.Error(ex1);
//如果從註冊表中取到的值發現被篡改,則直接試用版到期,不給使用。
LicenseModel licenseErr = new LicenseModel()
{
CustomMachineCode = GetMachineCode(),
CustomRole = RoleType.Trial,
LastUseTime = DateTime.Now,
ExpireTime = DateTime.Now
};
}
}
}
catch(Exception ex)
{
//_log.Error(ex);
}
return LicenseManage.ApplicationLicense;
}
/// <summary>
/// 客戶端驗證License,存儲
/// </summary>
/// <param name="lic">服務端授權給客戶的License密文</param>
/// <returns></returns>
public static bool VerifyLicense(string lic)
{
if(string.IsNullOrEmpty(lic)) return false;
try
{
string strlic = EncodeHelper.AESDecrypt(lic, aeskey);
byte[] licbyte = Convert.FromBase64String(strlic);
LicenseModel lm = SerializeHelper.DeserializeWithBinary<LicenseModel>(licbyte);
//簡單驗證機器碼、role、期限。具體角色權限限制需要在業務系統中實現。
if (VerifyLicense(lm))
{
LicenseManage.ApplicationLicense = lm;
return true;
}
}
catch
{
//_log.Error(ex);
}
//否則直接返回原始試用版
return false;
}
/// <summary>
/// 簡單驗證licensemode對象是否合法,不存儲
/// </summary>
/// <param name="licmod"></param>
/// <returns></returns>
public static bool VerifyLicense(LicenseModel licmod)
{
//簡單驗證機器碼、role、期限。具體角色權限限制需要在業務系統中實現。
bool isHaveRight = false;
if (licmod.CustomMachineCode == GetMachineCode())
{
if (licmod.CustomRole == RoleType.Free)
{
isHaveRight = true;
}
else if (licmod.LastUseTime < DateTime.Now && licmod.ExpireTime > DateTime.Now)
{
isHaveRight = true;
}
}
if (isHaveRight)
{
licmod.LastUseTime = DateTime.Now;
RegistryHelper.WriteRegedit(regeditkey, CreateLicenseString(licmod));
}
return isHaveRight;
}
public static void DeleteLicense()
{
RegistryHelper.DeleteRegist(regeditkey);
LicenseManage.ApplicationLicense = null;
}
}
}
管理器的使用Demo如下:
做好了License架構,是不是軟件版權保護就完成了呢?答案是否定的。現在我們已經造了銀行保險櫃的門,卻只有籬笆一樣容易攻破的牆,黑客直接反編譯你的軟件,去掉權限驗證的邏輯再重新編譯,就可以終身永久免費使用你的勞動成果,所以不加密的軟件在黑客眼裏就像在裸奔。混淆、加殼是另一個更加複雜的技術,下面介紹幾款混淆工具。
4.混淆工具介紹:
Eziriz .NET Reactor:
主要功能包括:NecroBit IL(轉爲非託管代碼)、反 ILDASM(反編譯器)、混淆代碼、合併、壓縮源碼、支持命令行等,支持所有 .NET 框架和幾乎所有開發語言,如 C#、C++.NET、VB.NET、Delphi.NET、J# 等等。
使用教程:https://blog.csdn.net/jyxyscf/article/details/78478631
ConfuserEx:
是一款開源.net混淆器。
使用教程:https://www.cnblogs.com/tuyile006/p/8461326.html
Dotfuscator:
這款大家應該比較熟悉了,官方自帶的。