首页
论坛
专栏
课程

[原创]QQ 浏览器 JceStruct 协议

2019-4-17 20:28 3165

[原创]QQ 浏览器 JceStruct 协议

2019-4-17 20:28
3165

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

 

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;
    }

函数功能非常简单.

  1. 字节低4位为type 表示序号对应的类型
  2. 字节高4位为tag 表示序号
  3. 如果tag为0xF,下一个字节为tag.

总结一下:

  1. Key由tag和type组成
  2. Key为1个字节或两个字节.两个字节时第二个字节为tag否则第一个字节的高4位为tag.type始终为第一个字节的低4位.
  3. type最多有16种类型,tag最大为255.

type

根据分析type的类型如下:

类型
0 byte 或 bool
1 Short
2 Int
3 Long
4 Float
5 Double
6,7 String
8 Map
9 List
10 STRUCT_BEGIN 继承了JceStruct类的开始
11 STRUCT_END 继承了JceStruct类的结束
12 ZERO_TAG

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 (byte) 0:
                return this.bs.get();
            case (byte) 1:
                return this.bs.getShort();
            case (byte) 2:
                return this.bs.getInt();//就直接geInt就Ok
            case (byte) 12:
                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 (byte) 6:
                    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 (byte) 7:
                    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 (byte) 8:
                    int read = read(0, 0, true);//读取一个int
                    if (read < 0) {
                        throw new JceDecodeException("size invalid: " + read);
                    }
                    for (int i2 = 0; i2 < read; i2++) {
                        map.put(read(key, 0, true), read(value, 1, true));
                    }
                    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((byte) 0, i, z));
        }
        if (t instanceof Boolean) {
            return Boolean.valueOf(read(false, i, z));
        }
        if (t instanceof Short) {
            return Short.valueOf(read((short) 0, 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.");
        }
}
  1. 如果类型为8,也就是说类型是Map,那么接下来的值是一个Int表示这个Map有n个成员.
  2. 然后他的n个成员都是按照Key-Value,Key-Value.....这样子连续存储下来.

List解析

List解析调用的是readArray函数.

    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 (byte) 9:
                    int read = read(0, 0, true);
                    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, 0, true);
                    }
                    return objArr;
                default:
                    throw new JceDecodeException("type mismatch.");
            }
        } else if (!z) {
            return null;
        } else {
            throw new JceDecodeException("require field not exist.");
        }
    }

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

  1. 如果类型为9,也就是说是List,那么接下来的值是一个Int表示接下来List有n个成员
  2. 然后他的n个成员都是按照Value,Value,....这样子连续存储下来.

解析继承JceObject的对象

有人和觉得解析这个可能会比前面都难,但是其实也挺简单,单单就是调用JceObject类的抽象函数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 != (byte) 10) {//判断类型是否为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翻译过来了.

 

进过项目解析之后:

 

 

最近996很流行啊.

 

996 ICU

 

项目看附件.



[公告]LV6级以上的看雪会员可以免费获得《2019安全开发者峰会》门票一张!!

最后于 2019-4-22 20:58 被chpeagle编辑 ,原因: 修改错别字
上传的附件:
最新回复 (14)
LowRebSwrd 2 2019-4-17 21:12
2
0
好像多处用的这种,赞
Vn小帆 2019-4-17 22:47
3
0
QQ的所有协议  都用的这种
Editor 2019-4-18 09:15
4
0
感谢分享~
曹无咎 1 2019-4-18 20:01
5
0
可以对照下wup-1.0.0-SNAPSHOT这个jar包
walkersky 2019-4-18 20:29
6
0
TX系的很多应用都用的是JceStruct。
岭南散人 1 2019-4-18 21:31
7
0
walkersky TX系的很多应用都用的是JceStruct。
这个是TX自己定的标准吗?
暖洋洋 2019-4-18 22:10
8
0
感谢分享了,学习了
cattyabcd 2019-4-19 02:20
9
0
深夜拜读,感谢楼主,学习到了新姿势,QQ还有个Protobuf的,希望楼主有时间的话还能继续分享
saloyun 2019-5-4 18:33
10
0
jce在github上面不是有源码么....
葫芦娃 1 2019-5-4 18:46
11
0
有开源的+1...
最后于 2019-5-4 18:48 被葫芦娃编辑 ,原因:
hackdaliu 1 2019-5-6 10:39
12
0
楼上的咨询下,github开源的地址是什么呀?搜jcestruct搜不到呀?
chpeagle 2019-5-6 10:57
13
0
hackdaliu 楼上的咨询下,github开源的地址是什么呀?搜jcestruct搜不到呀?
这个我也不太知,只是在逆向QQ浏览器时把这种结构复原出来。
saloyun 2019-5-9 20:19
14
0
chpeagle 这个我也不太知,只是在逆向QQ浏览器时把这种结构复原出来。
https://github.com/playbar/taf
yepengasm 2019-5-11 11:33
15
0
我都是直接调用qq的jar的没啥问题,不用重新写
游客
登录 | 注册 方可回帖
返回