首页
论坛
专栏
课程

[原创]某交友APP协议分析与加解密还原

2019-6-2 16:19 7452

[原创]某交友APP协议分析与加解密还原

2019-6-2 16:19
7452

# 插曲

为啥新注册的号还不能在技术版块发文章?
好吧,只好先在茶余饭后版块做吃瓜群众了
站长呢,快出来给我个权限啊啊啊啊啊啊啊!
PS:最近在研究Android内核的水滴漏洞,有点搞不定,有兴趣的可以一起交流一下。

# 前言

自从做了Android,我开始觉得自己就是一个苦逼的程序猿... 
没错,之前我一直认为自己是一个虽然菜鸡但是快乐的黑客的
觉得自己一天天的不快乐,所以只好写点文章来找找存在感,希望能够回到最初的状态。
某陌,先给你道个歉,对不起了...

# 确定目标


任意抓取两个HTTPS数据包

第一个数据包
Post请求部分:
header中的未知字段:
X-SIGN: ABkKoAlXiGOixtT1tVgWMeKU27c=
X-LV: 1								
X-KV: 31e0d37e						
携带session值:cookie: SESSIONID=47E50390-FD1A-1FCB-78C1-D1406A9FCAEC
post参数为:
mzip=AgOEsqhrADZqZE%2B%2BQEM1Fq5RSTda0PrL9sPUxKU7RPmQwkPe3%2BDRzuWDj%2FpsculdJUzWZni9L1ClNvptmUS65a......

Post应答部分内容为:
 jP    K   A
		b6 rt+B    , 0 d U)  y b7w ' I ? &AI.2 z  ٹ T ......

查看十六进制

	02 03 20 6A 50 20 20 20 20 4B 20 20 20 08 41 0D
	0A 62 36 20 72 74 2B 17 42 20 20 20 20 2C 20 30
	20 64 20 7F 13 55 29 20 20 79 20 62 37 77 20 27
	20 49 1F 20 ......

第二个数据包:
Post请求部分:
header中的未知字段:
X-SIGN: NJX937HCIey888zMUntRbxCfgLo=
X-LV: 1
X-KV: 31e0d37e

携带session值:
cookie: SESSIONID=47E50390-FD1A-1FCB-78C1-D1406A9FCAEC

post参数为:
mzip=AgNiOLZhALY595shFYVA14Xykx%2B8NpJGPbfB3g%2FQ9fG799C6RDvpwJCgTvW1%2FnNe6%2BBWrKRUhSAQ1lR......

