前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >移动安全入门之常见抓包问题二

移动安全入门之常见抓包问题二

作者头像
FB客服
发布2023-03-29 16:02:30
1.3K0
发布2023-03-29 16:02:30
举报
文章被收录于专栏:FreeBuf

证书绑定

概述

证书绑定即客户端在收到服务器的证书后,对该证书进行强校验,验证该证书是不是客户端承认的证书,如果不是,则直接断开连接。浏览器其实已经这样做了,但是如“前面”所说,选择权交给了用户,且浏览器由于其开放性允许让用户自导入自己的证书到受信任区域。但是在APP里面就不一样,APP是HTTPS的服务提供方自己开发的客户端,开发者可以先将自己服务器的证书打包内置到自己的APP中,或者将证书签名内置到APP中,当客户端在请求服务器建立连接期间收到服务器证书后,先使用内置的证书信息校验一下服务器证书是否合法,如果不合法,直接断开。

认证方式:证书锁定

证书锁定(SSL/TLS Pinning)顾名思义,将服务器提供的SSL/TLS证书内置到移动端开发的APP客户端中,当客户端发起请求时,通过比对内置的证书和服务器端证书的内容,以确定这个连接的合法性。证书锁定需要把服务器的公钥证书(.crt 或者 .cer 等格式)提前下载并内置到App客户端中,创建TrustManager 时将公钥证书加进去。当请求发起时,通过比对证书内容来确定连接的合法性。

但由于证书存在过期时间,因此当服务器端证书更换时,需同时更换客户端证书。既然是要锁定证书,那么我们客户端上应该事先存在一个证书,我们才能锁定这个证书来验证我们真正的服务端,而不是代理工具伪造的服务端。如果是锁定证书,那通常情况下会将证书放置在app/asset目录下。

认证方式:公钥锁定

公钥锁定则需提取证书中的公钥内置到客户端中,通过比对公钥值来验证连接的合法性,由于证书更换依然可以保证公钥一致,所以公钥锁定不存在客户端频繁更换证书的问题。指 Client 端内置 Server 端真正的公钥证书。在 HTTPS 请求时,Server 端发给客户端的公钥证书必须与 Client 端内置的公钥证书一致,请求才会成功。

实现方式:配置文件(android7.0及以上)

通过res/xml/network_security_config.xml配置文件对证书进行校验。对apk反编译后查看res/xml目录下的network_security_config.xml文件,打开看到<domain-config>标签,说明使用了证书绑定机制。生效范围:app全局,包含webview请求。

证书锁定

代码语言:javascript
复制
<network-security-config>   <domain-config>     <domain includeSubdomains="true">example.com</domain>      <trust-anchors>       <certificates src="@raw/my_ca"/>     </trust-anchors>   </domain-config> </network-security-config>

(向右滑动,查看更多)

公钥锁定

代码语言:javascript
复制
<network-security-config>   <domain-config>     <domain includeSubdomains="true">example.com</domain>      <pin-set expiration="2099-01-01">       <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin>     </pin-set>   </domain-config></network-security-config> (向右滑动,查看更多)

实现方式:代码配置

生效范围:配置了该参数的实例。

证书锁定

