对某旅行APP的逆向分析

华盟原创文章投稿奖励计划

目标APP 采用了很多开源库进行改写,怀疑下一步就是拿开源ssl.so在so里进行自写TCP发包了

protobuffer 解析是 io.protobuf 自写魔改了(字段处理)

压缩使用了两套方案, Zstd 或者 GZip ,Zstd 是 facebook开源的压缩库,采用固定等级三

token生成是随机数+魔改MD5签名(这个上两篇文章已经写了)

加解密有两套 XOR 或 AEScbc

每个库都有原始库,就是他用啥魔改的,找到对应的原始库就很好解决了

TCP分析流程

抓包抓不到包,甚至失败的链接都没有那就排除检测,直接怀疑TCP

Java.use("java.net.SocketInputStream").socketRead0.overload('java.io.FileDescriptor''[B''int''int''int').implementation = function(fd, bytearry, offset, byteCount, timeout) {    varresult = this.socketRead0(fd, bytearry, offset, byteCount, timeout);    let base64_str = base64_enc(bytearry);    if(base64_str.length < 1000 || base64_str.indexOf("AAAAAAAAAAA") > -1) {        returnresult;    }    showStacks();    console.log("get data: "+ base64_str)    returnresult;}Java.use("java.net.SocketOutputStream").socketWrite0.overload('java.io.FileDescriptor''[B''int''int').implementation = function(fd, bytearry, offset, byteCount) {    varresult = this.socketWrite0(fd, bytearry, offset, byteCount);    showStacks();    console.log("\nsend data: "+ base64_enc(bytearry))    returnresult;}

通过hook SocketInputStream、SocketOutputStream方法打印堆栈就可以定位到目标方法

自动草稿

自动草稿

搜一下,果然

自动草稿

理论上建议大家先从buileResponse来,因为 buileRequest 有可能会存在随机数等情况造成同一个值加密后的结果不一致,而解密响应体,必然不可能随机,只有解成功和解失败    

这里 buileRequest 的坑比较多,就从这里开始吧

这里可以看到 buileRequest 有三种情况的返回,挨个hook 就会发现基本走的都是 getRequestDataBeanV6

自动草稿

V6 这里就很明显可以看到返回值,以及 上文写的他发送的实际是 返回对象的totelData属性

自动草稿

 这里就到了加密流程,ListUtil.combineByteArr 作用是连接合并两个数组    

加密过程分析

那么 buileRequestHeadOfPrefixV6 就是头信息,传入的是encode长度+6 和 加密方式代表的数字

自动草稿

里面也没做啥特殊的,就是把传进来的两个数字给转换成byte[],SerializeWriter这里先不看,后面会一起解决

那现在唯一的问题就是 Encode 了

从上面V6的代码中不难看到encode 的产生方式有三种,我们不好确定用的是哪个,这里上面分析了buileRequestHeadOfPrefixV6 传的第二个参数就是加密方式,所以hook一下这个入参就好了

发现进来的值是5,int i12 = 5; 而且中间值没有改变

那就确定加密方式用的 encodeByXor,压缩用的 getCompressProvider().compress

自动草稿

 encodeByXor 点过去就能看到

