服务器校验客户端证书分析及代码

当前app安全越来越受到重视,很多app采取了HTTPS的协议,但是一般app都不会对证书进行校验,一些app只是客户端对服务端证书进行了强校验,也就是通常呢,抓包的时候app提示无网络连接或者网络连接错误,这种情况就是app发现证书是伪造的,并不是服务器的证书,因此中断了通讯。这个时候可以通过逆向app,找到校验的代码,过掉即可,偷懒的话可以尝试justTustMe模块来过掉。今天我们的重点是如何在服务器强校验客户端证书的情况下,抓到数据包,相关内容在网上很少见,以下仅仅抛砖引玉做个简单的示范,不涉及高级逆向。

可能大家遇到过这种情况,在抓包的时候,

fiddler提示:服务器请求客户端证书。

相关访问返回内容为:

<html>

<head><title>400 No required SSL certificate was sent</title></head>

<body bgcolor="white">

<center><h1>400 Bad Request</h1></center>

<center>No required SSL certificate was sent</center>

<hr><center>nginx</center>

</body>

</html>

这种情况是由于客户端内置了客户端证书。常规条件下,客户端在与服务器进行通讯的时候,会将自己的证书发送给服务器,服务器校验证书的合法性,当我们采用fiddler代理的时候,默认使用的是fiddler的证书,当然对方服务器不认可fiddler的证书,所以会提示错误。

这种情况怎么办?

这儿我们以翼健康app为例进行说明。

使用的工具:

Fiddler,抓数据包

JEB,反编译app

安装好xposed的雷电模拟器

Eclipse,编写xposed模块

...

以上工具均可以百度到,所以这儿不放链接了。

 

首先,随便抓一条数据包:

可以看到,对于数据包的内容,我们是可以清楚的看到的,但是服务器返回错误。

 

我们直接解压app,在./assets目录下,发现客户端证书client.pfx

当我们进行双击安装证书的时候发现需要密码,这个时候没有办法,我们只能尝试逆向app。

 

我们在manifest文件中可以看到,app包名是com.gdhbgh.activity,入口是com.telecom.vhealth.ui.activities.WelcomeActivity,那么在逆向app的时候,要重点关注telecom下的http的相关类或者方法,这个是常规思路,证书操作肯定会和通讯类放在一起。

 

使用JEB逆向app,果然发现了有com.telecom.vhealth.http.utils.HttpUtils类。这儿给大家解释一下我为什么第一时间找到这个类,首先根据入口定位到了com.telecom.vhealth。App一般代码都是模块化,集成化。所以我们需要第一时间关注相关http包,在包里面我们重点关注工具类或者服务类。当然了,如果比较难找,我们也可以采取别的思路,比如直接搜索证书的名字,定位关键位置,或者搜索网页访问的地址,我们定位一下http访问的类,在进入到类里面看看是怎么样设置证书的等等,方法有很多,大家需要发挥一下,不要拘泥于一种方法。

言归正传,运气很好,在HttpUtils类里面我们发现了getSocketFactory获取连接工厂这个函数。不管是从方法名还是具体的代码,我们都基本确定关键函数就在这儿了。

摘录其中关键语句给出注释:

InputStream v3 = arg7.getAssets().open("client.pfx");  // 读入证书文件

KeyStore v4 = KeyStore.getInstance("PKCS12");  // Key存储器初始化

v4.load(v3, v0_1);  // Key存储器载入客户端证书文件,参数一为证书,参数二为密码

我们向上找找v0_1是从哪儿蹦出来的,发现关键语句:

char[] v0_1 = EncryptUtils.getHttpSign(arg7).toCharArray();

进入EncryptUtils类,发现是两个native函数,相关代码在so里面,

分析so还是比较麻烦的,直接省略分析so文件,我们使用xposed来hook这个参数。这儿呢,我选择hook了KeyStore的load的方法。原因呢,我个人认为容错性比较高,省的来回写代码。

相关xposed的入门教程请大家百度。Xposed还是很容易入门以及上手的。

贴出代码:

运行模块,成功拿到证书密码:vhealth2016

拿到密码,我们转到电脑端,首先双击client.pfx,将证书文件安装进电脑,然后本地在证书管理里面将证书导出为base64编码的cer证书文件或者百度证书转换工具,将pfx证书转换为pem文件,pem文件包含了公钥和私钥,我们手动提取出公钥数据新建一个cer文件就ok,觉得不够严谨可以使用openssl命令来转换。Cer文件是用于存储公钥证书的文件格式。

 

我们把生成的ClientCertificate.cer文件放入fiddler的文件目录下。然后重启fiddler,就可以发现成功抓包:

对于数据中的sign,我们也可以顺便分析一下:

以下是跟踪路径,自己分析就好,我列的代码可直接忽略:

((Map)v4).put("sign", HttpUtils.getHttpSign());

接下来:

public static String getHttpSign() {

        UnifiedUserInfo v1 = c.c();

        String v0 = "";

        if(v1 != null) {

            v0 = v1.getPhoneNumber();

        }



        return HttpUtils.getSign("|", new String[]{String.valueOf(System.currentTimeMillis()), v0});

}

