某网课平台m3u8 key解密算法分析以及python实现
本帖最后由 Zizz 于 2023-10-26 17:58 编辑目标:aHR0cHM6Ly9kYXRhbmcwMS55dW54dWV0YW5nLmNuLw==
播放任意一个视频,F12开发者工具找到keyurl的请求,响应结果中有一个encryptedVideoKey,应该是加密后的key
然后来看一下js,不出意外应该是这个了。
点进去搜索ctrl+F 搜索关键词decrypt,发下有90多个结果
一个个看太费劲了,搜prototype.decrypt(别问为什么搜这个,问就是 经验......)
这样就舒服多了。
ModeOfOperationCBC.prototype.decrypt 这个方法比较像,最后返回的plaintext可能就是解密后的key。
所以在return plaintext处打个断点,然后刷新网页重新播放视频。
但是却没有像预期的那样断下来。显然不是这个并不是解密方法。
那我们就把所有的xxxx..prototype.decrypt方法都打上。重新刷新网页
这一次在ModeOfOperationECB.prototype.decrypt处断了下来。
验证下plaintext是不是解密。
这里使用python将uint8Array转为十六进制字符串,代码如下
Uint8Array =
hex_string = bytes(Uint8Array).hex() # 716c435948704e73584146706654337a
然后使用N_m3u8DL下载:
(这里十六进制key会被N_m3u8DL自动Base64编码)
可以下载。
key解密位置已经找到。下面来分析一下key的具体解密算法以及如何使用python实现(便于批量下载)
https://static.52pojie.cn/static/image/hrline/1.gif
回到刚才,我们看下调用堆栈紧挨着的这个匿名函数
点击进去:
这里我们看到了一些熟悉的东西,videoKeyId , playerId,encryptedVideoKey。也就是keyurl返回的那些,
很显然,这段js代码就是整个加密key到解密key的算法。
下面来分析代码逻辑:
var _encryptedVideoKey = _response.encryptedVideoKey;
将keyrul返回的encryptedVideoKey取出来
var _key = _aes2["default"].utils.utf8.toBytes("72Fhskjglp8qjpqx")
将"72Fhskjglp8qjpqx"传给_aes2["default"].utils.utf8.toBytes()方法生成_key
var _aesEcb = new _aes2["default"].ModeOfOperation.ecb(_key)
创建一个AES.ECB模式的加密器_aesEcb,使用_key进行初始化。
var _bbb = []
for (var i = 0; i < _encryptedVideoKey.length; i += 2) {
_bbb.push(parseInt(_encryptedVideoKey + _encryptedVideoKey, 16)
}
创建空数组_bbb,将十六进制字符串encryptedVideoKey转为字节数组添加到_bbb中
var _decryptedBytes = _aesEcb.decrypt(_bbb)
使用_aesEcb.decrypt方法对_bbb解密得到解密后的key
这一段代码使用python实现:
写累了................
以为发布了可以设置成仅自己自己可见,好像并不行,那就先这样吧,有时间再继续写。
第一次写文章,真累{:301_972:}
https://static.52pojie.cn/static/image/hrline/1.gif
接着来看这部分代码:
var _encryptedVideoKey = _response.encryptedVideoKey;
// 将keyrul返回的encryptedVideoKey取出来
var _key = _aes2["default"].utils.utf8.toBytes("72Fhskjglp8qjpqx");
/* 将"72Fhskjglp8qjpqx"传给_aes2["default"].utils.utf8.toBytes()方法生成_key
其实也就是将字符串72Fhskjglp8qjpqx转为字节 */
var _aesEcb = new _aes2["default"].ModeOfOperation.ecb(_key);
// 创建一个AES.ECB模式的加密器_aesEcb,使用_key进行初始化。
var _bbb = [];
for (var i = 0; i < _encryptedVideoKey.length; i += 2) {
_bbb.push(parseInt(_encryptedVideoKey + _encryptedVideoKey, 16)}
/* 创建空数组_bbb,将十六进制字符串encryptedVideoKey转为字节数组添加到_bbb中
其实就是将十六进制字符串key切片成每两个字符一组,再将每组字符转为十六进制整数*/
var _decryptedBytes = _aesEcb.decrypt(_bbb);
var _decryptedText = _aes2["default"].utils.utf8.fromBytes(_decryptedBytes);
/* 调用刚刚的生成的加_aesEcb下的decrypt方法将_bbb解密。得到解密后的key。
这个decypt方法也就是刚刚我们断下的ModeOfOperationECB.prototype.decrypt方法 */
python实现:
_encryptedVideoKey= '848e11b61279fd861d0b7d4805906ce4'
_key = list(b'72Fhskjglp8qjpqx')
aesEcb = ModeOfOperation(_key)
_bbb = []
for j in range(0,len(enc_key),2):
_bbb.append(int(enc_key,16))
key_intArray = aesEcb.decrypt(_bbb)
decypt_key_str = bytes(key_intArray).decode('utf-8') # qlCYHpNsXAFpfT3z
缺什么找什么,
我们进入_aes2["default"].ModeOfOperation.ecb方法(进入方法如下图,鼠标选中这个方法在弹出的页面点击FunctionLocation后面的链接)
可以看到它是一个类
也就是我们我们刚刚代码里提到的,_key实例化了这个ModeOfOperationECB类。然后又调用了ModeOfOperationECB类下的decrypt方法对_bbb去解密。
我们先用python写一下相关代码,然后再去分析下一步。
此处python代码实现:
class ModeOfOperation:
def __init__(self,key):
self.key=key
self.aes =AES(self.key) # 这里的aes是另外一个类的实例化
def decrypt(self,ciphertext):
plaintext = * len(ciphertext)
block = * 16
for i in range(0, len(ciphertext), 16):
copy_array(ciphertext, block, 0, i, i + 16)
block = self.aes.decrypt(block)
copy_array(block, plaintext, i)
return plaintext
def encrypt(self,ciphertext):
pass
# 用不到,不做分析
我们继续进入AES这个类内部。需要先在this._aes = new AES(key)处打个断点,再用上文的进入方法
AES内部:
AES类下同样有很多方法_prepare() ,encrypt ,decrypt等,其中只有一部分我们需要用到。
如:_prepare()就是它的实例化方法(实际上它的作用就是生成this._Kd和this._Ke,后续解密需要用得到这两个参数),
decrypt()方法实际上在ModeOfOperationECB类下的decrypt方法被调用。此处不贴图了,可以回头看一下上面的图片。
接下来就是用python去写AES类,过程太复杂,这里直接贴代码了。
其中涉及到的S,Si,T1-T8,U1-U4等常量,由于长度原因这里不贴出来了,js里都有。
class AES:
def __init__(self,key):
self.key = key
self.Ke = []
self.Kd = []
self.prepare()
def prepare(self):
key_len = len(self.key)
rounds = 10
for i in range(11):
self.Ke.append()
self.Kd.append()
roundKeyCount = 44
kc = 4
tk = convert_to_int32(self.key)
index = None
for i in range(kc):
index = i >> 2
self.Ke = tk
self.Kd = tk
rconpointer = 0
t = kc
tt = None
while t < roundKeyCount:
tt = tk
tk ^= S << 24 ^ S << 16 ^ S << 8 ^ S ^ rcon << 24
rconpointer += 1
if kc != 8:
for _i in range(1, kc):
tk ^= tk
else:
for _i2 in range(1, kc // 2):
tk ^= tk
tt = tk
tk ^= S ^ S << 8 ^ S << 16 ^ S << 24
for _i3 in range(kc // 2 + 1, kc):
tk ^= tk
i = 0
r = None
c = None
while i < kc and t < roundKeyCount:
r = t >> 2
c = t % 4
self.Ke = tk
self.Kd = tk
i += 1
t += 1
for r in range(1, rounds):
for c in range(4):
tt = self.Kd
self.Kd = U1 ^ U2 ^ U3 ^ U4
# 这里字节序原因会导致与js生成的结果不一致,但是不影响最终计算
def decrypt(self,ciphertext):
rounds = len(self.Kd) - 1
a =
t = convert_to_int32(ciphertext)
for i in range(4):
t ^= self.Kd
for r in range(1, rounds):
for i in range(4):
a = T5 >> 24 & 255] ^ T6 >> 16 & 255] ^ T7 >> 8 & 255] ^ T8 & 255] ^ self.Kd
t = a[:]
result = * 16
tt = None
for i in range(4):
tt = self.Kd
result = (Si >> 24 & 255] ^ tt >> 24) & 255
result = (Si >> 16 & 255] ^ tt >> 16) & 255
result = (Si >> 8 & 255] ^ tt >> 8) & 255
result = (Si & 255] ^ tt) & 255
return result
最终结果验证:
这个应该是阿里云加密吧,我记得有一个aliyun-m3u8-downloader下载器可以直接使用 才发现审核过了,那明天接着更新后面的部分。{:301_971:} 为什么为什么为什么这么diao yunxuetang 律师函安排;www ameiz 发表于 2023-10-23 13:27
yunxuetang 律师函安排
怎么知道是yunxuetang的,楼主一开始的目标是网址吗?
最近公司的网络学院变成的yuanxuetang的,正好在研究自动刷课{:1_937:} GoldenGodwin 发表于 2023-10-23 15:58
怎么知道是yunxuetang的,楼主一开始的目标是网址吗?
最近公司的网络学院变成的yuanxuetang的,正好在 ...
你试一下那堆字符串base64解码 某浪的key能不能搞出来? 虽然看不懂,也要学习一下 Python代码没有贴上来