第三层加密
接着上一篇,来看看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]
[/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]
[/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的实现