2024年为止最难的非商业DRM视频加密分析,某利威DRMV13七层加密模型(第二篇)

查看 326|回复 10
作者:漁滒   
@TOC
第三层加密
接着上一篇,来看看m3u8的内容


18.jpg (233.54 KB, 下载次数: 0)
下载附件
2024-4-17 10:46 上传

这里key的地址,iv的值,以及pts的完整地址都已经给出来了,但是key的地址直接访问会出现响应码400,那么查看一下网页是怎么请求key的


19.jpg (43.13 KB, 下载次数: 0)
下载附件
2024-4-17 10:46 上传

可以看到m3u8里面的链接相当于实际请求的缺少了两部分。前面部分是固定值,直接加上就可以,后面的token是基于网页的api返回的,pid是一个随机数。
因为不同网站获取token的api请求各不相同,所以这里不对具体网站分析,这里假设已经通过api获得了token,拼接后请求就可以获取一个32字节长度的结果


20.jpg (45.41 KB, 下载次数: 0)
下载附件
2024-4-17 10:46 上传

但是一般来说,密钥的长度都是16字节,这个32字节长度很有可能就是加密了,接着分析分析如何解密。根据以往多个版本的惊讶,key的解密都是在wasm中的,所以猜测在v13版本中也不例外
播放视频后查看网页代码结构,发现果然后wasm被加载了,并且是在lib_player.js这个js中加载的


