软件安全实验-硬件加密锁技术及应用

软件安全实验-硬件加密锁技术及应用

【实验目的】

本实验使用商用硬件加密锁,将软件中的关键算法代码移植到加密锁中执行,并通过宿主程序调用加密锁中的自定义函数。通过对加密锁初始化、模板代码生成、函数编译写入、程序运行验证以及TraceMe个性化保护方案的实现,加深对软件保护、授权验证和硬件加密锁应用方式的理解。

【实验原理】

硬件加密锁保护思想。 硬件加密锁将关键算法从普通软件进程中移出,下载到外部加密锁内部执行。宿主程序本身只保留调用接口和数据传递逻辑,真正的核心计算由加密锁完成。这样即使攻击者逆向分析宿主程序,也难以直接获得完整算法,从而提升软件保护强度。

自定义函数调用流程。 本实验中,在softkey.yt中编写需要下载到加密锁中的函数,如addsubabbsGenKey。通过VSCode中的yttool插件执行D8MakeCode命令后,工具会将函数编译并写入加密锁,同时生成VC模板工程。宿主程序调用函数时,先通过FindD8查找指定校验码的加密锁,再使用SetVar写入参数,调用RunFuntion执行锁内函数,最后通过GetVar取回返回值。

TraceMe保护方案。 TraceMe程序原本在软件内部完成序列号生成和验证。个性化保护时,将序列号生成算法移植到TraceMe.yt中,并写入硬件加密锁。TraceMe程序运行时需要调用当前加密锁中的GenKey函数生成序列号,若加密锁不存在或校验码不匹配,则无法完成正确验证。

Step 1 开发环境准备与加密锁初始化

(1)安装VSCode插件

实验首先安装Visual Studio Code,并在VSCode中安装yttool插件。该插件用于识别.yt文件,并提供D8ReSetD8MakeCodeD8Complie等命令,完成加密锁初始化、函数编译、模板代码生成以及代码写入。

(2)初始化加密锁

将硬件加密锁插入计算机USB接口后,在VSCode中打开实验目录,按下Ctrl+Shift+P,执行D8ReSet命令对加密锁进行初始化。初始化完成后,加密锁处于可下载代码的状态,为后续生成模板工程和写入自定义函数做准备。

初始化加密锁

Step 2 自定义函数编写、编译与写入

(1)编写softkey.yt函数

softkey.yt中定义需要运行在加密锁中的函数。本实验首先实现了三个基础测试函数:加法函数add、减法函数sub和绝对值函数abbs,用于验证宿主程序能否正常传参、执行锁内函数并取回结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int add(int a, int b)
{
return a + b;
}

int sub(int a, int b)
{
return a - b;
}

// ref代表传入变量引用
int abbs(ref int a)
{
return abs(a);
}

其中abbs函数使用ref int a传入引用参数,便于观察加密锁函数对参数和返回值的处理方式。基础函数结构简单,适合作为模板工程调用是否正确的验证对象。

(2)编译并写入加密锁

完成softkey.yt函数编写后,在VSCode中执行D8MakeCode命令,选择VC作为开发语言,并输入下载密钥。工具会将.yt中的函数编译为锁内可执行代码,下载到当前加密锁中,同时生成vc_sample模板工程。

编译并写入加密锁

生成模板代码后,实验继续对不同函数进行编译写入和调用测试。将abbs函数加入后重新生成代码,可以看到工具重新编译并将新的锁内函数写入加密锁。

Step 3 模板工程调用测试

(1)宿主程序调用加密锁函数

进入生成的VC模板工程后,关键调用代码位于RunFunc.cpp中。模板代码通过FindD8查找加密锁,并使用固定校验码绑定当前锁。若查找失败,则程序无法继续调用锁内函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
int RunFunc()
{
char KeyPath[MAX_PATH];
LastErr = mD8.FindD8(0, "67C3FB834B84FAED", KeyPath);
if (LastErr != 0) return LastErr;

char szText[64] = { 0 };
char user[50] = "E42314060";
wsprintf(szText, "%d", GenKey(user, KeyPath));
MessageBox(NULL, szText, "GenKey", MB_OK);

return LastErr;
}

