查看原文
其他

QQ 浏览器 JecStruct 协议

chpeagle 看雪学院 2019-05-25


发现自己写的文章更多是偏向于理论,现在就写点实战的文章吧。

 

QQ浏览器返回和发送的数据都是通过JceStruct协议来传输的,当然这个传输过程是经过层层加密的.解密后的内容就是JceStruct协议。

 

JceStruct 协议和Protobuf 协议有点像,也是采用Key-Value的形式。但感觉比Protobuf更简洁一点,也更简单一点。







JceStruct 的内容组织形式





Key



我们先来看看Key是怎么组织的。

 

描述Key的类为:HeadData



public static class HeadData {
public int tag;
public byte type;
}



可以看到Key被分为了两部分,一部分是tag,一部分是type。

 

读取Key的函数为



public static int readHead(HeadData headData, ByteBuffer byteBuffer) {
byte b = byteBuffer.get();//获取一个byte
headData.type = (byte) (b & 0xF);//低4位为类型
headData.tag = (b & 0xF0) >> 4;//高4位为tag,
if (headData.tag != 0xF) {//如果tag为0xF 则下一个字段为tag
return 1;
}
headData.tag = byteBuffer.get() & 0xFF;
return 2;
}



函数功能非常简单

①字节低4位为type 表示序号对应的类型

②字节高4位为tag 表示序号

③如果tag为0xF,下一个字节为tag


总结一下:

①Key由tag和type组成

②Key为1个字节或两个字节.两个字节时第二个字节为tag,否则第一个字节的高4位为tag,type始终为第一个字节的低4位。

③type最多有16种类型,tag最大为255。



type



根据分析type的类型如下:




Value解析



解析标准的Java类型


根据不同的类型对Value的解析就会有所不同。

 

其中byte,bool,Short,Int,Long,  Float,Double的解析都是一样的,只不过读取的字节数不同而已.

 

我们以Int为例:



public int read(int i, int i2, boolean z) {
if (skipToTag(i2)) {
HeadData headData = new HeadData();
readHead(headData);
switch (headData.type) {
case (byte0:
return this.bs.get();
case (byte1:
return this.bs.getShort();
case (byte2:
return this.bs.getInt();//就直接geInt就Ok
case (byte12:
return 0;
default:
throw new JceDecodeException("type mismatch.");
}
else if (!z) {
return i;
else {
throw new JceDecodeException("require field not exist.");
}
}



可以看到非常简单,读到指定类型,那么getXXX就可以了,这是Java的标准类型。


解析String


我们先看一下解析String的Java代码.



public String readString(int i, boolean z) {
if (skipToTag(i)) {
HeadData headData = new HeadData();
readHead(headData);
int i2;
byte[] bArr;
switch (headData.type) {
case (byte6:
i2 = this.bs.get();
if (i2 < 0) {
i2 += 256;
}
bArr = new byte[i2];
this.bs.get(bArr);
try {
return new String(bArr, this.sServerEncoding);
catch (UnsupportedEncodingException e) {
return new String(bArr);
}
case (byte7:
i2 = this.bs.getInt();
if (i2 > 104857600 || i2 < 0 || i2 > this.bs.capacity()) {
throw new JceDecodeException("String too long: " + i2);
}
bArr = new byte[i2];
this.bs.get(bArr);
try {
return new String(bArr, this.sServerEncoding);
catch (UnsupportedEncodingException e2) {
return new String(bArr);
}
default:
throw new JceDecodeException("type mismatch.");
}
else if (!z) {
return null;
else {
throw new JceDecodeException("require field not exist.");
}
}



我们知道String的类型为6,7。

 

当类型为6时,String的长度为一个字节。

 

当类型为7时,String的长度为一个Int.且字符串的最大长度为104857600个字节。

 

读取了长度之后,之后的字节就是String的内容.其中sServerEncoding字段指定了字符串的编码类型。

 

在我分析的时候都是UTF-8.


解析Map


依然是我们看代码说话:



private <K, V> Map<K, V> readMap(Map<K, V> map, Map<K, V> map2, int i, boolean z) {
if (map2 == null || map2.isEmpty()) {
return new HashMap();
}
Entry entry = (Entry) map2.entrySet().iterator().next();
Object key = entry.getKey();
Object value = entry.getValue();
if (skipToTag(i)) {
HeadData headData = new HeadData();
readHead(headData);
switch (headData.type) {
case (byte8:
int read = read(00true);//读取一个int
if (read < 0) {
throw new JceDecodeException("size invalid: " + read);
}
for (int i2 = 0; i2 < read; i2++) {
map.put(read(key, 0true), read(value1true));
}
return map;
default:
throw new JceDecodeException("type mismatch.");
}
else if (!z) {
return map;
else {
throw new JceDecodeException("require field not exist.");
}
}



我们看一下read(key,0,true),read(value,1,true)调用的函数是



public <T> Object read(T t, int i, boolean z) {
if (t instanceof Byte) {
return Byte.valueOf(read((byte0, i, z));
}
if (t instanceof Boolean) {
return Boolean.valueOf(read(false, i, z));
}
if (t instanceof Short) {
return Short.valueOf(read((short0, i, z));
}
if (t instanceof Integer) {
return Integer.valueOf(read(0, i, z));
}
if (t instanceof Long) {
return Long.valueOf(read(0, i, z));
}
if (t instanceof Float) {
return Float.valueOf(read(0.0f, i, z));
}
if (t instanceof Double) {
return Double.valueOf(read(0.0d, i, z));
}
if (t instanceof String) {
return readString(i, z);
}
if (t instanceof Map) {
return readMap((Map) t, i, z);
}
if (t instanceof List) {
return readArray((List) t, i, z);
}
if (t instanceof JceStruct) {
return read((JceStruct) t, i, z);
}
if (t.getClass().isArray()) {
return ((t instanceof byte[]) || (t instanceof Byte[])) ? read((byte[]) null, i, z) : t instanceof boolean[] ? read((boolean[]) null, i, z) : t instanceof short[] ? read((short[]) null, i, z) : t instanceof int[] ? read((int[]) null, i, z) : t instanceof long[] ? read((long[]) null, i, z) : t instanceof float[] ? read((float[]) null, i, z) : t instanceof double[] ? read((double[]) null, i, z) : readArray((Object[]) t, i, z);
else {
throw new JceDecodeException("read object error: unsupport type.");
}
}



①如果类型为8,也就是说类型是Map,那么接下来的值是一个Int表示这个Map有n个成员.

②然后他的n个成员都是按照Key-Value,Key-Value.....这样子连续存储下来.


List解析


List解析调用的是readAllay函数.



public <T> List<T> readArray(List<T> list, int i, boolean z) {
int i2 = 0;
if (list == null || list.isEmpty()) {
return new ArrayList();
}
Object[] readArrayImpl = readArrayImpl(list.get(0), i, z);
if (readArrayImpl == null) {
return null;
}
ArrayList arrayList = new ArrayList();
while (i2 < readArrayImpl.length) {
arrayList.add(readArrayImpl[i2]);
i2++;
}
return arrayList;
}

private <T> T[] readArrayImpl(T t, int i, boolean z) {
if (skipToTag(i)) {
HeadData headData = new HeadData();
readHead(headData);
switch (headData.type) {
case (byte9:
int read = read(00true);
if (read < 0) {
throw new JceDecodeException("size invalid: " + read);
}
Object[] objArr = (Object[]) Array.newInstance(t.getClass(), read);
for (int i2 = 0; i2 < read; i2++) {
objArr[i2] = read((Object) t, 0true);
}
return objArr;
default:
throw new JceDecodeException("type mismatch.");
}
else if (!z) {
return null;
else {
throw new JceDecodeException("require field not exist.");
}
}



可以看到和Map差不多,只不过是没有Map的Key部分而已。

①如果类型为9,也就是说是List,那么接下来的值是一个Int表示接下来List有n个成员

②然后他的n个成员都是按照Value,Value,....这样子连续存储下来


解析继承JectObject的对象


有人和觉得解析这个可能会比前面都难,但是其实也挺简单,单单就是调用JectObject类的抽象函数readFrom



public abstract class JceStruct implements Serializable {

...
public abstract void readFrom(JceInputStream jceInputStream);
...

}
public JceStruct read(JceStruct jceStruct, int i, boolean z) {
JceStruct jceStruct2;
if (skipToTag(i)) {
try {
jceStruct2 = (JceStruct) jceStruct.getClass().newInstance();
if (jceStruct2 == null) {
return null;
}
HeadData headData = new HeadData();
readHead(headData);
if (headData.type != (byte10) {//判断类型是否为10
throw new JceDecodeException("type mismatch.");
}
jceStruct2.readFrom(this);
skipToStructEnd();
catch (Exception e) {
throw new JceDecodeException(e.getMessage());
}
else if (z) {
throw new JceDecodeException("require field not exist.");
else {
jceStruct2 = null;
}
return jceStruct2;
}



readFrom就是看每个继承了JceStruct类怎么组织自己的内容,然后按照自己的字段组织在里面顺序调用各个read重载函数就可以了。




实例



通过抓包工具抓取下来一个包,并对其进行解密后得到的就是JceStruct.这个包是返回Home页内容的.

 

 

我用IDEA写了一个Java项目.通过把JceStruct的代码抽取出来.其中只有少许的改动.因为有几个函数经过反编译是编译结果是错误的.只能手动从Smali翻译过来了.

 

进过项目解析之后:

 





- End -






看雪ID:chpeagle             

https://bbs.pediy.com/user-793792.htm




本文由看雪论坛 chpeagle 原创

转载请注明来自看雪社区




热门图书推荐:


立即购买!





热门文章阅读

1、信号/槽原理分析 | 如何查看与分析QT的源码实现

2、IIS6.0缓冲区溢出漏洞深度分析(CVE-2017-7269)

3、用 Lua 简单还原 OpCode 顺序







公众号ID:ikanxue

官方微博:看雪安全

商务合作:wsc@kanxue.com




点击下方“阅读原文”,查看更多干货!

    您可能也对以下帖子感兴趣

    文章有问题?点此查看未经处理的缓存