AIDA64 (原EVEREST)是一款综合性的系统软硬件检测工具,最权威的电脑硬件监测、监控与测试软件,硬件检测大师,也是每位高手玩家、菜鸟用户必备的硬件检测利器!AIDA64不仅提供诸如协助超频,硬件侦错,压力测试和传感器监测等多种功能,而且还可对处理器,系统内存和磁盘驱动器性能进行全面评估。
下载地址:https://www.aida64.com/
本次分析的是Extreme 最新版本5.99.4900
1 注册算法分析
是UPX的压缩壳,为了静态分析方便先脱了壳。脱壳后显示是Delphi的程序。
打开程序随意输入字符串和key,发现字符串
但是程序中并没有搜索到中文。但是能显示中文应该用了翻译文本。到程序目录下发现了lanugae文件夹,在此文件夹下找到了cn.txt
打开文件搜索上边的错误文本。可以定位到对应的英文字符串。
在IDA中定位到字符串,查看引用只有一处
直接来到关键代码处。定位到函数开头处
在X32dbg中设置断点。此软件在网上有流传出的可用Key。格式如
U3FI1-Y3ED6-FNDAK-3DM54-ULYKW 一个共5段每段5个字符。
IDA大致看下注册流程,载入代码签名后看可以轻松不少。前面一堆不知道在干什么,
但是发现了获取输入key的函数,然后去除空格,转换成大写,去除字符串中的’-’
为了验证分析对不对,在软件中输入key,然后程序断下.F8走完上述代码。可以看到key已经被去除了“-“
接着又获取了key的长度,判断是不是等于25,然后调用了一个函数看名就知道是解析key的
以下的代码都是根据此函数的参数返回值做判断,显示错误信息。
最后到达成功
接下来主任务就是分析TAIDA64_ParseProductKey这个函数了
进入函数发现与上一层许多相同的代码。真正校验Key是从calc1开始
第一个计算函数不是很复杂,对key前24位求了一个哈希值。算法直接抠出到VS工程。我们先正向还原注册流程后,再分析流程上的关系写注册机。X32dbg也走到此取得返回值。VS工程中也测试下判断抠出的算法是不是正确。
//前24位取校验值
int calc1(char*str)
{
//只取前24个
DWORD len = 24;
int v1 = 0;
if (len>0)
{
int i= 0;
do
{
v1 ^=str[i] << 8;
int v4 = 8;
do
{
if ((v1 & 0x8000) == 0)
v1 *= 2;
else
v1 = 2 * v1 ^ 0x8201;
--v4;
} while (v4);
v1 = v1&0xffff;
++i;
--len;
} while (len);
}
return v1;
}
接着(返回值 % 0x9987 )的余数传入第二个计算函数calc2。第二个函数中返回值是字符串长度3。
动态跟踪后发现 此函数就是通过计算后的三组索引值去索引出数组的值然后合并成字符串返回。
数组arry[]={
0x44, 0x59, 0x31, 0x34, 0x55, 0x46, 0x33, 0x52,
0x48, 0x57, 0x43, 0x58, 0x4C, 0x51, 0x42, 0x36,
0x49, 0x4B, 0x4A, 0x54, 0x39, 0x4E, 0x35, 0x41,
0x47, 0x53, 0x32, 0x50, 0x4D, 0x38, 0x56, 0x5A,
0x37, 0x45
}; 转换成c++
//通过校验值取特征码
char* calc2(int b,char* buff)
{
WORD v4= b % 0x484 / 0x22;
WORD v5= b % 0x484 % 0x22;
WORD index = b / 0x484;
char *rRet = 0;
if (b<=0x9987)
{
if (index <= 0x21 && v4 <= 0x21u && v5 <= 0x21u)
{
buff[0] = arry[index];
buff[1] = arry[v4];
buff[2] = arry[v5];
rRet = buff;
}
else
{
printf("error!\n");
return 0;
}
}
else
{
printf("error!\n");
return 0;
}
return rRet;
}
然后取出返回值中第2位值和key的最后一位判断是否相等。相等进入下一级检测
下面进入了黑名单检测 会循环与黑名单中的KEY比较相同就返回0 ,同时设置标志位。黑名单两组一共4602个
黑名单全部复制出来保存到文件留着备用。
我们自己写个从文件读取来检测黑名单函数
//黑名单检测
bool check_blacklist(char* key)
{
HANDLE hFile=CreateFileA("black_list.txt", GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, 0);
if (hFile==INVALID_HANDLE_VALUE)
{
printf("打开黑名单错误!\n");
return 0;
}
char buff[26] = { 0 };
DWORD dwsize = 0;
SetFilePointer(hFile, 0, 0, FILE_BEGIN);
int i = 0;
while (1)
{
i++;
ReadFile(hFile, buff, 25, &dwsize, 0);
if (dwsize!=0)
{
if (!strcmp(buff, key))
{
printf("在黑名单第%d行!\n", i);
CloseHandle(hFile);
return false;
}
}
else
{
CloseHandle(hFile);
return true;
}
SetFilePointer(hFile, 2, 0, FILE_CURRENT);
}
}
再往下进入Key的精确计算区。把剩余的24个分成9组分别进行计算
0,1 | 2,3 | 4,5 | 6,7 | 8,9,10,11 | 12,13,14,15 | 16,17,18 | 19,20,21 | 22 ,23 | 24
Calc3的算法逻辑 输入进来的分段key ,去数组arry中寻找到,返回索引值。然后求 返回的值再乘以索引值。然后累加上一次的结果。后返回。通过此函数可以知道,所有的key都是此数组中的值。
还原C++代码
//某几位Key取校验值
int check_num(char* num)
{
int len=strlen(num);
int re = 0;
for (int i=0; i<len;i++)
{
for (int k=0;k<0x22;k++)
{
if (num[i] == arry[k])//判断Key在特征字符串的位置
{
//计算幂 pow(base,指数)
double n=pow( 34.0, (double)(len - i - 1) );
int ret = 0;
ret = trunc(n);//取整
ret*=k;
re += ret;
break;
}
}
}
return re;
}
最后对结果进行一些列判断后 调用delphi 的时间封装函数
Vs 中没有这个函数。只能抠出它的代码。有一点注意的是。有一处内存地址的数组。需要去内存里直到。
WORD data[] = {
0x001F, 0x001C, 0x001F, 0x001E, 0x001F, 0x001E, 0x001F, 0x001F,
0x001E, 0x001F, 0x001E, 0x001F, 0x001F, 0x001D, 0x001F, 0x001E,
0x001F, 0x001E, 0x001F, 0x001F, 0x001E, 0x001F, 0x001E, 0x001F
};
bool __fastcall IsLeapYear(unsigned __int16 a1)
{
return !(a1 & 3) && (a1 % 0x64u || !(a1 % 0x190u));
}
int EncodeDate(int a1,int a2,int a3)
{
unsigned __int16 v4; // bx
unsigned __int16 v5; // di
WORD* v6; // esi
int v7; // eax
signed int v8; // ecx
char v10; // [esp+11h] [ebp-3h]
unsigned __int16 v11; // [esp+12h] [ebp-2h]
double a4=0;
v4 = a3;
v5 = a2;
v11 = a1;
v10 = 0;
v6 = (WORD*)(24 * (IsLeapYear(a1) & 0x7F) + (char*)data);
if (v11 >= 1 && v11 <= 0x270F&&
v5 >= 1 && v5 <= 0xC &&
v4 >= 1&& v4 <= *(v6 + 2 * v5 - 2))
{
v7 = v5 - 1;
if (v7 > 0)
{
v8 = 1;
do
{
v4 += *(v6 + 2 * v8++ - 2);
--v7;
} while (v7);
}
a4 = (long double)(v4 + (v11 - 1) / 400 + (v11 - 1) / 4 + 365 * (v11 - 1) - (v11 - 1) / 100 - 0xA955A);
v10 = 1;
}
return a4;
}
再下面也是一些判断然后v29必须为ture。 至此注册流程算法已经分析完成
完整代码
//注册流程
int Reg(char*key)
{
int a=calc(key);
char check1[4] = { 0 };
calc2((a % 0x9987)&0xffff, check1);
if (check1[1]==key[24])//条件1
{
//黑名单检测
if (check_blacklist(key))//条件2
{
char buff1[3] = { 0 };
buff1[0] = key[22];
buff1[1] = key[23];
int v23 = check_num(buff1);
char buff2[3] = { 0 };
buff2[0] = key[0];
buff2[1] = key[1];
int v24 = check_num(buff2);
WORD v56 = (0xff & v23) ^ v24 ^ 0xBF; //n1 必须等于2
char buff3[3] = { 0 };
buff3[0] = key[2];
buff3[1] = key[3];
int v88 = check_num(buff3);
WORD a4 = (0xff & v23) ^ v88 ^ 0xED; //0x64<= a4 <= 0x3DE
char buff4[3] = { 0 };
buff4[0] = key[4];
buff4[1] = key[5];
int v89 = check_num(buff4);
WORD v53 = (0xff & v23) ^ v89 ^ 0x77; //n2 v53<=0x64
char buff5[3] = { 0 };
buff5[0] = key[6];
buff5[1] = key[7];
int v90 = check_num(buff5);
WORD v54 = (0xff & v23) ^ v90 ^ 0xDF; //n3 v54<=0x64
char buff6[5] = { 0 };
buff6[0] = key[8];
buff6[1] = key[9];
buff6[2] = key[10];
buff6[3] = key[11];
int v91 = check_num(buff6);
int a5 = v23 ^v91 ^ 0x4755; // 1<= a5 < 0x31E
char buff7[5] = { 0 };
buff7[0] = key[12];
buff7[1] = key[13];
buff7[2] = key[14];
buff7[3] = key[15];
int v92 = check_num(buff7);
WORD v25 = v23 ^v92 ^ 0x7CC1; //n4 v25>>9 >0 v25>>5 <0xC
char buff8[5] = { 0 };
buff8[0] = key[16];
buff8[1] = key[17];
buff8[2] = key[18];
int v93 = check_num(buff8);
WORD a7 = (v23 & 0xff) ^ v93 ^ 0x3FD; //a7<0xec4
char buff9[5] = { 0 };
buff9[0] = key[19];
buff9[1] = key[20];
buff9[2] = key[21];
int v94 = check_num(buff9);
WORD a8 = (0xff & v23) ^ v94 ^ 0x935; //1<=a8 <= 0xE4C
if (a4 != 0x3E7 && //条件3
v53 <= 0x64u && //条件4
v54 <= 0x64u && //条件5
a5 != 0xFFFF && //条件6
a7 <= 0xE4C && //条件7
a8 <= 0xE4C && //条件8
(a7 != 0xE4C || a8 != 0x726)) //条件9
{
double a6 = 0;
int v26 = v25 >> 9;
v26 = ((v25 >> 9) & 0x1F) + 0x7D3; //最大就7F2
int v27 = (v25 >> 5) & 0xF; //最大F
int v28 = v25 & 0x1F; //最大1F
if (0xffff & v26 >= 0x7D4 //条件10
&& v26 <= 0x833 //条件11
&& v27 >= 1 //条件12
&& v27 <= 0xC
&& v28 >= 1 //条件13
&& v28 <= 0x1F)
{
a6 = EncodeDate(v26, v27, v28);
}
//条件14
bool v29 = v56 == 2 && a4 >= 0x64 && a4 < 0x3DE && a5 && a5 < 0x31E && a6 != 0.0 && a8;
if (v29)
{
printf("success!\n");
}
}
}
}
return 0;
}
2 编写注册机 通过上面分析发第22和23位 的校验值 要与其他所有的分段key的校验值进行计算
此图解释 以v23 v24位例 V56=v23^v24^0xBF
所以v23的值是核心 要先确定。
再看其他关系 注册流程的最后几次判断有大量信息
由上可得
1. V56=2
2. 0x64<= a4 <= 0x3DE
3. v53<=0x64
4. v54<=0x64
5. 1<= a5 < 0x31E
6. 0<(((v25>>9 )&0x1f) <=0x1F 1<= (v25>>5)&0xf <0xC
7. a7=0 // 上面信息可得a7<0xec4。但是实际调试中发现a7>0就通不过校验
8. 1<=a8 <= 0xE4C //这个值在外部会参与对时间值的检测
由于没有固定的位。所有的key值都需要随机生成
代码如下,先随机出22,23位 0位和1 然后依次随机后面的分段
int UnReg()
{
srand(time(0));
int time = 0;
start:
while (1)
{
time++;
char buff[3] = { 0 };
buff[0] = newkey[22] = arry[rand() % 0x21];
buff[1] = newkey[23] = arry[rand() % 0x21];
v0= check_num(buff);
char buff1[3] = { 0 };
buff1[0] = newkey[0] = arry[rand() % 0x21];
buff1[1] = newkey[1] = arry[rand() % 0x21];
v1 = check_num(buff1);
if (((v0&0xff)^v1 ^ 0xBF) == 2)
{
//printf("v1尝试%d次成功\n", time);
break;
}
}
time = 0;
while (1)
{
time++;
char buff2[3] = { 0 };
buff2[0] = newkey[2] = arry[rand() % 0x21];
buff2[1] = newkey[3] = arry[rand() % 0x21];
v2 = check_num(buff2);
if ( (v2^ (v0 &0xff) ^0xED)>=0x64 && (v2^ (v0 & 0xff) ^ 0xED)<=0x3DE)
{
//printf("v2尝试%d次成功\n", time);
break;
}
}
time = 0;
while (1)
{
time++;
char buff3[3] = { 0 };
buff3[0] = newkey[4] = arry[rand() % 0x21];
buff3[1] = newkey[5] = arry[rand() % 0x21];
v3 = check_num(buff3);
if (((v0 &0xff) ^v3^0x77) <=0x64)
{
//printf("v3尝试%d次成功\n", time);
break;
}
}
time = 0;
while (1)
{
time++;
char buff4[3] = { 0 };
buff4[0] = newkey[6] = arry[rand() % 0x21];
buff4[1] = newkey[7] = arry[rand() % 0x21];
v4 = check_num(buff4);
if (((v0 & 0xff) ^v4 ^ 0xDF) <=0x64)
{
//printf("v4尝试%d次成功\n", time);
break;
}
}
time = 0;
while (1)
{
time++;
char buff5[5] = { 0 };
buff5[0] = newkey[8] = arry[rand() % 0x21];
buff5[1] = newkey[9] = arry[rand() % 0x21];
buff5[2] = newkey[10] = arry[rand() % 0x21];
buff5[3] = newkey[11] = arry[rand() % 0x21];
v5= check_num(buff5);
if (((v0 & 0xffff) ^v5^0x4755)>=1 && ((v0 & 0xffff) ^v5 ^ 0x4755)<=0x31E)
{
//printf("v5尝试%d次成功\n", time);
break;
}
}
time = 0;
while (1)
{
time++;
char buff6[5] = { 0 };
buff6[0] = newkey[12] = arry[rand() % 0x21];
buff6[1] = newkey[13] = arry[rand() % 0x21];
buff6[2] = newkey[14] = arry[rand() % 0x21];
buff6[3] = newkey[15] = arry[rand() % 0x21];
v6 = check_num(buff6);
if (((((v0 & 0xffff) ^v6 ^ 0x7CC1) >> 9)<=0xF) &&((((v0 & 0xffff) ^v6 ^ 0x7CC1) >>9)>8) && ((((v0 & 0xffff) ^v6 ^ 0x7CC1) >> 5)&0xf)<0xc)
{
//printf("v6尝试%d次成功\n", time);
break;
}
}
time = 0;
while (1)
{
time++;
char buff7[4] = { 0 };
buff7[0] = newkey[16] = arry[rand() % 0x21];
buff7[1] = newkey[17] = arry[rand() % 0x21];
buff7[2] = newkey[18] = arry[rand() % 0x21];
v7 = check_num(buff7);
if (((v0 & 0xff) ^v7^0x3FD) ==0) //外部检测要为0
{
//printf("v7尝试%d次成功\n", time);
break;
}
//随机不出来就重新开始
if (time>100000)
{
time = 0;
goto start;
}
}
time = 0;
while (1)
{
time++;
char buff8[4] = { 0 };
buff8[0] = newkey[19] = arry[rand() % 0x21];
buff8[1] = newkey[20] = arry[rand() % 0x21];
buff8[2] = newkey[21] = arry[rand() % 0x21];
v8 = check_num(buff8);
if (((v0 & 0xff) ^v8 ^ 0x935) < 0xE4C && (v0^v8 ^ 0x935)>=1)
{
//printf("v8尝试%d次成功\n", time);
break;
}
}
int a = calc(newkey);
char check2[4] = { 0 };
calc2((a % 0x9987) & 0xffff, check2);
newkey[24] = check2[1];//赋值最后一位
//黑名单检测
if (!check_blacklist(newkey))
{
//printf("已在黑名单重新开始\n");
time = 0;
goto start;
}
//日期检测
int year = ((((v0 & 0xffff) ^ v6 ^ 0x7CC1) >> 9) & 0x1F) + 0x7D3;
int month = (((v0 & 0xffff) ^ v6 ^ 0x7CC1) >> 5) & 0xF;
int day = ((v0 & 0xffff) ^ v6 ^ 0x7CC1) & 0x1F;
double data = EncodeDate(year, month, day);
if (data + ((v0 & 0xff) ^ v8 ^ 0x935) <43409) // 43423-14 < data+a8
{
//printf("时间校验未通过,重新开始!\n");
time = 0;
goto start;
}
printf("The key is: %C%C%C%C%C-%C%C%C%C%C-%C%C%C%C%C-%C%C%C%C%C-%C%C%C%C%C\n",
newkey[0], newkey[1], newkey[2], newkey[3], newkey[4],
newkey[5], newkey[6], newkey[7], newkey[8], newkey[9],
newkey[10], newkey[11], newkey[12], newkey[13], newkey[14],
newkey[15], newkey[16], newkey[17], newkey[18], newkey[19],
newkey[20], newkey[21], newkey[22], newkey[23], newkey[24]
);
//复验
Reg(newkey);
Sleep(1000);
return 0;
}
解释下此处代码
if (data + ((v0 & 0xff) ^ v8 ^ 0x935) <43409) // 43423-14 < data+a8
{
//printf("时间校验未通过,重新开始!\n");
time = 0;
goto start;
} ((v0 & 0xff) ^ v8 ^ 0x935) 这个值就是正向注册中的a8
在出了TAIDA64_ParseProductKey函数后 ,取出了a8的值到浮点寄存器与时间值data相加
然后取出固定值浮点数43423-14=43409
这个两个值再进行比较 如果data+a8>=43409 就通过
测试下注册机
完整项目工程打包在了附件
[竞赛]2024 KCTF 大赛征题截止日期08月10日!
上传的附件: