抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

起因

昨天很多人 @我 说:F 上新版了,Flutter 写的,还有 Web 端,关键是 ReFLV 中对 F 的解密失效了;于是昨天睡前下了个新版看了下:本来看到 Flutter 心凉了半截,感觉大规模重写可能会作大修改,毕竟上次 F 的加密还是挺麻烦的,结果和老版相比竟总体变化不大,依旧是魔改的 ijkplayer ver.0.8.8 - ffmpeg ver.3.4,鉴于之前已经制作了 Til 文件,预计分析起来会快很多,所以决定搞一下。

F 的初步分析

分析 FLV 文件

Wireshark 抓个 rtmp 链接,或者用 Web 端 IDM 会自动抓到 http-flv 的源,下载一会,打开:

注意到:tag header 部分是完整未修改的,解析的结果也都是正确的;tag data 部分是完全加密的,整个部分完全解析错误了。

由此可知,解密过程必然在 Demuxer 及之前,因为解复用时如果拿不到完整的 Video/Audio Tagheader 必然失败。所以解复用及之前的逻辑是关注的重点,主要是:ijkplayer 中调用 av_read_frame 及其之前。

导出表中的加密

根据上次经验,首先尝试 frida-trace 导出表中的各种加密,打开一个直播然后执行:

1
2
3
4
5
6
7
frida-trace -UF -a "libfqffmpeg.so!*decry*"
frida-trace -UF -a "libfqffmpeg.so!*EVP*"
frida-trace -UF -a "libfqffmpeg.so!*aes*"
frida-trace -UF -a "libfqffmpeg.so!*AES*"
frida-trace -UF -a "libfqffmpeg.so!*DES*"
frida-trace -UF -a "libfqffmpeg.so!*hmac*"
frida-trace -UF -a "libfqffmpeg.so!*RSA*"

毫无结果。

还在 Demuxer 中吗?

接下来看看解复用过程中是否存在解密,通过静态和动态分析,发现 Demuxer 时传入的 pb 已经是解密好的 tag data 了。

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
// get the caller of a native function
function Caller(baseAddr, funAddr, context, fun) {
console.log(
fun + " Base address: " + baseAddr + " funAddr: " + funAddr + " ==> " +
Thread.backtrace(context, Backtracer.ACCURATE).map(DebugSymbol.fromAddress).join("\n")
);
}

// get tag data from AVFormatContext
function read_pb_from_AVFC(ptr) {
// start is NativePointer of tag data
let start = ptr.add(0x10).readPointer().add(0x04).readPointer();
let datasize = (start.add(1).readU8() << 16)
+ (start.add(2).readU8() << 8)
+ start.add(3).readU8();
console.log(
hexdump(start, {length: datasize + 11, header: false})
);
}

function hookfqff() {
let baseAddr = Module.findBaseAddress("libfqffmpeg.so");

/////// Demuxer 获取到的是 解密 之后的明文

// flv_read_packet
Interceptor.attach(baseAddr.add(0x1fda15), {
onEnter: function (args) {
Caller(baseAddr, baseAddr.add(0x1fda15), this.context, "flv_read_packet")
read_pb_from_AVFC(args[0])
},
});

// ff_read_packet
let ff_read_packet_ptr = Module.findExportByName("libfqffmpeg.so", "ff_read_packet")
Interceptor.attach(ff_read_packet_ptr, {
onEnter: function (args) {
Caller(baseAddr, ff_read_packet_ptr, this.context, "ff_read_packet")
read_pb_from_AVFC(args[0])
}
})

// read_frame_internal
Interceptor.attach(baseAddr.add(0x235039), {
onEnter: function (args) {
Caller(baseAddr, baseAddr.add(0x235039), this.context, "read_frame_internal")
read_pb_from_AVFC(args[0])
},
});

// av_read_frame
let av_read_frame_ptr = Module.findExportByName("libfqffmpeg.so", "av_read_frame")
Interceptor.attach(av_read_frame_ptr, {
onEnter: function (args) {
Caller(baseAddr, av_read_frame_ptr, this.context, "av_read_frame")
read_pb_from_AVFC(args[0])
}
})
}

Java.perform(function () {
hookfqff();
});

对照源码,经过细致的静态分析,发现确实不存在魔改。基于此,其解密过程并不在解复用过程中。

那接下来,怀疑的目标有两个,分别是 avformat_open_inputread_threadavformat_open_inputav_read_frame 之间的部分;因第二部分在 ffplayer 中,先来看第一部分,其在 ffmpeg 中,分析起来更方便,这部分主要是看 rtmp 协议在 FFmpeg 中的调用流程。

RTMP Protocol

不废话了,直接上调用图:

黄色虚线框之外的部分,都处于 rtmp 协议中 packet 读取之前,包括 handshark 和 connect 等过程,黄色框中才是循环读取 rtmp 流中的 packet,所以框外的部分必然无法做手脚,否则协议变更 rtmpdump 不会下载成功,框内部分需要细心查找,很快发现关键位置,在 get_packet 中:

最终只是一个简单的异或罢了。

想来也合理,如果不加密,很容易被 CDN 拦截,用户抓取也很方便,但原版那种复杂的双端分别加解密,会使直播延迟时间大幅增长,这种简单的在读取 packet 时做异或才是真的经济实惠的方式。

细节

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
elif self.TAGheader.tagtype == 9:
self.parse_video_header()
if self.VideoTAGheader.codecid == 7 or self.VideoTAGheader.codecid == 12:
pass
elif self.VideoTAGheader.codecid == 8 or self.VideoTAGheader.codecid == 13:
dec_data = bytearray(self.TAG.data)
for i in range(2, self.TAGheader.datasize):
dec_data[i] = dec_data[i] ^ dec_data[i - 1]
dec_data[0] = self.TAG.data[0] - 1
dec_data[1] = 1
self.TAG.data = dec_data
logging.debug(f"Tag {self.serial}: has been processed {self.TAGheader.datasize}bytes!")
else:
logging.fatal(f'UNKNOWN CODECID: {self.VideoTAGheader.codecid} at {self.serial}th packet\n'
f'{hexdump(self.TAG.data, result="return")}')
self.reset_timestamp(self.VideoTAGheader.frametype == 1)

rtmpdump

rtmpdump 下载 rtmp 时由于某些不可知原因,会自动在 FLV header 之后添加一个可删除的 Metadata,这将导致 split 失败,可以避免使用 rtmpdump 或者去除代码中的 split 逻辑。

评论