目标 App:com.globe.gcash.android(GCash)6.00.2 build 1213
对象层:自研 DynamicSecurity(RequestEncryption / GAESCipher / GRSACipher),
本文结构:按分析顺序记录「假设 → 实验 → 否证/坐实 → 数据」;密钥/私钥只露前缀
需要回答三个问题:
起点条件:App 有反系统代理、同进程多套 Alipay 系 RPC、native 存在 APSE 白盒、进入注册页易崩溃。下面按分析顺序记录,包含走过的弯路。
先确立判据:能否用纯 Python 组包、打到真网关,使服务器验签通过、发出真 OTP,并跑完 generate → verify → isGcashRegistered → register 全链路。该判据区分「组包看似正确」与「服务器实际接受」两种情况,下文多数篇幅用于排除前者。
Java 层关键方法普遍插了 goto 噪声(if-eqz v0, :cond_1 自环),jadx 报 Method not decompiled。分析时直接读 smali 调用序,不依赖 Java 源。
开系统 HTTP 代理,注册步弹:
关掉代理后流程恢复,接码可收到 6 位 OTP。SSL unpin(TrustManagerImpl.verifyChain 放行)在同环境是生效的。
拦截的不是 TLS pin 证书,而是「系统代理设置被探测」。在 Windows + 系统代理 + mitmproxy 上直接抓注册 RPC,入口不可用;后续需透明路由,或在进程内、加密前抠明文。
此步无「解密失败的密文」可分析——请求未按预期路径发出。由此确定方向:该协议的明文只存在于进程内、RequestEncryption 组包之前,线路上只有一段 sign。
注册走 Alipay mobilegateway / mPaaS RPC,hook com.alipay.mobile.common.rpc.* 或动态 Proxy 即可。
进程内 hook Proxy.newProxyInstance,对带 @OperationType 的 InvocationHandler 挂 invoke。同进程实际两套:
抓到的明文指纹 RPC(真机冷启动,本次复测 logcat),JsonSerializer / RPC hook 同时打出(URL 仍是 imgw):
同次启动 TokenResult 缓存:
另有配置类 RPC(冷启动):ap.mobileamcs.cloud.fetch.config / ap.mobileprod.amcs.config.local.fetch,明文里同样带 spoof 后的 mobileModel / osVersion / clientVersion。
据此确认:
注册页按 Next,没有发码 operation-type。名字带 Otp 的 OtpVerificationFacade(Quake)在注册路径上也不触发——静态确认它挂在 reset_mpin,不是注册。
结论:RPC 线是旁路证据,不是注册 OTP 主路径。需从 UI 往下静态追。
OtpRepositoryImpl.generateOtpCodeWc 核心逻辑(去噪后语义):
接口注解(jadx smali,本次复现):
Host 不在注解里,实测基址 d0fK9s2c8@1M7s2y4Q4x3@1q4Q4x3V1k6Q4x3V1k6S2M7r3W2Q4x3X3g2E0P5h3&6@1i4K6u0W2P5s2W2*7(与旧 c4/v1/otp/* Map body 路径并存;注册新路径走 v2.3 + WCSign)。
正确 choke point:RequestEncryption.generateSignedBody(dex 层,免壳,是整个协议里唯一能一次获取「明文 body + 明文 header + 本地 aesKey/iv + 最终 sign」的点)。
类:gcash.common.android.util.encryption.RequestEncryption
模型:WCSign{ sign, aesKey, iv }(字段 jadx 坐实)
b() 再拆:
n():
l():
线路模型:sign = base64(RSA_sign(payload)) + "." + base64(gson(WCEncrypt))。@Body(WCSign) 只 @Expose 了 sign 一个字段;aesKey/iv 本地留存(用于解响应、解字段),服务器侧的密钥在 sec.key/sec.initializer 中被 RSA 封装。
smali 明确分支:
组包时此处易错:把 X-Env-Info 也做 AES 会与真机不一致。「非空才加密」是一个隐含条件——Authorization 在注册期是空串,d() 判空后置 null,gson 省略 null 键,因此最终 payload 中没有 Authorization 键,sec.enc 清单里也不出现它。这条「空 → 缺席」连锁在复现时容易被忽略。
语义:gson → JsonObject → 按 path 遍历 → 叶子 e() AES → 回写。注册 generate 的 path 列表为 ["msisdn"],故 udid 明文留在 body。原实现支持 a.b[0].c 嵌套路径遍历,但注册/登录/建号全程只用扁平字段名,无需实现嵌套。
GRSACipher.encrypt 的原语(§6.2 展开):服务器公钥为 X509/SubjectPublicKeyInfo,填充是 RSA/ECB/PKCS1Padding(PKCS#1 v1.5),输出 Base64 NO_WRAP。封装对象是本请求随机生成的 aesKey(32 字符)与 iv(16 字符);服务器用自身私钥解出这一对,即可解 sec.enc 清单点名的密文。
oracle 解出的 sec.enc 真值(本次复现 generate):
verify 会多一条 "request.body.code"。sec.enc 是「本次哪些叶子被 AES」的路径索引,须与 d()/c() 实际加密的字段逐一对齐,多一条少一条服务器解密都会错位。
该类各 helper 方法如下。方法名在 dex 里被混淆成单字母,调用图仍清晰:
调用树:
需区分 e()(AES)与 m()(Base64):头里只有 X-Env-Info 走 m(),其余敏感头与 body 点名字段走 e()。sec.enc(=j()+k())是「本次哪些叶子走了 e()」的路径清单,服务器据此反查解密。
原语为 AES-CBC + SHA256withRSA。复现的关键在于序列化字节需与真机完全一致,否则签名无法通过;本文的多数细节集中于此。
关键细节:key/iv 是「可打印字符串」,直接取 UTF-8 字节作密钥,不是 raw bytes。
固定材料自检(任何语言可对拍):
GRSACipher 承担三类操作,填充与密钥格式不同:
私钥空时 sign 会 blockingGet 触发握手再签——对应「第一次请求前必握手」(§8)。
签名字节来自 new Gson() 的默认序列化,本地需逐字节复刻。四条约束:
① HTML 转义。new Gson() 默认把 = < > & ' 转成 \u00XX 形式。AES 密文 Base64 常带 == padding,进 gson 后 padding 变成 \u003d\u003d。
oracle 真实 payload 片段(本次复现,注意 = 已被转义成 \u003d):
固定 key 演示(左侧是 AES 原始密文里的 ==,右侧是它进 gson 后变成的样子):
缺此步则本地 payload 字节与 App 不一致(每个 = 变 6 字符 \u003d),SHA256withRSA 无法通过。
② 紧凑分隔符:, 与 :,无空格。
③ 省略 null 键:值为 null 的键 gson 默认不输出(前面 Authorization 空 → 缺席、method=POST → null → 省略,都依赖此)。
④ 字段顺序 = ART 字段名字母序。此条容易被忽略:gson 按 Java 反射得到的字段顺序序列化,ART 上该顺序为字段名字母序。本地组包时 dict 的键须按此序构造,否则 payload 字节改变、签名不通过。三个容器实测顺序:
EncryptedHeader 同理,其 JSON key 输出顺序(省略 null 后)实测为:
OTP 路径只设了其中 Time / X-Correlator-Id / X-Env-Info / X-FlowId / X-Package-Id / X-Reg-Channel / X-Tracker / X-UDID(与实测 header_keys 一致),其余键在场为 null → 省略。
四条中缺任一条:本地 payload 字节即与 App 不一致,SHA256withRSA 无法通过服务端验签。其表现为验签失败而非解密失败——服务端多回 422 Invalid Signature 或网关拒绝,易被误判为加密实现错误(§11 的 register 段即出现过一次 code:400 Failed verification at prehandling)。
思路:先不打真网关,用自造服务器密钥对验证「组包结构可解回」。该步骤将「密码学是否正确」与「服务器业务/风控是否接受」解耦;§10 的 key:null 判定依赖它排除签名错误的可能。
七步全部通过即证明:签名字节等价(步 5)、密钥封装正确(步 6)、字段级加密与 sec.enc 索引一致(步 7)。
离线单测 24 PASS / 0 FAIL。
密钥非硬编码:每台设备装机时本地生成一对 RSA-2048,再与服务器交换公钥。握手基址来自 Configuration.getDomainV5("tc_login_key_agreement_endpoint"),实测落在 api.mynt.xyz。
现网存在两个版本,AgreementAPICallImpl 里都有:
POST 中客户端公钥切成 100 字符一块、逐块用服务器公钥 RSA 封装后上传(单次 RSA/ECB/PKCS1 明文长度受限,公钥 b64 共 392 字符,需分块)。服务器保存客户端公钥后,即可验证客户端私钥签名的 WCSign,因此握手是首个业务请求的前置。
本次实测:
本地 prefs 语义(GHashConfigPrefService):
触发时机:GRSACipher.sign 发现 agreement_private_key 为空时,blockingGet 阻塞执行握手,握手完成后再签。因此「首个请求前必有一次握手」由 lazy-init 决定,非显式编排。
注册 body 几乎只有号与 udid。设备画像在 X-Env-Info JSON 中(再 Base64 进 WCSign header,X-Env-Info 走 m() 即纯 Base64,不 AES——见 §5.2)。
getMobileEnvInfo() 产出的顶层 + extendInfo:
然后 d() 会把 MobileEnvInfo 转成 Map,再往同一个 JSON 里塞一批镜像键——这些键塞进 X-Env-Info 这个 JSON 对象里,不是 HTTP 头:
最后 getEnvInfo(scenario) 再补 scenario_id。真机样本(token 脱敏):
deviceId: "amtK7+u7WlUDAGJU3vpuQ3gI" 即 utdid(24 字符 Base64)——§9.3 展开其生成算法。
对齐后,OTP generate 路径的硬要求:
A/B 思路(只改 env 一个维度,看 generate 的 code)已坐实:
X-Env-Info 里三个 native 相关标识,逆向后可复现性差别较大,决定整条离设备链的可行性。
umid(伪线索):modules.x.o.a(ctx) 解析链最终 fallback 到 UTDevice.getUtdid(即 utdid),铸造 RPC 下行无 umid 字段,且 GCash 配置 needUmid=false → createStaticRequest 里 umidToken="",X-Env-Info 实测无此键。Alibaba SecurityGuard 的 IUMIDComponent.getSecurityToken(libsgmain)在 GCash 里 0 xref,存在但未接线。无需复现 umid。
utdid(com.ta.utdid2 / UTUtdid.a(),18 字节 → Base64 NO_WRAP = 24 字符):
utdid 半可复现:无法从设备确定性反推(含时间戳 + 随机),但算法已知,可铸一个结构合法、HMAC 自洽的新值(gen_utdid())。持久化较顽固(跨 app、跨卸载):Settings.System["mqBRboGZkQPcAkyk"](明文)/["dxCRMxhQkdGePGnp"](加密),SP Alvin2/UTDID2,外置 /sdcard/.UTSystemConfig/Global/Alvin2、/sdcard/.DataStorage/ContextData——任一命中就回填其余。更换 utdid 需清除上述全部位置,否则仅清 app 数据无效。
小结:三者中 umid 无关、utdid 可铸,唯一硬缺口为 apdidToken,下一节展开。
早期 unidbg 路线卡在 PARAM_ERROR,形态上类似未跑通 16 字节 getColorInfo VM 白盒信封。要判断它是否为硬门槛,先逆清 App 原本铸 apdid 的整条 native 链(jadx 看 com.alipay.alipaysecuritysdk.*、ghidra/strings 看 libAPSE_9.0.2.so),再与「离设备直铸」对照。
入口与配置(com.gcash.iap.apsecurity.AntApSecurityServiceImpl,Kotlin,基本没混淆):
铸造流水线(ApdidManager):
dataMap 字段族(createStaticRequest 组包,采集面主体;离设备直铸时将其替换为一个随机 default):
谁在 Java、谁在 native(区分「改机盖得到」与「盖不到」):
native JNI 面(libAPSE_9.0.2.so,ApdidJNIBridge):
硬件指纹计算、ed/ek 加密、dataMap 的 AES 封装均在此,Java 层不可见——这是「必须复现白盒」这一判断的来源。
落库:saveToStorage 把服务器铸的 token 写进 SharedPreferences 文件 openapi_file_pri / key openApiGCash(加密);相关 vkeyid_profiles_v4(dynamic_key)、last_apdid_env、native crash-guard filesDir/sc_edge。之后 getToken() 都从这里取缓存,不重算。
该链完整时,「apdid 不可离设备铸造」看似成立:native 采集 + 双重加密 + mgw 签名 RPC + 服务器铸,共四道门。但下述删减实验将其推翻。
对铸造端点做输入删减:
本次 mint 形态:apdid 头 eYOIk…,token 尾 nwEAAA==(与真机 harvest 一致)。一次 HTTP POST 铸一份,约几百毫秒,替代了在真机上 hook getToken 收割的方式。
方法:对一个看似必须逆向的 native 白盒,通过在铸造端点做输入删减 A/B(逐字段删除、观察服务器响应)即可判定其是否被校验,无需逆向 VM 字节码。
历史实网会话 F6C07A5E(profile vivo 1806 / Android 10):
key:null + "Something went wrong." 形态上类似被风控拦截。以下对照数据逐一排除该假设:
① 同形态在相反状态下逐字段一致 → 不携带状态信息:
key:null 在未注册与已注册下出现且完全一致 → 不可能是「拦截/放行」信号。
② 错码对照证明真 OTP 路径已过 OTP 比对:
若为签名/加密错误,错码与真码应同样在验签前失败;实际错码走到了业务层的 OTP 比对(1011),真码过了比对(code:0),说明加密签名层正确。
③ 反证「拦截说」:key:null 的 verify 之后,isGcashRegistered 立刻回 200 + 可导航业务体。若 verify 被风控拦,下一跳应是 403 {code:143}(该形态确实复现过——即「没有前置 verify 就裸调 isGcashRegistered」,补上 verify 即 200),而非业务体。
④ 换真机铸造 token 仍 key:null:A/B 过「离设备空壳 mint 的 apdid」与「Pixel6 真机跑完整 getColorInfo 白盒铸的 token」,两者 verify 都是 key:null,排除「token 太水被拦」。
⑤ jadx 坐实:SuccessVerifyBody 只有 key 一个字段;新注册 UI 的成功 handler(g1)不读 key。"Something went wrong." 同时是客户端本地 OtpCodeUtilImp.GENERIC_HEADER 文案,服务端也回了同句——文案表示失败,字段表示成功。
结论:验码成功判据为 code:0(OtpAccepted),key:null 是与成功并存的噪声。「号源信誉 / 设备图谱 / KYC 挂起」等假设不成立:虚拟号 + 离设备铸 apdid 即可完整通过 generate → verify(code:0) → isGcashRegistered(200 可导航体)。
OTP 闭环之后,注册协议还有最后一跳 register(将 PII + MPIN + apdidToken 打包建号)。该步骤用于验证组包字节级正确性:服务器需完成解密、执行 register 业务逻辑并返回结构化 KYC 信封,方可确认整套 WCSign(header / body / sec.enc)正确。
端点 c4/v3.4/gcash/register(GcashRegistrationApiService.register,@Body WCSign),header 走 RegistrationDataSourceImpl.getHeader()(只有 Content-Type / X-UDID / X-FlowId / X-Env-Info / X-Reg-Channel,无 Package/Time/Tracker),env 走 getEnvInfo()(不带 scenario_id,但 Map 里补一个 X-UDID 键)。body 19 个加密字段:
明文 body 里还有 referralCode、version="1213"、rdsData、termsAndConditions="true"(后两者明文,不进加密集),udid 键置 null(gson 省略,服务器读 X-UDID 头)。caZipcode/paZipcode 是小写 c——这类「1213 版精确键名」错一个字母服务器就当缺字段。
明文 body 里的 rds_data 易被想当然填成非空。但 jadx 确认:注册期 PinEnhancePresenter.h() 将 rdsData 硬编码为空串,注册链路不执行 RDSClient/zipAndEncryptData(setRdsData 0 xref)。影响:
这印证 §6.3:此处的错误是多填 rdsData 导致签名字节不等价,而非少填。
过了 prehandling 之后是业务风控:
13449 按设备指纹速度累计(deviceThreshold / maxDevicePreCom):复用同一枚陈旧 apdid 或同一份 PII 会累计风险值。对应处理是每个身份现铸 fresh apdid(§10.1,清 device velocity)+ fresh 合成菲律宾 PII(避免 PII velocity)。成功样本:
至此判据满足:纯离设备(无真机、无 frida)跑通 handshake → generate → verify(code:0) → isGcashRegistered → register(code:0 KYC APPROVED),整套 WCSign 组包字节级正确,服务器全程接受。register 段在此仅用作协议正确性证据,不展开为建号工具。
真机冷启动 log(钩子就绪,尚未进 OTP UI):
generateSignedBody 的 BODY/HEADER 日志要等注册发码/验码真正组包才出现;冷启动只先打指纹 RPC(§3.2)和配置 RPC。一处 generateSignedBody 即注册协议明文全集。真机实抓到的 generate BODY(6.01.0):{"msisdn":"0955…","udid":"ANDxdAv…"},scenario_id=first_time_otp,appVersion=6.01.0:1216,X-FlowId=95a81226-…,与离线 oracle 组的包结构逐字段吻合。
GCash 存在 native 完整性自校,通用 Dobby/inline hook 会触发崩溃:
处理(保活栈,非本文重点,简述):删除 code_cache 与 /data/local/tmp 下的坏 libgcash_sc.so,seccomp 改 RET_TRACE(plain exit(93) 放行),配 ptrace guard 持续 FREEZE exit_group,vmtrace 关闭(Dobby 挂 libAPSE 会触发 CRC)。处理后 OtpMsisdnActivity 存活 ≥25–35s,主 pid 稳定。
generateSignedBody 位于 dex/ART 层,不落入被 CRC 自校的 native .text,因此 Java 层 hook 既可获取完整明文,又不触发 libgcash_sc / libAPSE 的完整性检查。
完成了脱机注册协议的复现:无真机、无 Frida,纯 Python 跑通握手 → 发码 → 验码 → 查号 → 建号,实网建号成功。
| 层 |
本次复现 |
产出 |
| 静态 |
jadx-headless 加载 gcash_1213_base.apk,class_count=82230,load≈58s |
关键类 smali(Java 反编译被 anti-decompile 干扰,smali 可读) |
| 密码 oracle |
本地临时 RSA 对 + 组包 + 服务器视角解封 |
24 项离线单测 PASS |
| 握手 |
直连 api.mynt.xyz key-agreement(v1 GET+POST) |
真实 flowId + 服务器 X509 公钥 |
| 铸 apdid |
iclientgw-sea.alipay.com/imgw.htm |
{apdid,token} 形态与真机一致 |
| 历史实网 |
lab 会话 F6C07A5E(2026-07-20) |
generate/verify/isreg 明文响应 |
| 真机动态 |
进程内 sergei GCashHook 冷启动(6.01.0) |
指纹 RPC 明文 + DynSec 钩子就绪(见 §3.2 / §12) |
| 全链路闭合 |
纯离设备 register(虚拟号 + 铸 apdid) |
KYC level 1 APPROVED / FOR CREATION(见 §11) |
| 框架 |
Handler |
注册期实际用途 |
| IAP AC |
com.iap.ac.android.rpc.RpcInvocationHandler |
配置 / 设备指纹上报 |
| Quake |
com.alipay.imobile.network.quake.rpc.RpcInvocationHandler |
改 MPIN 等 |
| 打点 |
为何空 |
| 标准 mPaaS RPC |
不是那条线 |
| Quake OtpVerificationFacade |
改 MPIN,不是注册 |
| 明文 okhttp body |
@Body 已是 WCSign,字段已 AES |
传递专业知识、拓宽行业人脉——看雪讲师团队等你加入!!