请选择 进入手机版 | 继续访问电脑版


[dz模板] [Android 原创]【i春秋】-网络安全-缓存视频文件解密

[复制链接]
查看: 153|回复: 0

该用户从未签到

211

主题

211

帖子

190

积分

注册会员

Rank: 2

积分
190

官方

发表于 2018-8-9 11:27:15 | 显示全部楼层 |阅读模式
本帖最后由 joybou 于 2018-8-10 13:44 编辑

注:本文不提供任何视频文件,也请大家不要传播使用盗版视频,尊重讲师劳动成果。

由于VIP快到期了,就想把视频下载下来慢慢看,但是它只能在手机上使用官方app观看,不仅占空间而且还不敢保证到期后缓存的视频还能继续看,于是想把它提取出来,经查找缓存文件放在/Android/data/com.ni.ichunqiu/videocache目录下,每个视频被分片,即m3u8,且使用AES加密,当然密钥文件也在本地,但是很明显这个密钥文件本身也是被加密的,于是需要解密key->解密分段视频->合成视频这几步,其中后面两步很简单,关键就在第一步,仔细观察只能猜出密钥是base64编码存放的,看不出是什么加密,只能分析app啦。
脱壳直接在官方下载发现文件使用了360加固,惹不起躲得起,在豌豆荚找到旧版发现依然能用,就从旧版入手,虽然还是有壳,但是应该会好脱很多了。
  • 下载drizzleDumper
  • 自动脱壳:

    1
    2
    3
    4
    5
    6


    adb push x86/drizzleDumper /storage/sdcard/
    chmod +x drizzleDumper
    adb shell
    /storage/sdcard/drizzleDumper com.ni.ichunqiu
    exit
    adb pull xxx.dex E:\\


很顺利的得到两个dex
分析在使用本地缓存时应该需要读取文件,其中部分路径应该是硬编码的,使用/key和.m3u8作为关键字搜索字符串直接找到了关键点:
[Android 原创]【i春秋】-网络安全-缓存视频文件解密,广荣社区-分享能共享的一切!
下载加密分析经分析发现本类为下载处,也好,看看加密过程也就知道解密啦!
1.首先它通过video.key.get方法获取到服务端返回的key,然后调用a.getKey(key)处理它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54


public static String getKeyFromServer(String token, String url, CourseM3u8Info courseinfo) {
    InputStream httpIStream;
    URLConnection conn;
    String timestamp;
    if(url.startsWith("http://")) {
        try {
            String vid = url.substring(url.lastIndexOf("=") + 1);
            timestamp = URLEncoder.encode(new Date().getTime() + "", "UTF-8");
            HashMap v2 = new HashMap();
            v2.put("app_key", "100001");
            v2.put("ver", "1");
            v2.put("timestamp", timestamp);
            v2.put("method", "video.key.get");
            v2.put("os", "android");
            v2.put("mac", "00000");
            v2.put("from", "app.android");
            v2.put("token", token);
            v2.put("vid", vid);
            LLLLLLLLLLl.sign(v2);
            StringBuilder urlParas = new StringBuilder();
            Iterator v2_1 = v2.entrySet().iterator();
            while(v2_1.hasNext()) {
                Object v0_4 = v2_1.next();
                urlParas.append(((Map$Entry)v0_4).getKey()).append("=").append(((Map$Entry)v0_4).getValue()).append("&");
            }

            timestamp = String.format("%s?%s", userInfo.a().b(), urlParas.toString());
            d.LOG("newUrl =" + timestamp);
            conn = new URL(timestamp).openConnection();
            ((HttpURLConnection)conn).setConnectTimeout(5000);
            ((HttpURLConnection)conn).setRequestMethod("GET");
            if(((HttpURLConnection)conn).getResponseCode() != 200) {
                d.LOG("请求url失败 url 是 =" + timestamp);
                CacheFileEvent.sendTsAnalysisError(courseinfo.getChapter_id(), courseinfo.getSection_id(), 0);
                return "";
            }

            httpIStream = ((HttpURLConnection)conn).getInputStream();
        }
        catch(Exception v0) {
            ....
        }
        try {
            timestamp = LLLLLLLLLLl.ISGetString(httpIStream, "UTF-8");  // 从服务端得到base64编码的key
            ((HttpURLConnection)conn).disconnect();
            return a.getKey(timestamp);
        }
        catch(Exception v0_1) {
            ....
        }
    }

    return null;
}

