本文最后更新于:2026年9月2日 下午
[猿人学 App 逆向系列]第九题「借问酒家何处有」。这题没有 sign ——请求体只有一个 page。 它的全部防护压在传输层:服务器开了 mTLS(双向 TLS) ,握手时要求客户端出示一张 App 内置的客户端证书 ,拿不出来就连不上,自然也抓不到、爬不动。 破法很直接:把客户端证书从 APK 里抠出来,自己在 Python 里当客户端证书用。难点全在”证书藏在哪、密码是什么”——而这些字符串都被混淆了。
⚠️ 猿人学闯关 App 是面向逆向练习的 CTF 式靶场;本文仅用于授权范围内的学习研究,请遵守相关服务条款与法律。
本文目录
请求模型:没有 sign,只有 mTLS
读 Fragment:KeyManagerFactory 出卖了一切
字符串被混淆:上 JVM 预言机
抠证书:BKS → PKCS12 → PEM
纯 Python 带客户端证书复现
一、请求模型:没有 sign,只有 mTLS 先看 Retrofit 接口,第九题的方法干净得反常:
1 2 3 @FormUrlEncoded @POST("/api/app9") o0OoOo0<ChallengeOneResultBean> OooOO0O (@Field("page") Integer num) ;
只有 page,没有 sign、没有 token。 那防护在哪?看它的 OkHttpClient 是怎么建的——答案在 TLS 层。
二、读 Fragment:KeyManagerFactory 出卖了一切 ChallengeNineFragment.onCreate() 里手搓了一个 SSLContext:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 TrustManagerFactory tmf = TrustManagerFactory.getInstance(...); tmf.init(OooO()); KeyStore ks = KeyStore.getInstance("BKS" ); ks.load(getAssets().open("<混淆>" ), "<混淆密码>" .toCharArray());KeyManagerFactory kmf = KeyManagerFactory.getInstance(...); kmf.init(ks, "<混淆密码>" .toCharArray());SSLContext ctx = SSLContext.getInstance("TLS" ); ctx.init(kmf.getKeyManagers(), tmf.getTrustManagers(), new SecureRandom ()); builder.sslSocketFactory(ctx.getSocketFactory(), (X509TrustManager) tm); builder.hostnameVerifier(...);
KeyManagerFactory.init(ks, ...) 是 mTLS 的铁证 ——普通 HTTPS 客户端不需要 KeyManager,只有服务器要求”你也得给我一张证书”时才配。
于是任务清单很清楚:从 assets 里那个 BKS keystore + 它的密码,拿到客户端证书。 可这两样都是混淆字符串:
1 2 3 ks.load(getContext()...getAssets().open( o0ooOoOO.o00O0O.OooO00o(-245062378877207L )), o0ooOoOO.o00O0O.OooO00o(-245126803386647L ).toCharArray());
三、字符串被混淆:上 JVM 预言机 o0ooOoOO.o00O0O.OooO00o(long) 是一套自包含的位运算解密(内部 o00Oo0/o00Ooo 做移位/异或的 PRNG)。这种”纯算术、不依赖 Android API”的解密函数,最省事的还原方式不是照着抠算法重写,而是把这几个 class 拉出来直接在 JVM 里调 ——预言机对拍,零翻译误差:
1 2 3 4 5 6 7 8 public class Decrypt9 { public static void main (String[] a) { long [] offs = { -244740256330007L , -245113918484759L , -245062378877207L , -245126803386647L , -244641472082199L }; for (long o : offs) System.out.println(o0ooOoOO.o00O0O.OooO00o(o)); } }
1 2 3 d2j-dex2jar base.apk -o app.jar javac -cp app.jar Decrypt9.java java -cp "app.jar;." Decrypt9
一把梭,全出来了:
混淆偏移
明文
baseUrl
https://47.95.8.136:8443
KeyStore 类型
BKS
客户端证书文件
clientCA.bks
keystore / key 密码
MZ4cozY8Qu32UzGe
SSLContext
TLS
CertificateFactory
X.509
HostnameVerifier CN 前缀
CN=
服务器是 47.95.8.136:8443(和第四/八题同一台机的另一个端口),客户端证书是 assets/clientCA.bks,BKS 格式,密码 MZ4cozY8Qu32UzGe。
四、抠证书:BKS → PKCS12 → PEM 从 APK 里取出证书文件:
1 unzip -j base.apk assets/clientCA.bks
BKS 是 BouncyCastle 的 keystore 格式 ,Python 的 ssl/cryptography 不认。先用带 BC provider 的 keytool 把它转成通用的 PKCS12:
1 2 3 4 5 keytool -importkeystore \ -srckeystore clientCA.bks -srcstoretype BKS -srcstorepass MZ4cozY8Qu32UzGe \ -destkeystore client9.p12 -deststoretype PKCS12 -deststorepass MZ4cozY8Qu32UzGe \ -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider -providerpath bcprov.jar
再用 cryptography 把 PKCS12 拆成证书 / 私钥 PEM:
1 2 3 4 5 from cryptography.hazmat.primitives.serialization import pkcs12, Encoding, PrivateFormat, NoEncryption key, cert, _ = pkcs12.load_key_and_certificates(open ("client9.p12" ,"rb" ).read(), b"MZ4cozY8Qu32UzGe" )open ("client9_cert.pem" ,"wb" ).write(cert.public_bytes(Encoding.PEM))open ("client9_key.pem" ,"wb" ).write(key.private_bytes(Encoding.PEM, PrivateFormat.TraditionalOpenSSL, NoEncryption()))
五、纯 Python 带客户端证书复现 有了客户端证书 + 私钥,requests 的 cert= 参数直接就能做 mTLS。服务器证书是 CN=xx 的自签、又不是域名/IP,主机名对不上,所以 verify=False 跳过服务器校验(mTLS 的关键是我们出示客户端证书 ,不是去验服务器):
1 2 3 4 5 6 7 8 9 10 11 12 13 import requests, urllib3 urllib3.disable_warnings() URL = "https://47.95.8.136:8443/api/app9" CERT = ("client9_cert.pem" , "client9_key.pem" ) sess = requests.Session() total = 0 for page in range (1 , 101 ): r = sess.post(URL, data={"page" : page}, cert=CERT, verify=False , timeout=20 ) vals = [int (x["value" ]) for x in r.json()["data" ]] total += sum (vals)print ("ANSWER =" , total)
跑通:status 200 ,每页 20 个数正常返回,1-100 页求和即得答案。不带 cert= 时握手直接失败——服务器在 ClientHello 之后就要证书,这正是 mTLS 的拦法。
小结 第九题是本系列里少见的”算法零难度、传输层设卡 “的题:
没有 sign ,别去请求体里找签名——防护在 TLS 层;
KeyManagerFactory.init(keystore, pwd) 是 mTLS 的标志,顺藤摸瓜找客户端证书 ;
关键字符串(证书文件名、密码)被混淆,JVM 预言机 直接调解密函数最稳,不用抠算法;
证书是 BKS 格式,keytool + BouncyCastle 转 PKCS12 再拆 PEM;
requests 的 cert=(证书, 私钥) 就能离机做 mTLS。
一句话:双向证书的”钥匙”就打包在 APK 里,把它抠出来,你就是合法客户端。