【名称】Navicat Premium
【版本】12.1.18 32位
【所用工具】X32dbg IDA vs2017 crypto++ Openssl wxWidgets Detoures
Navicat 的注册机网上有不少。但详细分析的文章很少。本着学习的精神,把注册流程走了一遍。此次是官网最新版本
1. 注册入口寻找
试了很多断点,不好使。发现CE还是挺不错的。
找到地址后下硬件断点
断下后栈回溯 可发现是宽字符串转多字符
对调用栈大致浏览,发现如下四个相同的函数较为可疑
实际就是取4个编辑框值的函数
拼接完后会判断长度是否达到16位。
然后送入导入函数 libcc.dll #4789函数
跟入
继续跟入
继续进入
继续
进入后对key 第一次变换
替换完后进入的第一个函数。关键call 函数返回数据会进行对比,跟进重点分析
2注册码算法分析
进入函数
接下来填充了一块内存区域 通过一个固定字符串 "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"
缓冲区内填充的是索引值。就是个固定数组arry
算法
char *str = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; //初始化数组 void Inti_arry() { memset(arry, 0xff, 0x400); int i = 0; int len = strlen(str); while (i < len) { if (str[i] - 0x30 > 1 && str[i] - 0x30 < 8) { //数字 arry[str[i]] = i; } else { arry[str[i]] = i; //大写的 arry[str[i] + 0x20] = i;//小写的 } i++; } }
接着初始化0x3c区域 传入函数74dce70
对象结构初始化后
0$ ==> 0893DAF0 libcc.0893DAF0
1$+4 0893DBB4 libcc.0893DBB4
2$+8 00000000 i值
3$+C 00000000
4$+10 00000000
5$+14 00000000
6$+18 0014E384 指向arry首地址
7$+1C 00000000
8$+20 00000005
9$+24 00000005
10$+28 00000000 k
11$+2C 00000000
12$+30 00000000
13$+34 00000005
14$+38 0C5E4948 指向key计算结果区
15$+3c arry
接着送入计算。Key二次变换 16位KEY 变成10位
算法
while (1) { if (i >= 16) break; Index_Value = arry[key[i]]; i++; if (Index_Value < 0x100) { if (!k && !v17) // 第一次时初始化新缓冲区 { memset(buff, 0, 5); // 大小5字节 } v17 += 5; if (v17 > 8) { buff[k] |= (Index_Value >> (v17 - 8)); buff[k + 1] |= Index_Value << (16 - v17); } else { buff[k] |= Index_Value << (8 - v17); } if (v17 >= 8) { //v21 = k; do { v17 -= 8; k++; } while (v17 >= 8); } if (k == 5) // 5次退出循环 5[9]=5 { //调用call break; } } } 化简后 int change2() { buff[0]=( arry[key[0]]<< 3) | (arry[key[1]]>> 2); buff[1]=(( arry[key[1]] << 6) | arry[key[2]] << 1) | (arry[key[3]] >> 4); buff[2]=( arry[key[3]] << 4) | (arry[key[4]]>> 1); buff[3]=( arry[key[4]] << 7) | (arry[key[5]] << 2) | (arry[key[6]] >> 3); buff[4]=( arry[key[6]]<< 5) | arry[key[7]]; return 0; }
以上函数会执行两次产生10个字节数据
转换完后buff 的前两字节必须是 0x68 0x2A
此验证通过后,取后8位
接下来开始验证后8位。跟进
拷贝了一块数据区,用途不知道
进入计算函数
跟入
来到加密核心函数 进入
进入来到此
进入加密核心后 有三个连续加密函数,大致浏览下像DES,(就是DES)
经过后面分析拷贝的常量数据就是DES 的加密密钥
int M[] ={ 0x3C362C32, 0x263C3B23, 0x152D3B32, 0x1A1C2619, 0x36321632, 0x051D1A27, 0x0E1F0C1F, 0x32280D34, 0x2F22272D, 0x370B3B0C, 0x032F3119, 0x3837303B, 0x31043D22, 0x3D191D13, 0x1F1D0A2F, 0x043D063E, 0x11090722, 0x37173D23, 0x293D0E1F, 0x091D053F, 0x3E0C3B3A, 0x07322B0C, 0x2C3B3011, 0x2C2A1A2F, 0x3B031D07, 0x1B261D10, 0x1038072D, 0x393F361E, 0x371C3B3C, 0x0A052033, 0x01320A0D, 0x2F27371F };
第二计算中用到8组常量数据 每组64个
一共16轮计算
大致流程 倒序值1倒序值2-> 前置换->16轮加密->后置换->倒序值1 倒序值2
这个两个值拷贝到返回空间进行校验
解密DES
逆序密钥
int N[] = { 0x01320A0D,0x2F27371F,0x371C3B3C,0x0A052033, 0x1038072D,0x393F361E,0x3B031D07,0x1B261D10, 0x2C3B3011,0x2C2A1A2F,0x3E0C3B3A,0x7322B0C, 0x293D0E1F,0x091D053F,0x11090722,0x37173D23, 0x1F1D0A2F,0x043D063E,0x31043D22,0x3D191D13, 0x032F3119,0x3837303B,0x2F22272D,0x370B3B0C, 0x0E1F0C1F,0x32280D34,0x36321632,0x051D1A27, 0x152D3B32,0x1A1C2619,0x3C362C32,0x263C3B23 };
然后进行校验
校验1
校验2
校验3
校验4
进入
索引了一块常量数组判断
也就是值1的最后一个要是0xCE 值2第一个要是0x32
校验5 与校验4差不多,判断是不是0x33
{常数 0x975c52c
0x00000089, 0x000000AC, 0x00000001, 0x0000009A,
0x000000AA, 0x00000002, 0x00000033, 0x000000CE,
0x00000003, 0x00000083, 0x000000AC, 0x00000004,
0x00000061, 0x000000B5, 0x00000005, 0x00000061,
0x000000B1, 0x00000006, 0x00000021, 0x000000FA,
0x00000007, 0x00000011, 0x000000AE, 0x00000008,
0x0000004A, 0x000000CD, 0x00000009, 0x00000056,
0x000000BB, 0x0000000A, 0x00000017, 0x000000EE,
0x0000000B, 0x00000000, 0x02B36191, 0x88000200,
0x00000000
};
第二次判断是不是33 .也就是值2的第一个可以是0x32或者0x33
3注册码总结
注册码注册机思路
1. 先随机出DES计算后的值
2.变换后送入DES 解密函数 得到8个字节 再合并0x68 0x2A
得到二次变换算法后的10个字节
3.10个字节分两组 前5 后5
4.逆序二次变换算法,算法水平有限,没关系算法不行随机数来凑。通过随机数验证后发现 第三位始终不变。先随机出第3位。其余的7个顺次推算出来
int Rand_First8(char*a1) { char *ret = a1; char arr[8] = {}; int timestart = GetTickCount(); int timenow = 0; while (1) { arr[1] = rand() % 31; arr[2] = rand() % 31; arr[3] = rand() % 31; arr[4] = rand() % 31; //第3个唯一 if (ret[1] == (char)(((arr[1] << 6) | (arr[2] << 1)) | (arr[3] >> 4))) {//进入后arr[2]确定 if (ret[2] == (char)((arr[3] << 4) | (arr[4] >> 1))) {//进入后arr3确定 //printf("0x%02x 0x%02x ", arr[2], arr[3]); break; } } timenow = GetTickCount(); if (timenow - timestart> 500) { return -1; } } while (1) { arr[0] = rand() % 31; arr[1] = rand() % 31; if (ret[1] == (char)(((arr[1] << 6) | (arr[2] << 1)) | (arr[3] >> 4))) {//ret1确定 if ((char)((arr[0] << 3) | (arr[1] >> 2)) == ret[0]) {//ret 0确定 //printf("0x%02x 0x%02x 0x%02x 0x%02x ", arr[0], arr[1], arr[2], arr[3]); break; } } timenow = GetTickCount(); if (timenow - timestart > 1000) { return -1; } } while (1) { arr[4] = rand() % 31; arr[5] = rand() % 31; arr[6] = rand() % 31; // if (ret[2] == (char)((arr[3] << 4) | (arr[4] >> 1))) {//ret4确定 if (ret[3] == (char)(((arr[4] << 7) | (arr[5] << 2)) | (arr[6] >> 3))) {//ret 5确定 //printf("0x%02x ,0x%02x, 0x%02x, 0x%02x,0x%02x, 0x%02x ", arr[0], arr[1], arr[2], arr[3], arr[4], arr[5]); break; } } timenow = GetTickCount(); if (timenow - timestart > 1500) { return -1; } } while (1) { arr[6] = rand() % 31; arr[7] = rand() % 31; if (ret[3] == (char)(((arr[4] << 7) | (arr[5] << 2)) | (arr[6] >> 3))) {//ret6确定 if (ret[4] == (char)((arr[6] << 5) | arr[7])) {//ret7确定 //printf("0x%02x,0x%02x,0x%02x,0x%02x,0x%02x,0x%02x,0x%02x,0x%02x ", arr[0], arr[1], arr[2], arr[3], arr[4], arr[5], arr[6], arr[7]); break; } } timenow = GetTickCount(); if (timenow - timestart > 2000) { return -1; } } int i = 0; char k1, k2, k3, k4, k5, k6, k7, k8 = 0; while (i < 0x61) { if (arry[i] == arr[0]) { k1 = i; } if (arry[i] == arr[1]) { k2 = i; } if (arry[i] == arr[2]) { k3 = i; } if (arry[i] == arr[3]) { k4 = i; } if (arry[i] == arr[4]) { k5 = i; } if (arry[i] == arr[5]) { k6 = i; } if (arry[i] == arr[6]) { k7 = i; } if (arry[i] == arr[7]) { k8 = i; } i++; } sprintf_s(Edit8, "%C%C%C%C%C%C%C%C", k1, k2, k3, k4, k5, k6, k7, k8); return 1; }
5.再通过数组索引到字符 得到前8个KEY
6.后5个同样操作后得到后8个KEY
7.拼接即可
4激活
早有耳闻激活用的是RSA2048 算法
断网状态 点手动激活,会产生请求码。Base64编码
点击手动激活后,断下
栈回溯两层。此时数据已经加密
到达此处进行base64编码
由于已经进行base64编码了所以数据应该在上面函数产生
Sub_9FDA960 函数拼接公钥。该函数F5后有5千多行。电脑卡了好久。这5000多行就是一点一点的拼接公钥。里面应该不止一组公钥。
最后得到公钥如下: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw1dqF3SkCaAAmMzs889IqdW9M2dIdh3jG9yPcmLnmJiGpBF4E9VHSMGe8oPAy2kJDmdNt4BcEygvssEfginva5t5jm352UAoDosUJkTXGQhpAWMF4fBmBpO3EedG62rOsqMBgmSdAyxCSPBRJIOFR0QgZFbRnU0frj34fiVmgYiLuZSAmIbs8ZxiHPdp1oD4tUpvsFci4QJtYNjNnGU2WPH6rvChGl1IRKrxMtqLielsvajUjyrgOC6NmymYMvZNER3htFEtL1eQbCyTfDmtYyQ1Wt4Ot12lxf0wVIR5mcGN7XCXJRHOFHSf1gzXWabRSvmt1nrl7sW6cjxljuuQawIDAQAB
再往上就是发送数据格式化成json格式
发送格式
K为注册码 DI为机器码,P系统
{"K":"NAVCFMOWYGS3HSEB", "DI":"TJwDCin7AARnhsXG5NEA", "P":"WIN"}
数据加密发送到服务器 私钥解密后,再添加上购买Key时的用户名,公司名,加上时间
再用私钥加密base64编码后发送回来
如果有正确激活码。公钥解密后的格式如下
前面 K 和DI不变。N为用户名 O 为公司名 T为激活时间
{"K":"NAVCFMOWYGS3HSEB", "DI":"TJwDCin7AARnhsXG5NEA", "N":"jilvan", "O":"jilvan1234", "T":1556150400}
5反激活
1. 要解密加密后的数据必须要有私钥。私钥在服务器无法得到。只能用自己的公钥替换程序中的公钥
2. 替换完公钥后用自己的私钥解密请求码。
3. 添加用户名 公司名以及时间重新封装数据
4. 用私钥加密数据后base64编码得到激活码
那现在核心点就在替换原始公钥了
有几种方案
1.直接修改公钥产生函数 一共需要修改差不多100多处。比较麻烦。
2.DLL劫持注入。在libcc.dll加载时修改函数返回值为自己的公钥。可能会有安全软件拦截
3.libcc.dll导入表注入。给libcc.dll添加一个导入表为自己的dll 在自己的dll加载时 hook 公钥产生函数返回代码。把自己的公钥复制到原始公钥缓冲区,实现替换。本文就使用此方法
添加导入表写的话还挺麻烦,不过没关系有工具啊。Detoures库下有个setdll 工具可以添加导入表。
Hook 点在函数返回时eax 存放公钥地址指针。在返回时跳转至自己函数执行
Hook 后
Passlibcc.dll 关键代码如下
dllmian.cpp const char *PubKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxmdOvC2w DW+sH1S9hOvBa9xPLVh73n2sT0AENz0dkYzYRHcUHzjVvCx3cr/AMLWmHi1raP0Mhv8WXtm X+WsqG+kjt1lzBAcEum4I1RIl6qMR2Fp/LOX7PgP6yUa6ob5HHKYT++02X5szObS0By57RO7yJ Hc8qCPvsx1XF0GU1wsNjLBeQiwrkfve09r6Vy4ZvG/YtxZuk5gmSgCgrEaceZwzJbsKHeZyeRdBsU SGA0y+slSbIjKh+CEp5RFRmqKP0C78BHSdNGio21uLW6pvPmBSxU8FlMPdhGr3jkAPM0K2zFA ypUQw7UF8cxXIBvzxfwA6zmPu9oNWrYzVWpBXvwIDAQAB"; DWORD RetAddr = 0; BOOL APIENTRY DllMain( HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved ) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: case DLL_THREAD_ATTACH: { BYTE code[5] = { 0xE9,0,0,0,0 }; //获取Libcc加载基址 HMODULE hModule = 0; GetModuleHandleExA(0, "libcc.dll", &hModule); //定位获取公钥函数返回地址 DWORD offset =0x116AE1E; DWORD NowAddr = (DWORD)hModule + offset; DWORD jmpoffet=(DWORD)(&ReplacePublicKey) - ((DWORD)hModule + offset) - 5; memcpy(&code[1], &jmpoffet, 4); //hook DWORD oldProtect=0; VirtualProtect((void*)NowAddr, 0x1000, PAGE_EXECUTE_READWRITE, &oldProtect); memcpy((char*)NowAddr, code, 5); VirtualProtect((void*)NowAddr, 0x1000, oldProtect,0); } case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; }
Passlibcc.cpp
//必须有导出函数 extern "C" _declspec(dllexport) int haha() { int a = 123456; return a; } void _declspec(naked) ReplacePublicKey() { _asm { PUSHAD; mov ecx, 0x188; mov esi, [PubKey];//自己公钥 mov edi, [eax];//eax存放的原始公钥 rep movsb; POPAD; //被hook的代码 mov esp, ebp; pop ebp; ret 4; } }
给libcc.dll添加导入dll passlibcc.dll
接下来就是用私钥加密解密了。首选了crypto++库。可以正常私钥解密。但是私钥加密,都是签名方案。没找到直接私钥加密方案。又转到openssl库,有直接私钥加密的函数RSA_private_encrypt。结果注册机就成了两个库的结合体。。。
关键代码如下:UI使用wxWidgets库
//解析请求码 AutoSeededRandomPool rng; std::string result; FileSource fs2("RegPrivateKey.pem", true); CryptoPP::RSA::PrivateKey privateKey; PEM_Load(fs2, privateKey); RSAES_PKCS1v15_Decryptor priv(privateKey); StringSource((byte*)ReqCode, strlen(ReqCode), true, new Base64Decoder(new PK_DecryptorFilter(rng, priv, new StringSink(result)))); int t = time(0); char time1[10] = { 0 }; _itoa(t, time1, 10); //修改解密后的请求码 FixRequestCode(RequestCode, nameCode, orgCode, time1)) //加载私钥 errno_t err = fopen_s(&hfile, "RegPrivateKey.pem", "rb"); if (err) { wxString msg = wxString::FromUTF8("加载私钥文件错误"); wxMessageBox(msg, _("Error")); return; } //2.从私钥中获取 加密的秘钥 if ((p_rsa = PEM_read_RSAPrivateKey(hfile, NULL, NULL, NULL)) == NULL) { wxString msg = wxString::FromUTF8("加载私钥错误"); wxMessageBox(msg, _("Error")); fclose(hfile); return; } Out = (char *)malloc(300); memset(Out, 0, 300); //用私钥加密 int WriteBytes = RSA_private_encrypt(strlen(RequestCode), (const unsigned char*)RequestCode, (unsigned char*)Out, p_rsa, RSA_PKCS1_PADDING); if (WriteBytes == 0) { wxString msg = wxString::FromUTF8("私钥加密失败"); wxMessageBox(msg, _("Error")); if (p_rsa) RSA_free(p_rsa); if (hfile) fclose(hfile); return; } string valuecode; //转换base64 StringSource((byte *)Out, 256, true, new Base64Encoder(new StringSink(valuecode))); *Activity_Text << valuecode.c_str(); //6.释放秘钥空间, 关闭文件 if (p_rsa) RSA_free(p_rsa); if (hfile) fclose(hfile); //修改json 格式 int FixRequestCode(char* In, const char*name, const char*Org, char* date) { char N[30] = { ""N":"" }; char O[30] = { ""O":"" }; char T[20] = { ""T":" }; if (strlen(name) > 20 || strlen(Org) > 20) { return 0; } strcat(N, name); N[strlen(N)] = '"'; N[strlen(N) + 1] = '0'; strcat(O, Org); O[strlen(O)] = '"'; O[strlen(O) + 1] = '0'; strcat(T, date); T[strlen(T)] = '}'; T[strlen(T) + 1] = '0'; for (int i = 0; i < strlen(In); i++) { if (*(In + i) == 'P') { if (*(In + i - 1) == '"' && *(In + i + 1) == '"') { *(In + i - 1) = '0'; //截断机器码以后内容 break; } } } strcat(In, N); strcat(In, ", "); strcat(In, O); strcat(In, ", "); strcat(In, T); return 1; }
最后附张激活图
Hosts文件没有修改敏感操作会报毒。可以自行添加127.0.0.1 activate.navicat.com
总结
Navicat 各版本注册机制变化不大,可能不同版本校验值不同,其余版本没看过应该大致方法差不多。
[培训]《安卓高级研修班(网课)》月薪三万计划,掌 握调试、分析还原ollvm、vmp的方法,定制art虚拟机自动化脱壳的方法