代码语言:javascript
复制
// 获取证书输入流InputStream openRawResource = getApplicationContext().getResources().openRawResource(R.raw.bing); Certificate ca = CertificateFactory.getInstance("X.509").generateCertificate(openRawResource);// 创建 Keystore 包含我们的证书KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());keyStore.load(null, null);keyStore.setCertificateEntry("ca", ca);// 创建一个 TrustManager 仅把 Keystore 中的证书 作为信任的锚点TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); // 建议不要使用自己实现的X509TrustManager,而是使用默认的X509TrustManagertrustManagerFactory.init(keyStore);// 用 TrustManager 初始化一个 SSLContextsslContext = SSLContext.getInstance("TLS");  //定义:public static SSLContext sslContext = null;sslContext.init(null, trustManagerFactory.getTrustManagers(), new SecureRandom());
OkHttpClient pClient2 = client.newBuilder().sslSocketFactory(sslContext.getSocketFactory(), (X509TrustManager) trustManagerFactory.getTrustManagers()[0]).build();Request request2 = new Request.Builder()    .url("https://www.bing.com/?q=SSLPinningCAfile")    .build();try (Response response2 = pClient2.newCall(request2).execute()) {    message.obj += "\nhttps SSL_PINNING_with_CA_file access bing.com success";    Log.d(TAG, "https SSL_PINNING_with_CA_file access bing.com success return code:"+response2.code());} catch (IOException e) {    message.obj += "\nhttps SSL_PINNING_with_CA_file access bing.com failed";    Log.d(TAG, "https SSL_PINNING_with_CA_file access bing.com failed");    e.printStackTrace();    }(向右滑动,查看更多)

公钥锁定

代码语言:javascript
复制
client = OkHttpClient.Builder()    .certificatePinner(new CertificatePinner.Builder()      .add("xxxxxx.com", "sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=")      .build())    .build();(向右滑动,查看更多)

证书校验

javax.net.ssl.X509TrustManager接口被用来校验证书是否被信任。通常会校验 CA 是否为系统内置权威机构,证书有效期等。这个接口有三个方法,分别用来校验客户端证书、校验服务端证书、获取可信证书数组。其中我们重点关注checkServerTrusted方法。

代码语言:javascript
复制
// 该方法检查客户端的证书,若不信任该证书则抛出异常@Overridepublic void checkClientTrusted(X509Certificate[] chain, String authType)throws CertificateException {    ... ...}
// 该方法检查服务器的证书,若不信任该证书同样抛出异常@Overridepublic void checkServerTrusted(X509Certificate[] chain, String authType)throws CertificateException {    ... ...}
// 返回受信任的X509证书数组@Overridepublic X509Certificate[] getAcceptedIssuers() {    return new X509Certificate[0];}

(向右滑动,查看更多)

常规解决方法

  • App中会有相关函数对内置的证书或者公钥进行对比,我们可以让其一直返回通过,常见利用工具有 xpose的justTrustme模块,也可以使用现成的frida hook脚本或使用objection,其核心都是hook HTTP请求库中用于校验证书的API,将结果返回正常。
  • 反编译app修改其中代码逻辑后重新打包。
  • 若使用配置文件方式可以直接将文件中校验的部分<trust-anchors>或<pin-set>注释掉,再重新打包和签名即可。

案例一

设置完代理后打开某app提示网络错误无法正常使用。

日志查看关键词发现存在证书绑定校验。

通过日志中的关键词定位关键代码,当然也可以直接搜索checkServerTrusted。

编写hook脚本。

