tree_fly 发表于 2026-6-20 15:55:51

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

本帖最后由 tree_fly 于 2026-6-25 00:15 编辑

AnyGo 8.3.x 分析破解记录

前言: 国行 Apple Watch 的 一些房颤检测、睡眠呼吸监测等功能的开通,要用到更改Location的工具,搞起!
声明:本文仅为逆向技术学习讨论交流用!!!

使用Hopper Demo分析一波,收集一些关键信息。

一、整体架构分析

AnyGo 的注册保护分为两层:



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

该方法调用 snvrfy:

    // 格式清理(去除换行、空白等)
    r15 = [ 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 函数,伪代码如下:

/* 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 = 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) == plaintext
            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_idLE
                  *(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 提取硬编码公钥


strings /Applications/AnyGo.app/Contents/MacOS/libsnvrfy.dylib \
| grep -A2 "BEGIN PUBLIC KEY"
输出:
-----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一个论坛专用码吧
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 = sha1
    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 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,同时注入库路径:




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

3.1 在线注册验证接口分析
分析 checkRegisterInfoValid 揭示了 POST 请求的构建方式:
// 构建签名原文
NSString *signRaw = [NSString stringWithFormat:
    @"code=%@&email=%@&uuid=%@&vi=!?*@luckydogsoft2019",
    regCode, email, uuid];

// MD5 签名
NSString *v = ;

// 构建请求体(含签名)
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];
;
];


3.2 Mock Server 实现
# 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": {}})


部署步骤:
# Step 1:生成受信任证书
JAVA_HOME="" mkcert order.luckydogsoft.com

# Step 2:DNS 重定向
echo "127.0.0.1order.luckydogsoft.com" | sudo tee -a /etc/hosts

# Step 3:启动(需要 :443 端口权限)
sudo python3 mock_server.py



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

4.1 持久化位置


AnyGo 为非沙盒应用,NSUserDefaults 写入:
~/Library/Preferences/com.itoolab.AnyGo.plist

运行时验证(checkRegisterInfoValid)读取的是内存中的 isRegister、regEmail、regCode;这三者在启动时由 regInfo 恢复。
removeRegisterInfo 注销时删除的也正是这个键:
/* hopper/remove.c */
;
;
;
;


4.2 regInfo 格式
注册成功时,app 将 email 与注册码拼接后做 Base64 编码写入:
/* hopper/registerEmail.c*/
rax = ;
r15 = [ retain];
[ setValue:r15 forKey:@"regInfo"];
[ synchronize];
;

明文格式:
<email>\n<serial>

示例:
字段


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

Base64
dHJlZV9mbHlAY2hpbmFweWcuY29tCkhBSEVPQlQtRVJEMk1GUi1TMkpBQlNHLVREUlI0


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

方法一:defaults 命令

# 先退出 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

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 强制刷新
killall -u "$USER" cfprefsdcfprefsd 会自动重启,重启后从磁盘重新载入刚才写入的 plist。
验证一下是否写入成功
defaults read com.itoolab.AnyGo regInfo | base64 -d
# 应该输出:
# [email protected]
# XXXXXXXXX-XXXXXXXXX-XXXXXXXX

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


五、懒人代码包

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

Step 1: 新建 anygo_reg.sh,粘贴以下内容:
**** Hidden Message *****




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

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


tree_fly/P.Y.G
五月五 2026




782878952 发表于 2026-6-20 19:11:24

好好学习一下

linxiansen 发表于 2026-6-20 21:17:02

PYG有你更精彩!

飞天梦 发表于 2026-6-21 06:15:21

谢谢分享

pimnotepad 发表于 2026-6-21 19:10:01

支持楼主,拿一个懒人代码包

chchhau 发表于 2026-6-22 08:45:57

PYG有你更精彩!

zzlflash 发表于 2026-6-22 08:50:03

PYG有你更精彩!

xingbing 发表于 2026-6-22 09:51:33

支持,学习。

hungrylee 发表于 2026-6-22 11:45:57


PYG is even more amazing with you!!!

飞天 发表于 2026-6-22 21:09:38

感谢分享干货好文章。
页: [1] 2
查看完整版本: AnyGo 8.3.x 算法分析记录初稿