21.jpg (14.91 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

还是用刚刚的方法,直接用浏览器的替换功能,在代码的ccall中下debugger
为什么在这里下呢?因为大部分情况下,js层调用wasm层代码,都会通过这个函数封装后调用,这是为了更加方便的调用wasm函数


22.jpg (94.73 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

刷新后运行,又出意外了,又没有断下来。好家伙,说明没有用到这个函数,那么就是自己处理了字符串,wasm内存,字符串指针的关系。那么这时候又肯定会调用到malloc函数,在这个函数下断点。
部分wasm可能会抹除符号,但是这个没有,可以直接搜索到


23.jpg (42.89 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

再次刷新运行,这次可以断下了,但是现在调用栈还在【initRuntime】阶段,先不用管继续放它运行


24.jpg (102.28 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

放行两次后,就已经到了不是【initRuntime】阶段,返回上一次调用栈查看


25.jpg (112.3 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

这是一个【openDecode】的函数,目测是用来绑定wasm中解码后音视频处理的js层函数,这里没有与key操作相关的,那么就先不管,继续运行。下一次断下就进入到【decryptW】函数,这里看起来就有与key相关的了,那么就可以将前面的malloc的断点去掉,在【decryptW】函数下断点


26.jpg (84.41 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

重新刷新到decryptW函数断下


27.jpg (70.75 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

根据调用栈查看,可以很容易知道e是请求的pts数据,t是一个回调函数,n是分片的序号,r是版本固定为2
里面出了对token进行一个简单的计算,其他都是为了将pts,key,iv,seed_const等数据复制到wasm内存中,最重要的是最后调用了【_sendData】函数,如果继续跟进函数的话,会发现进入到wasm内部了


28.jpg (90.71 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

此时就需要分析wasm了,但是现在还需要解决两件事,wasm文件在哪里?入参是什么?
首先搜索【wasmBinaryFile】,可以看到一段base64编码的字符串,这一段字符串就是wasm二进制的base64编码,直接解码就可以得到wasm的二进制文件,然后使用wasm一键转o工具转为o文件就可以使用ida分析


29.jpg (57.11 KB, 下载次数: 0)
下载附件
2024-4-17 10:47 上传

根据js层的调用,_sendData一共有10个参数,分别为
[ol]
  • pts指针
  • iv指针
  • pts长度
  • key指针
  • key长度
  • seed_const
  • token指针
  • token长度
  • pts序号
  • 版本号
    [/ol]
    使用ida打开生成的o文件(文件比较大,可能比较久),找到sendData函数


    30.jpg (57.18 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:47 上传

    为了更加方便的调试分析,修改一下参数类型和参数名


    31.jpg (128.48 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:47 上传

    接着往下,现在的目标是先key做了什么操作,可以看到key传入到了【f2525】函数


    32.jpg (131.58 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    进入到f2525后,只有在一处f2526引用了key


    33.jpg (73.31 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    进入到这里之后,感觉就有种对key做处理的感觉了


    34.jpg (116.29 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    此时就可以在浏览器中的f2526下一个断点


    35.jpg (89.4 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    此时就需要动静态结合分析,
    58行: 首先有一个key_len > 0的判断,我们传入的key都是32位的,所以这里必然会执行内存复制
    60行: 这里执行一个f2705函数,参数是一个定值,在浏览器查看这里的内存


    36.jpg (57.83 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    发现是一个固定字符串【X0iNdstzWb】,同时可以在浏览器中发现返回值是10,也就是这个字符串的长度,那么可以猜测f2705函数就是strlen
    61行: 这里根据上面字符串的长度申请一段新的内存
    62行: 如果上面的字符串存在,则把字符串复制到申请的内存处,但是是逆序复制
    72行: 复制字符串的结尾(c语言中字符串的结尾为字节0)
    74行: 执行了一个f2778函数,传入了一个指针和刚刚复制的字符串,点进去查看猜测是字符串复制,通过浏览器调试发现确实把字符串复制到对应指针处,所以猜测f2778函数就是strcpy
    那么现在先总结一下


    37.jpg (156.19 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    接着往下走就是和前面类似的逻辑了
    75行: 计算固定字符串【aOq3D8】的长度


    38.jpg (70.24 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    76-104行: 申请新内存,复制内存,和前面逻辑一样,但是复制是按照两个字符串为单位逆序复制
    106行:执行了一个f2813函数,里面有strlen和strcpy,可以猜测这个函数就是strcat,拼接后的字符串后面很有可能会用到
    接着继续往下走分析


    39.jpg (142.29 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    107-116行: 复制复制固定偏移(215840)的是数据到v32 + 288处,长度是32字节,其中的第33字节是0,和前面一样表示结尾
    117-137行: 期中偏移(457057)是一个0-9的数字表,每次取seed_const的最低位,可以猜测这里是为了把seed_const从整形转换为字符串型
    138-158行: 因为seed_const是从最低位开始转换的,所以当seed_const长度大于1的时候,需要逆序才是真正的结果
    159行:
    点击进入函数,可以看到调用了多个函数


    40.jpg (123.47 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    因为f2705已经知道是什么函数了,那么进入f2675函数看看


    41.jpg (66.89 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    这是4个md5的标准模数,那么很有可能这就是一个md5函数,那么在浏览器验证一下


    42.jpg (79.03 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    浏览器的入参是字符串的【235】


    43.jpg (77.28 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:48 上传

    浏览器的结果是字符串的【577ef1154f3240ad5b9b413aa7346a1e】


    44.jpg (181.35 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:49 上传

    对比发现这就是标准的md5函数
    161行: 进入到一个f2679函数,里面函数很短,就是对seed_const_md5进行一个简单的变换


    45.jpg (76.1 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:49 上传

    166行: 内存复制到指定的偏移
    167行: 设置字符串的结尾,可以知道上一行复制的内存大概率是一个字符串
    168行: 根据前面的经验已经知道是计算md5了,那么在浏览器查看一下入参是什么


    46.jpg (64.92 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:49 上传

    可以看到入参就是前面经过计算后的token
    170行: 计算token_md5的长度,这个就肯定是返回32了
    171行: 和前面seed_const_md5类似,进入一个变换函数,这个函数稍微长一点,实际是将顺序进行替换,只需要直接记录变化的序号,而序号怎么来的可以不关注
    173-175行: 计算三个字符串的长度并相加
    181行: 执行了一个f2773函数,参数分别是一个指针,上面计算的字符串长度,一个固定字符串,最后一个字符串长度


    47.jpg (70.57 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:49 上传

    根据字符串内容是【%s%s%s】,以及上面三个字符串是连续的,可以猜测这个函数就是snprintf
    182行: 又是一个md5函数,刚好验证一下上面的字符串拼接是否正确,发现确实是是三个字符串的拼接


    48.jpg (67.11 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:49 上传

    183-186行: 复制md5计算的结果。但是出参的地址是【v32 + 16】,但是复制的开始地址是【v32 + 25】,说明偏移了9字节
    189行:运行了一个f2672函数,参数是一个指针和刚刚复制偏移9的字符串
    里面又调用了一个f2671函数,这里有非常多的运算,非常让人头晕,但是有一个偏移(240080)出现了非常多次,难道有什么特别的?


    49.jpg (218.72 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:49 上传

    那么在浏览器看看这里是什么东西


    50.jpg (101.46 KB, 下载次数: 0)
    下载附件
    2024-4-17 10:49 上传

    有一点熟悉啊,这不就是aes的sbox,而这里这么快就用到了sbox,那就是说这里使用的是查表法实现的aes,那么这个f2672函数大概率就是creat_box一类的函数
    190行: 感觉上面,这里的f2674大概率就是轮函数了,里面出现了aes的分组长度16,同时里面的循环调用了f2673,里面也出现了aes的其他常量表,更加确定了这里就是使用aes算法
    这里的参数分别是
    [ol]
  • 通过前面creat_box函数生成的表的指针
  • 密文的长度(这里传入的就是32位的key)
  • iv的指针
  • 密文的指针
  • 明文的指针
    [/ol]
    191行: 这里运行了一个f2681函数,和前面token_md5类似,也是进行了一个变化,也是只需要直接记录变化的序号
    196行: 返回变换后的指针,这里基本就是解密后的key了
    完整的ida伪代码如下
    unsigned int __cdecl w2c_v13_f2526_decrypt_key(w2c_v13 *w2c, int key_ptr, int key_len, int seed_const, int a5, int a6)
    {
      _DWORD *v6; // eax
      __int8 v7; // bl
      __int8 v8; // bl
      __int8 v9; // bl
      __int64 v10; // rax
      __int64 v11; // rax
      __int64 v12; // rax
      __int64 v13; // rax
      _DWORD *v14; // eax
      int v16; // [esp+8h] [ebp-70h]
      __int64 v17; // [esp+30h] [ebp-48h]
      __int64 v18; // [esp+30h] [ebp-48h]
      __int8 v19; // [esp+40h] [ebp-38h]
      __int8 v20; // [esp+40h] [ebp-38h]
      __int8 v21; // [esp+40h] [ebp-38h]
      __int8 v22; // [esp+40h] [ebp-38h]
      u32 v24; // [esp+44h] [ebp-34h]
      u32 v25; // [esp+48h] [ebp-30h]
      int v26; // [esp+4Ch] [ebp-2Ch]
      int v27; // [esp+54h] [ebp-24h]
      __int8 v28; // [esp+54h] [ebp-24h]
      int v29; // [esp+58h] [ebp-20h]
      int v30; // [esp+5Ch] [ebp-1Ch]
      int v31; // [esp+5Ch] [ebp-1Ch]
      u32 v32; // [esp+60h] [ebp-18h]
      __int8 v33; // [esp+64h] [ebp-14h]
      u32 v34; // [esp+64h] [ebp-14h]
      u32 v35; // [esp+64h] [ebp-14h]
      int v36; // [esp+68h] [ebp-10h]
      int v37; // [esp+68h] [ebp-10h]
      __int8 v38; // [esp+68h] [ebp-10h]
      int v39; // [esp+6Ch] [ebp-Ch]
      int v40; // [esp+6Ch] [ebp-Ch]
      int v41; // [esp+6Ch] [ebp-Ch]
      int v42; // [esp+6Ch] [ebp-Ch]
      unsigned int v43; // [esp+6Ch] [ebp-Ch]
      u32 v44; // [esp+6Ch] [ebp-Ch]
      int a2a; // [esp+84h] [ebp+Ch]
      int a2b; // [esp+84h] [ebp+Ch]
      int a2c; // [esp+84h] [ebp+Ch]
      unsigned int a2d; // [esp+84h] [ebp+Ch]
      int a2e; // [esp+84h] [ebp+Ch]
      int _seed_const; // [esp+8Ch] [ebp+14h]
      int a4b; // [esp+8Ch] [ebp+14h]
      int a6a; // [esp+94h] [ebp+1Ch]
      v39 = 0;
      v6 = __emutls_get_address(&__emutls_v_wasm_rt_call_stack_depth);
      if ( ++*v6 > 0x1F4u )
        wasm_rt_trap(8);
      v32 = w2c->w2c_g7;
      w2c->w2c_g7 = v32 + 672;
      v24 = w2c_env_0x5Fllvm_stacksave(w2c->w2c_env_instance);
      v25 = w2c->w2c_g7;
      w2c->w2c_g7 = ((key_len + 15) & -16) + v25;
      if ( key_len > 0 )                            // 长度为32为,必定执行内存复制v25
        w2c_v13_0x5Fmemcpy_0(w2c, v25, key_ptr, key_len);
      v30 = w2c_v13_f2705_strlen(w2c, 456973);      // 固定字符串:X0iNdstzWb
      v27 = w2c_v13_0x5Fmalloc_0(w2c, v30 + 1);     // 申请对应长度的内存
      if ( v30 > 0 )                                // 如果字符串存在,则复制到上面申请的内存处
      {
        for ( a2a = v30; ; --a2a )                  // 逆序复制,既复制结果为bWztsdNi0
        {
          v7 = i32_load8_s(w2c->w2c_env_memory, a2a - 1 + 456973, 0);
          i32_store8(w2c->w2c_env_memory, (v27 + v39++), v7);
          if ( a2a w2c_env_memory, (v27 + v30), 0);// 复制字符串的结尾 字节0
      v26 = v32 + 336;
      w2c_v13_f2778_strcpy(w2c, v32 + 336, v27);    // 字符串复制到指定地方
      v36 = w2c_v13_f2705_strlen(w2c, 456984);      // 固定字符串:aOq3D8
      v40 = w2c_v13_0x5Fmalloc_0(w2c, v36 + 1);     // 申请对应长度的内存
      if ( v40 )
      {
        v31 = v36 - 1;
        if ( v36 > 1 )                              // 如果字符串存在,则复制到上面申请的内存处
        {
          a2b = 0;
          do
          {
            v19 = i32_load8_s(w2c->w2c_env_memory, (a2b | 1) + 456984, 0);// 两个字符串为单位逆序复制,既复制结果为Oa3q8D
            i32_store8(w2c->w2c_env_memory, (v40 + a2b), v19);
            v8 = i32_load8_s(w2c->w2c_env_memory, a2b + 456984, 0);
            i32_store8(w2c->w2c_env_memory, (a2b | 1u) + v40, v8);
            a2b += 2;
          }
          while ( a2b w2c_env_memory, v31 + 456984, 0);
          i32_store8(w2c->w2c_env_memory, (v31 + v40), v9);
        }
        i32_store8(w2c->w2c_env_memory, (v36 + v40), 0);
      }
      else
      {
        w2c_v13_f2833(w2c, &loc_6F924);
        v40 = 0;
      }
      v29 = v32 + 208;
      w2c_v13_f2813_strcat(w2c, v26, v40);          // 拼接字符串,结果为 bWztsdNi0XOa3q8D
      v10 = i64_load(w2c->w2c_env_memory, 215840, 0);// 复制32字节长度数据到对应地址
      i64_store(w2c->w2c_env_memory, v32 + 288, v10);
      v11 = i64_load(w2c->w2c_env_memory, 215848, 0);
      i64_store(w2c->w2c_env_memory, v32 + 288 + 8LL, v11);
      v12 = i64_load(w2c->w2c_env_memory, 215856, 0);
      i64_store(w2c->w2c_env_memory, v32 + 288 + 16LL, v12);
      v17 = i64_load(w2c->w2c_env_memory, &loc_34B38, 0);
      i64_store(w2c->w2c_env_memory, v32 + 288 + 24LL, v17);
      v20 = i32_load8_s(w2c->w2c_env_memory, w2c_v13_establishStackSpace_0, 0);
      i32_store8(w2c->w2c_env_memory, v32 + 288 + 32LL, v20);
      v37 = 0;                                      // seed_const数值转字符串
      for ( a2c = seed_const; ; a2c /= 10 )
      {
        v41 = v37 + 1;
        v28 = i32_load8_s(w2c->w2c_env_memory, a2c % 10 + 457057, 0);
        i32_store8(w2c->w2c_env_memory, (v29 + v37), v28);
        if ( (a2c + 9) = 0 )
      {
        v33 = v28;
      }
      else
      {
        i32_store8(w2c->w2c_env_memory, (v29 + v41), 45);
        v41 = v37 + 2;
        v33 = 45;
      }
      i32_store8(w2c->w2c_env_memory, (v29 + v41), 0);
      if ( v41 > 1 )                                // seed_const逆序
      {
        v42 = v41 - 1 + v29;
        v21 = i32_load8_s(w2c->w2c_env_memory, v29, 0);
        i32_store8(w2c->w2c_env_memory, v42, v21);
        i32_store8(w2c->w2c_env_memory, v29, v33);
        a2d = v32 + 209;
        v43 = v42 - 1;
        if ( v32 + 209 w2c_env_memory, v43, 0);
            v22 = i32_load8_s(w2c->w2c_env_memory, a2d, 0);
            i32_store8(w2c->w2c_env_memory, v43, v22);
            i32_store8(w2c->w2c_env_memory, a2d++, v38);
            --v43;
          }
          while ( a2d w2c_env_memory, v32 + 192, 0);
      a2e = w2c->w2c_g7;
      w2c->w2c_g7 = ((a6 + 16) & -16) + a2e;
      if ( a6 > 0 )
        w2c_v13_0x5Fmemcpy_0(w2c, a2e, a5, a6);     // 内存复制到指定偏移
      i32_store8(w2c->w2c_env_memory, (a6 + a2e), 0);// 字符串结尾
      w2c_v13_f2528_md5(w2c, a2e, v32 + 112);       // 计算token的md5
      i32_store8(w2c->w2c_env_memory, v32 + 144, 0);// token_md5字符串结尾
      v16 = w2c_v13_f2705_strlen(w2c, v32 + 112);
      w2c_v13_f2680(w2c, v32 + 112, v16, v32 + 64); // 对token_md5进行变换
      i32_store8(w2c->w2c_env_memory, v32 + 96, 0); // 变换结果的结尾
      v34 = w2c_v13_f2705_strlen(w2c, _seed_const) + 1;
      v35 = w2c_v13_f2705_strlen(w2c, v32 + 64) + v34;
      a6a = w2c_v13_f2705_strlen(w2c, v26) + v35;
      v44 = w2c->w2c_g7;
      w2c->w2c_g7 = ((a6a + 15) & -16) + v44;
      i32_store(w2c->w2c_env_memory, v32 + 384, v32 + 64);
      i32_store(w2c->w2c_env_memory, v32 + 388, v26);
      i32_store(w2c->w2c_env_memory, v32 + 392, _seed_const);
      w2c_v13_f2773_snprintf(w2c, v44, a6a, &loc_6F986, v32 + 384);// 字符串格式化
      w2c_v13_f2528_md5(w2c, v44, v32 + 16);
      v13 = i64_load(w2c->w2c_env_memory, v32 + 25, 0);// 复制字符串,偏移9位
      i64_store(w2c->w2c_env_memory, v32, v13);
      v18 = i64_load(w2c->w2c_env_memory, v32 + 33, ((v32 + 25) + 8) >> 32);
      i64_store(w2c->w2c_env_memory, v32 + 8LL, v18);
      a4b = w2c->w2c_g7;
      w2c->w2c_g7 = ((key_len + 15) & -16) + a4b;
      w2c_v13_f2672_aes_creat_box(w2c, v32 + 384, v32);// aes查表法
      w2c_v13_f2674_aes_crypt_block(w2c, v32 + 384, key_len, v32 + 288, v25, a4b);
      w2c_v13_f2681(w2c, a4b, v32 + 368);           // 对解密后的内存进行变换
      w2c_env_0x5Fllvm_stackrestore(w2c->w2c_env_instance, v24);
      w2c->w2c_g7 = v32;
      v14 = __emutls_get_address(&__emutls_v_wasm_rt_call_stack_depth);
      --*v14;
      return v32 + 368;                             // 返回解密的key
    }
    对应的python代码
    import base64
    import traceback
    from Crypto.Hash import MD5
    from Crypto.Cipher import AES
    from Crypto.Util import Padding
    def decrypt_key(seed_const, token, enc_key):
        seed_const_md5 = MD5.new((str(seed_const)).encode()).hexdigest()
        left_text = bytes([-(-(each + seed_const - 97) % 26) + 97 if each + seed_const - 97
    参考文献
    1.某视频网站wasm简要分析
    2.wasm一键转c
    3.【密码学】分析crypto-js当中AES的实现

    字符串, 下载次数

  • hsuehly   

    渔歌NB!!!!!!!!
    zm886   

    请教渔歌,遇到一个v12的,是pdx格式的,解密出m3u8和key下载的只有音频应该是什么原因导致的呢
    开心的一逼   

    学习了,谢谢你
    LuckyClover   

    对于这么牛批的分析,我只能献出我的币以示尊敬
    OVVO   

    渔滒我爱你
    rrl88888   

    学习了学习了,感谢大佬的详细分享
    daoye9988   

    非常详细
    bluepeb   

    对你的敬佩无以言表,太NB了!
    1188   

    这TMD才叫精华!
    您需要登录后才可以回帖 登录 | 立即注册

    返回顶部