2.a.getKey(key)做如下处理:

1
2
3
4
5
6
7
8
9
10
11
12
13


public static String getKey(String key1) {  
    String v0 = null;
    if(!TextUtils.isEmpty(((CharSequence)key1))) {
        String uid = key.getUserId();                                   //为一串数字哈
        String realKey = key.rc4decrypto(key1, key.stringAppVideoKey);  //使用AppVideoKey解密key1
        if(TextUtils.isEmpty(((CharSequence)uid))) {
            return v0;
        }

        v0 = key.rc4encrypto(realKey + "____" + uid, key.stringAppVideoKey);
    }
    return v0;
}

其中key是com.ichunqiu.libglobal.tool.f这个类,它是一个很重要的解密类,里面主要包含两个RC4加解密函数,加解密用的密钥是AppVideoKey这个字符串,它来自flytv.run.monitor.MyApplication匿名内部类的run方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18


.method public run()V
          .registers 5
00000000 const/4             v3, 0
00000002 iget-object         v0, p0, MyApplication$1->a:MyApplication
00000006 const-string        v1, "PUSH_APPID"
0000000A const/4             v2, 0
0000000C invoke-virtual      MyApplication->a(String, String)String, v0, v1, v2
00000012 move-result-object v0
00000014 new-instance        v1, IChunqiuJni
00000018 invoke-direct       IChunqiuJni->()V, v1
0000001E invoke-virtual      IChunqiuJni->stringAppVideoKeyJNI()String, v1
00000024 move-result-object v2
00000026 sput-object         v2, key->stringAppVideoKey:String
0000002A invoke-virtual      IChunqiuJni->stringAppSecretKeyJNI()String, v1
00000030 move-result-object v1
00000032 sput-object         v1, key->stringAppSecretKey:String
...............
.end method

看到它其实IChunqiuJni这个native层的动态库,解压文件即可得到它,用阿达打开即可得到密钥:

1
2
3
4
5
6
7
8
9
10
11
12


