某加密到牙齿的APP数据加密分析
2020-07-02 16:24:48 Author: bbs.pediy.com(查看原文) 阅读量:314 收藏

某加密到牙齿的APP数据加密分析

在某特上关注了一点乱七八糟的东西,然后就看到了这么一款app。无聊业余时间的时候爬取了一些福利app的数据,于是就想顺便看下这个东西的数据是否也可以爬取。

图片解析


习惯性的打开HttpCanary抓包,目前一切正常。
httpcanary
数据都能获取到,既然要爬数据,肯定是要能够看到图片,这个是最起码的。 图片链接如下: https://ssimg.bdxxo.cn/tv_adult/avid5c33013611e90.jpg?k=0bf5566b1e365d4f0cf78f566269e769&t=1593571053
看到后面的k和t,忽然觉得这个东西可能没这么简单,应该是服务器进行访问校验了,先不管这个,直接访问下看看。
encrypt_image
这个,尼玛,厉害了。带着key和token访问直接返回了个黑窗口(如果时间超过了链接中的t参数表示的时间,那么直接就403了)。
把图片下载下来,拉入010,果然是加密处理了。
010en
没有找到图片文件的文件头,所以浏览器或者图片查看器也就没有办法解析这个图片。
如何解析图片,那就要从apk入手进行分析了。最终在package net.idik.lib.cipher.so;下面找到了可疑的key和iv:

public static final String dbImgKey() {
        return CipherCore.get("29993fb387b37c932b56fd54b130e0c6");
    }

    public static final String decodeImgIv() {
        return CipherCore.get("f3d9434408e52778164db2214e3a0a22");
    }

通过交叉引用,可以定位到图片解密代码位于com.ilulutv.fulao2.other.g.b:

    public static byte[] b(byte[] arg3, byte[] arg4, String arg5) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {
        Cipher v0 = Cipher.getInstance("AES/CBC/PKCS5Padding");
        v0.init(2, new SecretKeySpec(arg3, "AES"), new IvParameterSpec(arg4));
        return v0.doFinal(Base64.decode(arg5, 2));
    }