接下来:

public static String getSign(String arg3, String[] arg4) {

        StringBuilder v1 = new StringBuilder();

        int v0;

        for(v0 = 0; v0 < arg4.length; ++v0) {

            v1.append(arg4[v0]);

            if(!TextUtils.isEmpty(((CharSequence)arg3)) && v0 < arg4.length - 1) {

                v1.append(arg3);

            }

        }



        return HttpUtils.encryptByPk(v1.toString());

}



最后定位到:

private static String encryptByPk(String arg5) {

        String v0_2;

        String v1 = null;

        try {

            Object v0_1 = new ObjectInputStream(YjkApplication.getContext().getAssets().open("public.ppk")).readObject();

            byte[] v2 = arg5.getBytes("utf-8");

            Cipher v3 = Cipher.getInstance("RSA/ECB/PKCS1Padding");

            v3.init(1, ((Key)v0_1));

            v0_2 = f.a(v3.doFinal(v2));

        }

        catch(Exception v0) {

            s.a(((Throwable)v0));

            v0_2 = v1;

        }



        return v0_2;

}

发现sign是采用了assets目录下的public.ppk文件作为公钥,RSA/ECB/PKCS1Padding为加密方式。我们顺便在xposed代码hook一下相关地方,瞅瞅加密前数据是什么,加密后的数据是什么,密钥是什么。

贴出完整xposed代码:

import java.io.InputStream;



import de.robv.android.xposed.IXposedHookLoadPackage;

import de.robv.android.xposed.XC_MethodHook;

import de.robv.android.xposed.XposedBridge;

import de.robv.android.xposed.XposedHelpers;

import de.robv.android.xposed.callbacks.XC_LoadPackage;



import java.security.Key;



public class Hook implements IXposedHookLoadPackage {



    private static String package_name = "com.gdhbgh.activity";



    //byte[]转16进制

    final protected static char[] hexArray = "0123456789ABCDEF".toCharArray();

    public static String bytesToHex(byte[] bytes) {

        char[] hexChars = new char[bytes.length * 2];

        for ( int j = 0; j < bytes.length; j++ ) {

            int v = bytes[j] & 0xFF;

            hexChars[j * 2] = hexArray[v >>> 4];

            hexChars[j * 2 + 1] = hexArray[v & 0x0F];

        }

        return new String(hexChars);

    }

    

    @Override

    public void handleLoadPackage(XC_LoadPackage.LoadPackageParam loadPackageParam) throws Throwable {



        if (!loadPackageParam.packageName.equals(package_name)) {

         return;

        }

        XposedBridge.log(loadPackageParam.packageName +" -> load");

        

        //通过hook密钥加载函数来得到证书密钥

        final Class<?> clazz = XposedHelpers.findClass("java.security.KeyStore", loadPackageParam.classLoader);

        XposedHelpers.findAndHookMethod(clazz,"load", InputStream.class,char[].class,new XC_MethodHook(){

         @Override

         protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

         if((char[])param.args[1]==null) return;

         String s = new String((char[])param.args[1]);

         if(s==null) return;

         XposedBridge.log("java.security.KeyStore.load param1: " + s);

            }

        });

        

        //hook加密方法瞅瞅加密前数据是个啥玩意儿

        XposedHelpers.findAndHookMethod("com.telecom.vhealth.http.utils.HttpUtils",loadPackageParam.classLoader,"encryptByPk", String.class,new XC_MethodHook(){

         @Override

         protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

         XposedBridge.log("com.telecom.vhealth.http.utils.HttpUtils.encryptByPk param0:" + (String)param.args[0]);

            }

         @Override

         protected void afterHookedMethod(MethodHookParam param) throws Throwable {

         XposedBridge.log("com.telecom.vhealth.http.utils.HttpUtils.encryptByPk result: " + (String)param.getResult());

            }

        });

        

        //hook一下RSA的公钥数据

        XposedHelpers.findAndHookMethod("javax.crypto.Cipher",loadPackageParam.classLoader,"init", int.class,Key.class,new XC_MethodHook(){

         @Override

         protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

         Key key1=(Key)param.args[1];

         byte[] keyBytes=key1.getEncoded();

         XposedBridge.log("javax.crypto.Cipher.init param1:" + bytesToHex(keyBytes));

            }

        });

        

        //hook一下加密前后的数据

        XposedHelpers.findAndHookMethod("javax.crypto.Cipher",loadPackageParam.classLoader,"doFinal", byte[].class,new XC_MethodHook(){

         @Override

         protected void beforeHookedMethod(MethodHookParam param) throws Throwable {

         byte[] data=(byte[])param.args[0];

         XposedBridge.log("javax.crypto.Cipher.doFinal param0:" + bytesToHex(data));

            }

         @Override

         protected void afterHookedMethod(MethodHookParam param) throws Throwable {

         byte[] data=(byte[])param.getResult();

         XposedBridge.log("javax.crypto.Cipher.doFinal result:" + bytesToHex(data));

            }

        });

        

    }

}

 

分析完毕!

完整的项目文件,样本文件,相关工具请大家自己动手完成。

  • 8
    点赞
  • 20
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值