授权注册-软件保护工具(2):说说技术点

前言

上一篇我从流程上剖析了“注册机”的应用场景、流程以及大逻辑,在最后也放上了两张从软件设计角度触发的解析,并提炼了几个重要的节点,这一篇就重点从这几个节点说说在开发过程中遇到的坑以及提炼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则通过ISecurityStrategyGetStrategyVersion()函数定位具体的策略去反射创建对应的安全生成策略子类(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)

设备唯一标识的提取的确费了我一些功夫,其关键的难点在于如何获取?什么方式获取靠谱?是否不易更改?

  1. 跨平台的可能性我暂时没有考虑,所以首先想到的就是调用Windows提供的系统API去完成。
  2. 网上大多对于硬件信息的提取都很直接——WMIC,但是这并不靠谱,如果windows系统下没有wmic.exe怎么办?那么机械码就不能唯一了。
    最开始我就是通过
// strCMD 类似 WMIC DISKDRIVE GET SerialNumber
std::string strCmd = strCMD + " > " + STR_TMP_TXT;
system(strCmd.data());

去获取硬件信息,实在过于取巧。所以最后转战Windows纯API调用的方式去完成,但似乎入了深坑…

  1. 哪些硬件信息可以让设备唯一设别
    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)

这个其实就是在本地的存根,一个只读且隐藏的验证文件,虽然授权码只对本机有用,但还是建议对内容序列号加密后再存入本地文件,对应的读取则多一层解析即可,这也是可扩展的点,具体逻辑,怎么安全怎么来。


结语

以上就是授权注册软件的整个分享了,整个开发周期从需求下来,到“一拍脑袋”做,大概一周(还是下班之后不多的空余时间),所以如果有纰漏请及时联系我,我也是在学习中摸索,摸索中提炼,然后在分享中反思,期待一起学习。

  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值