软件License设计
如何保护软件版权,最常用的办法就是设计一套license验证框架。
1、我们的常规需求如下:
.可以限制软件只能在一台机器上使用; 目前很多软件都是一机一码的销售,软件换一台机器则不能使用,想要几台机器使用就得购买几个license;
.可以设置一个使用期限; 试用版软件一般有几十天的免费使用期,销售时也可以分为一年版、终生版等; .可以设置能使用的权限; 试用版软件对处理能力有限制,比如短信发送软件设置发送条数限制,抽奖软件设置总人数限制,打印软件试用版插一个软件广告等等;
进一步分析如下:
试用版:无需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=,
/// <summary>
/// 有期限版
/// </summary>
Expiration=,
/// <summary>
/// 终身免费版
/// </summary>
Free=
}
}
结构说明:
为什么这样设计就可以基本达到要求呢?首先一机一码就要包含客户机器的唯一标识,可以通过获取机器硬件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 = ; i < bt.Length; i++)
{
byte b = bt[i];
int n, n1, n2;
n = (int)b;
n1 = n & ;
n2 = (n >> ) & ;
if (n2 > )
s += ((char)(n2 - + (int)'A')).ToString();
else
s += n2.ToString();
if (n1 > )
s += ((char)(n1 - + (int)'A')).ToString();
else
s += n1.ToString();
if ((i + ) != bt.Length && (i + ) % == ) 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, , data.Length);
stream.Position = ;
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()
};
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:
这款大家应该比较熟悉了,官方自带的。
使用教程:https://www.cnblogs.com/tuyile006/p/9183925.html
软件License设计的更多相关文章
- 基于ARM处理器的反汇编器软件简单设计及实现
写在前面 2012年写的毕业设计,仅供参考 反汇编的目的 缺乏某些必要的说明资料的情况下, 想获得某些软件系统的源代码.设计思想及理念, 以便复制, 改造.移植和发展: 从源码上对软件的可靠性和安全性 ...
- 手机软件mockup设计工具
软件界面设计工具 UIDesigner v2.5 详见 http://www.downyi.com/downinfo/26770.html
- gambit软件license文件
最近自己的gambit软件license文件已经到期,后面采用fluent的license文件后,可以使用,但不能导入文件.不过通过努力,终于找到了可以实现导入文件的代码,并且可以实现无限期的使用fl ...
- 20155338课程设计个人报告——基于ARM实验箱的Android交友软件的设计与实现
课程设计个人报告--基于ARM实验箱的Android交友软件的设计与实现 个人贡献 实验环境的搭建 代码调试 在电脑上成功运行 研究程序代码撰写小组报告 一.实验环境 1.Eclipse软件开发环境: ...
- 软件License认证方案的设计思路
销售license是商业软件的贯用商业模式.用户向商家购买软件安装盘搭载license许可,才可以使用该软件.我们作为软件开发者,为了保护自身的权益,在软件开发过程中也不可避免的会设计license管 ...
- 开源软件License汇总
用到的open source code越多,遇到的开源License协议就越多.License是软件的授权许可,里面详尽表述了你获得代码后拥有的权利,可以对别人的作品进行何种操作,何种操作又是被禁止的 ...
- tl;drLegal ——开源软件license的搜索引擎
TLDRLegal - Open Source Licenses Explained in Plain English可以很方便查询各个开源license的总结(能做什么,不能做什么),还能比较不同的 ...
- 谈谈java程序代码保护及license设计
理论上讲,不存在牢不可破的漏洞,只是时间和成本问题.通常我们认为的不可破解,说的是破解需要难以接受的时间和成本.对于java程序来说,class文件很容易被反编译,所以理论上而言,对java程序做li ...
- 课程设计个人报告——基于ARM实验箱的Android交友软件的设计与实现
个人贡献 熟悉试验箱各元件功能以及连接组装试验箱 一.实验内容 研究实验箱串口.USB线的调通连接 二.实践步骤 1.打开实验箱,首先了解各元件功能 这个是LTE模块,也叫4G模块,具体的作用是硬件将 ...
随机推荐
- hdu1071
#include <iostream> #include <stdio.h> using namespace std; int main() { int t; double x ...
- Unity 中动态修改碰撞框(位置,大小,方向)
在Unity中,玩家处于不同的状态,要求的碰撞框的 位置/大小/方向 会有所改变,怎么动态的修改碰撞框呢? 下面是Capsure Collider(胶囊体)的修改: CapsuleCollider.d ...
- [笔记]解决git本地仓库不能上传到gitee远程仓库的问题
关键词:git.gitee.码云.上传远程仓库失败 1.gitee有一个远程仓库名字是CommandModel,里面只有两个README文件 2.假如我目录 D:\eclipse\workspace ...
- 2018CCPC网络赛A(优先队列,思维)
#include<bits/stdc++.h>using namespace std;priority_queue<pair<int,int>>q;int main ...
- c#中ToString("yyyyMMdd") 与ToString("yyyymmdd")区别
string a= DateTime.Now.ToString("yyyyMMdd") ; string b=DateTime.Now.ToString("yyyymmd ...
- puppet批量管理500多台服务器
前言 puppet使用了有一段时间了,之前写的手顺书一直未发布到blog上来,今天正好有空,写下一点笔记.公司在用的服务器有500多台,基本都为CentOS,版本有5和6两种,管理起来很不方便,尤其是 ...
- CentOS系统日志
日志分类: 一./var目录 一些数据库如MySQL则在/var/lib下,用户未读的邮件的默认存放地点为/var/spool/mail 二.:/var/log/ 系统的引导日志:/var/log/b ...
- (转)TComboBox patch for Delphi 7
unit D7ComboBoxStringsGetPatch; // The patch fixes TCustomComboBoxStrings.Get method . interface {$I ...
- 练习六:斐波那契数列(fibonacci)
题目:斐波那契数列. 程序分析:斐波那契数列(Fibonacci sequence),又称黄金分割数列,指的是这样一个数列:0.1.1.2.3.5.8.13.21.34.……. 在数学上,斐波那契数列 ...
- mycat学习日记:关于联表查询
https://www.cnblogs.com/toulon/p/4832895.html 在使用数据库中间件之前,我就想到分库分表的操作对于联表操作可能会显得非常复杂.因为如果数据是分片存储的,如果 ...