本帖最后由 wystudio 于 2024-01-05 23:16 编辑
ts帧加密案例(一)一、前言最近比较忙,期末了,抽了点空写了下(抽了半天复习了一下,然后就考试了), 二、配置相关环境以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 dslt,没有报错说明没有问题了 #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 dslt"; 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 dslt!!!"; 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转cwasm文件下载,大多数网站并不会直接给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,查看对应函数,然后,就没有然后了 分析是不可能分析的,这辈子都不可能 那就只有靠猜了,利用插件查找加密函数,发现有一个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还是要先提取码流在合并,除非你是二进制合并。
注:若转载请注明大神论坛来源(本贴地址)与作者信息。
|