某网络工具 license 签名逆向/伪造

查看 108|回复 9
作者:Vvvvvoid   
License 分析
众所周知, 某工具 的 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

下载次数, 函数

666888tzq   

沙发,期待楼主完善教程。
xixicoco   

surge的分析啊,顶你
gaoyanchen   

大佬强6666
qq882011   

谢谢分享
nmweizi   

不错不催,继续继续
ALMASS   

Old6 要哭晕在厕所里了
zwmfyy   

高手,虽然没看懂,哈哈。
01z8z0   

大佬强66666666
lippone   

大佬真NIu学习了
您需要登录后才可以回帖 登录 | 立即注册

返回顶部