public static byte[] encodeByXor(byte[] bArr) {    if(bArr ==null || bArr.length < 1) {        returnbArr;    }    byte[] bArr2 =new byte[bArr.length];    for(inti12 =0; i12 < bArr.length; i12++) {        bArr2[i12] =(byte) (bArr[i12] ^ -1);    }    returnbArr2;

压缩的这个getCompressProvider().compress

按照正常流程,肯定要先看一下 getCompressProvider 返回的哪个对象,尝试跟了一下没跟到,所以就直接看compress,点进方法



1

2

3

4

5

public interface SOTPCompressProvider {

        byte[] compress(byte[] bArr) throws Exception;

        byte[] uncompress(byte[] bArr) throws Exception;

    }

 这里可以看到是一个接口,直接复制 byte[] compress(byte[] bArr),去搜索看看谁实现了这个方法

自动草稿

实际上是调用了 CTZ.a 方法  自动草稿

 CTZ 搜了半天没有结果,但是我在搜索这个注释的zstdBytes的时候发现了惊喜

自动草稿

这个 zstd 就是facebook开源的一个压缩库,第二个参数3明显就是写死的压缩等级了,而且经过测试压缩没有经过魔改

当然有些请求走的其他加密流程,压缩换成 Gzip 或者加解密换成 AEScbc ,区别不大

protobuff反序列化分析

  (别问为什么不直接用 blacboxprotubuf,因为领导要看每个字段对应的 tag,而且请求和字段多了后,一直用 blackbloxprotubuff 很不方便)

 加解密和压缩的流程基本都没问题了,现在问题就是 buileRequestHeadV6 方法做了什么    

自动草稿

clientToken 就是前面两篇文章说过的魔改MD5对随机数进行签名,这里就不进行赘述了

自动草稿

这里的Serialize.writeMessage 就是将对象序列化成字节数组

点进去这个方法之后,就能看到

自动草稿

就是这个方法,让我感觉一阵的熟悉,那就是我之前搞过某个航司APP用的开源库io.protostuff,用法跟这里几乎一模一样

所以我尝试使用老方法,把 request 对象完整的抠出来,然后传给io.protostuff  

public classRequestHead extends BusinessBean {
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =14type=ProtoBufferField.Datatype.STRING)    public String appId;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =12type=ProtoBufferField.Datatype.STRING)    public String authToken;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =5type=ProtoBufferField.Datatype.STRING)    public String clientId;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =6type=ProtoBufferField.Datatype.STRING)    public String clientToken;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =7type=ProtoBufferField.Datatype.STRING)    public String clientVersion;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =9type=ProtoBufferField.Datatype.STRING)    public String exSourceId;
    @ProtoBufferField(label =ProtoBufferField.Label.REPEATED, tag =13type=ProtoBufferField.Datatype.MESSAGE)    public ArrayList<Extention> extentionList;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =3type=ProtoBufferField.Datatype.STRING)    public String language;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =11type=ProtoBufferField.Datatype.STRING)    public String messageNumber;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =1type=ProtoBufferField.Datatype.ENUM)    public SerializeCode serializeCode;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =10type=ProtoBufferField.Datatype.STRING)    public String serviceCode;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =8type=ProtoBufferField.Datatype.STRING)    public String sourceId;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =2type=ProtoBufferField.Datatype.STRING)    public String systemCode;
    @ProtoBufferField(label =ProtoBufferField.Label.OPTIONAL, tag =4type=ProtoBufferField.Datatype.STRING)    public String userId;
    public RequestHead() {        AppMethodBeat.i(62638);        this.extentionList =new ArrayList<>();        AppMethodBeat.o(62638);    }}自动草稿

这里的ProtoBufferField 注解中 tag 参数,很明显就能看出来就是 io,protostuff

但是我把对象构建完传进去后发现,程序报错,对象中存在没有tag的属性存在

public classBusinessBean implements Serializable, Cloneable {    public static ChangeQuickRedirect changeQuickRedirect =null;
    @NO_PERSISTENCE    private static final longserialVersionUID =1;    private String cacheKey;
    @NO_PERSISTENCE    protected byte[] dataBody;        public intpk;
    @NO_PERSISTENCE    private longprocessingDataBodyTime;
    @NO_PERSISTENCE    protected String realServiceCode ="";
    @NO_PERSISTENCE    protected String charsetName ="";
    @NO_PERSISTENCE    protected String jsonBody ="";    public intcachedSerializedSize =-1;}

哦豁,这些都不是tag,那为啥他没报错呢?

那就看一下 io,protostuff 的源代码,找到 writeMessage 方法,这里可以发现调用的实际上还是 toByteArray方法,而且看代码,一样的getclass,一样的模板类

自动草稿

然后跟一下会发现调用到了 writeto,一个for循环,遍历所有tag

自动草稿

但是在目标APP源码中再跟一步就会发现 不一样的地方

<M extends CtripBusinessBean> void u(M m12, ProtoBufferOutput protoBufferOutput) {    for(FieldInfo fieldInfo : f()) {        Objecte12 =e(m12, fieldInfo);        if(e12 !=null) {            inti12 =fieldInfo.f58010a;            ProtoBufferField.Datatype datatype =fieldInfo.f58012c;            ProtoBufferField.Label label =fieldInfo.d;            if(!label.isRepeated()) {                z(protoBufferOutput, i12, e12, datatype);            elseif(label.isPacked()) {                x(protoBufferOutput, (List) e12, i12, datatype);            else{                y(protoBufferOutput, (List) e12, i12, datatype);            }        }    }}在标准的 Io.protostuff 中这里就只是一个循环然后调用方法,但是这里对方法标签做了很多判断,根据不同的判断结果调用不同的方法

所以如果对这个库比较熟悉,直接在这个库的源码中改就行了,然后重新打包jar包调用

 我这里为了防止后面有很多需要扣代码的地方有互相依赖的情况,直接把这个库全部抠出来了    

自动草稿

代码量也不是很大,有点耐心很快就可以扣完

自动草稿

在扣代码的过程中可以发现一个很难受的地方,它的代码复制粘贴出来会有很多以下性能监控的垃圾代码

自动草稿自动草稿

流程结束,魔改MD5上两篇已经写过了,接下来就是一个sign和一个AES

文章来源:看雪论坛     


黑白之道发布、转载的文章中所涉及的技术、思路和工具仅供以安全为目的的学习交流使用,任何人不得将其用于非法用途及盈利等目的,否则后果自行承担!

如侵权请私聊我们删文


END

本文来源看雪论坛 ,经授权后由华盟君发布,观点不代表华盟网的立场,转载请联系原作者。

发表回复