客户端校验
SSL PINNING 是Google官方推荐的校验方式,原理是在客户端(安卓APP)中预先设置好证书信息,握手时与服务端返回的证书进行比较。
如果有这种客户端校验,那么charles等抓包工具,就无法抓包了。因为charles作为中间人返回给客户端的证书信息与原客户端预先设置的不一致,所以,客户端检测到,就拒绝发送请求了。
1.公钥校验(Pinner) 开发一个安卓应用,了解下这个验证到底是怎么实现的。例如:百度APP。
1.1 基于代码 1.1.1 获取sha256公钥 http://slproweb.com/products/Win32OpenSSL.html
1 >>>openssl s_client -connect www.baidu.com:443 -servername www.baidu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
1 Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=
1 >>>openssl x509 -in baidu.pem -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
1 下载PEM证书 => 转换公钥 => SHA256加密 => Base64编码
也可以同代码生成:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 info = """MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqi/MQY0lroPp9CfEALM5 bw6YKlV9B+WASYL609OFmLXfe2+7At3teOQMByueHoZL9mqGWNdXbyFZEdhvlm7S 3jYo9rTjzpUyKQDBZY5psAD+Ujf0iD+LbQ+78OzFwDHvrbUMBmatvtxDE8RmsF3P VlPi0ZaCHAa7m1/tYI3S7fPSUO67zbI2l8jOe9JLt1y0iMo3bovO+Zb9tPVHtSB3 u/yonYGybPjHCWrdIm6DP6dT3/HaLylrIsPpHWXoxaC6E04WPwOT8KVZihqA6Cd9 SSPf0flLl7cBxBn18cX/kTPQoXTG7tTP9jgM7b1eqkT7iPd7mXB2NFV+VdIPnr+U kwIDAQAB""" import base64import hashlibv1 = base64.b64decode(info) obj = hashlib.sha256(v1) res = obj.digest() v2 = base64.b64encode(res) print (v2)
1 >>>openssl s_client -connect www.baidu.com:443 -servername www.baidu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha1 -binary | openssl enc -base64
1 ogF5HvFnVP8hLIG2b0JNw3ajJAE=
1.1.2 配置 在安卓中开启网络请求的权限。
1 2 <uses-permission android:name="android.permission.INTERNET"/> <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
1 2 3 4 5 6 7 8 9 10 11 12 13 14 dependencies { // implementation 'androidx.appcompat:appcompat:1.6.1' // implementation 'com.google.android.material:material:1.11.0' implementation 'androidx.appcompat:appcompat:1.4.1' implementation 'com.google.android.material:material:1.6.0' implementation 'androidx.constraintlayout:constraintlayout:2.1.4' testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.5' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' implementation "com.squareup.okhttp3:okhttp:3.14.9" }
1 implementation "com.squareup.okhttp3:okhttp:3.14.9"
1.1.3 发送请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 private void doRequest () { new Thread () { @Override public void run () { final String CA_PUBLIC_KEY = "sha256/Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=" ; final String CA_DOMAIN = "www.baidu.com" ; CertificatePinner pinner = new CertificatePinner .Builder().add(CA_DOMAIN, CA_PUBLIC_KEY).build(); OkHttpClient client = new OkHttpClient .Builder().certificatePinner(pinner).build(); Request req = new Request .Builder().url("https://www.baidu.com/?q=defaultCerts" ).build(); Call call = client.newCall(req); try { Response res = call.execute(); Log.e("请求成功" , "状态码:" + res.code()); } catch (IOException ex) { Log.e("请求失败" , "异常" + ex); } } }.start(); }
1.2 基于配置 详见示例:NetDemo2.zip
1.2.1 获取sha256公钥
1 >>>openssl s_client -connect www.baidu.com:443 -servername www.baidu.com | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
1 Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=
1.2.2 配置
1 2 3 4 5 6 7 8 9 10 <?xml version="1.0" encoding="utf-8" ?> <network-security-config > <domain-config > <domain includeSubdomains ="true" > baidu.com</domain > <pin-set > <pin digest ="SHA-256" > Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU=</pin > </pin-set > </domain-config > </network-security-config >
1.2.3 发送请求
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 package com.nb.netdemo2;import androidx.appcompat.app.AppCompatActivity;import android.os.Bundle;import android.util.Log;import android.view.View;import android.widget.Button;import java.io.IOException;import okhttp3.Call;import okhttp3.OkHttpClient;import okhttp3.Request;import okhttp3.Response;public class MainActivity extends AppCompatActivity { private Button btn1; @Override protected void onCreate (Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); btn1 = findViewById(R.id.btn1); btn1.setOnClickListener(new View .OnClickListener() { @Override public void onClick (View v) { doRequest(); } }); } private void doRequest () { new Thread () { @Override public void run () { OkHttpClient client = new OkHttpClient .Builder().build(); Request req = new Request .Builder().url("https://www.baidu.com/?q=defaultCerts" ).build(); Call call = client.newCall(req); try { Response res = call.execute(); Log.e("请求发送成功" , "状态码:" + res.code()); } catch (IOException ex) { Log.e("Main" , "网络请求异常" + ex); } } }.start(); } }
2.证书校验 2.1 基于代码 详见示例:NetDemo3.zip
2.1.1 获取PEM证书
2.1.2 配置
2.1.3 发送请求
2.2 基于配置 详见示例:NetDemo4.zip
2.2.1 获取PEM证书
2.2.2 配置
2.2.3 发送请求
3.Host校验 详见示例:NetDemo5.zip
3.1 配置
3.2 发送请求
验证,请求的地址必须是指定域名,verify方法返回:
此处,你可能会有疑问,我的请求已经www.baidu.com 了,何必再在HostnameVerifier
中的verfy
再校验一次呢?
1 2 3 4 5 一般Host校验会跟证书校验结合,在发送请求时,先校验证书、再校验Host。 以百度为例,很多域名共用一个证书,例如:[baidu.com, baifubao.com, www.baidu.cn, www.baidu.com.cn, mct.y.nuomi.com, apollo.auto, dwz.cn, *.baidu.com, *.baifubao.com, *.baidustatic.com, *.bdstatic.com, *.bdimg.com, *.hao123.com, *.nuomi.com等。 那么,当预设证书后无论访问 baidu.com 或 hao123.com均能通过校验。 如果再结合 HostnameVerifier的verfy再次校验,就能在客户端对请求进一步认证。
4.证书+Host校验 详见示例:NetDemo6.zip
5.如何过客户端校验? 想要解决客户端的校验,本质上就是通过Hook的机制将原本校验的位置替换成自定义逻辑(直接不校验)。
5.1 frida hook脚本
上述是以okhttp为例的截图,真正开发时开发会使用其他的模块来发送网络请求,frida中提供了常见网络库相关绕过客户端校验的Hook脚本,例如:
1 2 3 4 5 6 7 /* * This script combines, fixes & extends a long list of other scripts, most notably including: * * - https://codeshare.frida.re/@akabe1/frida-multiple-unpinning/ * - https://codeshare.frida.re/@avltree9798/universal-android-ssl-pinning-bypass/ * - https://pastebin.com/TVJD63uM */
详见:demo/frida_multiple_unpinning.js
1 2 3 4 Run with: frida -U -f [APP_ID] -l frida_multiple_unpinning.js 重启app frida -UF -l frida_multiple_unpinning.js 不重启
5.2 JustTrustMe 本质上是xposed的Hook脚本,只不过可以打包安装在手机直接运行。
安装 Magisk 面具(手机root)
在面具中刷入 LSPosed框架
安装 JustTrustMe
在LSPosed框架中配置并启动 JustTrustMe
5.2.1 Magisk面具 请根据自己手机的机型去root并安装面具,参考链接:
1 2 3 https://www.bilibili.com/video/BV1Ly4y1u7YE/ https://www.bilibili.com/video/BV1er4y1C7wU https://magiskcn.com/
提示:后续的操作均使用的Magisk面具 24.0 版本。
5.2.2 刷入LSPosed
Riru-LSPosed
1 2 - 先刷Riru https://github.com/RikkaApps/Riru/releases - 在刷Riru-LSPosed https://github.com/LSPosed/LSPosed/releases
Zygisk-LSPosed(推荐)
1 - 刷Zygisk-LSPosed https://github.com/LSPosed/LSPosed/releases
注意:在面具中可以根据是否开启 Zygisk,来切换和生效。
示例1:红米8A
问题:LSPosed无图标 刷入成功后,就可以看到 LSPosed 的图标,如果没有出现的话,就去手机的 /data/adb/lspd/
目录下找apk包,然后再点击安装即可。
问题:LSPosed未激活 正常安装完LSPosed会直接激活,如果LSPosed显示未激活,请点击 图1 中Magisk安装,根据步骤点击安装,之后LSPosed就可以激活了。
示例2:红米Note 9Pro
5.2.3 安装并启动JustTrustMe 将 JustTrustMePlus.apk
安装到手机(跟安装其他app一样)。
安装成功后,在LSPosed的模块列表中可以看到 JustTrustMe
.
6.实战案例 6.1 安居客 版本:v16.13.2
https://www.wandoujia.com/apps/280945/history_v322000
7 笔记 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 抓包专题 1.为什么要做抓包专题? - 防护,防止你抓包 - 实现抓包 2.今日概要:客户端证书校验(安居客) - 什么是客户端证书校验? - 如何绕过?【傻瓜式】 - 校验的原理 & 绕过的机制 3.什么是客户端证书校验? - http请求,没有任何的校验(很少有人http) - https请求 - 唯品会APP,安卓开发者没有在代码中编写验证证书的逻辑。 - 安居客APP,安卓开发者有在代码中编写验证证书的逻辑。 - 现象:唯品会APP vs 安居客APP - 绕过客户端证书校验,本质:通过Hook机制实现 - frida + js代码 -> Hook绕过 【方法1】 - xposed + java代码 -> Hook绕过 xposed + justtrustme组件 -> Hook绕过 【方法2】 3.1 Frida的Hook绕过客户端证书校验 问题: 你知道客户端证书校验的代码是怎么实现的? 如果知道,你就可以了解他是执行的那个方法进行校验。 直接找到响应的包和方法Hook就能绕过。 1.去课件中找到 frida_multiple_unpinning.js 2.frida执行进行hook 3.运行环境 Python3.7.9 + frida==16.0.1 + frida-tools==12.0.1 >>>frida -U -f com.anjuke.android.app -l frida_multiple_unpinning.js 注意:安居客APP里面有frida检测,先解决frida检测,删除 libmsaoaidsec.so 总结经验: SSL handshake with client failed: An unknown issue occurred processing the certificate (certificate_unknown) `尝试`使用frida_multiple_unpinning.js进行Hook绕过 3.2 xposed + java代码(justtrustme组件) 1.安装xposed(LSPosed) - Magisk面具+ROOT - LSPosed是面具的模块(zip) 第1步:获取LSPosed的zip文件(课件+https://github.com/LSPosed/LSPosed/releases) 第2步:将zip文件上传到手机 adb push 本地目录/xxx.zip /sdcard/Download/LSPosed-v1.8.4-6609-zygisk-release.zip\ 第3步:在Magisk面具中刷入LSPosed模块 - 打开Magisk面具 - 点击模块 - 本地安装 + 刷入 - 手机重启,手机上就会出现lsposed的图标 注意:如果没有lsposed图片,就去 `/data/adb/lspd/` 【mt管理器 or np管理】,手动安装 注意:如果是未激活 2.JustTrustMe组件(java代码 -> apk) -> 实现绕过客户端证书校验 - 安装JustTrustMe组件 adb install JustTrustMePlus.apk - 看到LSPosed组件 - 打开LSPosed组件中的JustTrustMe组件 - 配置JustTrustMe组件去Hook指定的APP(重启手机生效) 4.校验的原理 & 绕过的机制 - 公钥校验(Pinner) - 证书校验 - Host校验 4.1 公钥校验(Pinner) 安卓开发工程师: - 获取后台的证书PEM文件(后端项目Https部署) - PEM文件内容处理 下载PEM证书 => 转换公钥 => SHA256加密 => Base64编码 "Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU="" - 发送网络请求 - 以前的网络请求发送 OkHttpClient client = new OkHttpClient.Builder().build(); Request req = new Request.Builder().url("https://www.baidu.com/?q=defaultCerts").build(); Call call = client.newCall(req); try { Response res = call.execute(); Log.e("请求成功", "状态码:" + res.code()); } catch (IOException ex) { Log.e("请求失败", "异常" + ex); } - 客户端校验 final String CA_PUBLIC_KEY = "sha256/Zhv4cvwdHmEmE0edWEcIdmLfwsqxrrOmp+vbngwNnrU="; final String CA_DOMAIN = "www.baidu.com"; //校验公钥 // 1.找到CertificatePinner对象 // 2.执行CertificatePinner中的check方法【 证书内容 vs 动态生成 】 CertificatePinner pinner = new CertificatePinner.Builder().add(CA_DOMAIN, CA_PUBLIC_KEY).build(); OkHttpClient client = new OkHttpClient.Builder().certificatePinner(pinner).build(); Request req = new Request.Builder().url("https://www.baidu.com/?q=defaultCerts").build(); Call call = client.newCall(req); try { Response res = call.execute(); Log.e("请求成功", "状态码:" + res.code()); } catch (IOException ex) { Log.e("请求失败", "异常" + ex); } 完整体验: 1.【Net01】创建安卓项目 + 发送https网络请求(无客户端证书校验) 2.【Net02】创建安卓项目 + 发送https网络请求(客户端证书校验)【百度工程师】 - 下载PEM证书 => 转换公钥 => SHA256加密 => Base64编码 -> Q6TCQAWqP4t+eq41xnKaUgJdrPWqyG5L+Ni2YzMhqdY= - 安装APP运行 3.运行 - 未抓包(未使用charlse抓包) - Net01 OK - Net02 OK - 抓包 + 分析 + 逆向(使用charlse抓包) - Net01 OK -> 例如 唯品会案例 - Net02 异常 -> 例如 安居客 4.猜想:关于安居客APP - 安卓工程师 - 提前自己网址-> 下载PEM证书 => 转换公钥 => SHA256加密 => Base64编码 - 集成项目 5.如何绕过? # 集成到APP中的公钥 final String CA_PUBLIC_KEY = "sha256/Q6TCQAWqP4t+eq41xnKaUgJdrPWqyG5L+Ni2YzMhqdY="; final String CA_DOMAIN = "www.baidu.com"; # 进行校验 CertificatePinner.check 方法 # - 读取本地集成公钥(sha256+base64) # - 获取请求地址证书(sha256+base64) CertificatePinner pinner = new CertificatePinner.Builder().add(CA_DOMAIN, CA_PUBLIC_KEY).build(); OkHttpClient client = new OkHttpClient.Builder().certificatePinner(pinner).build(); Request req = new Request.Builder().url("https://www.baidu.com/?q=defaultCerts").build(); 如果要Hook: okhttp3.CertificatePinner.check,如果让check方法不报错。 再看frida脚本: 永远让check不报错。 答疑: 1.关于https请求? 2.抓包不是密文?中间人 3.我可以拿到任何网站的公钥? - 你任职于bilibili公司 - 开发自己公司APP -> 【APP】 + 【后端API】 - 根据后端域名 -> 公钥->Base64编码 - 集成到APP中 - 我的APP能正常向我的后端发送请求 - 如果有中间人抓包,一定会报错 4.2 证书校验 安卓开发工程师: - 下载PEM证书 & 放入到APP中 - ... 如果绕过? ... 4.3 Host校验 安卓开发工程师: ... 问题:为什么frida_multiple_unpinning.js有700多行代码? - 发送网络请求 okhttp3(几乎) - 其他的网络请求 ... 5.感觉: 如果遇到客户端证书校验?【通杀】 - Frida的hook脚本 - xposed+justtrustme组件 代码混淆问题无法解决。 APP未混淆: okhttp3.CertificatePinner var okhttp3_Activity_1 = Java.use('okhttp3.CertificatePinner'); okhttp3_Activity_1.check.overload('java.lang.String', 'java.util.List').implementation = function(a, b) { console.log('[+] Bypassing OkHTTPv3 {1}: ' + a); return; }; APP混淆 a.c b.e d.b 如果想要绕过: var okhttp3_Activity_1 = Java.use('a.c'); okhttp3_Activity_1.bbb.overload('java.lang.String', 'java.util.List').implementation = function(a, b) { console.log('[+] Bypassing OkHTTPv3 {1}: ' + a); return; };
__END__