目标APP 采用了很多开源库进行改写,怀疑下一步就是拿开源ssl.so在so里进行自写TCP发包了
protobuffer 解析是 io.protobuf 自写魔改了(字段处理)
压缩使用了两套方案, Zstd 或者 GZip ,Zstd 是 facebook开源的压缩库,采用固定等级三
token生成是随机数+魔改MD5签名(这个上两篇文章已经写了)
加解密有两套 XOR 或 AEScbc
每个库都有原始库,就是他用啥魔改的,找到对应的原始库就很好解决了
抓包抓不到包,甚至失败的链接都没有那就排除检测,直接怀疑TCP
通过hook SocketInputStream、SocketOutputStream方法打印堆栈就可以定位到目标方法

requestData 通过task.getRequestData()获取
那就看这个 this.requestData; 赋值

这里很明显可以看到有一个很清晰的 buileRequest.totelData 赋值,十有八九就是这个了
看到这个方法,第一反应就是,buileRequest,那就肯定还有个buileResponse
搜一下,果然

理论上建议大家先从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 点过去就能看到
直接扣过来就好了
压缩的这个getCompressProvider().compress
按照正常流程,肯定要先看一下 getCompressProvider 返回的哪个对象,尝试跟了一下没跟到,所以就直接看compress,点进方法
这里可以看到是一个接口,直接复制 byte[] compress(byte[] bArr),去搜索看看谁实现了这个方法
结果很明显就能看到
实际上是调用了 CTZ.a 方法

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

这个 zstd 就是facebook开源的一个压缩库,第二个参数3明显就是写死的压缩等级了,而且经过测试压缩没有经过魔改
当然有些请求走的其他加密流程,压缩换成 Gzip 或者加解密换成 AEScbc ,区别不大
(别问为什么不直接用 blacboxprotubuf,因为领导要看每个字段对应的 tag,而且请求和字段多了后,一直用 blackbloxprotubuff 很不方便)
加解密和压缩的流程基本都没问题了,现在问题就是 buileRequestHeadV6
方法做了什么

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

这里的Serialize.writeMessage 就是将对象序列化成字节数组
点进去这个方法之后,就能看到
就是这个方法,让我感觉一阵的熟悉,那就是我之前搞过某个航司APP用的开源库 io.protostuff,用法跟这里几乎一模一样
所以我尝试使用老方法,把 request 对象完整的抠出来,然后传给io.protostuff
这里的ProtoBufferField 注解中 tag 参数,很明显就能看出来就是 io,protostuff
但是我把对象构建完传进去后发现,程序报错,对象中存在没有tag的属性存在
哦豁,这些都不是tag,那为啥他没报错呢?
那就看一下 io,protostuff 的源代码,找到 writeMessage 方法,这里可以发现调用的实际上还是 toByteArray方法,而且看代码,一样的getclass,一样的模板类

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

但是在目标APP源码中再跟一步就会发现 不一样的地方
在标准的 Io.protostuff 中这里就只是一个循环然后调用方法,但是这里对方法标签做了很多判断,根据不同的判断结果调用不同的方法
所以如果对这个库比较熟悉,直接在这个库的源码中改就行了,然后重新打包jar包调用
我这里为了防止后面有很多需要扣代码的地方有互相依赖的情况,直接把这个库全部抠出来了

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

在扣代码的过程中可以发现一个很难受的地方,它的代码复制粘贴出来会有很多以下性能监控的垃圾代码
这里我写了个代码用于删除以上垃圾代码,每次复制完跑一遍就行了(我感觉这个东西jadx去处理应该更方便更快,但是懒得研究了,也没看到对应的资料)

OK,流程结束,魔改MD5上两篇已经写过了,接下来就是一个sign和一个AES
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;
}
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;
}
public byte[] getRequestData() {
return
this.requestData;
}
public byte[] getRequestData() {
return
this.requestData;
}
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;
}
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;
}
if
(xxx) {
compress2
=
GzipUtil.compress(buileRequestHeadV6, bArr);
i12
=
3
;
}
else
{
compress2
=
CommConfig.getInstance().getCompressProvider().compress(byteMerge(buileRequestHeadV6, bArr));
}
Encode
=
SOTPEncodeUtil.encodeByXor(compress2);
i13
=
i12;
if
(xxx) {
compress2
=
GzipUtil.compress(buileRequestHeadV6, bArr);
i12
=
3
;
}
else
{
compress2
=
CommConfig.getInstance().getCompressProvider().compress(byteMerge(buileRequestHeadV6, bArr));
}
Encode
=
SOTPEncodeUtil.encodeByXor(compress2);
i13
=
i12;
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;
}
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;
}
public interface SOTPCompressProvider {
byte[] compress(byte[] bArr) throws Exception;
byte[] uncompress(byte[] bArr) throws Exception;
}
public interface SOTPCompressProvider {
byte[] compress(byte[] bArr) throws Exception;
byte[] uncompress(byte[] bArr) throws Exception;
}
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;
}
public byte[] compress(byte[] bArr) {
if
(bArr
=
=
null || bArr.length <
=
0
) {
[注意]看雪招聘,专注安全领域的专业人才平台!