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
好好学习一下 PYG有你更精彩! 谢谢分享 支持楼主,拿一个懒人代码包
PYG有你更精彩! PYG有你更精彩!
支持,学习。
PYG is even more amazing with you!!!
感谢分享干货好文章。
页:
[1]
2