旅行APP流程分析(TCP+protobuf)

admin 2025年2月18日20:51:36评论13 views字数 7339阅读24分27秒阅读模式

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

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

旅行APP流程分析(TCP+protobuf)

requestData 通过task.getRequestData()获取

    public byte[] getRequestData() {
return this.requestData;
}

那就看这个 this.requestData; 赋值

旅行APP流程分析(TCP+protobuf)

这里很明显可以看到有一个很清晰的 buileRequest.totelData 赋值,十有八九就是这个了

看到这个方法,第一反应就是,buileRequest,那就肯定还有个buileResponse

搜一下,果然:

旅行APP流程分析(TCP+protobuf)

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

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

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

旅行APP流程分析(TCP+protobuf)

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

旅行APP流程分析(TCP+protobuf)

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

加密过程

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

    private static byte[] buileRequestHeadOfPrefixV6(int i12, int i13) {

SerializeWriter serializeWriter = new SerializeWriter(14, Serialize.charsetName_ASCII);
serializeWriter.writeInt(i12, 8);
serializeWriter.writeInt(i13, 4);
serializeWriter.writeInt(6, 2);
byte[] byteArr = serializeWriter.toByteArr();
AppMethodBeat.o(59015);
return byteArr;
}

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

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

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

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

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

            if (xxx) {
compress2 = GzipUtil.compress(buileRequestHeadV6, bArr);
i12 = 3;
} else {
compress2 = CommConfig.getInstance().getCompressProvider().compress(byteMerge(buileRequestHeadV6, bArr));
}
Encode = SOTPEncodeUtil.encodeByXor(compress2);
i13 = i12;

encodeByXor 点过去就能看到

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

直接扣过来就好了

压缩的这个getCompressProvider().compress

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

public interface SOTPCompressProvider {
byte[] compress(byte[] bArr) throws Exception;

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

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

结果很明显就能看到

            public byte[] compress(byte[] bArr) {
if (bArr == null || bArr.length <= 0) {
byte[] bArr2 = new byte[0];
return bArr2;
}
byte[] a12 = CTZ.a(bArr);
return a12;
}

实际上是调用了 CTZ.a 方法

旅行APP流程分析(TCP+protobuf)

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

旅行APP流程分析(TCP+protobuf)

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

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

protobuff 序列化过程

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

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

旅行APP流程分析(TCP+protobuf)

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

旅行APP流程分析(TCP+protobuf)

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

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

    public <M> byte[] toByteArray(M m12) {
Preconditions.b(m12, "message");
byte[] s12 = b(m12.getClass()).s(m12);
return s12;
}

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

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

public class RequestHead extends BusinessBean {

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 14, type = ProtoBufferField.Datatype.STRING)
public String appId;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 12, type = ProtoBufferField.Datatype.STRING)
public String authToken;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 5, type = ProtoBufferField.Datatype.STRING)
public String clientId;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 6, type = ProtoBufferField.Datatype.STRING)
public String clientToken;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 7, type = ProtoBufferField.Datatype.STRING)
public String clientVersion;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 9, type = ProtoBufferField.Datatype.STRING)
public String exSourceId;

@ProtoBufferField(label = ProtoBufferField.Label.REPEATED, tag = 13, type = ProtoBufferField.Datatype.MESSAGE)
public ArrayList<Extention> extentionList;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 3, type = ProtoBufferField.Datatype.STRING)
public String language;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 11, type = ProtoBufferField.Datatype.STRING)
public String messageNumber;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 1, type = ProtoBufferField.Datatype.ENUM)
public SerializeCode serializeCode;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 10, type = ProtoBufferField.Datatype.STRING)
public String serviceCode;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 8, type = ProtoBufferField.Datatype.STRING)
public String sourceId;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 2, type = ProtoBufferField.Datatype.STRING)
public String systemCode;

@ProtoBufferField(label = ProtoBufferField.Label.OPTIONAL, tag = 4, type = ProtoBufferField.Datatype.STRING)
public String userId;

public RequestHead() {
AppMethodBeat.i(62638);
this.extentionList = new ArrayList<>();
AppMethodBeat.o(62638);
}
}

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

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

public class BusinessBean implements Serializable, Cloneable {
public static ChangeQuickRedirect changeQuickRedirect = null;

@NO_PERSISTENCE
private static final long serialVersionUID = 1;
private String cacheKey;

@NO_PERSISTENCE
protected byte[] dataBody;

public int pk;

@NO_PERSISTENCE
private long processingDataBodyTime;

@NO_PERSISTENCE
protected String realServiceCode = "";

@NO_PERSISTENCE
protected String charsetName = "";

@NO_PERSISTENCE
protected String jsonBody = "";
public int cachedSerializedSize = -1;
}

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

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

旅行APP流程分析(TCP+protobuf)

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

旅行APP流程分析(TCP+protobuf)

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

    <M extends CtripBusinessBean> void u(M m12, ProtoBufferOutput protoBufferOutput) {
for (FieldInfo fieldInfo : f()) {
Object e12 = e(m12, fieldInfo);
if (e12 != null) {
int i12 = fieldInfo.f58010a;
ProtoBufferField.Datatype datatype = fieldInfo.f58012c;
ProtoBufferField.Label label = fieldInfo.d;
if (!label.isRepeated()) {
z(protoBufferOutput, i12, e12, datatype);
} else if (label.isPacked()) {
x(protoBufferOutput, (List) e12, i12, datatype);
} else {
y(protoBufferOutput, (List) e12, i12, datatype);
}
}
}
}

在标准的 Io.protostuff 中这里就只是一个循环然后调用方法,但是这里对方法标签做了很多判断,根据不同的判断结果调用不同的方法

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

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

旅行APP流程分析(TCP+protobuf)

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

旅行APP流程分析(TCP+protobuf)

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

        if (PatchProxy.proxy(new Object[]{m12, new Integer(i12), obj}, this, changeQuickRedirect, false, 11083, new Class[]{CtripBusinessBean.class, Integer.TYPE, Object.class}).isSupported) {
return;
}
AppMethodBeat.i(6074);

这里我写了个代码用于删除以上垃圾代码,每次复制完跑一遍就行了(我感觉这个东西jadx去处理应该更方便更快,但是懒得研究了,也没看到对应的资料)

旅行APP流程分析(TCP+protobuf)

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

旅行APP流程分析(TCP+protobuf)

看雪ID:Lychow

https://bbs.kanxue.com/user-home-972841.htm

*本文为看雪论坛优秀文章,由 Lychow 原创,转载请注明来自看雪社区
旅行APP流程分析(TCP+protobuf)

原文始发于微信公众号(看雪学苑):旅行APP流程分析(TCP+protobuf)

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年2月18日20:51:36
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   旅行APP流程分析(TCP+protobuf)https://cn-sec.com/archives/3757813.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息