代码语言:javascript
复制
Java.perform(function(){    let SecureX509TrustManager = Java.use("com.xxx.xxxx.xxxx.common.ssl.SecureX509TrustManager");    SecureX509TrustManager["checkServerTrusted"].implementation = function (x509CertificateArr, str) {        console.log('checkServerTrusted is called' + ', ' + 'x509CertificateArr: ' + x509CertificateArr + ', ' + 'str: ' + str);        return;

(向右滑动,查看更多)

spawn模式启动即可抓到app的数据包。

这里也可以直接使用objection启动并执行android sslpinning disable来绕过证书邦定。

代码语言:javascript
复制
objection -g com.xxxx.xxxx explore -s "android sslpinning disable"(向右滑动,查看更多)

案例二

在测试机中打开某APP发现存在root检测,查壳发现使用的是某加固企业版,这里可以用pixel2测试机,非root环境,利用提权漏洞提权后运行frida,不过分析代码后发现了检测是否为root环境的方法。

跟踪发现在so层中实现,直接hook对应方法即可。

当然也可以通过重新编译安卓源码,去掉相关特征的方式去解决root检测问题。点击wifi添加代理地址,再次打开app提示网络风险,经过分析代码发现存在代理检测。我们使用postern工具对流量进行转发,但是发现只能抓到一点数据包。

查看日志的错误信息。

其使用了com.android.org.conscrypt.TrustManagerImpl.verifyChain方

法去校验证书,编写hook脚本。

代码语言:javascript
复制
var TrustManagerImpl = Java.use('com.android.org.conscrypt.TrustManagerImpl');    TrustManagerImpl.verifyChain.implementation = function(untrustedChain, trustAnchorChain, host, clientAuth, ocspData, tlsSctData) {      console.log("[+] bypass success!");      return untrustedChain;};

(向右滑动,查看更多)

将上一篇文章中绕过代理检测的代码组合后加载脚本。

代码语言:javascript
复制
frida -U -f com.xxxx.xxx -l anti-ssl.js --no-pause

成功抓到目标数据包。

双向证书认证

概述

双向认证要求服务器和用户双方都有证书,客户端会去验证服务端的证书,然后服务端也会去验证客户端的证书,双方拿到了之后会通过对通信的加密方式进行加密这种方法来进行互相认证,最后客户端再随机生成随机码进行对称加密的验证。

实现方式

代码语言:javascript
复制
public class SSLHelper {
  /** * 存储客户端自己的密钥 */ private final static String CLIENT_PRI_KEY = "client.bks";  /** * 存储服务器的公钥 */ private final static String TRUSTSTORE_PUB_KEY = "publickey.bks";  /** * 读取密码 */ private final static String CLIENT_BKS_PASSWORD = "123321";  /** * 读取密码 */ private final static String PUCBLICKEY_BKS_PASSWORD = "123321";  private final static String KEYSTORE_TYPE = "BKS";  private final static String PROTOCOL_TYPE = "TLS";  private final static String CERTIFICATE_STANDARD = "X509";  public static SSLSocketFactory getSSLCertifcation(Context context) {      SSLSocketFactory sslSocketFactory = null;      try {          // 服务器端需要验证的客户端证书,其实就是客户端的keystore          KeyStore keyStore = KeyStore.getInstance(KEYSTORE_TYPE);          // 客户端信任的服务器端证书          KeyStore trustStore = KeyStore.getInstance(KEYSTORE_TYPE);          //读取证书          InputStream ksIn = context.getAssets().open(CLIENT_PRI_KEY);          InputStream tsIn = context.getAssets().open(TRUSTSTORE_PUB_KEY);          //加载证书          keyStore.load(ksIn, CLIENT_BKS_PASSWORD.toCharArray());          trustStore.load(tsIn, PUCBLICKEY_BKS_PASSWORD.toCharArray());          //关闭流          ksIn.close();          tsIn.close();
          //初始化SSLContext          SSLContext sslContext = SSLContext.getInstance(PROTOCOL_TYPE);          TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(CERTIFICATE_STANDARD);          KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(CERTIFICATE_STANDARD);
          trustManagerFactory.init(trustStore);          keyManagerFactory.init(keyStore, CLIENT_BKS_PASSWORD.toCharArray());
          sslContext.init(keyManagerFactory.getKeyManagers(), trustManagerFactory.getTrustManagers(), null);          sslSocketFactory = sslContext.getSocketFactory();      } catch (KeyStoreException e) {          e.printStackTrace();      } catch (IOException e) {          e.printStackTrace();      } catch (CertificateException e) {          e.printStackTrace();      } catch (NoSuchAlgorithmException e) {          e.printStackTrace();      } catch (UnrecoverableKeyException e) {          e.printStackTrace();      } catch (KeyManagementException e) {          e.printStackTrace();      }      return sslSocketFactory;  }
}

(向右滑动,查看更多)

解决方法

如果是正常的双向验证的话,只需要导入存储在APP端的CA证书即可,但是基于ssl pinning的双向验证的话,需要导入存储在APP端的CA证书,而且还需要绕过客户端的强校验.

获取证书:

一般存放在App的raw或者assets目录下,常见证书后缀如下:

代码语言:javascript
复制
.p12.bks.pfx

也可能无后缀名,如果在安装包内找不到证书的话,也可以使用objection hook java.io.File定位。

代码语言:javascript
复制
objection -g cn.soulapp.android explore --startup-command "android hooking watch class_method java.io.File.$init  --dump-args(向右滑动,查看更多)

获取密码:

逆向apk,可以通过找到的证书名去搜索如 "client.p12" ,".p12" 等,或者定位java.security.KeyStore.open()方法,找到使用证书的地方。定位关键点后通过代码去查看是否存在实体编码的证书密码,若不存在明文密码则可以通过去hook相关方法获取密码。

常见关键词。

代码语言:javascript
复制
.p12"getAssets().open

案例一(某app上古版本,仅作分析)

使用jadx反编译安卓apk文件,由于存在混淆搜索关键词没有获取什么有价值的信息,更换工具为GDA或Jeb(jeb的反混淆优化更好一些),这里使用GDA,搜索关键词client.p12。

上图第二个红框中的load函数的第一个参数是证书的字节输入流,第二个参数就是证书的密码,由第一个红框我们可以知道str的定义,跟进SoulNetworkSDK.a方法。

跟进发现a的返回值来源于native层的函数getStorePassword。

查看具体加载的是哪个so文件。

用IDA反编译soul-netsdk,定位函数getStorePassword。

ios同理,ios可使用frida-ios-dump或fd_mac进行砸壳,获取证书"client.p12",完成后我们解压缩然后使用IDA加载二进制文件并在String窗口搜索证书的名称client,搜索后进入对应的类。

通过跟踪发现了该证书密钥,如下:

案例二

app抓到包返回400。

疑似使用了双向证书认证,对app进行脱壳查看代码,直接搜索.p12发现几处关键点。

最终定位到密钥来源getvalue方法。

查看该方法发现该方法来源于so层中。

直接hook该方法返回值。

代码语言:javascript
复制
Java.perform(function () {    console.log("start..... ");        var hook = Java.use('com.xxx.xxx.xxx');    console.log(hook);    hook.getValue.implementation=function(a){      console.log(a);      console.log(this.getValue(a));      return this.getValue(a);    }});

(向右滑动,查看更多)

成功获取证书密码。

Flutter框架

Flutter使用Dart编写,因此它不会使用系统CA存储,Dart使用编译到应用程序中的CA列表,Dart在Android上不支持代理,因此请使用带有iptables的ProxyDroid。

判断flutter应用

可以通过设备信息app查看,也可以通过⽇志grep flutter,如果有输出,⾃然也可以说明是flutter的。

代码语言:javascript
复制
logcat |grep flutter

证书校验实现方式

代码语言:javascript
复制
static bool ssl_crypto_x509_session_verify_cert_chain(SSL_SESSION *session,                                                      SSL_HANDSHAKE *hs,                                                      uint8_t *out_alert) {  *out_alert = SSL_AD_INTERNAL_ERROR;  STACK_OF(X509) *const cert_chain = session->x509_chain;  if (cert_chain == nullptr || sk_X509_num(cert_chain) == 0) {    return false;  }
  SSL *const ssl = hs->ssl;  SSL_CTX *ssl_ctx = ssl->ctx.get();  X509_STORE *verify_store = ssl_ctx->cert_store;  if (hs->config->cert->verify_store != nullptr) {    verify_store = hs->config->cert->verify_store;  }
  X509 *leaf = sk_X509_value(cert_chain, 0);  const char *name;  size_t name_len;  SSL_get0_ech_name_override(ssl, &name, &name_len);  UniquePtr<X509_STORE_CTX> ctx(X509_STORE_CTX_new());  if (!ctx ||      !X509_STORE_CTX_init(ctx.get(), verify_store, leaf, cert_chain) ||      !X509_STORE_CTX_set_ex_data(ctx.get(),                                  SSL_get_ex_data_X509_STORE_CTX_idx(), ssl) ||      // We need to inherit the verify parameters. These can be determined by      // the context: if its a server it will verify SSL client certificates or      // vice versa.      !X509_STORE_CTX_set_default(ctx.get(),                                  ssl->server ? "ssl_client" : "ssl_server") ||      // Anything non-default in "param" should overwrite anything in the ctx.      !X509_VERIFY_PARAM_set1(X509_STORE_CTX_get0_param(ctx.get()),                              hs->config->param) ||      // ClientHelloOuter connections use a different name.      (name_len != 0 &&       !X509_VERIFY_PARAM_set1_host(X509_STORE_CTX_get0_param(ctx.get()), name,                                    name_len))) {    OPENSSL_PUT_ERROR(SSL, ERR_R_X509_LIB);    return false;  } if (hs->config->verify_callback) {    X509_STORE_CTX_set_verify_cb(ctx.get(), hs->config->verify_callback);  }
  int verify_ret;  if (ssl_ctx->app_verify_callback != nullptr) {    verify_ret =        ssl_ctx->app_verify_callback(ctx.get(), ssl_ctx->app_verify_arg);  } else {    verify_ret = X509_verify_cert(ctx.get());  }
  session->verify_result = X509_STORE_CTX_get_error(ctx.get());
  // If |SSL_VERIFY_NONE|, the error is non-fatal, but we keep the result.  if (verify_ret <= 0 && hs->config->verify_mode != SSL_VERIFY_NONE) {    *out_alert = SSL_alert_from_verify_result(session->verify_result);    return false;  }
  ERR_clear_error();  return true;}

(向右滑动,查看更多)

解决方法

我们如果测试flutter应用时,抓包看到错误日志:CERTIFICATE_VERIFY_FAILED,那么说明采用了证书校验,可以通过hook修改ssl_crypto_x509_session_verify_cert_chain函数返回值的方式解决抓包问题。

案例

确认目标app报错日志为CERTIFICATE_VERIFY_FAILED,由于证书校验链逻

辑在libflutter.so中实现,可以通过搜索 ssl_client和ssl_server字符来定位函数,用IDA打开libflutter.so,搜索对应字符。

双击跳到字符串定义。

ctrl+x查看交叉引用。

3ecc00函数就是我们要找的CERTIFICATE_VERIFY_FAILED。

编写hook脚本。

代码语言:javascript
复制
function hook_ssl() {    var base = Module.findBaseAddress("libflutter.so");    console.log("base: " + base);    var ssl_crypto_x509_session_verify_cert_chain = base.add(0x5c6b7c);    Interceptor.attach(ssl_crypto_x509_session_verify_cert_chain, {        onEnter: function(args) {         },        onLeave: function(retval) {            console.log("校验函数返回值: " + retval);            retval.replace(0x1);      }  });}

(向右滑动,查看更多)

加载 hook 脚本绕过。

代码语言:javascript
复制
frida -U xxxxx -l hook.js

精彩推荐

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2023-03-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FreeBuf 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 证书绑定
    • 认证方式:证书锁定
      • 认证方式:公钥锁定
        • 实现方式:配置文件(android7.0及以上)
          • 实现方式:代码配置
            • 证书校验
              • (向右滑动,查看更多)
                • 常规解决方法
                  • 案例一
                    • (向右滑动,查看更多)
                      • 案例二
                        • (向右滑动,查看更多)
                        • 双向证书认证
                          • 实现方式
                            • (向右滑动,查看更多)
                              • 解决方法
                                • 案例一(某app上古版本,仅作分析)
                                  • 案例二
                                    • (向右滑动,查看更多)
                                    • Flutter框架
                                      • 判断flutter应用
                                        • 证书校验实现方式
                                          • (向右滑动,查看更多)
                                            • 解决方法
                                              • 案例
                                                • (向右滑动,查看更多)
                                                相关产品与服务
                                                SSL 证书
                                                腾讯云 SSL 证书(SSL Certificates)为您提供 SSL 证书的申请、管理、部署等服务,为您提供一站式 HTTPS 解决方案。
                                                领券
                                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档