做移动端安全测试的,可能都有过这种经历:代理工具装好,用户证书也进设备了,打开目标App,结果界面上一个请求都看不到,全是SSL错误或者直接网络异常。排查来排查去,发现挡在面前的其实是两道墙——Android 7.0 之后默认不信任用户CA证书的机制,以及应用自己在客户端做的证书锁定(Certificate Pinning)。apk-mitm 这个开源工具,干的就是把这两道墙推平的事。
它的思路听起来简单,做起来却很巧妙:把 APK 解包,改掉应用里面校验证书的逻辑,重新签名装回去,让应用自己把检查关掉。整个流程不需要源码,不需要 root,也不用手动写复杂的反检测脚本。这篇文章我就把 apk-mitm 的底翻出来看看——它到底改了什么、为什么能绕过去、有哪些坑,以及从防御角度我们该怎么防。适合做渗透测试、App 安全评估的朋友,也适合想搞清楚 HTTPS 客户端校验机制的 Android 开发。
1. 先搞清楚挡路的两道墙
1.1 代理工具为什么需要一张"假证书"
中间人抓包的基本逻辑,是把测试设备的网络出口指向一个代理工具,让所有请求先经过它再转发出去。问题在于HTTPS的通信内容是加密的,代理只看得到密文。要想解密分析,代理就得在应用和服务器之间,扮演"双向翻译"的角色:它向应用出示一张自己生成的证书,冒充服务器;应用信任了这张证书,就会把数据用这张证书的公钥加密。代理拿到密文后用自己的私钥解开,再用真正的服务器证书重新加密转发出去。
这个过程能成立的前提只有一个:应用愿意信任代理出示的那张证书。正常情况下,应用校验服务器证书时,会沿着CA信任链一路回溯,看看最终能不能找到一个系统信任的根CA。代理工具的证书通常是自己签发的,不在任何正规CA链上,所以默认情况下应用是不会认可的。早年的解决办法是手动把这证书装进系统或用户信任区,很多测试者也都习惯这个路子,那时候操作起来确实顺风顺水。
不过这里有个需要理解的底层概念:HTTPS 里"信任"不是二元的,而是一条链。你的代理证书虽然是自己签的,但只要它能被系统里某个受信任的根CA间接认证,应用就会放行。Android 的信任源分成"系统CA"和"用户CA"两套,这个区分在旧版本里影响不大,但后来成了抓包难易的分水岭。
1.2 Android 7.0 之后,用户CA证书大面积失效
真正让抓包变难的转折点,出现在Android 7.0(API 24)。从这一版开始,系统默认策略变成:应用只信任系统内置CA,不再信任用户后来安装的CA。你就算把代理证书装进了"用户凭据"区域,绝大多数第三方App也不会买账,因为系统在底层就把这条信任链切断了。
注意关键词是"默认"。系统给了开发者打开的口子,也就是网络安全配置(Network Security Config),允许应用自行声明是否信任用户CA。很多应用为了安全不会开这个口子,于是测试人员遇到越来越多的"装上证书也抓不到包"的情况。绕路方案当然也有,比如把证书塞进系统分区,但那个操作需要root权限,而且并不对所有机型都有效,系统一更新还要重来。
与其跟系统较劲,还不如直接改应用自己的信任策略。这条路也正是apk-mitm的核心出发点。你动不了系统,但你能动App本身——把一个不信任你证书的App,改成愿意信任的版本。
1.3 证书锁定:比信任链更硬的一道墙
如果说Android 7.0的改动是"系统不认你的证书",那么证书锁定就是"应用连系统都不太信"。证书锁定的设计初衷很合理:应用在编译期就把服务器证书的公钥指纹pin值写进代码,TLS握手时,除了走完CA信任链校验,还会额外比对服务器实际返回证书的指纹,和内置的pin是否一致。不一致就拒绝连接,不管这条链在系统层面看起来多可信。
这样一来,即使你拿到了一个能被系统信任的根CA,甚至把代理证书伪造成正规CA签发,只要公钥指纹对不上应用内存储的pin,连接依然会被掐断。常见的实现包括:自己实现X509TrustManager、在OkHttp里挂CertificatePinner、WebView里拦截onReceivedSslError,以及用第三方SDK做统一配置。每种实现的代码特征都不一样,这也是通用绕过工具需要花心思的地方。
证书锁定本质上是在说:客户端只认它"熟人"的公钥。所以想绕过它,不能只靠环境,必须进到代码内部,把"熟人"的判断逻辑给改掉。
2. apk-mitm 的底层思路:重打一套"自己人"的应用
2.1 APK的结构决定了"篡改"的可行性
先看APK包里到底有什么。它本质上是一个zip压缩包,里面有AndroidManifest.xml、classes.dex、res资源和META-INF签名目录。classes.dex是编译后的Dalvik字节码,可以用工具反汇编成smali——一种带一点汇编味道的中间表示,修改后再编译回dex。只要你能改dex,理论上应用里的任何Java逻辑都能被改掉。
apk-mitm盯上的修改点,主要集中在两处:AndroidManifest.xml和classes.dex对应的smali代码。Manifest控制的是应用的全局属性,比如是否可调试、是否引用自定义网络配置;smali代码控制的是应用内部的证书校验逻辑。二者合起来,正好覆盖前面说的两堵墙。资源部分基本不动,签名目录在重打包之后重做。
这个流程能成立,还有个冷知识做支撑:Android 对APK的修改不设防。只要最终包能通过签名校验,系统就认为它是合法应用。重打包工具用合法签名密钥换个新签名,系统层面并不觉得奇怪,真正有意见的反而是应用自己写的各种自校验逻辑。
2.2 从解包到重签名的完整流水线
把apk-mitm的运行过程拆开看,其实是一套标准的重打包流水线。
第一步是解包,工具调用apktool,将APK还原成可读的目录结构,二进制的Manifest被解析成XML,dex被反汇编到smali目录。第二步是修改,根据解包结果做两件事:在Manifest里注入网络配置相关属性,处理网络安全配置文件;同时扫描smali特征,决定如何绕过证书锁定。第三步是重打包,apktool把修改后的目录重新编译成APK,生成新的资源索引和dex。第四步是签名,apksigner对新的APK做v1/v2/v3签名,不然系统安装时直接拒绝。
每一步单独拿出来都不复杂,难的是把流程串起来并且兼容大量不同形态的APK。高版本target、资源混淆、加固壳、自定义proguard规则,任何一个都会让简单的"解包-重打包"翻车。apk-mitm的价值,恰恰在于把这一条容易踩坑的流水线做了工程化处理,让使用者不必理解每一步细节也能得到结果。
2.3 没源码也可以,没有root也可以,为什么
处理证书锁定不需要源码,是因为dex反汇编成smali之后,类名、方法名、字符串常量都在。工具不需要理解所有业务逻辑,只要按特征找到证书校验方法,然后把校验动作替换成"直接放行"就行。这就像你不用读懂一本小说的所有章节,只需要找到某一页的关键台词改掉。
不需要root,是因为整个过程只修改应用本身。你把重打包后的APK装进设备,这个应用运行的逻辑已经是不校验证书的版本了。设备系统层什么改动都没有,所以普通测试机、甚至userdebug版本都够用。这一点让apk-mitm在各类实际测试环境中都很有吸引力——不用和系统版本较劲,也不用面对root权限不统一的问题。
很多人会问:重打包后的App和原版在功能上有差别吗?答案是有,但差别通常只在证书校验和有签名自校验的场景里。正常业务功能、UI交互基本一致,这也是为什么安全测试中这招这么好用的原因。
3. 绕证书锁定的两大手法:静态改码与动态注入
3.1 定位校验点:看smali里的特征
apk-mitm第一步要做的是"找"。它会在smali代码里搜索证书锁定相关的特征,来判断应用用了哪种方案。目前最常见的是OkHttp的CertificatePinner,dex里通常会出现okhttp3/CertificatePinner这个类引用,并且伴随.pin方法调用和SHA256指纹字符串。搜索这个类名基本一抓一个准,因为OkHttp在Android开发里几乎是标配。
次常见的是自定义TrustManager。特征是实现javax.net.ssl.X509TrustManager接口,在checkServerTrusted方法里有证书链校验逻辑。这类特征属于"接口实现",不像OkHttp那样有明确类名,搜索时需要结合接口方法名和证书相关引用一起判断。WebView类应用则要查onReceivedSslError里对handler.proceed/cancel的调用,虽然场景相对少,但一旦遇到就是硬骨头。
还有一个容易被忽略的点:证书锁定不一定只存在一个位置。有些应用在主流程和子流程里分别做了不同方式的校验,比如网络请求走OkHttp、WebView另走一套。apk-mitm在处理时会尽量把能搜到的特征都扫一遍,但对于多校验点的应用,仍有可能遗漏,需要人工补充。
3.2 静态修改smali:干净利落但怕混淆
定位到校验点之后,静态修改的思路是直接改smali逻辑。对于OkHttp的CertificatePinner,常见做法是把关键的校验方法改成空实现,或者在方法入口直接return表示校验通过。翻译到smali层,就是删除校验分支相关指令,保留一个合理的返回值。改完重新编译,应用运行时的行为就变了,效果等同于"取消证书锁定"。
静态修改最大的优势是"无附加依赖"。修改后的APK里没有任何额外的运行组件,行为最接近原始版本,也不会被应用自带的反注入扫描发现。缺点则是怕混淆。当应用做了ProGuard或R8级别的代码混淆,方法名和类名被重写后,基于特征的定位就不再可靠。这时候就得靠第二种手法兜底。
我之前在某测试项目的目标App里,就碰到过一个用R8混淆得很彻底的情况,smali里方法名全是a、b、c,字符串也被编码处理过。静态特征搜索完全失灵,最后是换动态注入才拿下来的。
3.3 动态注入:把钩子埋进运行时
动态注入的思路完全不同:它往APK里塞入一套插桩框架组件,应用运行时,在类加载到目标方法时用脚本接管并改写方法行为。无论证书校验发生在哪个Java方法里,脚本都可以钩住并返回"校验通过"的结果。这样做不用关心方法名怎么混淆,因为插桩是按运行时的方法地址或类名映射来定位的,混淆改不了类加载信息。
这个方案的缺点是包体现在多了一套注入组件,体积变大,运行行为也更复杂。而且部分应用会做插桩组件检测,一旦识别出运行环境里多了一些不该有的东西就直接退出。与静态修改配合时,通用做法是先跑静态,命中就完事;静态找不到入口,或者应用有加固壳导致dex提取困难,再启用动态注入。
我自己的经验是:常见的基础应用用静态一把过,金融类、加固类应用八成要上动态。两种手段不是二选一,而是互相补充的关系,具体取舍要看你手里的目标用什么姿势写的代码。
4. 处理网络安全配置:让应用重新信任用户证书
4.1 配置文件的优先级:base-config、domain-config、debug-overrides
Android的网络安全配置是一个XML文件,通过Manifest里的android:networkSecurityConfig属性挂到应用上。它定义了应用在网络安全层面的行为,其中有三个节点要理解。base-config是全局默认配置;domain-config针对特定域名覆盖全局配置;debug-overrides则是调试模式专属配置,只有应用以debuggable=true启动时才会生效。
每个配置节点里都可以通过trust-anchors设置可信CA来源,src可以是system、user,也可以同时列出多个。默认状态是只信任system。想抓包,就是要在这个配置里把user也加进可信列表。理解了这几个节点的优先级,就明白apk-mitm为什么不是简单粗暴地替换整个配置文件,而是按应用原始配置做增量修改。
如果你手动写过这个XML,会发现它跟Web开发里的robots.txt有点像——声明式的,控制面很清晰。但正因为是声明式的,任何一处配置错误,影响也是全局性的,所以对待这个文件要格外小心。
4.2 apk-mitm 的具体修改策略
apk-mitm在网络配置上的动作分两种情况。第一种,应用没有引用任何networkSecurityConfig,它就往Manifest里补一个android:networkSecurityConfig属性,指向自己新建的配置文件。这个新建文件会把base-config里的trust-anchors配成同时包含system和user,这样安装的代理用户证书就能被应用正常信任。
第二种,应用本来就有自己的网络安全配置。这时候如果直接替换成宽松配置,可能会破坏应用对特定域名的特殊规则。稳妥的姿势是保留原配置里的domain-config等规则,只在base-config上放宽,同时把debug-overrides补上。工具的自动实现会尽量朝"最小副作用"的方向走,但如果遇到特别复杂的原始配置,还是建议手动核对一遍。
还有一个很容易被忽略的点:代理抓包时,有些应用的部分请求本身就是明文HTTP,而系统从Android 9开始默认禁止明文流量。apk-mitm在修改配置时会把cleartextTrafficPermitted设为true(如果应用没有更严格的业务要求),确保这类请求也能通过。这个细节虽然小,但决定了很多App能否在抓包工具里看到完整流量。
注意:如果应用因为业务合规要求必须禁用明文流量,而你强行打开,可能引发日志告警或安全策略异常。这种App要分别处理,别一刀切。
4.3 为什么还要打开debuggable
既然debug-overrides只在debuggable=true时生效,apk-mitm为了让目标应用在"普通运行环境"里也吃这一套规则,通常在修改Manifest时会顺手把application节点的android:debuggable属性置为true。这样一来,新加的用户CA信任规则就能在非调试构建上同样生效。
但这个开关不是没有代价。应用一旦处于调试模式,运行日志会更啰嗦,部分对调试模式敏感的应用会触发自我保护,反而闪退或拒绝运行。处理这类应用时,我的习惯是:如果静态修改网络配置已经够用,就不额外开debuggable,免得引入新问题。工具默认开,但人工介入时有必要按实际情况再关掉。
这里也顺带解释一个现象:为什么有时候你用debug版APK抓包容易,正式版抓包难?因为debug版天然让debug-overrides生效,很多开发者在本地调试时根本没意识到正式版会走另一套规则。apk-mitm把这个坑填平了,代价是让应用显得"不那么正式"。
5. 实操记录:把一个普通APK改成可抓包版本
5.1 前置环境准备
开工前先把三样东西准备好:JDK、Android SDK build-tools、apk-mitm本体。JDK版本别太老,最新LTS都行。build-tools里要保证有apksigner和zipalign,最好把build-tools目录加进PATH,省得工具找不到。apk-mitm通过npm安装:
npm install -g apk-mitm装完可以先执行一下apk-mitm --help看看版本和参数。环境对的时候,工具在缺依赖的情况下也会直接报错提示,不会默默失败。如果设备侧还想抓得更顺,可以在测试机里把代理工具的CA证书一并装好,后面验证省事很多。
5.2 一条命令完成解包、修改、重打包、签名
把目标APK放到一个独立目录,执行:
apk-mitm app.apk终端会输出一系列处理日志,从"解包中"到"检查网络安全配置""处理证书锁定""重打包""签名",整个过程一般几十秒到几分钟。跑完会在当前目录生成一个新的APK文件,文件名的规则通常是原名加一个类似-patched的后缀。
这个过程中,工具会自动调用apktool和apksigner,链条中任何一环失败都会在终端里直接反映出来。比如apktool版本太旧,会在解码阶段报错;签名工具缺了v2方案,会在安装阶段暴露。处理完的APK,先过一遍签名校验:
apksigner verify -v app-patched.apk只要输出里能看到v1、v2或v3方案对应的"Verified"字样,就说明签名这关过了。
5.3 装进设备验证抓包效果
把修改后的APK安装到测试机或模拟器,设备上先装好代理工具的CA证书。打开代理抓包,启动目标App,再刷新几个页面,正常情况下代理面板里应该能看到应用的HTTPS明文流量。验证点有两个:一是页面的接口数据能看到,说明HTTP层通了;二是应用没有报SSL错误,说明证书锁定确实被放过了。
实际操作中,我一般会再测一步:杀掉App进程后重新冷启动,确认没有启动闪退。很多应用会在启动阶段做完整性校验,如果重签名导致自校验失败,冷启动直接就闪退,这种情况不属于证书校验问题,而是遇到了抗重打包机制。遇到这个,就得进入第7节说的高级调试流程。
还可以顺手抓一抓WebView流量。主流程的HTTPS走通了,不代表WebView里的H5页面也走通了。如果目标是电商、资讯类App,很多业务都在WebView里,建议在代理工具里过滤一下相关域名,确认WebView场景没有遗漏。
5.4 批量场景下的效率技巧
如果手头是一批APK要批量处理,建议每个APK单独放一个目录,或者用脚本把输出文件移到独立输出目录:
for f in *.apk; do mkdir -p "out/${f%.apk}" apk-mitm "$f" -o "out/${f%.apk}" done输出目录参数能不能用,以你安装版本的帮助信息为准。实测下来,批量处理二三十个APK的耗时主要卡在机器性能上,工具本身几乎没有额外瓶颈。如果中途某个APK失败,也不会影响其他APK的处理流程,把它单独拎出来排查就好。
批量处理时有个细节值得留意:多个APK如果来自同一家厂商,往往共享同一套签名和代码结构,一个失败,其他可能也会失败。这时候先修好第一个,再批量跑,能省下不少来回折腾的时间。
6. 防御视角:站在对面思考如何阻截
6.1 让应用能发现"自己变了"
最基础的防御是完整性自校验。应用启动时,把自己关键文件(dex、so、Manifest)的哈希和内置期望值比对,不一致就拒绝启动。内置期望值别放在容易被发现的常量和资源里,藏进so文件或者服务端下发,能提高攻击者的篡改成本。
另一个直接有效的校验点是签名。通过PackageManager拿到当前APK的签名摘要,和内置参考值比对。重打包必然换签名,这个检测在原理上非常可靠。需要注意开发阶段签名不固定,应用要对debug版和release版做区分,否则把自家正常版本也拦了。
签名自校验这个防御一旦做扎实,对重打包工具的打击是实打实的。APK改得再完美,签名一换就露馅,攻击者必须先绕过自校验才能让应用正常跑起来。
6.2 混淆、加固、反调试:提高自动化工具的翻车率
apk-mitm在静态阶段依赖类名和方法名特征做定位,所以代码混淆能直接拉高它的定位难度。把CertificatePinner相关的类改成a、b、c这种短名字,把pin值从明文改成运行时计算,或者干脆把校验逻辑下沉到native层,用JNI实现证书公钥比对。这一套下来,smali层找不到特征,动态插桩也得多绕两道弯。
反调试和反注入也有用。检测debuggable标志、检测调试端口、检测插桩框架的典型特征,发现异常就走异常分支。还有设备环境检测:root状态、代理设置、模拟器特征,全部可以成为拒绝服务的触发条件。这些手段做不到绝对防御,但能把自动化工具的成功率从"一键搞定"拉低到"需人工深度破解",很多场景下这就已经足够达到防护目的了。
个人经验:别把全部防御寄托在单一手段上。混淆挡得住静态工具,挡不住动态调试;反调试挡得住动态调试,挡不住更耐心的逆向。要按威胁模型来做取舍,而不是追求一个不可能的"绝对安全"。
6.3 把最终判断权放到服务端
单机侧的措施再强也能被绕过,所以真正的兜底必须放在服务端。服务端在建立连接阶段,可以要求客户端提交设备完整性证明,来证明你手里运行的应用确实没有被篡改。如果客户端拿不出有效证明,或提交的参数有明显异常,服务端直接拒绝响应。这类方案的问题在于部署成本偏高,但对高风险应用(金融、电商、即时通讯)来说,这笔投入跟被批量抓包带来的损失比,是划算的。
我在给某个客户做方案时反复强调一个观点:不要在客户端做"绝对可靠"的防御,因为客户端永远不可控;要做的是"客户端自校验 + 服务端异常识别 + 快速响应"的分层体系。单靠客户端加固,只是把攻击者的门槛从门槛抬到墙头,而服务端能把墙壁真正立起来。
服务端校验的关键在于,逆向人员可以改客户端逻辑,但改不了服务端代码。只要服务端拿到的每个请求都要求携带有效的完整性标记,重打包版本就寸步难行。
7. 常见问题与排错实录
7.1 解码阶段报错:先怀疑apktool版本
apktool对新版本Android target的支持总是滞后于系统发布。解码时报资源解析异常、Manifest解析失败,第一反应就是升级apktool。第二个常见原因是应用启用了资源混淆,apktool解出来的资源会有大量warning,这种warning多数可以忽略,但如果多到重打包后资源索引错乱,就要考虑是否用其他方式代替整包重打。
还有一种情况是APK里塞了超出常规大小的配置文件,apktool处理时会卡在某一步。碰到这种,先把包体拆开看看是不是纯zip结构,有些"APK"实际是AAB转换出来的,处理工具要选对。
7.2 重签名后装不上:签名方案和zipalign背锅
安装失败大部分出在签名环节。Android 7.0以上要求v2签名,Android 11以上进一步要求v3签名,apksigner默认会尽量打全套。如果工具链里混入了老版jarsigner,很可能只打了v1签名,系统直接拒绝。另外zipalign漏掉,也会导致部分设备安装失败。手动处理流程时,这两步都要确认。
判断签名到底缺了哪个方案,最直接的办法是把APK扔到apksigner verify里看输出。如果只显示v1,低版本Android有时还能装,高版本直接白屏或报解析错误。
7.3 抓包还是失败:三类隐蔽原因
第一类是native层校验。证书比对在so里做,smali层根本找不到特征。第二类是加固壳场景,dex被加密,静态定位无从下手。这两类都需要动态插桩在运行时hook。第三类是应用启动时做签名自校验,重签名必然被发现,表现是安装后冷启动闪退。这类要用脚本hook住PackageManager的getPackageInfo方法,把返回的签名信息篡改成原始签名,让应用以为签名没变。
这个套路我在一次测试某金融类App时实际用过,App启动即退,日志里明确在比对签名哈希。hook之后才正常跑起来。这种场景自动化工具通常处理不了,需要测试人员根据目标App单独写脚本。
还有个容易忽略的:部分应用会检测当前进程是否处于调试模式,检测的方式不止看manifest里的debuggable标志,还会查系统属性、尝试连接JDWP端口。这类检测要一起绕,否则抓到一半应用自动退出,很折磨人。
7.4 快速排查速查表
| 现象 | 可能原因 | 处理方向 |
|---|---|---|
| 解包/解码失败 | apktool版本过旧或资源混淆 | 升级apktool;忽略低风险warning |
| 安装时提示包解析错误 | 签名方案不全,或漏zipalign | 用apksigner打v1+v2+v3,补zipalign |
| 抓包仍是密文或SSL错误 | native层校验或加固壳 | 动态插桩,hook目标校验方法 |
| 冷启动闪退 | 应用启动时自校验签名或完整性 | hook PackageManager或文件打开类调用 |
| 部分域名请求失败 | 原有networkSecurityConfig规则冲突 | 手动保留domain-config,只放宽base-config |
做移动端安全测试这些年,我越来越觉得,工具能自动化,但认知不能自动化。apk-mitm能一条命令搞定证书锁定和网络配置的绕过,靠的是把Android的信任模型、APK结构、签名机制吃透,再把这些知识固化成流水线。反过来,开发者想防住这类工具,也只有站在同等理解深度上,才可能设计出真正有效的防线。
最后再分享一点:这一整套操作,请务必在授权范围内使用。安全测试的核心价值是帮企业和开发者发现问题、修复问题,而不是把工具当成攻击他人的捷径。如果你看完这篇能自己动手分析一个App的证书校验逻辑,或者回头加固自己App的防线,那这篇文章的意义就达到了。