众所周知, 某工具 的 license 是存在本地的 macOS 文件系统的扩展属性(extended attribute)中的,以此为切入点, 我们开始逆向分析;
读取扩展属性的函数为 getxattr, 在 hopper 中搜索,查看引用;
1.png (871.39 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
定位到了函数 1001b3c4b
2.png (130.5 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
看代码,由函数 applicationSupportDirectoryPathWithName:@"com.nssurge.surge-mac"] 得知, 对应的文件为 com.nssurge.surge-mac
在查看 1001b3c4b的 引用,看看取出来之后做什么了
3.png (65.11 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
切到第一个引用看看;
4.png (56.37 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
看到它取出来了对应 com.nssurge.surge-mac 文件的 com.nssurge.surge-mac.nsa.3 项并转成了 json 然后返回;
在查看引用, 看到一个load 函数, 有点像入口的 license 加载;
跟进去看看;
5.png (53.18 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
6.png (518.84 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
这里 load 函数中 可以看到;
call sub_1001b3bfd // 返回 com.nssurge.surge-mac.nsa.3 的内容 转 json
赋值给 qword_100860e80;
如果 不为空, 则执行函数 call sub_1001b2827;
跟进去 sub_1001b2827 看看;
hopper 的 假码不如 ida, 换 ida 看看;
7.png (141.07 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
这里看到第九行,调用了函数 sub_1001C6455, 并且把我们的 json 传递了进去, 跟进去看看;
__int64 __fastcall sub_1001C6455(void *a1)
{
id v1; // r15
id v2; // rax
id v3; // rbx
char v4; // r14
v1 = objc_retain(a1);
v2 = objc_msgSend(&OBJC_CLASS___NSDate, "date");
v3 = objc_retainAutoreleasedReturnValue(v2);
v4 = sub_1001C64BC(v1, v3);
objc_release(v1);
objc_release(v3);
return (unsigned int)v4;
}
v1 = a1 = json;
接着看 sub_1001C64BC(v1,v3);
8.png (303.22 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
可以看到这里就是核心的验证函数了, 从 json 取了很多字段出来,我们挨个看看;
注意看 92 与 101 行, 这里俩个常量 100799BB0 与 100799BB0 分别是字符串 “policy” 与 “sign”;
从 json 中 取出了 “policy” 与 “sign” 并且 base64 解密后分别 赋值给了 v20, v21;
由此可知, json格式如下
{
"policy":"base64 xxx",
"sign":"base64 xxx"
}
9.png (370.73 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
接着传递参数 policy,sign 调用 函数 sub_1001C6B0B ,如果函数返回 true ,则会调用 120行, 将 “policy” 在转成 json;
如果 函数返回 false, 则退出;
往下看, 可以看到 policy 的字段信息;
r12 = [[rax objectForKeyedSubscript:@"deviceID"]];
rax = [var_58 objectForKeyedSubscript:@"type"]; // trial:licensed:revoked
rax = [var_58 objectForKeyedSubscript:@"product"]; // SURGEMAC5
lea rdx, qword [cfstring_expiresOnDate] ; // expiresOnDate
由此得知, sub_1001C6B0B 是用来校验 policy 与 sign 的;
跟进去看看;
10.png (308.3 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
分析 Sign 校验
函数 sub_1000379E6 返回了一个 v3 对象, 并且取了byte 跟 length, 跟进去;
id sub_1000379E6()
{
void *v0; // rbx
id v1; // rax
id v2; // rax
v0 = malloc(0x1C3uLL);
if ( dword_1008653C4 != 8 )
_InterlockedExchange(&dword_1008653C4, 8);
qmemcpy(
v0,
"-----BEGIN PUBLIC KEY-----\n"
"xxxxxx\n"
"-----END PUBLIC KEY-----\n",
451);
v1 = objc_alloc(&OBJC_CLASS___NSData);
v2 = objc_msgSend(v1, "initWithBytesNoCopy:length:freeWhenDone:", v0, 451LL, 1LL);
return objc_autoreleaseReturnValue(v2);
}
恭喜你, 我们找到了验证 sign 用的 公钥, 可以看到调用 NSData的 initWithBytesNoCopy 方法, 把 451位长度的 PUBLIC KEY 生成并且返回;
众所周知, sign 是有私钥加密 policcy 来的, 然后用 公钥在验证, 我们肯定是拿不到 私钥的;
要完整验证 我们只能自己创建一份公钥/私钥,
之后用 私钥 生成把 policy sign 之后 把 公钥替换成我们的;
替换密钥
首先我们创建一对 RAS 的密钥;
!! 注意 app 中,公钥长度为 451L, 这是标准的 rsa pkcs#8 编码; 而 macos/ios 下 默认的是 pkcs#1;
这里如果用 objc 来生成密钥对, 需要正对头不做一些处理,才能兼容 pkcs#8
10_1.png (278.11 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
public key :
-----BEGIN PUBLIC KEY-----
xxxxxxx
-----END PUBLIC KEY-----
privite key :
-----BEGIN PRIVATE KEY-----
xxxxxxxx
-----END PRIVATE KEY-----
之后我们拼接一个 policy , 然后 base64 一次:
{
"deviceID":"xxxxx",
"type":"licensed",
"product":"SURGEMAC5",
"expiresOnDate":"1746350567",
"p":"base64xx"
}
policy : base64_policy_xxxx
之后使用我们的私钥加密 policy 得道 sign 之后得到 sign 的 base64:
sign: base64_sign_xxxx
由此可以得到我们的 license
通过 xattr 命令, 将 licennse 写入文件
xattr -w com.nssurge.surge-mac.nsa.3 '{"policy":"base64_policy_xxxx","sign":"base64_sign_xxxx"}' "~/Library/Application Support/com.nssurge.surge-mac"
之后我们通过 hook initWithBytesNoCopy 函数来替换公钥;
- (NSData *) hk_initWithBytesNoCopy:(void *)bytes length:(NSUInteger)length freeWhenDone:(int)freeWhenDone {
if (length == 0x1c3) {
NSString *inputString = [[NSString alloc] initWithBytes:bytes length:length encoding:NSUTF8StringEncoding];
NSLog(@">>>>>> hk_initWithBytesNoCopy input string: %@", inputString);
// 替换公钥
NSString *replacementString = @"-----BEGIN PUBLIC KEY-----\n"
"xxxxxxxx"
"-----END PUBLIC KEY-----\n";
const char *replacementBytes = [replacementString UTF8String];
NSUInteger replacementLength = [replacementString lengthOfBytesUsingEncoding:NSUTF8StringEncoding];
memcpy(bytes, replacementBytes, replacementLength);
length = replacementLength;
}
NSData *ret = ((NSData*(*)(id, SEL,void *,NSUInteger,int))initWithBytesNoCopyMethodIMP)(self, _cmd,bytes,length,freeWhenDone);
return ret;
}
00000001001c6638 call sub_1001c6b0b ; sub_1001c6b0b
之后重启, 在 call sub_1001C6B0B 下断点, 然后下一步, 查看 al 寄存器, 为 1 , 则解析验证成功;
11.png (487.95 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
后面的代码解释解析 json 获取 license 信息了;
解析后接着看后面代码:
r12 = [[rax objectForKeyedSubscript:@"deviceID"] retain];
rax = sub_1001c6ca6();
rax = [rax retain];
var_90 = r12;
rsi = @selector(isEqualToString:);
r12 = _objc_msgSend_10073b570(r12, rsi);
这里取出了, deviceId 跟 sub_1001c6ca6() 做比较, 可见 sub_1001c6ca6 存放的是真实的 deviecId;
看来我们 license 需要存放正确的 deviceId;
接着看 1001c6ca6 的具体实现, 返回了 data_100860ee8, 接着我们查看哪个函数给 data_100860ee8 赋值;;
1001c6cb2 if (data_100860ee0 != -1)
1001c6ccf _dispatch_once(&data_100860ee0, &data_100743a78)
1001c6cbc return _objc_retainAutoreleaseReturnValue(data_100860ee8) __tailcall
定位到了 sub_1001c6ce7
分析 deviceid 参数
rbx = [[NSMutableArray array] retain];
rax = sub_1001b3dd7();
rax = [rax retain];
r12 = rax;
rdx = rax;
if (rax == 0x0) {
rdx = @"#";
}
[rbx addObject:rdx];
[r12 release];
rax = sub_1001c6f1e();
rax = [rax retain];
r12 = rax;
rdx = rax;
if (rax == 0x0) {
rdx = @"#";
}
[rbx addObject:rdx];
[r12 release];
rax = sub_1001c6f1e();
rax = [rax retain];
r12 = rax;
rdx = rax;
if (rax == 0x0) {
rdx = @"#";
}
[rbx addObject:rdx];
[r12 release];
rax = [NSNumber numberWithLongLong:sub_1001c6ff1()];
rax = [rax retain];
r13 = rax;
rdx = rax;
if (rax == 0x0) {
rdx = @"#";
}
r15 = @"#";
r14 = *_objc_msgSend;
[rbx addObject:rdx];
[r13 release];
rax = sub_1001c6ff1();
r13 = r14;
rax = [NSNumber numberWithLongLong:rax];
rax = [rax retain];
r12 = rax;
if (rax != 0x0) {
r15 = rax;
}
(r13)(rbx, @selector(addObject:), r15);
[r12 release];
rax = sub_1001b3e69();
rax = [rax retain];
r15 = rax;
if (rax != 0x0) {
[rbx addObject:r15];
}
r12 = *_objc_release;
rax = (r13)(rbx, @selector(componentsJoinedByString:), @"/");
rax = [rax retain];
r14 = rax;
rax = (r13)(rax, @selector(KD_MD5), @"/");
rax = [rax retain];
rdi = *qword_100860ee8;
*qword_100860ee8 = rax;
(r12)(rdi, @selector(KD_MD5), @"/");
(r13)(*qword_100860ee8, @selector(getCString:maxLength:encoding:), 0x100865340, 0x80, 0x1);
(r12)(r14, @selector(getCString:maxLength:encoding:), 0x100865340);
(r12)(r15, @selector(getCString:maxLength:encoding:), 0x100865340);
(r12)(rbx, @selector(getCString:maxLength:encoding:), 0x100865340);
分别获取了设备的 IOPlatformUUID,hw.model,machdep.cpu.brand_string,machdep.cpu.signature,hw.memsize,hardwareAddress,
之后用 / join 合并成一行字符串;
XASDQW1-VCSA-ASWE-2DCX-XNANDWQWE/MacBookProX,X/Intel(R) Core(TM) X CPU @ XGHz/501194/12134164124/f3:21:32:2c:44:30
然后转 MD5 就是 32 位的 设备 id 了;
我们可以自己用代码实现, 或者 获取内存指针来生成真实的 设备id;
获取设备id :
void *fun_device = (void *)_sub_0x1001c6ca6;
deviceId = ((NSString* (*)())fun_device)();
NSLog(@">>>>>> device id is %@",deviceId);
设备id 校验通过后, 接着往后看:
byte_100860ED0 = 1;
v35 = objc_alloc(&OBJC_CLASS___NSData);
v36 = objc_msgSend(v68, "objectForKeyedSubscript:", 'p');
v37 = objc_retainAutoreleasedReturnValue(v36);
v38 = objc_msgSend(v35, "initWithBase64EncodedString:options:", v37, 0LL);
((void (__fastcall *)(id))objc_release)(v37);
v39 = (void *)sub_1001C6CA6();
v40 = objc_retainAutoreleasedReturnValue(v39);
v41 = objc_msgSend(v40, "dataUsingEncoding:", 1LL);
v42 = objc_retainAutoreleasedReturnValue(v41);
((void (__fastcall *)(id))objc_release)(v40);
v43 = objc_retainAutorelease(v42);
v44 = objc_msgSend(v43, "bytes");
v45 = objc_msgSend(v43, "length");
((void (__fastcall *)(char *, id, id))loc_10045DCE0)(v69, v44, v45);
v46 = (char *)objc_msgSend(v38, "length") + 32;
if ( qword_100865330 )
free(qword_100865330);
qword_100865330 = malloc((size_t)v46);
v59 = 0LL;
v47 = objc_retainAutorelease(v38);
v48 = v46;
v49 = objc_msgSend(v47, "bytes");
v50 = objc_msgSend(v47, "length");
CCCrypt(1LL, 0LL, 1LL, v69, 32LL, v70, v49, v50, qword_100865330, v48, &v59);
objc_release(v43);
objc_release(v47);
v20 = v65;
if ( *(_BYTE *)qword_100865330 != 3 )
sub_1000B78FA(2LL, &off_100799C30, &off_100799C50);
v51 = objc_msgSend(v68, "objectForKeyedSubscript:", &off_100799C70);
v52 = objc_retainAutoreleasedReturnValue(v51);
这里获取了 p , 然后 base 解码, 之后调用 CCCrypt 函数, 解密的值 out 给 qword_100865330;
之后后面还有一些 qword_100865330 的判断;
看来 policy 的 json 除了 deviceid 还有一个 p 参数也要校验
分析 p 校验
我们来 hook CCCrypt 函数, 来获取解密用的 key 与 iv,从而可以通过自己加密来伪造一份 p
因为 key 与 iv 不是明文字符串, 所以我们要直接拿来用, 只能把 bytes 转为 base64 了;
防止拿错,我们加一个第五个参数 32LL 的判断
DobbyHook((void *) CCCrypt,(void *) hk_CCCrypt, (void **) &original_CCCrypt);
CCCryptorStatus hk_CCCrypt(
CCOperation op, /* kCCEncrypt, etc. */
CCAlgorithm alg, /* kCCAlgorithmAES128, etc. */
CCOptions options, /* kCCOptionPKCS7Padding, etc. */
const void *key,
size_t keyLength,
const void *iv, /* optional initialization vector */
const void *dataIn, /* optional per op and alg */
size_t dataInLength,
void *dataOut, /* data RETURNED here */
size_t dataOutAvailable,
size_t *dataOutMoved)
{
if (keyLength==32) {
NSData *keyData = [NSData dataWithBytes:key length:keyLength];
NSString *keyBase64 = [keyData base64EncodedStringWithOptions:0];
NSString *keyStr = [[NSString alloc] initWithData:keyData encoding:NSUTF8StringEncoding];
NSLog(@">>>>>> keyBase64 :%@",keyBase64);
NSLog(@">>>>>> keyStr :%@",keyStr);
if (iv!=nil) {
NSData *ivData = [NSData dataWithBytes:iv length:(unsigned int)getIVLength(alg)];
NSString *ivBase64 = [ivData base64EncodedStringWithOptions:0];
NSString *ivStr = [[NSString alloc] initWithData:ivData encoding:NSUTF8StringEncoding];
NSLog(@">>>>>> ivBase64 :%@",ivBase64);
NSLog(@">>>>>> ivStr :%@",ivStr);
}
}
CCCryptorStatus result = original_CCCrypt(op,alg,options,key,keyLength,iv,dataIn,dataInLength,dataOut,dataOutAvailable,dataOutMoved);
return result;
}
获取到 key iv 后, 我们来生成 p 并且CCCrypt 加密,之后在 base64 一次;
目前还没看明白这个 p 参数 到底是什么, 看到有一些是否等于3的判断;
至少先让他能正常参与加密解密吧,并且不等于0 把,
这里我就将第一位先设置成 1 了;
unsigned char charArray[] = {1, 10, 100, 200};
NSUInteger length = sizeof(charArray) / sizeof(unsigned char);
NSMutableData *data = [NSMutableData data];
[data appendBytes:charArray length:length];
NSData *key = [[NSData alloc] initWithBase64EncodedString:@"{key base64}" options:0];
NSData *iv = [[NSData alloc] initWithBase64EncodedString:@"{iv base64}" options:0];
NSData *encryptedData = [EncryptionUtils cccEncryptData:data withKey:key iv:iv];
NSString *encryptedDataBase64 = [encryptedData base64EncodedStringWithOptions:0];
NSLog(@"p %@", encryptedDataBase64);
if (*qword_100865330 != 0x0) {
free(*qword_100865330);
}
if (*(int8_t *)*qword_100865330 != 0x3) {
sub_1000b78fa(0x2, @"SGMain", @"System DNS update");
}
if (*(int8_t *)*qword_100865330 == 0x3) {
// rax = [SGDNSPacket queryPacketWithDomain:@"captive.apple.com" identifier:0x0 queryType:0x100];
dispatch_after(dispatch_time(0x0, (arc4random_uniform(0x258) + 0x3c) * 0x3b9aca00), [dispatch_get_global_queue(0x0, 0x0) retain], ^ {/* block implemented at sub_10000e09d */ } });
}
int sub_100013d50(int arg0, int arg1, int arg2, int arg3) {
rax = *qword_100865330;
if (*(int8_t *)rax == 0x3) {
rax = pthread_create(&var_8, 0x0, sub_100013d8e, 0x0);
if (rax == 0x0) {
rax = pthread_detach(var_8);
}
}
return rax;
}
之后重写生成 policy 与 sign, 重启;
然后调试, 发现 我们的 p 被正确的解密出来了;
12.png (595.24 KB, 下载次数: 0)
下载附件
2024-5-9 11:00 上传
至此 license 伪造, 完毕;
当然, 到目前为止, 软件功能还不能正常使用, 因为后面还有很多要处理的, 但是我先去刷碗了;
最后汇总下:
{
// policy json 之后转 base64
"policy":{
// 逆向比较函数,来获取真实的 deviceID
"deviceID": "",
// 枚举类型: trial|licensed|revoked
"type": "licensed",
// 固定值
"product": "SURGEMAC5",
// 时间戳
"expiresOnDate": 1746350567,
// hook CCCrypt 获取 key 与 iv, 之后将{1} 加密然后 base64
"p": "xkIQAJe6FhgdEh3Q1y7+Sg=="
},
// 将 policy RSA 加密后转 base64
"sign":""
}
相关的测试代码, 可参考:
https://github.com/marlkiller/mac_patch_helper/blob/main/mac_patch_helper_test/encryp_test.m