前言
上一篇我从流程上剖析了“注册机”的应用场景、流程以及大逻辑,在最后也放上了两张从软件设计角度触发的解析,并提炼了几个重要的节点,这一篇就重点从这几个节点说说在开发过程中遇到的坑以及提炼API的思考。
技术点
-
提供开发API,支持嵌入任意软件(开发环境支持);
-
加密策略可扩展,具有版本管控机制;
-
硬件信息提取使用Window原生API支持,未调用任何三方库,未使用WMIC查询机制;
-
开发环境:
RegisterCore100.dll: VS2010 C++工程
WarrantDialog.dll: VS2010 C++工程 + Qt界面平台
Serialize.dll: VS2010 C++工程(仅WarrantDialog.dll调用,用于序列化授权文件) -
抽象接口开发;
-
模块化结构,各功能耦合低;
-
轻量级注册工具,流程清晰,各个节点可复杂可简化。
一个是注册核心库(RegisterCore.dll),一个是验证逻辑库(WarrantDialog.dll),从这两大核心再延申细节节点:
- 加解密库(EncryptCode)
- 安全加密策略机制(ISecurityStrategy)
- 设备信息提取(DeviceExtactor)
- 验证逻辑(WarrantDialog Logic)
- 交互窗口(WarrantDialog)
- 本地验证存取与解析(AuthorFileOperator)
(再次用脑图软件“头脑风暴”了一下):
以下则是VS的工程结构,5个库:
RegisterCore的接口类:
加解密库(EncryptCode)
加解密模块我放到了RegisterCore中提出接口导出,因为目前不是太多复杂的算法支持,所以暂时未单独提成库,如果加密算法更丰富,可以随时提成库。
#ifndef EncryptCode_h__
#define EncryptCode_h__
#include <string>
#include "RegisterCoreExport.h"
namespace EncryptCodeNS
{
std::string REGISTERCORE_EXPORT EncodeMd5(const std::string& strContent);
std::string REGISTERCORE_EXPORT EncodeBase64(const std::string& strContent);
std::string REGISTERCORE_EXPORT DecodeBase64(const std::string& strEncrypt);
};
安全加密策略机制(ISecurityStrategy)
这一块结合IRegisterModule通过反射工厂创建安全算法策略。C++自身是没有反射机制的,所以需要自己实现。为什么要用反射机制呢?因为考虑到从请求码生成注册码的这一个节点上,对应的算法是枚举不完的,因为用户需求是多变的,这里必须考虑可扩展性,所以通过反射工厂(IRegisterModule)去管理和创建繁多的安全策略。
ISecureityStrategy.h --> 安全策略接口类如下:
#ifndef ISecurityStrategy_h__
#define ISecurityStrategy_h__
#include "RegisterCoreExport.h"
#include <string>
class REGISTERCORE_EXPORT ISecurityStrategy
{
public:
virtual ~ISecurityStrategy() {}
// 版本管理方法
virtual std::string GetStrategyVersion() = 0;
virtual std::string GetStrategyName() const = 0;
virtual std::string GetStrategyDescription() const = 0;
// 生成策略方法
virtual void SetRequestCode(const std::string& strRequestCode) = 0;
virtual std::string GetRequestCode() const = 0;
virtual std::string GenerateRegisterCode() = 0;
};
反射宏定义如下:(在ISecurityStrategy子类的cpp和h上分别调用宏即可)
#ifndef RegisterInnerConstant_h__
#define RegisterInnerConstant_h__
#include "../Include/ISecurityStrategy.h"
// [11/20/2019 Being]
typedef ISecurityStrategy* (*FuncCreateSecurityStrategy)();
// [11/20/2019 Being]
#define DECLARE_SECURITY_REFLECT \
public: \
static void RegisterIn(); \
static ISecurityStrategy* CreateSecuretyStrategy();
// [11/20/2019 Being]
#define IMPLEMENT_SECURITY_REFLECT(classname) \
class C##classname##Helper\
{\
public:\
C##classname##Helper()\
{\
classname::RegisterIn();\
}\
~C##classname##Helper() {}\
};\
C##classname##Helper classname##helper;\
void classname::RegisterIn()\
{\
CRegisterModule* pModule = GetGlobalInsRegisterModule();\
classname s;\
pModule->RegisterSecurityStrategy(s.GetStrategyVersion(), CreateSecuretyStrategy);\
}\
ISecurityStrategy* classname::CreateSecuretyStrategy()\
{\
return new classname;\
}
#endif // RegisterInnerConstant_h__
而IRegisterModule则通过ISecurityStrategy的GetStrategyVersion()函数定位具体的策略去反射创建对应的安全生成策略子类(StrategyVersion一定是唯一的,子类在实现的时候需用生成工具生成UUID,后期可考虑代码生成,初始化调用即可):
#ifndef IRegisterModule_h__
#define IRegisterModule_h__
#include "RegisterCoreExport.h"
#include <string>
#include <vector>
class ISecurityStrategy;
class REGISTERCORE_EXPORT IRegisterModule
{
public:
virtual ~IRegisterModule(void) {};
// Brief:安全策略版本管控[反射工厂] [2019/11/22 Being]
virtual void GetAllSecurityStrategyVersion(std::vector<const std::string>& vec0) const = 0;
virtual ISecurityStrategy* CreateSecurityStrategy(const std::string& strVersion) = 0;
virtual bool IsExistSecurityStrategy(const std::string& strVersion) const = 0;
virtual void DestroySecurityStrategy(ISecurityStrategy* pStrategy) = 0;
// Brief:请求码(机械码)生成 [2019/11/22 Being]
virtual std::string GenerateRequestCode() = 0;
};
设备信息提取(DeviceExtactor)
设备唯一标识的提取的确费了我一些功夫,其关键的难点在于如何获取?什么方式获取靠谱?是否不易更改?
- 跨平台的可能性我暂时没有考虑,所以首先想到的就是调用Windows提供的系统API去完成。
- 网上大多对于硬件信息的提取都很直接——WMIC,但是这并不靠谱,如果windows系统下没有wmic.exe怎么办?那么机械码就不能唯一了。
最开始我就是通过
// strCMD 类似 WMIC DISKDRIVE GET SerialNumber
std::string strCmd = strCMD + " > " + STR_TMP_TXT;
system(strCmd.data());
去获取硬件信息,实在过于取巧。所以最后转战Windows纯API调用的方式去完成,但似乎入了深坑…
- 哪些硬件信息可以让设备唯一设别
CpuID?不行,这个是CPU的型号,并不是产品的唯一序列号;
硬盘序列号?可以,但千万别将分区的序列号和硬盘序列号混淆了,我说的坑也正是在此,磁盘的分区序列号在格式化之后会变,甚至认为修改的方式也不复杂,但是硬盘序列号是出厂时就定下来的身份标识,这个可以保证。
网上搜索“获取计算机硬盘序列号”,其实大多给的是获取分区序列号。而我们需要的其实是WMIC DISKDRIVE GET SerialNumber输出后的内容。
// 头文件包含
//#include <stdlib.h>
//#include <string>
//#include <array>
//#include <fstream>
//#include <iostream>
//#include <windows.h>
// 注意:这个是获取分区磁盘信息的方法,而这并不是我们想要的
void CDeviceExtractor::GetPartitionInfo(StPartitionInfo& stInfo)
{
char szVolumeNameBuf[MAX_PATH] = {0};
DWORD dwVolumeSerialNum;
DWORD dwMaxComponentLength;
DWORD dwSysFlags;
char szFileSystemBuf[MAX_PATH] = {0};
DWORD dwFileSystemBuf = MAX_PATH;
// 分区信息
BOOL bGet = GetVolumeInformationA(stInfo.m_strPartion.data(),
szVolumeNameBuf,
MAX_PATH,
&dwVolumeSerialNum,
&dwMaxComponentLength,
&dwSysFlags,
szFileSystemBuf,
MAX_PATH);
}
// 这才是我们想要的,获取它需要调用设备IO接口
bool CDeviceExtractor::GetLocalDiskDriveInfo()
{
HANDLE hPhysicalDriveIOCTL = 0;
CHAR driveName [MAX_PATH];
for (int drive = 0; drive < 16; drive++)
{
bool done = false;
const int nLen = 1024;
char serialNumber [1000] = {0};
char modelNumber [1000] = {0};
sprintf_s (driveName, "\\\\.\\PhysicalDrive%d", drive);
// Windows NT, Windows 2000, Windows XP - admin rights not required
hPhysicalDriveIOCTL = CreateFileA (driveName, 0, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL);
// if (hPhysicalDriveIOCTL == INVALID_HANDLE_VALUE)
// printf ("Unable to open physical drive %d, error code: 0x%lX\n",
// drive, GetLastError ());
if ( DeviceIoControl (hPhysicalDriveIOCTL, IOCTL_STORAGE_QUERY_PROPERTY, & query, sizeof(query), &buffer, sizeof(buffer), &cbBytesReturned, NULL) ){
STORAGE_DEVICE_DESCRIPTOR * descrip = (STORAGE_DEVICE_DESCRIPTOR *) & buffer;
// 区分移动硬盘和本地磁盘
if (descrip->BusType != BusTypeUsb)
{
//printf("descrip->BusType %d\n", descrip->BusType);
strcpy_s (serialNumber,& buffer [descrip -> SerialNumberOffset]);
strcpy_s (modelNumber, & buffer [descrip -> ProductIdOffset]);
done = true;
}
}else{
DWORD err = GetLastError();
//printf ("\nDeviceIOControl IOCTL_STORAGE_QUERY_PROPERTY error = %d\n", err);
}
CloseHandle (hPhysicalDriveIOCTL);
}
if (done)
{
// 硬盘产品ID和硬盘序列号
m_stDeviceInfo.m_vecDeviceCaption.push_back(std::string(modelNumber));
m_stDeviceInfo.m_vecDeviceSerialNumber.push_back(std::string(serialNumber));
}
}
关于系统API调用,我也是搜索了很多,尝试了很多后摸索出来的,具体实现也是借鉴了其他博客
https://blog.csdn.net/fengdongfang/article/details/79141586
https://www.cnblogs.com/huhu0013/p/4283436.html 这个就是获取硬盘序列号的博客
https://www.cnblogs.com/ziqiu/p/10634883.html 这个是获取分区信息的,可以参考下
设备的其他硬件信息提取还在研究当中,硬盘的相关信息再加上其他的,初步可以完成设备唯一性定位了,后续像BIOS序列号、主板序列号这些,也是很值得参考的,也期待各位的分享和交流。
验证库逻辑与交互窗口
回到我们的目的——保护软件不被轻易扩散,所以自然在软件启动前,需要有一个验证机制,我的做法是在main函数中继承WarrantDialog实现一个Dialog或者直接调用WarrantDialog去完成验证逻辑(这是一个继承了QDialog的窗口):
/**
* @file WarrantDialog.h
* @brief 注册验证对话框
* @note 使用方法: 在[被保护软件]启动前进行验证或授权
* @author Being
* @date 2019/11/21
* @version V00.00.01
* @CopyRight
*/
/* 嵌在main中(即被保护程序程序启动前)eg:
WarrantDialog warrantDlg("6CD6FEFA-A532-4B6E-9E9F-9F8306D58EC5"); // 对应安全策略版本号的初始化
warrantDlg.InitialUiInfo("", "APP");
bool bJudge = warrantDlg.JudgeAccessWarrant();
if (!bJudge)
{
bool bWarrant = warrantDlg.ExecWarrantDialog();
if (!bWarrant)
{
return -1;
}
}
*/
#ifndef WARRANTDIALOG_H
#define WARRANTDIALOG_H
#include <QtWidgets/QDialog>
#include "WarrantDialogExport.h"
namespace Ui{class WarrantDialogClass;};
class ISecurityStrategy;
class WARRANTDIALOG_EXPORT WarrantDialog : public QDialog
{
Q_OBJECT
public:
WarrantDialog(const std::string& strStrategyVersion, QWidget *parent = 0);
~WarrantDialog();
void InitialUiInfo(const QString& strIcon, const QString& strTitle);
bool JudgeAccessWarrant();
bool ExecWarrantDialog();
protected slots:
void OnRegisterBtnClicked();
protected:
ISecurityStrategy* m_pSecurityStrategy;
private:
Ui::WarrantDialogClass* ui;
std::string m_strStrategyVersion;
};
具体的验证逻辑可以见上图的“头脑风暴”,不再赘述,因为这里可以根据各自的需求扩展更多的验证逻辑,不只是简单比对的逻辑。
本地验证存取与解析(AuthorFileOperator)
这个其实就是在本地的存根,一个只读且隐藏的验证文件,虽然授权码只对本机有用,但还是建议对内容序列号加密后再存入本地文件,对应的读取则多一层解析即可,这也是可扩展的点,具体逻辑,怎么安全怎么来。
结语
以上就是授权注册软件的整个分享了,整个开发周期从需求下来,到“一拍脑袋”做,大概一周(还是下班之后不多的空余时间),所以如果有纰漏请及时联系我,我也是在学习中摸索,摸索中提炼,然后在分享中反思,期待一起学习。