软件授权码设计方案
一、 应用场景
对于部分软件,不想被别人白嫖,只有在获取到授权后才能使用,常用的方法无非两种,其一是软件认证,其二是硬件绑定。
软件认证:顾名思义就是在软件层面的一种认证手段,常用的方法就是注册账号设置密码。只要账号密码正确,在任何设备上都可使用。
硬件绑定:就是将软件和硬件设备进行捆绑,也就是说一旦完成捆绑后,该软件就只能在该硬件设备上使用了。
两种授权方法各有优劣,因应用场景的不同而选择不同的方案,在此就不多做讨论了,本文主要探讨的是硬件绑定的方法,以在PC机的软件授权为背景进行授权码的设计。
二、 唯一标识确定
硬件绑定,那么首要做的就是确定该硬件的唯一标识,然后获取到其标识,而唯一的标识由正好是设备的基本功能,下面列举一下常见的PC机的唯一标识各种方法及其优缺点。
1、网卡MAC地址
MAC地址可能是最常用的标识方法,但是现在这种方法基本不可靠:一个电脑可能存在多个网卡,多个MAC地址,如典型的笔记本可能存在有线、无线、蓝牙等多个MAC地址,随着不同连接方式的改变,每次MAC地址也会改变。而且,当安装有虚拟机时,MAC地址会更多。MAC地址另外一个更加致命的弱点是,MAC地址很容易手动更改。因此,MAC地址基本不推荐用作设备唯一ID。
2、CPU ID
在Windows系统中通过命令行运行“wmic cpu get processorid”就可以查看CPU ID。目前CPU ID也无法唯一标识设备,Intel现在可能同一批次的CPU ID都一样,不再提供唯一的ID。而且经过实际测试,新购买的同一批次PC的CPU ID很可能一样。这样作为设备的唯一标识就会存在问题。
3、硬盘序列号
在Windows系统中通过命令行运行“wmic diskdrive get serialnumber”可以查看。硬盘序列号作为设备唯一ID存在的问题是,很多机器可能存在多块硬盘,特别是服务器,而且机器更换硬盘是很可能发生的事情,更换硬盘后设备ID也必须随之改变,不然也会影响授权等应用。因此,很多授权软件没有考虑使用硬盘序列号。而且,不一定所有的电脑都能获取到硬盘序列号。
4、自定义算法生成唯一ID
可以使用自制的一个特定算法(如GUID、或者一定位数的随机数)生成唯一的ID,然后写入到注册表或者设备上,作为其唯一ID。这种方法不依赖任何硬件特征,唯一性也可以自己完全控制,不过纯软件的实现缺点是这个ID很容易伪造,也很容易擦除;而且很可能还需要在线验证,后台存储所有ID的服务器必须保持在线。
5、Windows的产品ID(ProductId)
在“控制面板\系统和安全\系统”的最下面就可以看到激活的Windows产品ID信息,另外通过注册表“HKEY_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion”也可以看到看到“ProductId”字段。不过这个产品ID并不唯一,不同系统或者机器重复的概率也比较大。虚拟机中克隆的系统,使用同一个镜像安装激活的系统,其产品ID就可能一模一样。经过实测,笔者在两台Thinkpad笔记本上发现其ProductId完全一样。
6、MachineGUID
Windows安装时会唯一生成一个GUID,可以在注册表“HKEY_MACHINE\SOFTWARE\Microsoft\Cryptography”中查看其“MachineGuid”字段。这个ID作为Windows系统设备的唯一标识不错,不过值得注意的一点是,与硬件ID不一样,这个ID在重装Windows系统后应该不一样了。这样授权软件在重装系统后,可能就需要用户重新购买授权。
7、主板smBIOS UUID
在Windows系统中通过命令行运行“wmic csproduct get UUID”Linux下用“dmidecode -s system-uuid”命令可以获取UUID。主板UUID是很多授权方法和微软官方都比较推崇的方法,本文用的也是该方法。即便重装系统UUID应该也不会变,双系统一个windows一个Linux,。但是这个方法也有缺陷,因为不是所有的厂商都提供一个UUID,当这种情况发生时,wmic会返回“FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF”,即一个无效的UUID。
8、外置密码设备提供唯一ID
这种方法很多,比如U盾里面可以提供唯一的密钥标识,可信计算密码芯片里面的背书密钥EK等都是唯一固定在安全硬件里面的,而且通过良好的密码算法生成,唯一性和差异性都可以保证,安全性也更高。这种方法需要在计算设备连接外置密码芯片,增加经济负担和开发成本。而且,即便这种方法也存在欺骗攻击和代理攻击等破解方法。其实设备唯一标识其实也是指纹的一种,想要使用标识或者指纹时,首先必须明确自己的真实意图,是要标识一个用户(这样可以使用身份证、指纹、手机验证等方式),还是要标识一个设备(本文列举的各种设备ID)。根据自己的真实意图才能进一步思考具体使用的方式,不忘初衷。
9、BIOS序列号
有些bios里面看不到序列号,获取BIOS序列号指令wmic bios get serialnumber. 用户在使用计算机的过程中,都会接触到BIOS,它在计算机系统中起着非常重要的作用。一块主板性能优越与否,很大程度上取决于主板上的BIOS管理功能是否先进。 BIOS(Basic Input/Output System,基本输入输出系统)全称是ROM-BIOS,是只读存储器基本输入/输出系统的简写,它实际是一组被固化到电脑中,为电脑提供最低级最直接的硬件控制的程序,它是连通软件程序和硬件设备之间的枢纽,通俗地说,BIOS是硬件与软件程序之间的一个“转换器”或者说是接口(虽然它本身也只是一个程序),负责解决硬件的即时要求,并按软件对硬件的操作要求具体执行。BIOS芯片是主板上一块长方型或正方型芯片,BIOS中主要存放:自诊断程序:通过读取CMOS RAM中的内容识别硬件配置,并对其进行自检和初始化; CMOS设置程序:引导过程中,用特殊热键启动,进行设置后,存入CMOS RAM中; 系统自举装载程序:在自检成功后将磁盘相对0道0扇区上的引导程序装入内存,让其运行以装入DOS系统; 主要I/O设备的驱动程序和中断服务;由于BIOS直接和系统硬件资源打交道,因此总是针对某一类型的硬件系统,而各种硬件系统又各有不同,所以存在各种不同种类的BIOS,随着硬件技术的发展,同一种BIOS也先后出现了不同的版本,新版本的BIOS比起老版本来说,功能更强。重装系统BIOS序列后并不会改变。
三、 生成授权码
本文的设计思路其实也相对比较简单,第一步就是获取主板smBIOS UUID,第二步对UUID进行AES加密(AES算法逻辑及介绍欢迎查看笔者之前的文章)生成授权码,发送给用户。
验证的过程,可以分两种方法实现,一是软件启动后获取设备的UUID加密后与授权码进行比较,而是对授权码进行解密,解密后与UUID进行比较。两种方法均可。
四、 代码实现
1、获取uuid 代码实现
1 #include "GetUUID.h" 2 # pragma comment(lib, "wbemuuid.lib") 3 #include <iostream> 4 using namespace std; 5 6 /************************************* 7 函数名称:AsciiStrToHexArray 8 输入参数: 9 src - ASCII 数据串地址 10 len - ASCII 数据串长度 11 des - 输出的16进行地址 12 输出参数: 13 无 14 返回值: 15 false - 转换失败 16 true - 转换成功 17 18 功能:将ASCII码的字符串转换成16进制数据 19 20 作者:csk 21 日期:2022/11/02 22 备注:例如 src="4131423243334434"对应的字符为 "A1B2C3D4" 23 转换后 dec= 0xA1 0xB2 0xC3 0xC4 24 *************************************/ 25 26 bool AsciiStrToHexArray(unsigned char* src, int len, unsigned char* des) 27 { 28 unsigned char ch; 29 30 for (int index = 0x00; index < len; index++) 31 { 32 if ((src[index] >= '0') && (src[index] <= '9')) //数字0-9范围 33 { 34 ch = src[index] - 0x30; 35 } 36 else if ((src[index] >= 'A') && (src[index] <= 'F')) //字母A-F范围 37 { 38 ch = src[index] - 0x37; 39 } 40 else if ((src[index] >= 'a') && (src[index] <= 'f')) //字母a-f范围 41 { 42 ch = src[index] - 0x57; 43 } 44 else 45 { 46 return false; 47 } 48 if (index % 2 != 0x00) 49 { 50 des[index / 2] |= ch; 51 } 52 else 53 { 54 des[index / 2] = ch<<0x04; 55 } 56 } 57 return true; 58 } 59 60 /************************************* 61 函数名称:getuuID 62 输入参数: 63 uuid - 输出获取到的UUID存储地址 32byte 64 输出参数: 65 无 66 返回值: 67 false - 获取失败 68 true - 获取成功 69 70 功能:获取主板smBIOS UUID 71 72 作者:csk 73 日期:2022/11/02 74 备注:获取到的UUID是去除了"-"的 75 UUID 格式:XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX 76 *************************************/ 77 bool getuuID(unsigned char* uuid) 78 { 79 unsigned char temp[0x20] = {0x00}; 80 int offset = 0x00; 81 HRESULT hres; 82 // Step 1: -------------------------------------------------- 83 // Initialize COM. ------------------------------------------ 84 hres = CoInitializeEx(0, COINIT_MULTITHREADED); 85 if (FAILED(hres)) 86 { 87 //cout << "Failed to initialize COM library. Error code = 0x" << hex << hres << endl; 88 return false; // Program has failed. 89 } 90 91 // Step 2: -------------------------------------------------- 92 // Set general COM security levels -------------------------- 93 // Note: If you are using Windows 2000, you need to specify - 94 // the default authentication credentials for a user by using 95 // a SOLE_AUTHENTICATION_LIST structure in the pAuthList ---- 96 // parameter of CoInitializeSecurity ------------------------ 97 hres = CoInitializeSecurity( 98 NULL, 99 -1, // COM authentication 100 NULL, // Authentication services 101 NULL, // Reserved 102 RPC_C_AUTHN_LEVEL_DEFAULT, // Default authentication 103 RPC_C_IMP_LEVEL_IMPERSONATE, // Default Impersonation 104 NULL, // Authentication info 105 EOAC_NONE, // Additional capabilities 106 NULL // Reserved 107 ); 108 109 if (FAILED(hres)) 110 { 111 //cout << "Failed to initialize security. Error code = 0x"<< hex << hres << endl; 112 CoUninitialize(); 113 return false; // Program has failed. 114 } 115 // Step 3: --------------------------------------------------- 116 // Obtain the initial locator to WMI ------------------------- 117 118 IWbemLocator* pLoc = NULL; 119 120 hres = CoCreateInstance( 121 CLSID_WbemLocator, 122 0, 123 CLSCTX_INPROC_SERVER, 124 IID_IWbemLocator, (LPVOID*)&pLoc); 125 126 if (FAILED(hres)) 127 { 128 //cout << "Failed to create IWbemLocator object."<< " Err code = 0x"<< hex << hres << endl; 129 CoUninitialize(); 130 return false; // Program has failed. 131 } 132 133 // Step 4: ----------------------------------------------------- 134 135 // Connect to WMI through the IWbemLocator::ConnectServer method 136 IWbemServices* pSvc = NULL; 137 // Connect to the root\cimv2 namespace with 138 // the current user and obtain pointer pSvc 139 // to make IWbemServices calls. 140 141 hres = pLoc->ConnectServer( 142 _bstr_t(L"ROOT\\CIMV2"), // Object path of WMI namespace 143 NULL, // User name. NULL = current user 144 NULL, // User password. NULL = current 145 0, // Locale. NULL indicates current 146 NULL, // Security flags. 147 0, // Authority (e.g. Kerberos) 148 0, // Context object 149 &pSvc // pointer to IWbemServices proxy 150 ); 151 152 if (FAILED(hres)) 153 { 154 //cout << "Could not connect. Error code = 0x"<< hex << hres << endl; 155 pLoc->Release(); 156 CoUninitialize(); 157 return false; // Program has failed. 158 } 159 //cout << "Connected to ROOT\\CIMV2 WMI namespace" << endl; 160 161 // Step 5: -------------------------------------------------- 162 // Set security levels on the proxy ------------------------- 163 hres = CoSetProxyBlanket( 164 pSvc, // Indicates the proxy to se 165 RPC_C_AUTHN_WINNT, // RPC_C_AUTHN_xxx 166 RPC_C_AUTHZ_NONE, // RPC_C_AUTHZ_xxx 167 NULL, // Server principal name 168 RPC_C_AUTHN_LEVEL_CALL, // RPC_C_AUTHN_LEVEL_xxx 169 RPC_C_IMP_LEVEL_IMPERSONATE, // RPC_C_IMP_LEVEL_xxx 170 NULL, // client identity 171 EOAC_NONE // proxy capabilities 172 ); 173 if (FAILED(hres)) 174 { 175 //cout << "Could not set proxy blanket. Error code = 0x"<< hex << hres << endl; 176 pSvc->Release(); 177 pLoc->Release(); 178 CoUninitialize(); 179 return false; // Program has failed. 180 } 181 // Step 6: -------------------------------------------------- 182 // Use the IWbemServices pointer to make requests of WMI ---- 183 // For example, get the name of the operating system 184 IEnumWbemClassObject* pEnumerator = NULL; 185 hres = pSvc->ExecQuery( 186 bstr_t("WQL"), 187 bstr_t("SELECT * FROM Win32_ComputerSystemProduct"), 188 WBEM_FLAG_FORWARD_ONLY | WBEM_FLAG_RETURN_IMMEDIATELY, 189 NULL, 190 &pEnumerator); 191 if (FAILED(hres)) 192 { 193 //cout << "Query for operating system name failed."<< " Error code = 0x"<< hex << hres << endl; 194 pSvc->Release(); 195 pLoc->Release(); 196 CoUninitialize(); 197 return false; // Program has failed. 198 } 199 200 IWbemClassObject* pclsObj; 201 202 ULONG uReturn = 0; 203 //string test; 204 while (pEnumerator) 205 { 206 HRESULT hr = pEnumerator->Next(WBEM_INFINITE, 1, &pclsObj, &uReturn); 207 if (0 == uReturn) 208 { 209 break; 210 } 211 VARIANT vtProp; 212 // Get the value of the Name property 213 hr = pclsObj->Get(L"UUID", 0, &vtProp, 0, 0); 214 wcout << " OS Name : " << vtProp.bstrVal << endl; 215 216 offset = 0x00; 217 for (int i = 0; i < 32; i++) 218 { 219 if ((0x08 == i) || (0x0C == i) || (0x10 == i) || (0x14 == i)) 220 { 221 offset++; 222 } 223 temp[i] = vtProp.bstrVal[i + offset]; 224 } 225 226 if (!AsciiStrToHexArray(temp,0x20,uuid)) 227 { 228 return false; 229 } 230 231 VariantClear(&vtProp); 232 //pclsObj->Release(); 233 } 234 return true; 235 236 }
2、授权码生成及验证代码实现
1 // Register.cpp : 此文件包含 "main" 函数。程序执行将在此处开始并结束。 2 3 4 #define _WIN32_DCOM 5 #include <iostream> 6 7 #include "GetUUID.h" 8 #include "AES.h" 9 using namespace std; 10 11 12 13 14 int main(int argc, char** argv) 15 { 16 // 17 18 unsigned char uuid[0x10] = { 0x00 }; 19 unsigned char key[0x10] = { 0x4D,0x59,0x48,0x45,0x41,0x52,0x54,0x57,0x49,0x4C,0x4C,0x47,0x4F,0x4F,0x4E,0x21 }; // 加密密钥 MY HEART WILL GO ON ! 20 21 //1、获取UUID 22 if (getuuID(uuid)) 23 {//UUID获取成功 24 25 //2、进行AES加密生成授权码 26 AES aes(key); 27 printf("Input:\n"); 28 for (int i = 0; i < 0x10; i++) 29 { //输出获取到的uuid(设备的唯一标识),作为加密的原始数据 30 printf("%02x ", uuid[i]); 31 } 32 33 34 //加密 UUID->授权码 35 aes.Cipher(uuid); 36 printf("\nAfter Cipher:\n"); 37 for (int i = 0; i < 0x10; i++) 38 {//将uuid进行加密生成授权码 39 printf("%02x ", uuid[i]); 40 } 41 42 //解密 授权码->UUID 43 aes.InvCipher(uuid); 44 printf("\nAfter InvCipher:\n"); 45 for (int i = 0; i < 0x10; i++) 46 {//将授权码解密还原成uuid 47 printf("%02x ", uuid[i]); 48 } 49 } 50 51 } 52 53 54 // 运行程序: Ctrl + F5 或调试 >“开始执行(不调试)”菜单 55 // 调试程序: F5 或调试 >“开始调试”菜单 56 57 // 入门使用技巧: 58 // 1. 使用解决方案资源管理器窗口添加/管理文件 59 // 2. 使用团队资源管理器窗口连接到源代码管理 60 // 3. 使用输出窗口查看生成输出和其他消息 61 // 4. 使用错误列表窗口查看错误 62 // 5. 转到“项目”>“添加新项”以创建新的代码文件,或转到“项目”>“添加现有项”以将现有代码文件添加到项目 63 // 6. 将来,若要再次打开此项目,请转到“文件”>“打开”>“项目”并选择 .sln 文件
五、 总 结
当然还有很多其它方法,如可以获取声卡、CPU模式和频率、IDE控制器、内存等其他信息。甚至,可以收集设备的软硬件配置,通过统计方法和机器学习方法进行分类识别设备。学术上,还有各种密码算法,硬件不可克隆函数PUF等唯一标识的方法可以使用。
从软件授权这个简单的应用来看,购买外置密码设备硬件太过昂贵,可以采用简单的组合方法,推荐使用主板UUID作为主标识,当UUID返回无效的值时,可以进一步采用CPU ID、BIOS序列号、MachineGUID等方式作为次标识,这基本可以解决问题。不过,不管使用怎样的硬件信息或者牛气的算法来进行用户或者设备的标识,还是一句老话“道高一尺,魔高一丈”,都是可以被攻破的,即便你的标识伪造不了、克隆不了,攻击者也可以使用其它攻击方式,如逆向你的验证check代码,然后将其修改掉,使其check失灵。因此,无论设备标识或者用户标识,很多情况下可能只防君子、不防小人,甚至悲观者认为这些手段都是防止合法用户的,影响用户使用的方便性,大可以取消掉。笔者认为,没有必要这么悲观,知识产权等信息是尊敬人的价值和劳动的表现,即便不能完全防止小人,我们也要通过这些方法将一般的小人排除在技术门槛之外,并尽量增加高级小人破解时的代价。
工程路径:https://files.cnblogs.com/files/chenshikun/Register.rar?t=1667628033