抓包分析

这里笔者开始使用的burpsuite进行,但发现抓不到有用的协议,使用charles抓包后发现有很多失败的协议,查看失败的协议,如下图

发现是客户端主动段开的连接,这种情况有两种可能

  1. 客户端收到上游返回的证书后,会对证书的合法性进行验证,通常验证证书的签名是否可信、证书的域名是否与请求的域名匹配等,即HostnameVerifier
  2. 客户端通过证书绑定(即ssl pinning)对服务端身份进行验证。

如果是像下面这种由服务端主动断开的连接,则是服务端验证会验证客户端,也就是常说的双向认证。 这种情况需要从程序内提取出私钥。关于https的认证过程可以参考笔者之前的文章,这里不做过多讨论。

破解https验证

使用命令adb shell dumpsys activity | grep Focuse查看当前activity

1
2
mFocusedActivity: ActivityRecord{12d334a u0 com.eg.android.AlipayGphone/com.alipay.mobile.nebulax.integration.mpaas.activity.NebulaActivity$Lite1 t857}
mFocusedStack=ActivityStack{15c8721 stackId=1, 6 tasks} mLastFocusedStack=ActivityStack{15c8721 stackId=1, 6 tasks}

查看支付宝的manifest.xml,找到这个activity,发现它是运行在支付宝其他进程中的,所以我们用frida时一定要找对小程序的进程,这里是com.eg.android.AlipayGphone:lite1,否则就白忙活了。

分析过程

这里使用frida进行hook测试。代码已上传到github

因为笔者已经安装了burp、charles的根证书到系统,而且安卓系统是6.0的,所以猜测抓包失败的点有两个:

  1. 使用了HostnameVerifier验证了代理的证书与网站域名不符
  2. 使用了证书绑定

笔者先大概了解了下小程序的开发。发现支付宝小程序跟微信小程序类似,都是用js开发,所以猜测是运行在WebView引擎下的。WebView的证书验证方式是创建WebViewClient,重写onReceivedSslError方法里实现,例如

1
2
3
4
5
6
7
8
webview.setWebViewClient(new WebViewClient() {
@Override
public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
if (customCertificateCheck(error.getCertificate())) {
handler.proceed(); // 如果证书一致,忽略错误
}
}
}

搜索grep -Hrn '.method public onReceivedSslError' smali* | grep nebula,挨个分析查找到的类,定位到’com.alipay.mobile.nebulacore.web.H5WebViewClient’

所以使用如下代码尝试

1
2
3
4
5
6
7
8
// webview 证书绑定
const H5WebViewClient = Java.use('com.alipay.mobile.nebulacore.web.H5WebViewClient');
const SslErrorHandler = Java.use("android.webkit.SslErrorHandler");
H5WebViewClient.onReceivedSslError.implementation = function(webview, sslHandler, sslError){
console.log('H5WebViewClient onReceivedSslError called, proceed');
var handler = Java.cast(sslHandler, SslErrorHandler);
handler.proceed();
};

执行后发现没有效果,这个方法没有被调用。但在里面发现了log的打印方法。所以试着用如下代码打开日志

1
2
3
4
5
6
// h5小程序 log
const H5Log = Java.use("com.alipay.mobile.nebula.util.H5Log");
H5Log.d.overload("java.lang.String", "java.lang.String").implementation = function (tag, msg) {
console.log("debug: [", tag, "] - ", msg);

};

运行后继续抓包,请求失败后看到有日志

其中有如下信息

1
debug: [ H5HttpCallback ] -  onFailed 2 javax.net.ssl.SSLException: hostname in certificate didn't match: <app.gsxt.gov.cn> != <default.ssl.cdn.jiasule.com> OR <default.ssl.cdn.jiasule.com>

可以确定是证书的域名验证不通过。

  1. 查找’H5HttpCallback’,找到类’com.alipay.mobile.nebulaappproxy.ipc.handler.H5HttpCallback’,发现这条日志是由类的’onFailed’方法打印的,所以继续查找这个方法的调用。
  2. 查找’H5HttpCallback;->onFailed’定位到’com.alipay.mobile.nebulaappproxy.ipc.H5EventHandlerServiceImpl’类,在这个类的
    ‘public H5HttpRequestResult httpRequest’方法中通过’Future enqueue = new H5NetworkManager(H5Utils.getContext()).enqueue(h5HttpUrlRequest);’异步发起http请求。
  3. 继续跟进,得到整个http的执行流程,这个地方比较绕。最后发现httpclient对象是在’com.alipay.mobile.common.transport.http.inner.CoreHttpManager#getHttpClient’方法创建的,返回对象是’com.alipay.mobile.common.transport.http.AndroidHttpClient’。查看这个类的构造函数

    查看ZSSLSocketFactory
1
private X509HostnameVerifier a = org.apache.http.conn.ssl.SSLSocketFactory.BROWSER_COMPATIBLE_HOSTNAME_VERIFIER;

此时我们成功找到HostnameVerifier了,它继承自AbstractVerifier,所以注入以下代码

1
2
3
4
5
6
// disable ssl hostname check
const AbstractVerifier = Java.use("org.apache.http.conn.ssl.AbstractVerifier");
AbstractVerifier.verify.overload('java.lang.String', '[Ljava.lang.String;', '[Ljava.lang.String;', 'boolean').implementation=function(a,b,c,d){
console.log('HostnameVerifier wants to verify ', a, ' disabled');
return;
};

搞定

取得小程序源码

上面得到的协议中还没有加密参数,但如果有参数怎么办呢?支付宝小程序是用js写的,理论上只要拿到其源码就可以得到加密算法。所以下面介绍如何找到小程序的源码文件。

在抓包时发现如下请求

请求参数中带有tinyAppId,这个值应该是小程序的惟一id。然后进入adb shell,切换su后进入支付宝应用的数据目录
/data/data/com.eg.android.AlipayGphone, 在files/nebulaInstallApps/目录下存储了所有加载过的小程序(猜测小程序在支付宝部门里的项目名叫nebula)

文件夹名即小程序的tinyAppId值。目录结构如下

tar压缩文件即为小程序的源码包。这个包没有加密,使用adb pull拿出来,直接解压即可。如下是解压后的文件结构

解压后对js格式化下代码,就可以分析了。js代码没有混淆,条理很清晰。