sub函数为例,宿主程序调用时先将参数写入缓冲区,再传入加密锁执行锁内函数,最后从缓冲区取回返回值。该过程说明宿主程序并不直接执行减法逻辑,而是通过加密锁接口获得计算结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int sub(int a, int b, char *KeyPath)
{
int _sub = 0;
static BYTE Buf[8];

memcpy(&Buf[0], &a, 4);
memcpy(&Buf[4], &b, 4);

LastErr = mD8.SetVar(Buf, 4, 8, KeyPath);
if (LastErr != 0) return _sub;
LastErr = mD8.RunFuntion("sub", KeyPath);
if (LastErr != 0) return _sub;
LastErr = mD8.GetVar(Buf, 0, 4, KeyPath);
if (LastErr != 0) return _sub;

memcpy(&_sub, Buf, 4);
return _sub;
}

(2)基础函数测试结果

编译并运行模板工程后,分别测试自动生成代码、自定义sub函数以及abbs函数。程序能够正常找到当前加密锁并调用锁内函数,说明函数已经成功写入加密锁,宿主程序与加密锁之间的数据传递和函数执行流程正常。

自动生成代码测试自定义函数

测试sub函数

测试abbs函数

(3)拔出或更换加密锁后的运行结果

当拔出当前加密锁后再次运行程序,宿主程序无法通过FindD8找到指定校验码对应的锁,因此锁内函数无法执行,程序也无法得到正确结果。

拔出加密锁后无法运行TEST

将本人的加密锁与其他同学的加密锁互换后,由于模板工程中绑定的校验码与当前插入的加密锁不一致,程序同样无法正确运行锁内函数。这说明模板工程与加密锁之间存在绑定关系,只有写入对应代码并匹配校验码的加密锁才能完成运行。

交换加密锁后无法运行函数

Step 4 TraceMe个性化保护方案

(1)序列号生成算法移植

根据TraceMe程序中的序列号生成算法,将关键的GenKey函数写入TraceMe.yt。本实验使用学号E42314060作为用户名输入,函数从用户名的第4个字符开始取字符,结合固定权值数组计算序列号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int GenKey(string user)
{
int key = 0;
int i = 0;
int j = 0;
int t = 0;

string str;
int b[8] = {0xC, 0xA, 0x13, 0x9, 0xC, 0xB, 0xA, 0x8};

int len = strlen(user);
for (i = 3, j = 0; i < len; i++, j++)
{
str = substring(user, i, 1);
t = asc(str) * b[j & 7];
key = t + key;
}

return key;
}

该函数是TraceMe验证过程中的关键算法。将其移植到加密锁后,软件本体中不再完整保存序列号计算逻辑,只有插入正确加密锁时才能完成验证。

编译写入abbs函数

(2)生成序列号并验证TraceMe

GenKey加入模板工程后,在RunFunc函数中调用GenKey,输入用户名E42314060,通过消息框输出生成的序列号。该序列号随后作为TraceMe程序的注册码输入。

将GenKey添加到TEST项目并生成序列号

将生成代码中的校验码替换到TraceMe项目中,重新编译TraceMe程序。输入用户名和正确序列号后,程序能够通过验证,说明TraceMe已经能够通过加密锁中的GenKey函数完成注册验证。

TraceMe程序成功验证

(3)拔出加密锁后的验证结果

拔出加密锁后再次运行TraceMe并输入同一组用户名和序列号,程序无法完成正确验证。原因是序列号生成函数已经迁移到硬件加密锁中,软件在缺少加密锁时无法执行关键算法。

拔出加密锁后TraceMe无法验证

【思考与总结】

