软件License设计

如何保护软件版权,最常用的办法就是设计一套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;
        }
    }
}
View Code

说明:上面的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);
        }
    }
}
View Code

以及加解密工具: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;
            }
        }
    }
}
View Code

注册表操作需要用到管理员权限,否则会提示无权限操作注册表。解决办法是在项目中添加“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;
        }

    }
}
View Code

 

管理器的使用Demo如下:

 

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

 

posted @ 2019-05-20 11:09 小y 阅读(...) 评论(...) 编辑 收藏