ts帧加密案例(一)
一、前言
最近比较忙,期末了,抽了点空写了下(抽了半天复习了一下,然后就考试了),写这个主要是总结一下学的知识,做个记录,预计两到三个案例,争取春节前写完。
看本文之前先学习一下下面几篇,了解一下基础知识
FLV:不许动手动脚
m3u8的ts文件的PES加解密分析以及示例
某德地图矢量瓦片逆向(快速wasm逆向),c/c++/c#可调用,执行wasm2c翻译出来的c代码一
二、配置相关环境
以win10为例
配置FFmpeg
下载FFmpeg,在这里https://github.com/BtbN/FFmpeg-Builds/releases,选择ffmpeg-master-latest-win64-gpl-shared.zip
解压(最好不要有中文),添加到环境变量
测试环境
配置C语言
略
下载wabt工具包
https://github.com/WebAssembly/wabt/releases/tag/1.0.34
添加到环境变量,略
环境测试
我用的clion,其他工具应该差不多
创建一个项目,c和c++无所谓
在CMakeLists.txt里面添加
set(FFMPEG_DIR D:\\ffmpeg)#ffmpeg的路径
include_directories(${FFMPEG_DIR}\\include)
link_directories(${FFMPEG_DIR}\\lib)
link_libraries(avcodec avformat avutil )
运行下面代码,输出hello 52pojie,没有报错说明没有问题了
#include <string.h>
/*
for c
#include "libavutil/log.h"
#include "libavutil/aes_ctr.h"
*/
//c++ 必须加extern "C",否则会报错
extern "C" {
#include "libavutil/log.h"
#include "libavutil/aes_ctr.h"
}
int main(void) {
av_log_set_level(AV_LOG_INFO);
char tmp[128] = {0};
char plain[16] = "hello 52pojie";
char result[128] = {0};
int ret = 1;
const uint8_t *iv;
struct AVAESCTR *ae = av_aes_ctr_alloc();
struct AVAESCTR *ad = av_aes_ctr_alloc();
const char *key = "hello 52pojie!!!";
if (av_aes_ctr_init(ae, (const uint8_t *) key) < 0)
{
av_log(NULL, AV_LOG_ERROR, "init error\n");
goto ERROR;
}
if (av_aes_ctr_init(ad, (const uint8_t *) key) < 0)
{
av_log(NULL, AV_LOG_ERROR, "init error\n");
goto ERROR;
}
av_aes_ctr_set_random_iv(ae);
iv = av_aes_ctr_get_iv(ae);
av_aes_ctr_set_full_iv(ad, iv);
av_aes_ctr_crypt(ae, (uint8_t *) tmp, (uint8_t *) plain, sizeof(tmp));
av_aes_ctr_crypt(ad, (uint8_t *) result, (uint8_t *) tmp, sizeof(tmp));
av_log(NULL, AV_LOG_INFO, "%s", result);
ret = 0;
ERROR:
av_aes_ctr_free(ae);
av_aes_ctr_free(ad);
return ret;
}
`
三、怎么判断是否是帧加密
ts加密通常分为整体加密,文件头加密,帧加密以及混合加密。
整体加密
整体加密顾名思义就是对文件整体加密,这是最常见的加密方式。只需要按照188字节一组,判断开头是不是0x47就知道了
文件头加密
只对文件开头一定长度加密或者插入另外文件格式的文件头,盗版网站用的比较多,毕竟白嫖图床谁不喜欢。同样的只需要按照188字节一组,判断第多少组以后是0x47,或者直接看文件头内容
帧加密
帧加密就比较麻烦了,也分为很多种。
首先在学习了参考上面文章后,可以把帧加密分为,整体pes加密,nalu头加密,nalu内容加密。
首先是pes整体加密,有代表性的就是某里系列。
利用010,并下载h264模板来分析。
首先是未加密的ts
然后是加密的ts
很明显看到出来,少了很多nal单元,只有pes头,其余信息都不存在了。
然后是nal头加密,比较常见的就是v13
可以看到能识别出nalu,但是缺少了很多,比如sps,sei,还有这个pps大的离谱了,关键帧也不见了。
最后就是nalu内容加密,直接播放视频就行,有声音花屏,也有可能音频也加密了;
四、某网站ts加密分析
目标网站aHR0cHM6Ly90di5jY3R2LmNvbS8yMDIzLzEwLzMwL1ZJREVBSEdrWnBqMldYcm1oUWV3dzVVMDIzMTAzMC5zaHRtbD9zcG09Qzg0MTExLlBaTzIySm1qTWhKRS5TMTU0NDAuMTE=
js分析
js就不多分析了,就一个wasm加载的js文件是ob类混淆,唯一需要注意的是,流媒体播放基本都是采用多线程,worker来播放,需要手动添加断点替换js文件
断下以后,看看堆栈,根据不同nalu类型判断是否加密,首先实现解析出nalu类型以及长度
FFmpeg解析出pes
创建项目配置CMakeLists.txt和上面一样
首先初始化目录以及设置log,初始化上下文打开文件等
#include "libavcodec/avcodec.h"
#include "libavformat/avformat.h"
AVFormatContext *fmt_ctx = NULL;
const AVInputFormat *fmt = NULL;
const char *filename;
const char *audiofile;
const char *videofile ;
FILE *faudio;
FILE *fvideo;
int video_index = -1;
int audio_index = -1;
void init() {
if (chdir("../") < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot change directory\n");
return;
}
av_log_set_level(AV_LOG_INFO);
fmt_ctx = avformat_alloc_context();
faudio = fopen(audiofile, "wb+");//必须是wb+,否则会多出0x
fvideo = fopen(videofile, "wb+");
}
然后打开输入流,查找相关信息
if (avformat_open_input(&fmt_ctx, filename, fmt, NULL) < 0) {
av_log(NULL, AV_LOG_ERROR, "Cannot open input file\n");
goto END;
}
if (!find_streams()) {
av_log(NULL, AV_LOG_ERROR, "Cannot find video stream or audio stream\n");
goto END;
}
bool find_streams() {
for (int i = 0; i < fmt_ctx->nb_streams; i++) {
if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO) {
video_index = i;
} else if (fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_AUDIO) {
audio_index = i;
}
}
if (video_index == -1 || audio_index == -1) {
printf("find video stream or audio stream failed\n");
return false;
}
//av_dump_format(fmt_ctx, 0, filename, 0); //打印视频信息
return true;
}
分离音视频流,需要注意的是这里的packet并不是完整的pes流,而是纯码流,所以pes整体加密方式这样是不行的。
int split() {
AVPacket *packet = av_packet_alloc();
while (av_read_frame(fmt_ctx, packet) >= 0) {
if (packet->stream_index == video_index) {
fwrite(packet->data, 1, packet->size, fvideo);
} else if (packet->stream_index == audio_index) {
fwrite(packet->data, 1, packet->size, faudio);
}
av_packet_unref(packet);
}
av_packet_free(&packet);
return 0;
}
解析nalu单元
主要参考这个代码cgts_nal_adts_parse.c
int decode_video(AVPacket *packet) {
AVCodecParameters *codecpar = fmt_ctx->streams[video_index]->codecpar;
uint32_t buf_start_pos = 0;
uint32_t nal_start_pos = 0;
uint32_t nal_end_pos = 0;
uint8_t nalu_type;
uint32_t nalu_header_size=3;
while (find_nal_unit(packet->data, packet->size, buf_start_pos, &nal_start_pos, &nal_end_pos)) {
uint32_t len = nal_end_pos - nal_start_pos+1-nalu_header_size;
if (codecpar->codec_id == AV_CODEC_ID_H264) {
uint8_t *nalu_start_ptr = packet->data + nal_start_pos;
nalu_type = find_nal_type_avc(nalu_start_ptr);
} else if (codecpar->codec_id == AV_CODEC_ID_HEVC) {
uint8_t *nalu_start_ptr = packet->data + nal_start_pos;
nalu_type = find_nal_type_hevc(nalu_start_ptr);
}
if (nalu_type==1||nalu_type==25||nalu_type==5){
uint8_t *nal = packet->data + nal_start_pos+nalu_header_size;
}
buf_start_pos = nal_end_pos + 1;
}
return 0;
}
和在线对比一下,部分长度多了1
对应到ts上发现,这部分下一个文件头都是0x00,0x00,0x00,0x01开头的,修改下代码
find_nal_unit
if ( nalu_start_found == true && nalu_end_found == true ) {
(* nal_start_pos) = nalu_start_pos;
(* nal_end_pos) = nalu_end_pos;
return true;
}
if ( nalu_start_found == true && nalu_end_found == true ) {
if (buf[nalu_end_pos] == 0x00 && buf[nalu_end_pos+1] == 0x00 && buf[nalu_end_pos+2] == 0x00 && buf[nalu_end_pos+3] == 0x01) {
nalu_end_pos = nalu_end_pos - 1;
}
(* nal_start_pos) = nalu_start_pos;
(* nal_end_pos) = nalu_end_pos;
return true;
}
这次就一样了
wasm转c
wasm文件下载,大多数网站并不会直接给wasm文件地址,而是以base形式给出,直接搜索data:application/octet-stream;base64,
或者AGFzbQEAAAABm。除此之外,还有某些网站压缩了wasm,文件以br结尾。如果都不行就需要hook对应函数了。或者找到加载部分。
看了上面wasm转c转dll,案例可以知道,我们主要需要的是导入部分。
而web端导出参数,是在加载部分。开发文档介绍中
WebAssembly.Instance()
构造函数以同步方式实例化一个WebAssembly.Module
对象。然而,通常获取实例的方法是通过异步函数WebAssembly.instantiate()
.
很容易就定位到,传入了两个参数,一个wasm文件,一个导入参数
主要需要实现的是env,导入了函数,表,内存以及全局变量等
下面就讲讲如何用c去实现
首先利用wabt里面的wasm2c,将代码转换为c,wasm2c int.wasm -o out.c,然后将wbat工具包目录下的wasm-rt.h、wasm-rt-impl.c、wasm-rt-impl.h,以及转出来的文件放一起。
转出来的文件很大尽量不用动,新创建一个imp.c,方便调试需要调试对应代码,直接从out.c里面复制出来,不然直接卡死了。
然后就是在js端对所有导入函数下断点,判断是否用到或者视必须的。
#include "cctvn.c"
u32 *w2c_env_DYNAMICTOP_PTR(struct w2c_env * v) {
return &v->DYNAMICTOP_PTR;
}
u32 *w2c_env_0x5F_table_base(struct w2c_env *v) {
return &v->__table_base;
}
wasm_rt_memory_t *w2c_env_memory(struct w2c_env *v) {
return &v->memory;
}
static wasm_rt_funcref_table_t table;
wasm_rt_funcref_table_t *w2c_env_table(struct w2c_env * v) {
return &v->table;
}
void w2c_env_0x5F_0x5FsetErrNo(struct w2c_cctv*, u32){
return;
}
/* import: 'env' '___syscall140' */
u32 w2c_env_0x5F_0x5Fsyscall140(struct w2c_cctv*, u32, u32){
return 0;
}
/* import: 'env' '___syscall146' */
u32 w2c_env_0x5F_0x5Fsyscall146(struct w2c_cctv*, u32, u32){
return 31;
}
/* import: 'env' '___syscall54' */
u32 w2c_env_0x5F_0x5Fsyscall54(struct w2c_cctv*, u32, u32){
return 0;
}
/* import: 'env' '___syscall6' */
u32 w2c_env_0x5F_0x5Fsyscall6(struct w2c_cctv*, u32, u32){
return 0;
}
/* import: 'env' '__emscripten_fetch_free' */
void w2c_env_0x5F_emscripten_fetch_free(struct w2c_cctv*, u32){
return;
}
/* import: 'env' '_emscripten_asm_const_ii' */
u32 w2c_env_0x5Femscripten_asm_const_ii(struct w2c_cctv* v, u32 i, u32 i1){
u32 len=1;
uint8_t *d;
switch (i) {
case 0:
case 1:
if (28352 == i1) {
d = (uint8_t *) malloc(1);
memcpy(d, "", 1);
} else if(28384 == i1) {
d=(uint8_t *)malloc(56);
memcpy(d,"https://tv.cctv.com/3cba73e8-4f6c-4d45-a53f-9131c471990a",56);
}
case 2:
if (28480 == i1) {
d = (uint8_t *) malloc(5);
memcpy(d, "blob:", 5);
} else if(28512 == i1) {
d=(uint8_t *)malloc(56);
memcpy(d,"https://tv.cctv.com/3cba73e8-4f6c-4d45-a53f-9131c471990a",56);
}
}
u32 ret = w2c_cctv_0x5Fmalloc(v, len);
memcpy(v->w2c_env_memory->data + ret, d, len);
return ret;
}
/* import: 'env' '_emscripten_get_heap_size' */
u32 w2c_env_0x5Femscripten_get_heap_size(struct w2c_cctv* v)
{
return v->w2c_env_memory->size;
}
/* import: 'env' '_emscripten_is_main_browser_thread' */
u32 w2c_env_0x5Femscripten_is_main_browser_thread(struct w2c_cctv*){
return 0;
}
/* import: 'env' '_emscripten_memcpy_big' */
u32 w2c_env_0x5Femscripten_memcpy_big(struct w2c_cctv* v, u32 dst, u32 src, u32 len){
memcpy(v->w2c_env_memory->data + dst, v->w2c_env_memory->data + src, len);
}
/* import: 'env' '_emscripten_resize_heap' */
u32 w2c_env_0x5Femscripten_resize_heap(struct w2c_cctv*, u32){
return 0;
}
/* import: 'env' '_emscripten_start_fetch' */
void w2c_env_0x5Femscripten_start_fetch(struct w2c_cctv*, u32){
return;
}
/* import: 'env' 'abort' */
void w2c_env_abort(struct w2c_cctv*, u32){
return;
}
/* import: 'env' 'abortOnCannotGrowMemory' */
u32 w2c_env_abortOnCannotGrowMemory(struct w2c_cctv*, u32){
return 0;
}
/* import: 'env' 'getTempRet0' */
u32 w2c_env_getTempRet0(struct w2c_cctv*){
return 0;
}
/* import: 'env' 'jsCall_ii' */
u32 w2c_env_jsCall_ii(struct w2c_cctv*, u32, u32){
return 0;
}
/* import: 'env' 'jsCall_iidiiii' */
u32 w2c_env_jsCall_iidiiii(struct w2c_cctv*, u32, u32, f64, u32, u32, u32, u32){
return 0;
}
/* import: 'env' 'jsCall_iiii' */
u32 w2c_env_jsCall_iiii(struct w2c_cctv*, u32, u32, u32, u32){
return 0;
}
/* import: 'env' 'jsCall_jiji' */
u32 w2c_env_jsCall_jiji(struct w2c_cctv*, u32, u32, u32, u32, u32){
return 0;
}
/* import: 'env' 'jsCall_v' */
void w2c_env_jsCall_v(struct w2c_cctv*, u32){
return ;
}
/* import: 'env' 'jsCall_vi' */
void w2c_env_jsCall_vi(struct w2c_cctv*, u32, u32){
return ;
}
/* import: 'env' 'jsCall_vii' */
void w2c_env_jsCall_vii(struct w2c_cctv*, u32, u32, u32){
return ;
}
/* import: 'env' 'setTempRet0' */
void w2c_env_setTempRet0(struct w2c_cctv*, u32){
return ;
}
void w2c_cctv_f57(w2c_cctv* instance, u32 var_p0, u32 var_p1) {
我这里把一部分w2c_env替换了,主要是懒得复制内存,web也是共享的内存。
然后就是加载wasm,到这里可能就有疑问了,转出来的c和上面案例的格式都不太一样,还多了个参数。
实际上是官方,修改了代码,加入更多特性,案例参考git
参考官方案例
.c
void wasm_init(){
wasm_rt_init();//初始化wasm
wasm_rt_allocate_memory(&cctv_env.memory, 256, 256, false);//分配内存
cctv_env.DYNAMICTOP_PTR = 28144u;//设置DYNAMICTOP_PTR
cctv_env.__table_base = 0;//设置__table_base
wasm_rt_allocate_funcref_table(&cctv_env.table, 160, 160);//分配函数表
wasm2c_cctv_instantiate(&cctv, &cctv_env);//注册一个wasm实例
memcpy(cctv.w2c_env_memory->data + 28144u, data_dynamic_base, 4);//设置DYNAMICTOP_PTR的值,因为是外部导入的内存,所以需要手动设置
//cctv.w2c_env_DYNAMICTOP_PTR = &cctv_env.DYNAMICTOP_PTR;//设置DYNAMICTOP_PTR的指针
}
.h
#include <imp.c>
w2c_cctv cctv;
w2c_env cctv_env;
static const u8 data_dynamic_base[] = {
0x10, 0x6E, 0x50, 0x00,
};
然后就是实现调用函数
uint32_t i=1;
const char *cctva="https://tv.cctv.com";
uint8_t *decrypt( uint8_t *nal,uint32_t nal_len) {
u32 ptr_nal = w2c_cctv_0x5Fmalloc(&cctv, nal_len+1024);
uint32_t a=0;
memcpy(cctv.w2c_env_memory->data + ptr_nal, nal, nal_len);
if (i){
memcpy(cctv.w2c_env_memory->data + ptr_nal+nal_len,cctva,strlen(cctva));
a+=strlen(cctva);
i=0;
}
uint32_t len=w2c_cctv_0x5Fvodplay(&cctv, ptr_nal, nal_len, a);
uint8_t *out_nal = (uint8_t *) malloc(nal_len);
memcpy(out_nal, cctv.w2c_env_memory->data + ptr_nal, len);
w2c_cctv_0x5Ffree(&cctv, ptr_nal);
return out_nal;
}
printf("nalu_type:%d\tnalu_pyload_len:%d\n", nalu_type, len);
if (nalu_type==1||nalu_type==25||nalu_type==5){
uint8_t *nal = packet->data + nal_start_pos+nalu_header_size;
if (len>32){
uint8_t *out_nal = decrypt(nal,len);
memcpy(packet->data + nal_start_pos+nalu_header_size, out_nal, len);
}
}
运行,查看转出的h264,然后
看来还是有哪里不对,那就只有尝试还原了,然后把编译出的exe拖进ida,查看对应函数,然后,就没有然后了
分析是不可能分析的,这辈子都不可能
按下f5,
那就只有靠猜了,利用插件查找加密函数,发现有一个tea
然后在网页中调试,然后就发现func57是tea,func60传入了原始数据地址,解密后地址以及长度,还有个计数的感觉没啥用
那么就可以直接写了
u64 num=0u;
uint8_t *decrypt2( uint8_t *nal,uint32_t nal_len){
u32 in_nal = w2c_cctv_0x5Fmalloc(&cctv, nal_len+1024);
memcpy(cctv.w2c_env_memory->data + in_nal, nal, nal_len);
u32 out_nal = w2c_cctv_0x5Fmalloc(&cctv, nal_len+1024);
uint32_t len=w2c_cctv_f60(&cctv,nal_len, in_nal, out_nal, num);
num++;
uint8_t *out_nal2 = (uint8_t *) malloc(nal_len);
memcpy(out_nal2, cctv.w2c_env_memory->data + out_nal, len);
w2c_cctv_0x5Ffree(&cctv, in_nal);
w2c_cctv_0x5Ffree(&cctv, out_nal);
return out_nal2;
}
运行
终于可以过个跨年夜了,over!!
五、后记
有人可能会问,转出来的h264和acc怎么重新转为ts,直接用FFmpeg就行了,具体网上搜。但我个人不建议在转换为ts了,直接提取所有ts的码流,合并成mp4就行了。因为,你最后ts转mp4还是要先提取码流在合并,除非你是二进制合并。