飘云阁

 找回密码
 加入我们

QQ登录

只需一步,快速开始

查看: 960|回复: 13

[macOS] AnyGo 8.3.x 算法分析记录初稿

  [复制链接]
  • TA的每日心情
    开心
    2019-3-17 22:44
  • 签到天数: 132 天

    [LV.7]常住居民III

    发表于 2026-6-20 15:55:51 | 显示全部楼层 |阅读模式
    本帖最后由 tree_fly 于 2026-6-25 00:15 编辑
    AnyGo 8.3.x 分析破解记录

    前言: 国行 Apple Watch 的 一些房颤检测、睡眠呼吸监测等功能的开通,要用到更改Location的工具,搞起!
    声明:本文仅为逆向技术学习讨论交流用!!!
    截屏2026-06-19 01.50.18.png
    使用Hopper Demo分析一波,收集一些关键信息。

    一、整体架构分析


    AnyGo 的注册保护分为两层:
    截屏2026-06-18 02.00.06.png


    二、本地验证逆向:_snvrfy 函数
    2.1 发现入口
    Hopper 反编译 RegisterManager 类,找到 registerEmail:code: 方法。

    该方法调用 snvrfy:
    [C] 纯文本查看 复制代码
        // 格式清理(去除换行、空白等)
        r15 = [[self checkEmailAndRegisterCode:r13] retain];
    
        rax = snvrfy(var_30, r14, &var_40);
        rax = rax == 0x0 ? 0x1 : 0x0; // 0 = success → 返回 1
    


    snvrfy 从 libsnvrfy.dylib 动态链接,是本地验证的核心。返回值 0 表示成功,验证结果 JSON 写入第三个参数 output_buffer。

    2.2 snvrfy 算法完整逆向

    Hopper 反编译 libsnvrfy.dylib 中的 _snvrfy 函数,伪代码如下:

    [C] 纯文本查看 复制代码
    /* snvrfy 算法 */
    int _snvrfy(int arg0 /*email*/, int arg1 /*code*/, int arg2 /*&output_buf*/) {
    
        // 硬编码公钥(直接 memcpy 到栈)
        memcpy(&var_80,
            "-----BEGIN PUBLIC KEY-----\n"
            "MCwwDQYJKoZIhvcNAQEBBQADGwAwGAIRAMHFWywkLO5vdQpvM0UXlrsCAwEAAQ==\n"
            "-----END PUBLIC KEY-----\n",
            0x76);  // 118 字节(含 \0)
    
        // 去除破折号,最多处理 30 个字符(0x1e)!
        var_140 = (strlen(arg1) <= 0x1e) ? strlen(arg1) : 0x1e;
        for (i = 0; i < var_140; i++) {
            if (arg1 != '-')           // 0x2d = '-'
                stripped[var_138++] = arg1;
        }
    
    //  Base32 解码 → 应得 16 字节
        rax = _base32_decode(stripped, decoded_buf, 0x20);
        if (rax != 0x10) {
            sprintf(output, "{\"code\": -1, \"message\": \"decode length not equal encode length\"}");
            return -1;
        }
    
        //  RSA 公钥解密(X9.31 Padding)→ 应得 12 字节
        rax = _public_decrypt(decoded_buf, 0x10, &pubkey_pem, plaintext);
        switch (rax) {
            case 12:
                //  校验邮箱绑定:SHA1(email)[0:4] == plaintext[0:4]
                SHA1(arg0, strlen(arg0), sha1_buf);
                if (strncmp(plaintext, sha1_buf, 4) == 0) {
                    sprintf(output,
                        "{\"code\": 0, \"data\": {\"product_id\": %d, "
                        "\"month_limit\": %d, \"pc_limit\": %d, \"device_limit\": %d}, "
                        "\"message\": \"success\"}",
                        *(uint16_t*)(plaintext+4),   // product_id  LE
                        *(uint16_t*)(plaintext+6),   // month_limit LE
                        *(uint16_t*)(plaintext+8),   // pc_limit    LE
                        *(uint16_t*)(plaintext+10)); // device_limit LE
                    return 0;
                }
                sprintf(output, "{\"code\": -1, \"message\": \"unknown\"}");
                return -1;
            default:
                sprintf(output, "{\"code\": -1, \"message\": \"decrypted length not right\"}");
                return -1;
        }
    }



    12 字节明文结构:
    偏移  
    长度
    含    义
    0–3
    4
    SHA1(email) 前 4 字节
    4–5
    2
    product_id
    6–7
    2
    month_limit
    8–9
    2
    pc_limit
    10–11     
    2
    device_limit

    关键参数:
    • RSA:128-bit 模数(p、q 各 64-bit),公钥指数 e = 65537
    • Padding:RSA_X931_PADDING(OpenSSL 常量 = 5
    • 公钥:PKCS#8 SubjectPublicKeyInfo PEM,硬编码于 libsnvrfy.dylib 栈帧

    2.3 提取硬编码公钥

    [Bash shell] 纯文本查看 复制代码
    strings /Applications/AnyGo.app/Contents/MacOS/libsnvrfy.dylib \
      | grep -A2 "BEGIN PUBLIC KEY"
    
    输出:
    [Plain Text] 纯文本查看 复制代码
    -----BEGIN PUBLIC KEY-----
    MCwwDQYJKoZIhvcNAQEBBQADGwAwGAIRAMHFWywkLO5vdQpvM0UXlrsCAwEAAQ==
    -----END PUBLIC KEY-----
    Base64 解码该 DER,模数 n 为 128-bit(16 字节)。

    提取出的模数:
    n = 257565734864128986511771360560061847227  
       = 0xc1c55b2c242cee6f750a6f33451796bb

    等一下,等一下,大兄弟,RSA还在用模数n = 128-bit吗?


    2.4 分解原始公钥(Factor Original Key)
    128-bit RSA 在现代计算机上属于秒级可分解(安全强度约 20–30 bit,远低于 NIST 所要求的 3072-bit RSA)。
    「安全强度」的对比
    这里有个常见混淆:

    128-bit RSA 模数 → 分解难度约 20~30 bit 安全强度,极弱
    128-bit 安全强度(NIST 语境)→ 需要约 3072-bit RSA
    作为参考:

    RSA 模数位长        分解难度
    128-bit / 秒级
    256-bit / 分钟级
    512-bit / 小时~天级(90 年代末已破)
    768-bit / 约 2000 核心年(2009)
    1024-bit / 约百万核心年量级
    2048-bit / 目前……

    所以,FactorDB 查表一下
    http://factordb.com/api?query=257565734864128986511771360560061847227

    {"id":1100000004763160787,"status":"FF","factors":[["13995547319714966219",1],["18403405667551598033",1]]}

    私钥+算法,generate一个论坛专用码吧
    [Python] 纯文本查看 复制代码
    def generate_code(
        email: str,
        rsa_priv,
        crypto,
        product_id: int = 17,
        month_limit: int = 0,
        pc_limit: int = 0,
        device_limit: int = 0,
    ) -> str:
        sha1 = hashlib.sha1(email.encode()).digest()
        plain = bytearray(12)
        plain[0:4] = sha1[0:4]
        struct.pack_into("<H", plain, 4, product_id)
        struct.pack_into("<H", plain, 6, month_limit)
        struct.pack_into("<H", plain, 8, pc_limit)
        struct.pack_into("<H", plain, 10, device_limit)
    
        rsa_size = crypto.RSA_size(rsa_priv)
        out_buf = ctypes.create_string_buffer(rsa_size)
        ret = crypto.RSA_private_encrypt(
            len(plain), bytes(plain), out_buf, rsa_priv, RSA_X931_PADDING
        )
        if ret != rsa_size:
            raise RuntimeError(f"RSA_private_encrypt returned {ret}, expected {rsa_size}")
    
        code = base64.b32encode(bytes(out_buf[:rsa_size])).rstrip(b"=").decode()
        return "-".join(code[i : i + 7] for i in range(0, len(code), 7))


    AnyGo 是 x86_64 binary,在 Apple Silicon 上运行于 Rosetta;系统默认 Python 是 arm64,ctypes 加载 x86_64 的 libcrypto.1.1.dylib 时架构冲突。
    只好强制以 x86_64 运行 Python,同时注入库路径:

    截屏2026-06-18 02.41.31.png


    三、在线验证绕过(跳过订单检测)

    3.1 在线注册验证接口分析
    分析 checkRegisterInfoValid 揭示了 POST 请求的构建方式:
    [Objective-C] 纯文本查看 复制代码
    // 构建签名原文
    NSString *signRaw = [NSString stringWithFormat:
        @"code=%@&email=%@&uuid=%@&vi=!?*@luckydogsoft2019",
        regCode, email, uuid];
    
    // MD5 签名
    NSString *v = [self get.M.D.5.FromString:signRaw];
    
    // 构建请求体(含签名)
    NSString *body = [NSString stringWithFormat:
        @"code=%@&email=%@&uuid=%@&v=%@",
        regCode, email, uuid, v];
    
    // POST 到验证接口
    NSURL *url = [NSURL URLWithString:
        @"https://order.luckydogsoft.com/api/verification"];
    NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url
        cachePolicy:1 timeoutInterval:timeout];
    [req setHTTPMethod:@"POST"];
    [req setHTTPBody:[body dataUsingEncoding:NSUTF8StringEncoding]];



    3.2 Mock Server 实现
    [Python] 纯文本查看 复制代码
    # mock_server.py 
    
    class MockHandler(BaseHTTPRequestHandler):
    
        def do_POST(self):
            path = urlparse(self.path).path
    
            if path == "/api/verification":
                # 直接返回成功
                # product_id=17 匹配 registerEmail:code: 的判断逻辑
                self.send_json({
                    "error_code": 0,
                    "msg": "success",
                    "data": {"product_id": 17, "days_left": 36500, "status": 1}
                })
            elif path in ("/api/bluetooth/certify", "/api/bluetooth/enqueue"):
                self.send_json({"error_code": 0, "msg": "success", "data": {}})



    部署步骤:
    [Bash shell] 纯文本查看 复制代码
    # Step 1:生成受信任证书
    JAVA_HOME="" mkcert order.luckydogsoft.com
    
    # Step 2:DNS 重定向
    echo "127.0.0.1  order.luckydogsoft.com" | sudo tee -a /etc/hosts
    
    # Step 3:启动(需要 :443 端口权限)
    sudo python3 mock_server.py


    截屏2026-06-18 03.33.06.png

    四、终极破解:直接修改 plist
    以上完成了:逆向 RSA → 生成注册码 → 本地 snvrfy 验证 → mock 本地 verification 接口 → 写入持久化。
    注册成功后真正落盘的内容极其简单——并非加密签名,只是 Base64 编码的明文字符串。

    4.1 持久化位置

    截屏2026-06-19 23.26.46.png
    AnyGo 为非沙盒应用,NSUserDefaults 写入:
    [Plain Text] 纯文本查看 复制代码
    ~/Library/Preferences/com.itoolab.AnyGo.plist


    运行时验证(checkRegisterInfoValid)读取的是内存中的 isRegister、regEmail、regCode;这三者在启动时由 regInfo 恢复。
    removeRegisterInfo 注销时删除的也正是这个键:
    [Objective-C] 纯文本查看 复制代码
    /* hopper/remove.c */
    [rax removeObjectForKey:@"regInfo"];
    [self setIsRegister:0x0];
    [self setRegEmail:0x0];
    [self setRegSN:0x0];



    4.2 regInfo 格式
    注册成功时,app 将 email 与注册码拼接后做 Base64 编码写入:
    [Objective-C] 纯文本查看 复制代码
    /* hopper/registerEmail.c  */
    rax = [NSString stringWithFormat:@"%@\n%@", email, serial];
    r15 = [[rax base64Encoding] retain];
    [[NSUserDefaults standardUserDefaults] setValue:r15 forKey:@"regInfo"];
    [[NSUserDefaults standardUserDefaults] synchronize];
    [var_58 setIsRegister:0x1];


    明文格式:
    [Plain Text] 纯文本查看 复制代码
    <email>\n<serial>


    示例:
    字段

    明文
    [email protected]\nHAHEOBT-ERD2MFR-S2JABSG-TDRR4
    Base64
    dHJlZV9mbHlAY2hpbmFweWcuY29tCkhBSEVPQlQtRVJEMk1GUi1TMkpBQlNHLVREUlI0

    4.3 终极方案:跳过全部验证,直接写 plist
    只要 plist 中存在合法的 regInfo,app 重启后即可恢复 isRegister = 1,无需再走注册界面、无需调用 snvrfy; 同时修改HOSTS拒绝在线订单验证即可。

    方法一:defaults 命令

    [Bash shell] 纯文本查看 复制代码
    # 先退出 AnyGo,再写入
    EMAIL="[email protected]"
    SERIAL="HAHEOBT-ERD2MFR-S2JABSG-TDRR4"
    REGINFO=$(printf '%s\n%s' "$EMAIL" "$SERIAL" | base64)
    
    defaults write com.itoolab.AnyGo regInfo "$REGINFO"



    方法二:直接编辑 plist

    [Bash shell] 纯文本查看 复制代码
    plutil -p ~/Library/Preferences/com.itoolab.AnyGo.plist   # 查看
    # 用任意编辑器修改 regInfo 键,或用 plutil -replace
    plutil -replace regInfo -string \
      "dHJlZV9mbHlAY2hpbmFweWcuY29tCkhBSEVPQlQtRVJEMk1GUi1TMkpBQlNHLVREUlI0" \
      ~/Library/Preferences/com.itoolab.AnyGo.plist


    有一个坑要特别注意:
    macOS,NSUserDefaults 通过 cfprefsd 守护进程中转,该进程在内存里缓存各 domain 的偏好数据。直接删文件或用 cp 替换,cfprefsd 察觉不到——app 读到的还是 daemon 的内存缓存,daemon 下次 flush 时还会用缓存把你写的内容覆盖回去
    所以先关闭App,修改plist,再杀掉 cfprefsd 强制刷新
    [Bash shell] 纯文本查看 复制代码
    killall -u "$USER" cfprefsd
    cfprefsd 会自动重启,重启后从磁盘重新载入刚才写入的 plist。
    验证一下是否写入成功
    [Bash shell] 纯文本查看 复制代码
    defaults read com.itoolab.AnyGo regInfo | base64 -d
    # 应该输出:
    # [url=mailto:[email protected]][email protected][/url]
    # XXXXXXXXX-XXXXXXXXX-XXXXXXXX


    regInfo 不含任何签名校验,app 启动时直接信任 plist 里的内容。这是整条保护链里最薄弱的一环。


    五、懒人代码包

    上面的废话太烦人,下面才是你要的。
    把脚本存成文件,然后执行:

    Step 1: 新建 anygo_reg.sh,粘贴以下内容:
    游客,如果您要查看本帖隐藏内容请回复





    App功能未长期测试,或许App中还有其他暗桩,等着你们来探索吧~

    对了,如果提示你的位置不能使用app,记得hosts添加这样一条 127.0.0.1 ip-api.com


    tree_fly/P.Y.G
    五月五 2026




    截屏2026-06-18 01.50.18.png

    评分

    参与人数 3威望 +5 飘云币 +5 收起 理由
    Stevenson + 1 + 1 感谢发布原创作品,PYG有你更精彩!
    飞天 + 2 + 2 感谢发布原创作品,PYG有你更精彩!
    wgz001 + 2 + 2 感谢发布原创作品,PYG有你更精彩!

    查看全部评分

    本帖被以下淘专辑推荐:

    PYG19周年生日快乐!
  • TA的每日心情
    开心
    2024-4-5 00:38
  • 签到天数: 39 天

    [LV.5]常住居民I

    发表于 2026-6-20 19:11:24 | 显示全部楼层
    好好学习一下
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2025-1-14 13:49
  • 签到天数: 393 天

    [LV.9]以坛为家II

    发表于 2026-6-20 21:17:02 | 显示全部楼层
    PYG有你更精彩!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    开心
    2025-1-14 10:37
  • 签到天数: 1037 天

    [LV.10]以坛为家III

    发表于 2026-6-21 19:10:01 | 显示全部楼层
    支持楼主,拿一个懒人代码包
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    奋斗
    2025-1-13 09:04
  • 签到天数: 339 天

    [LV.8]以坛为家I

    发表于 2026-6-22 08:45:57 | 显示全部楼层
    PYG有你更精彩!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    郁闷
    2024-5-28 14:42
  • 签到天数: 5 天

    [LV.2]偶尔看看I

    发表于 2026-6-22 08:50:03 | 显示全部楼层
    PYG有你更精彩!
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    该用户从未签到

    发表于 2026-6-22 11:45:57 | 显示全部楼层
    PYG is even more amazing with you!!!

    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

  • TA的每日心情
    慵懒
    2025-1-14 16:50
  • 签到天数: 1592 天

    [LV.Master]伴坛终老

    发表于 2026-6-22 21:09:38 | 显示全部楼层
    感谢分享干货好文章。
    PYG19周年生日快乐!
    回复 支持 反对

    使用道具 举报

    您需要登录后才可以回帖 登录 | 加入我们

    本版积分规则

    快速回复 返回顶部 返回列表