配置jeb调试器,如果不修改ro.debuggable 直接附加进程,会出现下面的错误信息:
notice
确定之后就直接失败了:
error
修改安卓的ro.debuggable属性可以通过magisk 或者mprop([https://bbs.pediy.com/thread-215311.htm]),当然也有其他的办法,修改boot.img等等。不幸的是,我在夜神模拟器上安装magisk之后,卡在了检查更新上,没有办法继续安装。知道原因的还望不吝赐教。
magisk
另外一个办法,就是通过mprop修改,下载之后,直接运行对应的bat文件即可。
mprop
需要注意的是,运行完脚本之后需要重新打开apk,否则依旧无法进行附加。
附加之后,向下滚动页面加载内容。此时断点就断下来了。
brakpoint
单步执行到00000014 invoke-direct SecretKeySpec-><init>([B, String)V, v1, p0, v2 这一行就可以看到具体的key和iv的值了。
默认的jeb的局部变量类型全部为int,可以根据代码来修改变量类型,这里两个参数的类型都是B[,修改之后就可以看到具体的数值了, 如下:
image_key_iv
比较蛋疼的一点是,jeb直接复制的变量的值是下面的格式:

array@7671 (type=[B)
[-78(FFFFFFFFFFFFFFB2h), -13(FFFFFFFFFFFFFFF3h), -124(FFFFFFFFFFFFFF84h), 40(28h), 102(66h), -7, 88(58h), 61(3Dh), 30(1Eh), -50(FFFFFFFFFFFFFFCEh), 97(61h), -60(FFFFFFFFFFFFFFC4h), -32(FFFFFFFFFFFFFFE0h), 85(55h), -62(FFFFFFFFFFFFFFC2h), 85(55h)]

鉴于net.idik.lib.cipher.so的目录下的key比较多,并且key包含不可打印字符,直接复制数值也比较蛋疼。
此时frida hook就派上用场了:

var base64 = Java.use('android.util.Base64');
  var aes = Java.use('com.ilulutv.fulao2.other.g.b');
    // 图片加密处理
    aes.b.overload("[B", "[B", "java.lang.String").implementation = function(k, iv, source_string){
        send("Image_key:"+k);
        send(base64.encodeToString(k, 0))
        send("Image_iv:"+iv);
        send(base64.encodeToString(iv, 0))
        return this.b(k, iv, source_string);
    };

由于该函数的key和为是一个byte数据,所以直接通过send函数发送。接收到的数据是个Image_iv:[object Object] 无法正常显示,所以上面的代码对数据进行了base64编码之后发送。
实际接收到的数据为:

[*] ===========================override image a begin ===========================
[*] Image_key:[object Object]
[*] svOEKGb5WD0ezmHE4FXCVQ==

[*] Image_iv:[object Object]
[*] 4B7eYzHTevzHvgVZfWVNIg==

[*] ===========================override image a end ===========================

有了这两个数据就可以去解密图片内容了:

def aes_decrypt_raw(key, data, ivs):
    encodebytes = data
    cipher = AES.new(key, AES.MODE_CBC, ivs)
    text_decrypted = cipher.decrypt(encodebytes)
    unpad = lambda s: s[0:-s[-1]]
    text_decrypted = unpad(text_decrypted)
    return text_decrypted

def decode_image():
    f = open(r"H:\PyCharmProjects\frida_test\avid5c33013611e90.jpg", 'rb')  # 二进制读
    b = f.read()
    f.close()
    # array@7687 (type=[B)
    image_key_base64 = bytes('svOEKGb5WD0ezmHE4FXCVQ==', encoding='utf8')  # 图片解密key
    image_key = base64.decodebytes(image_key_base64)
    print(image_key)
    iv = base64.decodebytes(bytes('4B7eYzHTevzHvgVZfWVNIg==', encoding='utf8'))  # 图片解密iv
    de = aes_decrypt_raw(image_key, b, iv)
    f = open(r"H:\PyCharmProjects\frida_test\avid5c33013611e90_decode.jpg", 'wb')  
    f.write(de)
    f.close()

解密之后的图片内容:
decrypt image
鉴于图片内容比较暴力,这里就不展示了,感兴趣的自己去解析即可。到这里图片的内容算是处理完成了。

数据接口分析


图片可以查看之后,主要的目标是要爬取数据,那么数据来源就很关键。通过接口格式猜测,请求视频列表的接口应该是https://api-al.vipmxmx.cn/v1/videos/menu/0?payload=D%2FPrh8wy4ODFaRYJGqhokg%3D%3D.9VP71aDIZgmZFc6X3l%2BfPoETfpXd4Jt%2BTN49ks4edK8vgtl1XHAvEPzA9EC7mTBjU59pMvWwASxSl9nUQA%2BpzTqjNk0hzAO%2FTMZ6fBkTwtJ4S11%2F4RABwCQVs%2Flk5VuDxcF6DUYuV7XnKO%2FI25woZXbONYp47i%2F1h5OGcW3I91LfJ4G8c0HZI7kli5RLbgn2Rvqt6Jk897dHkmnj4n2tbhoS3nC%2Bp3hxauGMGH2%2Byl1kah6ZGKL%2FarjRwBKR8%2Bbv7XCApO%2BWMrjwxMdJBZjxnV6obnCF5KqYGtauUC5ZN31AjG%2F7ilKr7PGYqu2b%2FrSqpvPXWBizmuw9JF1e%2BnC41vj6bOBXx4swAHsBFFK64C46byIxosDNHN4i5dofoaQH
请求接口比较简洁,应该是把所有的参数都放到了payload下面
list_request
返回的数据比较复杂,头部包含了大量的信息:
list_response_header
并且返回的数据进行了加密:
list_response_text
要想通过接口访问数据,那么就要解析请求数据,解密返回的数据。
通过参数的payload最终可以定位到以下代码:

private void d() {  // 页面请求函数
        String v0_3;
        try {
            this.j.put("path", this.o.substring(1));
            this.j.put("timestamp", String.valueOf(System.currentTimeMillis() / 1000L));
            String v0_1 = new Gson().toJson(this.j);
            byte[] v1 = Base64.decode(CipherClient.apiEncryptParamsKey(), 0);
            // 下面一行jeb给解析成了一个数组,实际在代码中的是个随机函数。
            byte[] v2 = new byte[]{-73, 0x3F, 110, -34, 0xE1, -56, -7, -4, 88, 0x8E, 101, 38, -92, 21, -61, 17};
            // 下面是aes加密,如果要知道加密的key和iv只需要hook b.c函数即可。
            String v0_2 = b.c(v1, v2, v0_1);
            int v1_1 = this.l;
            if(v1_1 == 81002) {
            label_39:
                // b.a base64编码。v2为加密的iv, 通过base64编码iv之后 使用.将iv和加密后的请求数据链接。
                v0_3 = b.a(v2) + "." + v0_2;
            }
            else {
                if(this.l == 81004) {
                    goto label_39;
                }
                // b.g 为urlencoder函数
                v0_3 = b.g(b.a(v2) + "." + v0_2);
            }

            this.j.clear();
            this.j.put("payload", v0_3);
        }
        catch(NoSuchAlgorithmException v0) {
            v0.printStackTrace();
        }
    }

通过frida hook 对b.c函数进行hook:

// 接口加密处理
    aes.c.overload("[B", "[B", "java.lang.String").implementation = function(k, iv, source_string){
        send("===========================override c begin ===========================")
        send("key:"+k);
        send(base64.encodeToString(k, 0))
        send("iv:"+iv);
        send(base64.encodeToString(iv, 0))
        send("source_string:");
        send(source_string);
        var res = this.c(k, iv, source_string);
        send('result:');
        send(res)
        send("===========================override c end ===========================")
        return res;
    };

所以如果要进行接口加密,那么只需要知道加密的key即可,以为通过上面的代码分析可以知道,iv是一个随机函数生成的长度为16的数组。如果要想模拟的真实一点可以自己写一个随机函数,当然也可以直接使用jeb解析出来的数组去请求也是ok的。 捕获到的数据如下;

[*] ===========================override c begin ===========================
[*] key:[object Object]
[*] euZN1Gg3JIwWOEWhmE7C4l5dSSRU34fyuPMXjtuoqVs=

[*] iv:[object Object]
[*] D/Prh8wy4ODFaRYJGqhokg==

[*] source_string:
[*] {"timestamp":"1593572575","order":"time","video_type":"long","type":"uncover","page":"6","token":"eyJ1c2VyX2lkIjoyMTg4MjU2NCwibGFzdGxvZ2luIjoxNTkzMzI3NzQyfQ.c39375da9af6cf24aae0349c4f0b5641.9b0e72bc9a1eea26114dc955d730603e4c11f865d43ea9595d9fd29c","path":"v1/videos/menu/0"}
[*] result:
[*] 9VP71aDIZgmZFc6X3l+fPoETfpXd4Jt+TN49ks4edK8vgtl1XHAvEPzA9EC7mTBjU59pMvWwASxSl9nUQA+pzTqjNk0hzAO/TMZ6fBkTwtJ4S11/4RABwCQVs/lk5VuDxcF6DUYuV7XnKO/I25woZXbONYp47i/1h5OGcW3I91LfJ4G8c0HZI7kli5RLbgn2Rvqt6Jk897dHkmnj4n2tbhoS3nC+p3hxauGMGH2+yl1kah6ZGKL/arjRwBKR8+bv7XCApO+WMrjwxMdJBZjxnV6obnCF5KqYGtauUC5ZN31AjG/7ilKr7PGYqu2b/rSqpvPXWBizmuw9JF1e+nC41vj6bOBXx4swAHsBFFK64C46byIxosDNHN4i5dofoaQH
[*] ===========================override c end ===========================

将euZN1Gg3JIwWOEWhmE7C4l5dSSRU34fyuPMXjtuoqVs= base64 decode之后即可获得加密用的key。
有了这些数据,那么就可以发送请求了, 测试代码如下:

def new_video_get_test():
    request_key = base64.decodebytes(bytes('euZN1Gg3JIwWOEWhmE7C4l5dSSRU34fyuPMXjtuoqVs=', encoding='utf8'))
    # request_iv = b'\x49\x09\x3E\x49\x6D\x29\x50\xBB\xF1\x67\x9C\x5D\x52\x77\xBF\x4E'
    request_iv = base64.decodebytes(bytes('HTpKwS4MVfB2pktFSGRzvw==', encoding='utf8'))

    ss = '{"timestamp":"' + str(int(
        time.time())) + '","order":"time","video_type":"long","type":"uncover","page":"1","token":"eyJ1c2VyX2lkIjoyMTg4MjU2NCwibGFzdGxvZ2luIjoxNTkzMzI3NzQyfQ.c39375da9af6cf24aae0349c4f0b5641.9b0e72bc9a1eea26114dc955d730603e4c11f865d43ea9595d9fd29c","path":"v1/videos/menu/0"}'

    ds = AES_Encrypt_raw(request_key, ss, request_iv)
    print(ds)
    payload = 'HTpKwS4MVfB2pktFSGRzvw==.' + ds
    base_url = 'https://api-tc.bjsongmoxuan.cn/v1/videos/menu/0?payload=' + urllib.parse.quote(payload)

    print(base_url)
    resp = requests.get(base_url)
    print(resp.text)

此时虽然已经能够发送请求了,但是返回的数据是加密的,如果要想获取直接可用的数据,那么就需要对返回的数据进行解密。
通过层层分析,可以定位到响应数据的解密函数为:

    public static String a(String arg2, String arg3, String arg4) throws NoSuchAlgorithmException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException, InvalidAlgorithmParameterException, InvalidKeyException {  // 请求解密函数
        String v2 = b.e(arg2);  // MD5加密
        return b.a(new IvParameterSpec(arg3.getBytes(StandardCharsets.UTF_8)), new SecretKeySpec(v2.getBytes(StandardCharsets.UTF_8), "AES"), arg4);
    }

同样对对该函数进行hook:

// 接口解密处理
    aes.a.overload( "java.lang.String",  "java.lang.String", "java.lang.String").implementation = function(k, iv, source_string){
        send("===========================override a begin ===========================")
        send("key:"+k);
        //send(base64.encodeToString(k, 0))
        send("iv:"+iv);
        //send(base64.encodeToString(iv, 0))
        send("source_string:");
        send(source_string);
        var new_key = this.e(k);
        send("new key:" + new_key);
        var res = this.a(k, iv, source_string);
        send('result:');
        send(res)
        send("===========================override a end ===========================")
        return res;
    };

获取数据:

[*] ===========================override a begin ===========================
[*] key:fe34dd6bbd3020c2fb69abe73b5b973c
[*] iv:ce9337500ee76035
[*] source_string:
[*] Ev+s/klIcLdo5nodmhTvnxoUfD6wcyWiWBDBRgyS6sApaptgy/95gTgcmuyE1nGJzbFIj2aeJ2K5SjKQvD+soqLny5frYjbkvVm2IjrYkJfgeNRsA9zYrXSZ......1Qei+PMKE5mIzPWcLUfHVLNl6zQgbcu5oMLMs+qnKlgFRMjDCnAsL1PRXtkjHZ6XjDmimZIjMc125RbYNtF5EfPpsSvCue38a+pye4GuNNR8ssAJDx8NM9tSj1WmvZYaBh6LeC+3f7X5niBm18aNinI44Qu0wtwuWMSsLPqwsADLFPdOvvG3lFkuwOjuCCIEQ+LDGV7z2CBQlBO1NfYvueGyK/Ifm2eDEGoQ42BURtu2JiEeTV0SSQVRJt+Nk9OmDdrrtAlmJv/BHBaVjWez/zsF
[*] result:
[*] {"status":{"code":200,"message":"success"},"response":{"videos":[{"video_id":"62754","video_title":"清纯模特丽丽第二弹! 多姿势不间断各种大战表情超诱人","actor":"素人","thumb":"\/tv_adult\/avid5aa739d33cb71.jpg?k=3c31a0b2eb0c7f27f2bba952492dd7f4&t=1593573476","cover":"\/tv_adult\/avid5aa739d33cb71.jpg?k=3c31a0b2eb0c7f27f2bba952492dd7f4&t=1593573476","upload_date":1593356402,"release_date":-1,"video_duration":3660,"main_tag":[],"second_tag":["無"],"video_like":false},{"video_id":"112902","video_title":" 姑娘胯下含三次,胯界妹妹的中出日记! (FC2-PPV-1387608)","actor":"素人","thumb":"\/tv_adult\/avid5ed2ca1c2f19.jpg?k=232d13e7da07b264a567fc64c6a52f64&t=1593573476","cover":"\/tv_adult\/avid5ed2ca1c2f19.jpg?k=232d13e7da07b264a567fc64c6a52f64&t=1593573476","upload_date":1593354601,"release_date":-1,"video_duration":2940,"main_tag":[],"second_tag":["無"],"video_like":false},,"upload_date":1593325802,"release_date":1490371200,"video_duration":1020,"main_tag":[],"second_tag":["無"],"video_like":false}],"total_results":20435,"page":6}}
[*] ===========================override a end ===========================

多次请求就会发现,key是固定的。但是iv却是变的。跟踪iv的数据来源,最终可以定位到

 private void b(r arg5) {
        try {
            String v0_1 = b.a(arg5);
            if(v0_1 != null && !v0_1.isEmpty() && v0_1.getBytes(StandardCharsets.UTF_8) != null) {
                super.a(b.a(CipherClient.decodeKey(), v0_1, ((String)arg5.a())), this.m);
                return;
            }

            super.a(((String)arg5.a()), this.m);
        }
        catch(Exception v0) {
            Crashlytics.logException(new Exception(v0 + " blank " + arg5.c().toString() + " blank " + b.a(arg5)));
            super.a(905, String.valueOf(this.k));
        }
    }
  // b.a函数
    public static String a(r arg2) {
        s v2 = arg2.c();
        if(v2.a(CipherClient.headerIsEncryptKey()) != null) {
            return v2.a(CipherClient.headerIsEncryptKey()).equals(CipherClient.headerIsEncryptValue()) ? b.e(v2.a(CipherClient.headerKey())).substring(8, 24) : null;
        }

        return "";
    }
    // b.e 函数md5:
public static String e(String arg6) {
        try {
            MessageDigest v0 = MessageDigest.getInstance("MD5");
            v0.update(arg6.getBytes());
            byte[] v6_1 = v0.digest();
            StringBuilder v0_1 = new StringBuilder();
            int v2;
            for(v2 = 0; v2 < v6_1.length; ++v2) {
                String v3;
                for(v3 = Integer.toHexString(v6_1[v2] & 0xFF); v3.length() < 2; v3 = "0" + v3) {
                }

                v0_1.append(v3);
            }

            return v0_1.toString();
        }
        catch(NoSuchAlgorithmException v6) {
            v6.printStackTrace();
            return "";
        }
    }

为了简便期间,把上面的函数全部hook掉:

// 接口解密处理
    aes.a.overload( "java.lang.String",  "java.lang.String", "java.lang.String").implementation = function(k, iv, source_string){
        send("===========================override a begin ===========================")
        send("key:"+k);
        //send(base64.encodeToString(k, 0))
        send("iv:"+iv);
        //send(base64.encodeToString(iv, 0))
        send("source_string:");
        send(source_string);
        var new_key = this.e(k);
        send("new key:" + new_key);
        var res = this.a(k, iv, source_string);
        send('result:');
        send(res)
        send("===========================override a end ===========================")
        return res;
    };
    aes.a.overload("[B", "[B", "java.lang.String").implementation = function(k, iv, source_string){
        send("===========================override a bytes begin ===========================")
        send("key:"+k);
        send(base64.encodeToString(k, 0))
        send("iv:"+iv);
        send(base64.encodeToString(iv, 0))
        send("source_string:");
        send(source_string);
        var res = this.a(k, iv, source_string);
        send('result:');
        send(res)
        send("===========================override a bytes end ===========================")
        return res;
    };
    // md5函数
    aes.e.overload("java.lang.String").implementation = function(source_string){
        send("===========================override e begin ===========================")
        send("source_string:");
        send(source_string);
        var res = this.e(source_string);
        send('result:');
        send(res)
        send("===========================override e end ===========================")
        return res;
    };

    aes.a.overload("i.r").implementation = function(source_string){
        send("===========================override ia  begin ===========================")
        send("source_string:");
        send(source_string.toString());
        var res = this.a(source_string);
        send('result:');
        send(res)
        send("===========================override ia end ===========================")
        return res;
    };

    var iv_class = Java.use('e.s');
    iv_class.a.overload("java.lang.String").implementation = function(arg2){
        send("===========================iv test ===========================")
        send("source_string:");
        send(arg2);
        var res = this.a(arg2);
        send('result:');
        send(res)
        send("===========================iv test ===========================")
        return res;
    };

最终捕获到的数据如下:

[*] ===========================iv test ===========================
[*] ===========================override ia  begin ===========================
[*] source_string:
[*] Response{protocol=h2, code=200, message=, url=https://api-al.vipmxmx.cn/v1/videos/menu/0?payload=D%2FPrh8wy4ODFaRYJGqhokg%3D%3D.9VP71aDIZgmZFc6X3l%2BfPoETfpXd4Jt%2BTN49ks4edK8vgtl1XHAvEPzA9EC7mTBjU59pMvWwASxSl9nUQA%2BpzTqjNk0hzAO%2FTMZ6fBkTwtJ4S11%2F4RABwCQVs%2Flk5VuDxcF6DUYuV7XnKO%2FI25woZXbONYp47i%2F1h5OGcW3I91LfJ4G8c0HZI7kli5RLbgn2Rvqt6Jk897dHkmnj4n2tbhoS3nC%2Bp3hxauGMGH2%2Byl1kah6ZGKL%2FarjRwBKR8%2Bbv7XCApO%2BWMrjwxMdJBZjxnV6obnCF5KqYGtauUC5ZN31AjG%2F7ilKr7PGYqu2b%2FrSqpvPXWBizmuw9JF1e%2BnC41vj6bOBXx4swAHsBFFK64C46byIxosDNHN4i5dofoaQH}
[*] ===========================iv test ===========================
[*] source_string:
[*] X-App-Name
[*] result:
[*] app
[*] ===========================iv test ===========================
[*] ===========================iv test ===========================
[*] source_string:
[*] X-App-Name
[*] result:
[*] app
[*] ===========================iv test ===========================
[*] ===========================iv test ===========================
[*] source_string:
[*] X-VTag
[*] result:
[*] 1115682708
[*] ===========================iv test ===========================
[*] ===========================override e begin ===========================
[*] source_string:
[*] 1115682708
[*] result:
[*] d16ec86cce9337500ee76035d220d5a9
[*] ===========================override e end ===========================
[*] result:
[*] ce9337500ee76035
[*] ===========================override ia end ===========================
[*] ===========================override a begin ===========================
[*] key:fe34dd6bbd3020c2fb69abe73b5b973c
[*] iv:ce9337500ee76035

通过关联可以找到,iv的数据来源为X-VTag字段。所以请求之后从response header中取出X-VTag就可以解密数据了。
解密代码:

response_headers = resp.headers
vtag = response_headers.get('x-vtag')
print(vtag)
i = md5(vtag)
print(i)
iv = i[8:24]
print(iv)
new_key = md5('fe34dd6bbd3020c2fb69abe73b5b973c')
dds = AES_Decrypt(new_key.encode('utf8'),
                  resp.text,
                  iv.encode('utf8'))
print(dds)

到这里接口的解密基本就完成了,可以获取app的视频基础信息了。

最后一米


虽然现在视频列表数据已经有了,但是在视频信息中并没有播放地址。所以最后的工作就是获取视频的播放地址。继续抓包可以看到视频地址信息为:https://api.bdxxo.cn/v1/video/info/67432?payload=hAnCu1kQHy0hCrdZo4swYQ%3D%3D.BrHBQxJsA%2BevWaHMbZYNjOj6B7kDZk98IbSJ94j2EhhuMqcY9Rkv37MRgcYIzprPpxX0VJKcAc4sGAIG%2FtgQ3ZW%2FsnDAC%2FdUtA7Y2AfafmRsjxAhzazlbpOo6AXlh0WD91CaE7D%2FymW129p%2Fx5xMJc8NWvaRBGmQSQLIsle0hdipXQKeOKXN3RBbVLv143p7wOLcabVOhYK22AMBucZl0dCYo7Nz1%2Bv2UH8AlaiMIkwwa6JPnZW8CQayhJrrEXU91phsb%2Bam8zNr9CIvSfTxgUaI%2BOXryyt%2BEsmyBMm2CMBUUD52Q95HBrw0rSgRQSxFurjKQtgckxQqVyshwDnK%2F884URbCKU9WouvTjpEnHdc%3D
使用上面分析的数据,对于请求进行解密, 并且模拟请求:

def video_get_detail_test(video_id):
    request_key = base64.decodebytes(bytes('euZN1Gg3JIwWOEWhmE7C4l5dSSRU34fyuPMXjtuoqVs=', encoding='utf8'))
    request_iv = b'\x49\x09\x3E\x49\x6D\x29\x50\xBB\xF1\x67\x9C\x5D\x52\x77\xBF\x4E'
    request_iv = base64.decodebytes(bytes('HTpKwS4MVfB2pktFSGRzvw==', encoding='utf8'))

    # {"an_stream":"https://tv-as.00ph.cn","timestamp":"1593568332","an_quality":"240","token":"eyJ1c2VyX2lkIjoyMTg4MjU2NCwibGFzdGxvZ2luIjoxNTkzMzI3NzQyfQ.c39375da9af6cf24aae0349c4f0b5641.9b0e72bc9a1eea26114dc955d730603e4c11f865d43ea9595d9fd29c","path":"v1/video/info/65696"}
    ss = '{"an_stream":"https://tv-as.00ph.cn","timestamp":"' + str(int(
        time.time())) + '","an_quality":"240","token":"eyJ1c2VyX2lkIjoyMTg4MjU2NCwibGFzdGxvZ2luIjoxNTkzMzI3NzQyfQ.c39375da9af6cf24aae0349c4f0b5641.9b0e72bc9a1eea26114dc955d730603e4c11f865d43ea9595d9fd29c","path":"v1/video/info/' + video_id + '"}'

    ds = AES_Encrypt_raw(request_key, ss, request_iv)
    print(ds)
    payload = 'HTpKwS4MVfB2pktFSGRzvw==.' + ds
    base_url = 'https://api.bdxxo.cn/v1/video/info/' + video_id + '?payload=' + urllib.parse.quote(payload)

    print(base_url)
    resp = requests.get(base_url)
    print(resp.text)

    # 1216557403 x-vtag
    response_headers = resp.headers
    vtag = response_headers.get('x-vtag')
    print(vtag)

    i = md5(vtag)
    print(i)
    iv = i[8:24]
    print(iv)
    new_key = md5('fe34dd6bbd3020c2fb69abe73b5b973c')
    dds = AES_Decrypt(new_key.encode('utf8'),
                      resp.text,
                      iv.encode('utf8'))
    print(dds)

返回数据信息:

{
    "status":{
        "code":200,
        "message":"success"
    },
    "response":{
        "video_id":"64852",
        "video_title":"秀人网嫩模龙泽美曦宾馆与土豪援交!被玩到尖叫 汁液狂流",
        "actor":[
            "素人"],
        "video_urls":{
            "240":"https:\/\/tv-as.00ph.cn\/media\/240\/64852.m3u8?expire=1593570576&hash=4f11da5357d1b89828a952984fef1177",
            "480":"https:\/\/tv-as.00ph.cn\/media\/240\/64852.m3u8?expire=1593570576&hash=4f11da5357d1b89828a952984fef1177"
        },
        "cover_url":"\/tv_adult\/avid5ba324d210218.jpg?k=166aea03d3630b419d8f23ec3078202a&t=1593569676",
        "cover":"\/tv_adult\/avid5ba324d210218.jpg?k=166aea03d3630b419d8f23ec3078202a&t=1593569676",
        "thumb":"\/tv_adult\/avid5ba324d210218.jpg?k=166aea03d3630b419d8f23ec3078202a&t=1593569676",
        "upload_date":1593351002,
        "release_date":0,
        "video_duration":1260,
        "video_like":false,
        "video_publisher":"",
        "video_number":"avid5ba324d210218",
        "video_category":[
            "小視頻"],
        "video_tags":[
            "小视频",],
        "video_description":"",
        "status":2,
        "open_date":1593351002
    }
}

到这里全部的数据接本就都有了,不过还有最后一点需要处理那就是返回的m3u8文件也是加密的,需要进行解密。解密方式与其他请求的解密方式一致。
不仅如此,返回的播放列表的地址也是带有效期参数。
对于播放地址,请求之后进行解密就可以看到m3u8文件的全部内容了:

#EXTM3U
#EXT-X-VERSION:4
#EXT-X-PLAYLIST-TYPE:VOD
#EXT-X-INDEPENDENT-SEGMENTS
#EXT-X-TARGETDURATION:6
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-KEY:METHOD=AES-128,URI="https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/key.php",IV=0xB3DFF72D36D4408E5B4CDF180B9EE03B
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-0.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-1.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-2.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-3.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-4.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-5.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-6.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-7.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-8.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-9.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-10.ts
#EXTINF:6.000000,
https://stream.00ph.cn/tv_adult/avid5ba324d210218/240/ts/avid5ba324d210218-11.ts

解密后的m3u8文件就可以扔给ffmpeg下载了:
ffmepg

总结

  1. apk整体的数据加密做的比较全,如果要获取数据需要对全部的数据进行分析拆分。否则无法正常展示相关信息
  2. 所有的数据都存在有效期,过了有效期之后将无法访问,于是要爬取这个app的数据需要本地存储部分数据,
    • 解密后的图片资源
    • 解密后的m3u8文件
  3. 可能还有其他的数据需要解密,这个感兴趣的自己去处理吧。这里就不写了,处理方式可以参考上面的方法。
  4. 分析了一些app和网站,这个算是数据加密做的比较彻底的,基本服务器返回的数据全部进行加密了,没有任何明文的内容。安全意识不错。

apk文件下载地址: ZnUyLmxpdmUv

最后于 1天前 被obaby编辑 ,原因:


文章来源: https://bbs.pediy.com/thread-260422.htm
如有侵权请联系:admin#unsafe.sh