EXPORT Java_com_ni_ichunqiu_IChunqiuJni_stringAppSecretKeyJNI
Java_com_ni_ichunqiu_IChunqiuJni_stringAppSecretKeyJNI
; __unwind {
PUSH    {R3,LR}
LDR     R1, =(a00dfafa4ed6b64 - 0xCA4)
LDR     R2, [R0]
MOVS    R3, #0x29C
LDR     R3, [R2,R3]
ADD     R1, PC          ; "32dfafa4ed6b64f7644172c1ee9ad2f4"
BLX     R3
POP     {R3,PC}
; End of function Java_com_ni_ichunqiu_IChunqiuJni_stringAppSecretKeyJNI

3.当把密钥处理好以后,会存储在本地的/key文件下,并且更改m3u8的key uri为key:

1
2
3


user = LLLLLLLLLLl.getRealKey(user, keyuri, this.courseinfo);
LLLLLLLLLLl.string2file(this.a.substring(0, this.a.lastIndexOf("/")) + "/key", user, false);    //存储加密后的key
sb.append(aline.replace(((CharSequence)keyuri), "key") + "\n");                                 //.M3U8文件

小结其实内部还有很多处理与验证逻辑但是和解密key无关就省去了,通过分析发现:

1
2
3
4
5


key = LLLLLLLLLLl.ISGetString(httpIStream, "UTF-8");        // 1.从服务端得到base64编码的key
String uid = key.getUserId();                               // 2.得到用户id
String realKey = key.rc4decrypto(key1, key.stringAppVideoKey); // 3.使用从APP里得到的VideoKey解密服务端返回的key,里面会校验解密是否正确
localKey = key.rc4encrypto(realKey + "____" + uid, key.stringAppVideoKey);// 4.再使用VideoKey加密realKey + "____" + uid,之后存储在本地
                                                                        //也就是说本地存储的key其实适合用户绑定的

播放解密1.根据上面分析发现key这个类很关键,加解密都在这里,于是查看交叉引用发现另一个类com.google.android.exoplayer.b\nnnn自命名,忘了原名啦调用了它,其内部就是解密代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32


public nnnn(d arg7, byte[] key1, byte[] arg9) {      //经分析,后两个参数分别是本地存储的key和iv   
     String uid;
     String key;
     byte[] keykey;
     String keey1;
     int v0 = 0;
     super();
     if(key1 != null) {
         try {
             this.uid = key.getUserId();
             keey1 = new String(key1, "ASCII");
             key = key.rc4decrypto(keey1, key.stringAppVideoKey);            //完全的逆过程,解密key
             if(!key.contains("____")) {                                     //_____分割,第一部分为key第二部分为uid
                 goto label_117;
             }
             String[] v3 = key.split("____");
         label_62:
             uid = v3[1];
             if(!uid.equals(this.uid)) {                                     //判断uid是否相符,只有下载的人能够播放它
                 goto label_69;
             }

             keykey = this.a(v3[0]);
         }
         try {
         label_117:
             mylog.print("Aes128DataSourceKey 旧的的解析播放");
             keykey = this.a(key);        }                                                                                //处理解密后的key
     this.a = arg7;
     this.midKey = keykey;
     this.IV = arg9;
}

2.转到a()函数,在其内部先尝试以此为键取,失败就去生成它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22


public byte[] a(String s) {
    byte[] v0_4;
    Object v0_2;
    try {
        v0_2 = hls.hashmap.get(s);
        if(v0_2 != null) {
            goto label_13;
        }

        if(s == null) {
            goto label_16;
        }

        String v0_3 = hls.generaKey(s);
        v0_4 = hls.hexS2bytes(v0_3);
        goto label_13;
    }
label_16:
    v0_4 = null;
label_13:
    return ((byte[])v0_2);
}

3.又转到hls.generaKey(key)函数:
[table][tr][td]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79

[/td][td]<div align="left">
public static String generaKey(String s) throws IOException, HLsParserException {
    String v2_2;
    int hex = 16;
    String v0 = null;
    int v1 = 0;
    InputStream v2 = con.context.getAssets().open("dict.png");                                                //首先打开资源文件里面的一张图,可以解压apk得到
    if(s != null && !s.equals("")) {
        System.currentTimeMillis();
        new BitmapFactory$Options().inPreferredConfig = Bitmap$Config.ARGB_8888;
        Bitmap bitmap = BitmapFactory.decodeStream(v2);
        int i = 4;                        
        try {
            v2_2 = s.substring(0, i);                                                                                                //取出前缀2字节,还剩16字节,目标就在前方啊
        }
        catch(Exception v2_1) {
            v2_1.printStackTrace();
            v2_2 = v0;
        }

        if(v2_2 == null) {
            return v0;
        }

        int v5 = Integer.parseInt(v2_2, hex);                                                                                
        String v3_1 = hls.int2Hex(v5);
        if(bitmap == null) {
            new HLsParserException("请将工程目录下添加 dict资源");
        }

        if(s == null || (s.equals(""))) {
            new HLsParserException("请求生成原始的解密 key 不能为空!");
        }

        if(!s.contains(((CharSequence)v3_1))) {                                                                                //简单校验一下前缀
            return v0;
        }

        String v6 = s.replaceAll(v2_2, "");                                                                                        //去除前缀
        if(bitmap == null) {
            return v0;
        }

        int height = bitmap.getHeight();
        int width = bitmap.getWidth();
        Object intmap = new int[height][width];
        i = 0;
    label_46:                                                                                                                                                //以RGB方式处理位图
        if(i < height) {
            int j = 0;
        label_48:
            if(j < width) {
                int v9 = bitmap.getPixel(j, i);
                intmap<i>[j] = Color.blue(v9) / 85 + ((Color.red(v9) / 36  16; // r
                                        rgb[1] = (v9 & 0xff00) >> 8; // g
                                        rgb[2] = (v9 & 0xff); // b
                                        intmap<i>[j] = rgb[2] / 85 + ((rgb[0] / 36
0e38beeac1be2afa7bd5d19ffa99be16.png



上一篇:PC抖音短视频源码可看评论
下一篇:QQ空间艾特网Java版源码
*滑动验证:
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

精彩图文
关闭

提示上一条 /1 下一条

社区交流群:
①群:广荣社区官方①群
②群:广荣社区官方②群
关注本站公众号

Copyright   ©2015-2016  广荣社区-分享能共享的一切!  Powered by©Discuz!  技术支持:广荣社区     ( 冀ICP备16028691号 )