(1)在加密锁中自定义的关键代码应该具有什么样的特点?

放入加密锁中的代码应当具有较强的关键性和独立性。一方面,它应当是软件授权、注册验证或核心业务算法中的关键部分,攻击者无法轻易用常量替换或绕过;另一方面,它的输入输出应尽量简洁,计算过程相对独立,便于通过SetVarGetVar完成数据交换。本实验中的GenKey函数比较适合作为锁内函数,因为它直接决定用户名与序列号是否匹配。

(2)比较本次实验和先前实验中TraceMe计算序列号的保护强度。

先前实验中,TraceMe的序列号生成算法主要保存在软件内部,攻击者通过静态分析或动态调试有机会恢复算法并生成注册码。本次实验将GenKey函数移动到硬件加密锁中,宿主程序只保留调用接口和校验码,核心计算不直接暴露在普通程序代码中。因此,即使软件文件被复制到其他计算机上,只要没有对应的加密锁,也无法完成注册验证,保护强度明显提高。

(3)如何提高软件调用加密锁中自定义函数的安全性?

可以从三个方面提高调用安全性:第一,避免只在单一位置调用加密锁,可在程序多个关键流程中分散调用,增加绕过难度;第二,对传入加密锁的数据加入随机数、时间戳或挑战响应机制,避免简单重放固定输入输出;第三,对宿主程序中的校验码、错误处理和调用路径进行混淆,并对返回结果进行二次校验,防止攻击者直接修改分支跳转或伪造返回值。

(4)如何将加密锁模板代码应用到其他项目中?

模板代码应用到其他项目时,核心是提取生成工程中的接口文件、库文件以及RunFunc.cpp中封装好的函数调用逻辑。实际项目可以根据自身业务重新封装查找加密锁、设置参数、执行锁内函数和读取返回值的流程,再在需要授权判断或核心计算的位置调用这些封装函数。同时,需要将模板代码中的校验码替换为当前项目对应加密锁生成的校验码,并保证发布程序与加密锁中下载的函数版本一致。

通过本次实验可以看到,硬件加密锁的价值不仅在于“插锁才能运行“,更重要的是能够把关键算法放到软件外部执行。程序在没有正确加密锁、拔出加密锁或更换他人加密锁时均无法完成关键函数调用,这验证了硬件加密锁在软件授权和关键代码保护中的实际作用。

【附录-关键源代码】

softkey.yt中的自定义函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
int add(int a, int b)
{
return a + b;
}

int sub(int a, int b)
{
return a - b;
}

int abbs(ref int a)
{
return abs(a);
}

int GenKey(string user)
{
int key = 0;
int i = 0;
int j = 0;
int t = 0;
string str;
int b[8] = {0xC, 0xA, 0x13, 0x9, 0xC, 0xB, 0xA, 0x8};

int len = strlen(user);
for (i = 3, j = 0; i < len; i++, j++)
{
str = substring(user, i, 1);
t = asc(str) * b[j & 7];
key = t + key;
}
return key;
}

RunFunc.cpp中的GenKey调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int GenKey(char *user, char *KeyPath)
{
int _GenKey = 0;
static BYTE Buf[50];

memcpy(&Buf[0], user, strlen(user) + 1);

LastErr = mD8.SetVar(Buf, 4, 50, KeyPath);
if (LastErr != 0) return _GenKey;
LastErr = mD8.RunFuntion("GenKey", KeyPath);
if (LastErr == -7999) { ApiCall(KeyPath); }
if (LastErr != 0) return _GenKey;
LastErr = mD8.GetVar(Buf, 0, 4, KeyPath);
if (LastErr != 0) return _GenKey;

memcpy(&_GenKey, Buf, 4);
return _GenKey;
}

软件安全实验-硬件加密锁技术及应用
https://eleco.top/2026/07/02/软件安全实验-硬件加密锁技术及应用/
作者
Eleco
发布于
2026年7月2日
许可协议