Post应答部分内容为:
 n U Q   =  t 呵 X     %-  "i       !|J% r# , B  &   yg +& i V 
		\j Y K  4   o U   ( w ;oQ ഀ / ƾ    $ [ {  J~ >G  O ,  p 5  y G SLViӧ.....

查看十六进制:
	02 03 20 6E 20 55 20 51 20 12 20 20 3D 20 20 74 
	20 BA C7 20 13 58 20 20 20 20 20 25 2D 20 20 22
	69 20 20 20 20 20 20 20 21 17 7C 4A 0E 25 20 72
	23 7F 20 2C 20 14 ......

基于以上两个https包猜测:
1.header中的X-SIGN是整个包的校验值
2.session为登录时服务器分配的一个id
3.重新登录后发现X-KV的值发生变化,猜测是在登录时生成的一个标识
4.post参数采用为某种加密后,Base64再URLEncode得到
5.仔细观察两个应答包的十六进制数据后发现,数据的开头格式均为0x02,0x03,0x20
接下来开始验证这些猜想

# 验证猜想:

第一步:构造mzip包
对程序进行分析后,在com.immomo.momoenc.e.d()函数中,发现有代码段 const-string/jumbo v3, "mzip"
在此下断进行动态调试后发现在https包中的参数,与在此处put到Map中的mzip一致,动态调试public void d();
//在次只关注mzip,以下片段已剔除函数中与mzip计算无关的代码
    public void d() throws Exception {
        String v0_2;
        CharSequence v1 = null;
        if(f.b()) {
            this.h();
            if(this.g) {
                if(this.i != null && !this.i.isEmpty()) {	//this.i为post参数的Map对象
                    JSONObject v1_1 = new JSONObject();
                    Iterator v2 = this.i.keySet().iterator();
                    while(v2.hasNext()) {
                        Object v0 = v2.next();
                        try {
                            v1_1.put(((String)v0), this.i.get(v0));
                        }
                        catch(Exception v0_1) {
                            com.immomo.mmutil.b.a.a().a(((Throwable)v0_1));
                        }
                    }
					//将Map参数转为Json字符串v0_2
                    v0_2 = v1_1.toString();	
                    String v2_1 = this.c;	//this.c取出一个长度为48的字符串:GallR7c5B85g5Cy4ew+Cf9xboJnBystfGallR7c5B85g5Cy4
                    try {
                        byte[] v1_2 = v0_2.getBytes();
                        byte[] v3 = v2_1.getBytes();
                        byte[] v5 = new byte[Coded.getInstance().computeOutputLength(v1_2.length, 1)];	//computeOutputLength函数计算得到加密结果长度
                        int v2_2 = Coded.getInstance().aesEncode(v1_2, v1_2.length, v3, v3.length, v5);	//采用aes加密算法,其中v1_2为内容,v3为密钥
                        v1_2 = new byte[v2_2];
                        int v0_3;
                        for(v0_3 = 0; v0_3 < v2_2; ++v0_3) {	//将加密结果循环赋值给v1_2
                            v1_2[v0_3] = v5[v0_3];				//不同的明文,v1_2的开头结构为均为0203
                        }
                        v0_2 = com.immomo.mmutil.a.a(v1_2);	//进入a函数观察得,a为Base64的encode函数
                        goto label_50;
                    }
                }
            label_50:
                this.i.clear();
                if(!TextUtils.isEmpty(((CharSequence)v0_2))) {
                    this.i.put("mzip", v0_2);	//将v0_2关联mzip
                }
            }
        }
    }

经过以上分析可得,mzip的构造依赖与aesEncode函数
参数依次为:明文,明文长度,密钥,密钥长度,输出长度
	public int aesEncode(byte[] arg2, int arg3, byte[] arg4, int arg5, byte[] arg6) {
                return this.a49kdEba83h(arg2, arg3, arg4, arg5, arg6);
            }

经过几次调试后发现,传入aesEncode相同的明文和密钥,得到的加密结果不同,猜测该aes为带IV值的加密,或者在aes的基础上略有改动
进入aesEncode函数发现native函数a49kdEba83h调用
    public native int a49kdEba83h(byte[] arg1, int arg2, byte[] arg3, int arg4, byte[] arg5) {
    }

接下来开始对native函数a49kdEba83h进行分析
起初猜测该函数在libcoded.so中,反编译该so文件后并未找到
再反编译libcoded_jni.so,找到函数:native_a49kdEba83h 000018E0
在函数开始处,通过检测/proc/PID/status中TracerPid的状态来做反调试
向下分析发现,函数调用了外部函数aesEncode,而这个函数刚才在分析libcoded.so恰好见到过
		.............
	BLX             getpid
	LDR             R1, =(aProcDStatus - 0x190E)
	ADD.W           R4, SP, #0x518+s
	MOV             R2, R0
	ADD             R1, PC  ; "/proc/%d/status"		//轻量反调试
	MOV             R0, R4  ; s
	BLX             sprintf
	LDR             R1, =(aR - 0x191A)
	MOV             R0, R4  ; filename
	ADD             R1, PC  ; "r"
	BLX             fopen
	LDR.W           R9, [R7,#arg_0]
	MOV             R6, R0
		.............
	LDR             R4, =(aTracerpid - 0x192E)
	MOV             R10, R5
	ADD             R5, SP, #0x518+var_510
	ADD             R4, PC  ; "TracerPid"
		............


对aesEncode代码进行分析发现,数据进行加密运算前,对一个指针参数进行了操作(猜测该指针为数据的输出起点),
首先在指针前两个字节进行赋值0203,再对第七个字节赋值00,将该指针加2作为参数传入IV生成算法,并传入长度4,
随后调用标准的aes加密
		........
	ADD             R4, SP, #0x40+var_30
	MOV             R1, R2
	MOV             R2, R3
	MOV             R0, R4
	BLX             __aeabi_memcpy
	MOVW            R0, #0x302		// 02 03
	ADD.W           R6, R10, #2
	STRH.W          R0, [R10]		//0203作为数据的开头,即数据的byte形式为:02 03 .....
	MOVS            R0, #0
	STRB.W          R0, [R10,#6]	//00作为第七个字节,数据的byte形式为:02 03 xx xx xx xx 00.....
	MOV             R0, R6
	MOVS            R1, #4
	BLX             j_my_RAND_pseudo_bytes
	ADD.W           R9, SP, #0x40+var_20
	MOV             R0, R6			//R6此刻指向数据byte第三个字节
	MOVS            R2, #4			//R2为4,猜测是一个长度值,及bytes[2-5]
	MOV             R1, R9
	BLX             j_iv_generate	//IV生成算法
	ADD.W           R3, R10, #7		//R3指向数据的第8个字节
	MOV             R0, R11
	MOV             R1, R9
	MOV             R2, R4
	STR.W           R8, [SP,#0x40+var_40]
	BLX             j_aesEncrypt
	ADDS            R0, #7			
		........

于是现在的目标转换为IV的生成算法
IV的生成与两个函数有关,一个是j_my_RAND_pseudo_bytes,另一个是j_iv_generate
j_my_RAND_pseudo_bytes函数,只是得到了一个随机数
j_iv_generate函数,将随机数进行sha1运算,取加密结果的前十六个字节作为IV
		...............
	MOVS            R0, #0x14 ; size	
	BLX             malloc
	MOV             R5, R0		//开辟一块空间存放sha1的值
	MOV             R0, R6
	MOV             R1, R5	
	MOV             R2, R4
	BLX             j_momoEnc_sha1
	MOV             R0, R8		//R8为参数传进来的指针
	MOV             R1, R5
	MOVS            R2, #0x10	//取16字节
	BLX             __aeabi_memcpy
	MOV             R0, R5  ; ptr
	BLX             free
		...........

至此可得结论,mzip的输出形式为: Base64.encode(  2字节(0203)+4字节(用于计算IV的随机值)+1字节(00)+aes的结果  )
先不考虑aes的key值
想要构造一个完整的包,还需要得到X-SIGN的值
X-SIGN在mzip构造后出现
this.j.put("X-SIGN", this.a(v2_3, this.j, this.c));
即现在的目标为this.a()函数
经调试得参数依次为:mzip的byte数组,XKV和XLV的Map对象,aes的key值
函数内部取得user Agent的值,再将其与mzip拼接,经过sign函数计算得到XSIGN的值
//以下代码片段已剔除与计算xsign无关的代码
private String a(byte[] arg7, Map arg8, String arg9) {
        String v0_1;
        int v0 = 0;
        byte[] v3 = arg9.getBytes();
		//得到UserAgent
        byte[] v2 = c.a(arg8) ? f.a().c().getBytes(Charset.forName("UTF-8")) : "".getBytes(Charset.forName("UTF-8"));	
        if(arg7 != null) {
            try {
                byte[] v4 = new byte[arg7.length + v2.length];	//构造byte数组v4,长度为mzip的长度加user Agent的长度
                int v1;
                for(v1 = 0; v1 < v2.length; ++v1) {
                    v4[v1] = v2[v1];
                }
                while(v0 < arg7.length) {			//拼接两个byte数组
                    v4[v2.length + v0] = arg7[v0];
                    ++v0;
                }
                v0_1 = Coded.getInstance().sign(v4, v3);	//调用sign函数,参数依次为:拼接过的byte数组,aes密钥
            }
            return v0_1;
        }
    } 

sign函数内部调用了libcoded_jni.so中的函数sdbyecbu37x
sdbyecbu37x中调用了libcoded.so中的native函数sign
sign函数起初处有一个检测TracePid的反调试
然后是一个switch语句,经过分析发现,xsign的计算位于case 1中,
将参数1与参数2的前8个字节拼接,进行一次SHA1运算
	loc_6140                ; jumptable 000060F0 case 3
	ADD.W           R4, R11, #8
	MOV             R0, R4  ; size	//申请一块length+8的空间buff
	BLX             malloc	
	MOV             R1, R9
	MOV             R2, R11
	MOV             R5, R0			
	BLX             __aeabi_memcpy	//拷贝参数1至buff
	LDR.W           R0, [R10]
	MOV             R2, R4
	LDR.W           R1, [R10,#4]
	STR.W           R0, [R5,R11]
	ADD.W           R0, R5, R11
	STR             R1, [R0,#4]
	MOV             R0, R5
	MOV             R1, R8
	BLX             j_momoEnc_sha1
	MOV             R0, R5  ; ptr
	BLX             free
	MOVS            R0, #0
	B               loc_6120 ; jumptable 000060F0 cases 2,4

至此可得结论,X-SIGN的输出形式为:
 SHA1( (postData.getBytes+userAgent.getBytes) + secret.subString(0,8).getBytes )


一般而言,如果可以模拟程序的登录包,其他的包自然也可以模拟,所以分析到这里,我决定把目标切换为模拟程序登录。
抓包HTTPS得到
    code_version=2
    map_id=9864824510
    mzip=AgPs+lDzADrqaD/2zXNra9FWpFkFgiF3pwde4c2UeAQ3nfKDjwSLquaW9IqPRol/GQfy6Ap7S0Q...
    X-KV=32712cf9
    ck=AgPs+lDzAOE81bVd6TGkHR2gbX0bsZF9g/FRMmwAzvaogaOIDx/pOh7SRpeSw+mCazBCEX7AIQ...

可以看到,登录包中有5个参数,分别为:code_version,map_id,mzip,X-KV,ck
上面已经分析了mzip,code_version是一个固定值,所以现在需要分析的有ck和map_id,X-KV
在X-SIGN计算的下方,有如下代码:
    v0_2 = d.a().e();
    this.i.put("ck", d.a().f());
    this.i.put("code_version", d.a().g());
    this.i.put("map_id", v0_2);
    this.i.put("X-KV", d.a().h());


可以看到,程序通过d.a().e()获取mapid
追踪代码发现mapid的生成方式,一个时间戳经过计算后拼接一个随机数
	public String i() {
        long v4 = 100000;
        long v0 = System.currentTimeMillis() % 1000000;
        if(v0 < v4) {
            v0 += v4;
        }

        d.e = v0 + "" + ((((int)(Math.random() * 9000))) + 1000);
        return d.e;
    }

同理,追踪d.a().f()函数,发现ck的生成函数:com.immomo.mmutil.a.a(v2)
函数的参数v2的生成依赖于一个程序第一次启动时生成的公钥localPublicKey(生成算法过于复杂,在此不做展示,
读者如果想要验证本文的分析,可以在程序第一次启动时hook com.immomo.momoenc.d类中的函数得到这个值)
以公钥作为输入,字符串"Iu0WKHFy"作为密钥,进行aes128运算(需要注意的是,Iu0WKHFy只有8字节,需要在后面补八个字节零)
输出的值即为com.immomo.mmutil.a.a函数的参数v2
进入函数内部后发现,com.immomo.mmutil.a.a为Base64的encode函数
    byte[] v1 = com.immomo.mmutil.a.b(v7.a.getBytes());	//Base64.decode
    byte[] v3 = f.a().g().getBytes();	
    byte[] v5 = new byte[Coded.getInstance().computeOutputLength(v1.length, 1)];
    int v1_1 = Coded.getInstance().aesEncode(v1, v1.length, v3, v3.length, v5);
    byte[] v2 = new byte[v1_1];
    int v0_1;
    for(v0_1 = 0; v0_1 < v1_1; ++v0_1) {
        v2[v0_1] = v5[v0_1];
    }
    d.a().b(com.immomo.mmutil.a.a(v2));		//Base64.encode

得到ck的输出形式  
    aesEncode ( Base64.decode( localPublicKey ), "Iu0WKHFy".getBytes+ new byte[8]={0} ) 

登录时header中还需要一个X-KV字段
追踪d.a().h()函数
得到以下代码,其中j.b函数为判空函数,j.a函数为MD5,参数this.b为localPublicKey
	public String d() {
        String v0 = j.b(this.b) ? "" : j.a(this.b).substring(0, 8);
        return v0;
                }

即xkv的的输出为 
    MD5( localPublicKey ).substring(0,8);

至此,我们已经完成了整个登陆包所需的加密
需要的参数太多,各种手机信息位置信息网络信息硬件检测,具体的参数信息获取在此就不写了,参数获取不是很难,就是有些繁琐
最后稍微写一下respone的解密
解密跟mzip的加密正好相对,文章开头我们已经看到了,respone的格式为0203,想必已经有读者能够猜到解密的流程了
将respone进行分割:0203 + 4字节IV生成数 + Data
IV=SHA1(IV生......
data=.....嗯,应该不用写了

# 结语

        本来想附代码的,结果不让发...还是算了
程序获取了几百种的信息,烦人的要死,恨不得把你穿什么颜色的裤衩都上传...
至于程序的脱机,最难的其实是逃不过厂商的风控,不过,我还是逃过了一部分...
这只是程序配置相关https层的通讯,有关程序tcp层的通讯在此不做讲解...
有兴趣的可以联系我,联系我也不会告诉你的...


[公告]安全服务和外包项目请将项目需求发到看雪企服平台:https://qifu.kanxue.com

最后于 2019-6-3 11:37 被kanxue编辑 ,原因:
最新回复 (24)
ChenSem 2019-6-2 18:07
2
0
第四号 2019-6-3 02:45
3
0
kanxue 8 2019-6-3 10:50
4
0
感谢分享~
最后于 2019-6-3 11:38 被kanxue编辑 ,原因:
Ddddz 2019-6-3 14:35
5
0
膜,大佬
切丝怕怕 2019-6-3 15:35
6
0
DlyWtF700 2019-6-3 20:00
7
0
看得我很难受.文档功底?
kinglinzi 2019-6-3 20:43
8
0
厉害 学习了
pushmop 2019-6-5 10:47
9
0
排版?
fixother 2019-6-8 22:26
11
0
密钥的生成就是用的 那个ECDH 椭圆曲线算法啦
asacarum 2019-6-11 10:23
12
0
这个app是用什么脱壳的啊
MrDroid 2019-6-12 13:57
13
0
大神,公钥和私钥的管理逻辑,搞定了吗?
miyuecao 2019-6-14 14:57
14
0
膜拜,大佬
asacarum 2019-6-26 18:17
15
0
大佬,你那个是用dex2jar反编译源码的吗,我的就不行,是啥版本的。我qq: 3118295245
mb_gglbwkla 2019-7-13 03:33
16
0
某陌检测设备信息存在异常。。。
荧光之夏 2019-8-16 11:00
17
0
仰望
Vorblock 2019-8-16 16:31
18
0
mb_qknttudm 2019-8-26 16:45
19
0
能做某app协议分析软件吗
miyuecao 2019-9-9 09:57
22
0
获取几百种参数,这个有点吓人
最后于 2019-9-9 10:00 被miyuecao编辑 ,原因:
hackings 2019-9-10 15:20
23
0
膜拜大神
看血大叔 2019-9-11 21:37
24
0
大佬,你说的那个16进制0203是pe格式还是什么东西,给点提示我去学一学
淡定小胖子 2019-9-12 14:36
25
0
大佬
游客
登录 | 注册 方可回帖
返回