目 錄
第十四章 序列號的設計... 2
14.1 設計原則... 2
14.2 設計思想... 3
14.3 代碼實現... 4
14.4 代碼混淆... 18
14.5 代碼破解... 18
14.6 小結... 18
第十四章 序列號的設計
序列號作為軟件使用授權方式之一,被廣泛使用在應用軟件方面。主要考慮到這幾方面:1.對知識產權的保護,畢竟付出來腦力勞動和體力勞動。2.商業競爭中增加防守的能力,防止被競爭對手盜取。3.增強合同的執行效力,防止另一方由於各種原因破壞合作機制。
基於上述方面,從保護、防守思維模式角度考慮,增加序列號功能是有必要的。每個作者或公司設計序列號的方式不一樣,就是因為不一樣,所以才能達到我們增加該功能的效果。
14.1 設計原則
- 序列號長度盡可能短
主要是從成本角度考慮的。例如用戶現場需要一個正版軟件的序列號,你把序列號信息通過什么方式傳遞給用戶呢?假設我們用對稱或非對稱方式生成一個很長的序列號,如果口述告訴對方的話,那么對方肯定要用紙和筆進行記錄,最后輸入到軟件后還不一定正確;如果把序列號以文件的方式通過網絡傳遞給對方,那么需要占用網絡資源,另外對方的電腦不一定有網絡環境。不管如何,很長的序列號在生成和傳遞的過程中可能涉及到的成本包括:材料成本、流量成本、人力成本和時間成本等。
如果一個字符可以表達序列號所需要的完整信息,那么是最理想的。但是,這是理想狀態,是不可能實現的,至少以我現在的能力是無法完成的。所以,要以最佳的長度表達出序列號的全部信息。
- 避免出現容易混淆的字符生成一個序列號發給了用戶,這個序列號包括:數字0和字母O,數字1和字母l。難道讓用戶一遍一遍的試嘛,這樣的用戶體驗太差了,雖然嘴上不說出來,至少感覺不太舒服。
14.2 設計思想
設計的思想要看序列號要實現什么樣的功能和具備什么屬性。從功能角度考慮,包括:1.一個計算機一個序列號;2.盡管輸入的條件都一樣,每次生成的序列號都不一樣;3.對使用的時限進行驗證;4.序列號有注冊時限,超過規定的使用時間,序列號作廢,避免短時間多次注冊。從屬性角度考慮,包括:同樣的計算機、同樣的輸入條件生成的序列號都不一樣。
我們把上述因素考慮進去,序列號長度為25位字符,序列號生成格式和元素信息如下圖:

X01-X05:為計算機的特征碼,5位字符串,獲得機器某個部件的ID,這個部件可能為CPU、網卡、硬盤等信息,把ID進行MD5加密后取前5個字符作為特征碼,來實現一機一碼。這種方式,特征碼有可能有相同的情況,但是機率很小。
X06-X13:為生成序列號的日期,8位字符串,格式為:yyyyMMdd。與后邊的使用時間限制配合使用,來驗證軟件的使用期限。
X14-X15:為注冊時間限制,2位數字字符,從生成序列號日期算起,超過此注冊時間限制,序列號將無法正常進行注冊操作。
X16-X20:為使用時間限制,5位數字字符,與生成序列號日期配合使用來驗證軟件使用期限。
X21:為序列號的偏移量,1位字符,不管在什么場景下,每次生成序列號的偏移量都不一樣。
X22-X25:為保留數據位,暫時不使用。自定義一個序列號字典信息,例如:_Dictionary ="JCB8EF2GH7K6MVP9QR3TXWY4",把容易混淆的字符去掉,這個可以自定義。序列號的每個部分都是通過隨機生成的偏移量(X21),對字典進行位移,根據輸入的數字信息對應字典的下標提取相應的字符作為序列號的一個字符。
生成序列號的大概過程:
- 在字典信息的長度范圍內隨機生成一個偏移量數字。
- 根據偏移量數字對字典進行左或右的循環移動。
- 根據輸入的數字信息,例如:2015中的2,作為下標,從字典信息中提取出相應的字符。
反向解析大概過程類似,只需要根據X21字符,與字典的字符進行匹配,對應的下標作為偏移量,就可以反向解析出各項信息。
14.3 代碼實現
1.MD5操作類:
public class Safety
{
public static string MD5(string str)
{
string strResult = "";
MD5 md5 = System.Security.Cryptography.MD5.Create();
byte[] bData = md5.ComputeHash(Encoding.Unicode.GetBytes(str));
for (int i = 0; i < bData.Length; i++)
{
strResult = strResult + bData[i].ToString("X");
}
return strResult;
}
}
2.注冊信息類:
public class RegInfo
{
public RegInfo()
{
KeySn = "";
Date=DateTime.MinValue;
RegLimitDays = 0;
UseLimitDays = 0;
Offset = 0;
}
public string KeySn { get; set; }
public DateTime Date { get; set; }
public int RegLimitDays { get; set; }
public int UseLimitDays { get; set; }
public int Offset { get; set; }
}
3.偏移操作類型:
internal enum OffsetType
{
Left,
Right
}
4. 序列號管理類
public class LicenseManage
{
/// <summary>
/// 序列號字典,把數字和字母容易混淆的字符去掉。所產生的25位序列號從這個字典中產生。
/// </summary>
private static string _Dictionary = "JCB8EF2GH7K6MVP9QR3TXWY4";
/// <summary>
/// 可以自定義字典字符串
/// </summary>
public static string Dictionary
{
get { return _Dictionary; }
set
{
if (value.Length < 9)
{
throw new ArgumentOutOfRangeException("設置的字典長度不能小於9個字符");
}
_Dictionary = value;
}
}
/// <summary>
/// 生成序列號
/// </summary>
/// <param name="key">關鍵字,一般為CPU號、硬盤號、網卡號,用於與序列號綁定,實現一機一碼</param>
/// <param name="now">現在的時間</param>
/// <param name="regLimitDays">注冊天數限制,超過此天數,再進行注冊,序列號就失效了,不能再使用了</param>
/// <param name="useLimitDays">使用天數限制,超過此天數,可以設置軟件停止運行等操作</param>
/// <returns>返回序列號,例如:xxxxx-xxxxx-xxxxx-xxxxx-xxxxx</returns>
public static string BuildSn(string key, DateTime now, int regLimitDays, int useLimitDays)
{
if (regLimitDays < 0 || regLimitDays > 9)
{
throw new ArgumentOutOfRangeException("注冊天數限制范圍為0-9");
}
if (useLimitDays < 0 || useLimitDays > 99999)
{
throw new ArgumentOutOfRangeException("使用天數限制范圍為0-99999");
}
/*
*關鍵字用MD5加密后,取后5個字符作為序列號第1組字符
*/
string md5 = Safety.MD5(key);
string x1 = md5.Substring(md5.Length - 5);
/*
* 生成隨機偏移量
*/
Random rand = new Random();
int offset = rand.Next(1, Dictionary.Length - 1);
/*
* 第5組的第1個字符保存偏移量字符,其余4個字符隨機生成,作為保留位
*/
string x5 = Dictionary[offset].ToString();
for (int i = 0; i < 4; i++)
{
x5 += Dictionary[rand.Next(0, Dictionary.Length - 1)].ToString();
}
/*
* 以注冊時間(yyyyMMdd)和注冊時間限制生成第2組和第3組序列號,一共10位字符串
*/
string dateSn = GetDateSn(now, offset);
string regLimitSn = GetRegLimitSn(regLimitDays, offset);
string x2 = dateSn.Substring(0, 5);
string x3 = dateSn.Substring(dateSn.Length - 3) + regLimitSn;
/*
*以使用時間限制生成第4組序列號,一共5位字符串
*/
string x4 = GetUseLimitSn(useLimitDays, offset);
return String.Format("{0}-{1}-{2}-{3}-{4}", x1, x2, x3, x4, x5);
}
/// <summary>
/// 注冊序列號
/// </summary>
/// <param name="key">關鍵字,一般為CPU號、硬盤號、網卡號,用於與序列號綁定,實現一機一碼</param>
/// <param name="sn">序列號</param>
/// <param name="desc">描述信息</param>
/// <returns>注冊狀態,成功:0</returns>
internal static int RegSn(string key, string sn, ref string desc)
{
if (String.IsNullOrEmpty(key) || String.IsNullOrEmpty(sn))
{
throw new ArgumentNullException("參數為空");
}
LicenseInfo regInfo = GetRegInfo(sn);
string md5 = Safety.MD5(key);
if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
{
desc = "關鍵字與序列號不匹配";
return -1;//關鍵字與序列號不匹配
}
if (regInfo.Date == DateTime.MaxValue || regInfo.Date == DateTime.MinValue || regInfo.Date > DateTime.Now.Date)
{
desc = "序列號時間有錯誤";
return -2;//序列號時間有錯誤
}
TimeSpan ts = DateTime.Now.Date - regInfo.Date;
if (ts.TotalDays > 9 || ts.TotalDays < 0)
{
desc = "序列號失效";
return -3;//序列號失效
}
if (regInfo.UseLimitDays <= 0)
{
desc = "使用期限受限";
return -4;//使用期限受限
}
Application.UserAppDataRegistry.SetValue("SN", sn);
desc = "注冊成功";
return 0;
}
/// <summary>
/// 檢測序列號,試用於時鍾定時調用
/// </summary>
/// <param name="key">關鍵字,一般為CPU號、硬盤號、網卡號,用於與序列號綁定,實現一機一碼</param>
/// <param name="desc">描述信息</param>
/// <returns>檢測狀態,成功:0</returns>
internal static int CheckSn(string key, ref string desc)
{
if (String.IsNullOrEmpty(key))
{
throw new ArgumentNullException("參數為空");
}
object val = Application.UserAppDataRegistry.GetValue("SN");
if (val == null)
{
desc = "未檢測到本機的序列號";
return -1;
}
string sn = val.ToString();
LicenseInfo regInfo = GetRegInfo(sn);
string md5 = Safety.MD5(key);
if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
{
desc = "關鍵字與序列號不匹配";
return -2;//關鍵字與序列號不匹配
}
if ((DateTime.Now.Date - regInfo.Date).TotalDays > regInfo.UseLimitDays)
{
desc = "序列使用到期";
return -3;//關鍵字與序列號不匹配
}
desc = "序列號可用";
return 0;
}
/// <summary>
/// 獲得剩余天數
/// </summary>
/// <param name="key">關鍵字,一般為CPU號、硬盤號、網卡號,用於與序列號綁定,實現一機一碼</param>
/// <returns>剩余天數</returns>
internal static int GetRemainDays(string key)
{
if (String.IsNullOrEmpty(key))
{
throw new ArgumentNullException("參數為空");
}
object val = Application.UserAppDataRegistry.GetValue("SN");
if (val == null)
{
return 0;
}
string sn = val.ToString();
LicenseInfo regInfo = GetRegInfo(sn);
string md5 = Safety.MD5(key);
if (String.CompareOrdinal(md5.Substring(md5.Length - 5), regInfo.KeySn) != 0)
{
return 0;//關鍵字與序列號不匹配,不能使用。
}
//<=0的情況,證明不可以使用。
return regInfo.UseLimitDays - (int)(DateTime.Now.Date - regInfo.Date).TotalDays;
}
/// <summary>
/// 根據序列號,反推注冊信息
/// </summary>
/// <param name="sn">序列號</param>
/// <returns>注冊信息</returns>
private static LicenseInfo GetRegInfo(string sn)
{
LicenseInfo reg = new LicenseInfo();
string[] splitSn = sn.Split('-');
if (splitSn.Length != 5)
{
throw new FormatException("序列號格式錯誤,應該帶有'-'字符");
}
reg.KeySn = splitSn[0];
reg.Offset = Dictionary.IndexOf(splitSn[4][0]);
reg.Date = GetDate(splitSn[1] + splitSn[2].Substring(0, 3), reg.Offset);
reg.RegLimitDays = GetRegLimitDays(splitSn[2].Substring(3, 2), reg.Offset);
reg.UseLimitDays = GetUseLimitDays(splitSn[3], reg.Offset);
return reg;
}
/// <summary>
/// 以當前時間和偏移量生成當前時間對應的字符串
/// </summary>
/// <param name="now">當前時間</param>
/// <param name="offset">偏移量</param>
/// <returns>返回日期對應的字符串,8位字符串</returns>
private static string GetDateSn(DateTime now, int offset)
{
string dateSn = "";
string date = now.ToString("yyyyMMdd");
string newDic = Dictionary;
for (int i = 0; i < date.Length; i++)
{
newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
int num = int.Parse(date[i].ToString());
dateSn += newDic[num].ToString();
}
return dateSn;
}
/// <summary>
/// 根據注冊時間序列號反推注冊時間
/// </summary>
/// <param name="dateSn">時間字符串</param>
/// <param name="offset">偏移量</param>
/// <returns>時間</returns>
private static DateTime GetDate(string dateSn, int offset)
{
string dateStr = "";
string newDic = Dictionary;
for (int i = 0; i < dateSn.Length; i++)
{
newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
int num = newDic.IndexOf(dateSn[i]);
dateStr += num;
}
return new DateTime(int.Parse(dateStr.Substring(0, 4)), int.Parse(dateStr.Substring(4, 2)), int.Parse(dateStr.Substring(6, 2)));
}
/// <summary>
/// 以注冊時間限制和偏移量生成對應的字符串
/// </summary>
/// <param name="regLimitDays"></param>
/// <param name="offset"></param>
/// <returns>返回對應的注冊時間限制的字符串,2位字符串</returns>
private static string GetRegLimitSn(int regLimitDays, int offset)
{
string regLimitSn = "";
string regLimitStr = regLimitDays.ToString("00");
string newDic = Dictionary;
for (int i = 0; i < regLimitStr.Length; i++)
{
newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
int num = int.Parse(regLimitStr[i].ToString());
regLimitSn += newDic[num].ToString();
}
return regLimitSn;
}
/// <summary>
/// 根據注冊時間限制字符串反推注冊時間限制
/// </summary>
/// <param name="regLimitSn">注冊時間限制字符串</param>
/// <param name="offset">偏移量</param>
/// <returns>注冊時間限制</returns>
private static int GetRegLimitDays(string regLimitSn, int offset)
{
string regLimitStr = "";
string newDic = Dictionary;
for (int i = 0; i < regLimitSn.Length; i++)
{
newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
int num = newDic.IndexOf(regLimitSn[i]);
regLimitStr += num;
}
return int.Parse(regLimitStr);
}
/// <summary>
/// 以使用時間限制和偏移量生成對應的字符串
/// </summary>
/// <param name="useLimitDays">使用時間限制</param>
/// <param name="offset">偏移量</param>
/// <returns>使用時間限制對應字符串,5位字符串</returns>
private static string GetUseLimitSn(int useLimitDays, int offset)
{
string useLimitSn = "";
string useLimitStr = useLimitDays.ToString("00000");
string newDic = Dictionary;
for (int i = 0; i < useLimitStr.Length; i++)
{
newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
int num = int.Parse(useLimitStr[i].ToString());
useLimitSn += newDic[num].ToString();
}
return useLimitSn;
}
/// <summary>
/// 根據使用時間限制字符串反推使用時間限制
/// </summary>
/// <param name="regLimitSn">使用時間限制字符串</param>
/// <param name="offset">偏移量</param>
/// <returns>使用時間限制</returns>
private static int GetUseLimitDays(string useLimitSn, int offset)
{
string useLimitStr = "";
string newDic = Dictionary;
for (int i = 0; i < useLimitSn.Length; i++)
{
newDic = GetNewDictionaryString(newDic, offset, LicenseOffset.Left);
int num = newDic.IndexOf(useLimitSn[i]);
useLimitStr += num;
}
return int.Parse(useLimitStr);
}
/// <summary>
/// 根據字典、偏移量和偏移類型生成新的字典
/// </summary>
/// <param name="dic"></param>
/// <param name="offset"></param>
/// <param name="offsetType"></param>
/// <returns></returns>
private static string GetNewDictionaryString(string dic, int offset, LicenseOffset offsetType)
{
StringBuilder sb = new StringBuilder(dic);
if (offsetType == LicenseOffset.Left)
{
for (int i = 0; i < offset; i++)
{
string head = sb[0].ToString();
sb.Remove(0, 1);
sb.Append(head);
}
}
else if (offsetType == LicenseOffset.Right)
{
for (int i = 0; i < offset; i++)
{
string end = sb[dic.Length - 1].ToString();
sb.Remove(dic.Length - 1, 1);
sb.Insert(0, end);
}
}
return sb.ToString();
}
}
14.4 代碼混淆
從安全角度來講,.NET程序如果不加混淆的話,很容易被反編譯出源代碼的。從專業角度來講,即使增加了序列號功能,也無濟於事,專業的人員分分鍾可以破解掉,盡管這樣干的人很少,但是存在這種可能性。如果一個軟件人員想了解一個很好的軟件,第一反映可能就是反編譯。
對於公司或商業使用的軟件來講,增加混淆還是有必要的,盡管現在開源很流行。
14.5 代碼破解
不管.NET程序如何進行混淆,理論上都是可以破解的,理論的東西就不贅述了。通常接觸過的破解方式有兩種:注冊機方式和暴力方式。
注冊機的方式,需要通過軟件的驗證序列號的過程和機制反向推算出序列號的生成算法,根據反推的算法開發一個小軟件,用於生成脫離作者授權生成序列號。這種方式不會破壞程序本身的代碼,相對溫和。暴力的方式,就是找到序列號驗證部分的代碼,通過刪除或繞過驗證代碼等方式不讓代碼有效執行。這種方式會對程序本身的代碼進行改動,所以也存在一些風險。
14.6 小結
實現序列號有多種方式,上述方式不一定最好,但是希望對開發者有一定幫助。
最終實現效果圖如下:

作者:唯笑志在
Email:504547114@qq.com
QQ:504547114
.NET開發技術聯盟:54256083
文檔下載:http://pan.baidu.com/s/1pJ7lZWf
官方網址:http://www.bmpj.net
