吾爱破解 - LCG - LSG |安卓破解|病毒分析|www.52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1364|回复: 6
收起左侧

[Web逆向] ts帧加密案例(二)

[复制链接]
我是不会改名的 发表于 2024-1-19 22:39
本帖最后由 我是不会改名的 于 2024-1-19 22:42 编辑

ts帧加密案例(二)

一、前言

原本还有两篇案例的,基本都写完了,但考虑快过年了,加上最近...,所这篇主要综合两篇案例,讲讲怎么去生成一个加密的视频,并在线播放(web)。

二、前文回顾

2.1.bug修复

上一篇文章中第一次调用导出函数解密失败了,主要是这里长度忘记修改了,导致环境被检测了,修改一下就行了。当然都用c了,直接调用非导出函数也可以的。

1.png

2.2.nal Payload

在上一篇案例中,解密函数(f60 jeb)中有这样一段代码

2.png

含义很简单,就是判断是否存在000003,然后替换为0000。之所以这样的原因是,之前的文章中已经说明了,提取出来的帧,是由很多不同的nalu组成,而分割它们的是000001,那么如果nal原始的负载中出现000001,那么就会导致出错,所以引入了EBSP(Encapsulated Byte Sequence Payload)

  • 0x000000-->0x00000300

  • 0x000001-->0x00000301

  • 0x000002-->0x00000302

  • 0x000003-->0x00000303

所以需要注意这里和标准的代码并不完全一样,下面是直播部分(ida),这里和标准文档就一样了

3.png

三、基本环境配置

3.1.hls下载

使用前温馨提示

  1. 调试web的时候刷新的时候请按住 Ctrl+F5 键,不要只按F5
  2. 有时候代码没问题,但就是播放失败,甚至封面都解析出来了,拖中间就不行,需要在设置中清除缓存,然后重启浏览器

​                浏览器地址输入      edge://settings/clearBrowserData  或者 chrome://settings/clearBrowserData

​                只需要清除缓存的图像和文件就行了

下载地址 :https://github.com/video-dev/hls.js

git clone https://github.com/video-dev/hls.js.git
cd hls.js
# After cloning or pulling from the repository, make sure all dependencies are up-to-date
npm install ci
# Run dev-server for demo page (recompiles on file-watch, but doesn't write to actual dist fs artifacts)
npm run dev
# After making changes run the sanity-check task to verify all checks before committing changes
npm run sanity-check

克隆下来以后先不要运行,改一下cdn,以及log替换为静态的,不然很卡,或者挂梯子。

4.png

同时修改一下这里,把worker关闭,方便调试一点

5.png
然后运行,播放最后一个视频,或者直接点击,能正常播放并且配置文件没问题就行

6.png

3.2.bento4下载

下载网站:https://www.bento4.com/downloads/

下载源码因为需要简单分析修改: Source Snapshot (all platforms)

下载完成后,不出意外的话人家配置好了的,可以打开项目,修改一下工作目录,改成之前下载的hls项目中

7.png
然后简单修改一下开头代码,不用解析命令行部分,方便调试,手动或者自己用程序创建存放ts,key,m3u8位置,然后直接运行

    Options.verbose                        = false;
    Options.hls_version                    = 5;
    Options.pmt_pid                        = 0x100;
    Options.audio_pid                      = 0x101;
    Options.video_pid                      = 0x102;
    Options.audio_track_id                 = -1;
    Options.video_track_id                 = -1;
    Options.audio_format                   = AUDIO_FORMAT_TS;
    Options.output_single_file             = false;
    Options.show_info                      = false;
    Options.segment_duration               = 10;
    Options.segment_duration_threshold     = DefaultSegmentDurationThreshold;
    Options.pcr_offset                     = AP4_MPEG2_TS_DEFAULT_PCR_OFFSET;
    AP4_SetMemory(Options.encryption_key, 0, sizeof(Options.encryption_key));
    AP4_SetMemory(Options.encryption_iv,  0, sizeof(Options.encryption_iv));
    AP4_SetMemory(&Stats, 0, sizeof(Stats));
    AP4_Result result;
    Options.allow_cache="YES";
    Options.encryption_key_hex="000102030405060708090a0b0c0d0e0f";
    AP4_ParseHex(Options.encryption_key_hex, Options.encryption_key, 16);
    Options.encryption_iv_mode = ENCRYPTION_IV_MODE_RANDOM;
    AP4_System_GenerateRandomBytes(Options.encryption_iv, 16);
    Options.index_filename="test.m3u8";
    Options.encryption_key_uri = "./key/test.key";
    Options.segment_filename_template="./ts/test-%d.ts";
    Options.segment_url_template="./ts/test-%d.ts";
    Options.encryption_mode=ENCRYPTION_MODE_SAMPLE_AES;
    Options.input="10编译FFmpeg(WebAssembly版)库.mp4";
    Options.hls_version = 3;
    FILE* key_file = fopen(Options.encryption_key_uri, "wb");
    if (key_file == NULL) {
        fprintf(stderr, "ERROR: cannot open key file for writing\n");
        return 1;
    }
    unsigned char key[16];
    AP4_ParseHex(Options.encryption_key_hex, key, 16);
    fwrite(key, 1, 16, key_file);
    fclose(key_file);
    AP4_ByteStream* input = NULL;
    result = AP4_FileByteStream::Create(Options.input, AP4_FileByteStream::STREAM_MODE_READ, input);
    if (AP4_FAILED(result)) {
        fprintf(stderr, "ERROR: cannot open input (%d)\n", result);
        return 1;
    }
        // open the file
    AP4_File* input_file = new AP4_File(*input, true);

然后用hls去播放视频,能正常播放说明没有问题

9.png

四、源码简要分析

4.1.Mp42Hls加密流程

这里主要分析和加解密相关的,解复用之类的就不过多介绍。

  1. 首先是这里,设置了key以及随机的iv,然后设置了加密方式,目录支持不加密,整体aes加密以及SAMPLE_AES
    Options.encryption_key_hex="000102030405060708090a0b0c0d0e0f";
    AP4_ParseHex(Options.encryption_key_hex, Options.encryption_key, 16);
    Options.encryption_iv_mode = ENCRYPTION_IV_MODE_RANDOM;
    AP4_System_GenerateRandomBytes(Options.encryption_iv, 16);
    Options.encryption_mode=ENCRYPTION_MODE_SAMPLE_AES;

SAMPLE_AES是苹果提出来的一种对帧加密的方法,不仅加密视频帧,也加密了音频帧,所以之前生成ts文件,你直接播放没有声音也没有画面,后续我们也主要修改这来实现自己的帧加密。

  1. 然后就是解复用那一套,获取对应的码流,看不懂不重要,直接看怎么写入文件

10.png

  1. 根据不同加密方式设置加密函数,本质上就是一个aes-cbc

11.png
12.png

  1. 选择不同轨道来分别加密,主要看看视频加密部分

13.png

  1. nal加密部分,只加密了nal=1或者5的部分数据,至于为啥不知道苹果这样规定的

14.png

  1. 在后面就是写入文件了,先判断是h264还是h265,然后写入了sps/pps

15.png

  1. 写入了加密后的nalu

  2. 组装pes

    WritePES(pes_data.GetData(), pes_data.GetDataSize(), dts, true, pts, with_pcr, output)
  3. 生成pes头

    unsigned int pes_header_size = 14+(with_dts?5:0);
    AP4_BitWriter pes_header(pes_header_size);
    .....................................................
    pes_header.Write(1, 1);                    // market_bit
  4. 切分nal按188字节组装ts,然后就写入文件

16.png

  1. 一直循环上面操作,完成后就是生成m3u8文件,后面不重要了不分析了。

4.2.hls解密流程

  1. 请求加载文件,一次加载m3u8文件,key文件,ts文件

17.png
其中key加载部分,loadKeyHTTP函数,请求成功后回调函数中把,key赋值给了keyInfo.decryptdata.key = frag.decryptdata.key,

如果key有加密,大部分都在这里解密了,好处是只需要解密一次key,不需要额外修改函数逻辑,坏处就是一眼就看出来了

      loadKeyHTTP = function loadKeyHTTP(keyInfo, frag) {
         ..................
         return keyInfo.keyLoadPromise = new Promise(function (resolve, reject) {
       var loaderCallbacks = {
         onSuccess: function onSuccess(response, stats, context, networkDetails) {
         ......
           keyInfo.decryptdata.key = frag.decryptdata.key = new Uint8Array(response.data);
           frag.keyLoader = null;
           keyInfo.loader = null;
           resolve({
             frag: frag,
             keyInfo: keyInfo
           });
         }
         .....
  1. 然后是ts回调函数,完全看不懂,唯一看起来眼熟的frag,key就存在这里面,然后就进入了transmuxer

    function _handleFragmentLoadProgress(data) {
    var _frag$initSegment;
    var frag = data.frag,

    .........
    transmuxer.push(payload, initSegmentData, audioCodec, videoCodec, frag, part, details.totalduration, accurateTimeOffset, chunkMeta, initPTS);
    };

  2. 后面又是一大堆看不懂的,把key赋值给了decryptdata,然后判断是否开启了worker,就开始解复用了

    unction push(data, initSegmentData, audioCodec, videoCodec, frag, part, duration, accurateTimeOffset, chunkMeta, defaultInitPTS) {
       ................
         var decryptdata = frag.decryptdata;
         this.frag = frag;
         this.part = part;
         if (this.workerContext) {
           // post fragment payload as transferable objects for ArrayBuffer (no copy)
           this.workerContext.worker.postMessage({
             cmd: 'demux',
             data: data,
             decryptdata: decryptdata,
             chunkMeta: chunkMeta,
             state: state
           }, data instanceof ArrayBuffer ? [data] : []);
         } else if (transmuxer) {
           var _transmuxResult = transmuxer.push(data, decryptdata, chunkMeta, state);
           if (isPromise(_transmuxResult)) {
             transmuxer.async = true;
             _transmuxResult.then(function (data) {
               _this2.handleTransmuxComplete(data);
             }).catch(function (error) {
               _this2.transmuxerError(error, chunkMeta, 'transmuxer-interface push error');
             });
           } else {
             transmuxer.async = false;
             this.handleTransmuxComplete(_transmuxResult);
           }
         }
  3. 解复用之前,首先判断是否是ts整体加密

    
         var keyData = getEncryptionType(uintData, decryptdata);
         if (keyData && keyData.method === 'AES-128') {
           var decrypter = this.getDecrypter();
           // Software decryption is synchronous; webCrypto is not
           if (decrypter.isSync()) {
             var decryptedData = decrypter.softwareDecrypt(uintData, keyData.key.buffer, keyData.iv.buffer);
             uintData = new Uint8Array(decryptedData);
  4. 然后判断是否是SAMPLE,进入不同解析流程,解复完成后再进行解密内容

    function transmux(data, keyData, timeOffset, accurateTimeOffset, chunkMeta) {
         var result;
         if (keyData && keyData.method === 'SAMPLE-AES') {
           result = this.transmuxSampleAes(data, keyData, timeOffset, accurateTimeOffset, chunkMeta);
         } else {
           result = this.transmuxUnencrypted(data, timeOffset, accurateTimeOffset, chunkMeta);
         }
         return result;
       };
    function demuxSampleAes(data, keyData, timeOffset) {
         var demuxResult = this.demux(data, timeOffset, true, !this.config.progressive);
         var sampleAes = this.sampleAes = new SampleAesDecrypter(this.observer, this.config, keyData);
         return this.decrypt(demuxResult, sampleAes);
       };
  5. 解复用基本流程就不说了,主要看看这两个函数

    if (videoData && (pes = this.parsePES(videoData))) {
    this.videoParser.parseAVCPES(videoTrack, textTrack, pes, false, this._duration);
    }

  6. 首先是parsePES,这里传入的videoData包含了pes头,返回的数据才是纯nal内容

    return {
             data: pesData,
             pts: pesPts,
             dts: pesDts,
             len: pesLen
           };
  7. parseAVCPES中,先拆分了nal,var units = this.parseAVCNALu(track, pes.data);然后根据不同nalu类型进行处理,主要关注一下等于7的时候,也就是sps,这里设置了分辨率,时长,以及解码器。

    if (!track.sps || track.width !== config.width || track.height !== config.height || ((_track$pixelRatio = track.pixelRatio) == null ? void 0 : _track$pixelRatio[0]) !== config.pixelRatio[0] || ((_track$pixelRatio2 = track.pixelRatio) == null ? void 0 : _track$pixelRatio2[1]) !== config.pixelRatio[1]) {
    track.width = config.width;
    track.height = config.height;
    track.pixelRatio = config.pixelRatio;
    track.sps = [sps];
    track.duration = duration;
    var codecarray = sps.subarray(1, 4);
    var codecstring = 'avc1.';
    for (var _i = 0; _i < 3; _i++) {
    var h = codecarray[_i].toString(16);
    if (h.length < 2) {
    h = '0' + h;
    }
    codecstring += h;
    }
    track.codec = codecstring;
    }
    break;

  8. 后面就是解密函数,基本就和c一样了,唯一区别就是,js直接把加密的数据单独拿出来getAvcEncryptedData,解密后重新写入getAvcEncryptedData

  9. 后面的就和普通视频一样了,就不看了,看了也看不懂

五、实现帧加密

5.1 pes整体加密

前面已经说过了,加密时在WritePES这里传入了不含头的pes,解密时parsePES返回了不含头的pes,所以直接修改对应代码。

Options.encryption_mode=ENCRYPTION_MODE_NONE;//修改为不加密

func WritePES

unsigned int pes_header_size = 14+(with_dts?5:0)+1;//增加一字节,存放是否加密
.......
pes_header.Write(0x10, 8); // is_enc
bool first_packet = true;

简单写个异或,需要注意加密可能存在000001,所以需要填充03,不填充问题应该也不大,就怕运气好,所以还是填一下

unsigned int nalUnescape(uint8_t* nal, unsigned int nalSize, uint8_t* buffer)
{
    unsigned int i = 0;
    unsigned int j = 0;
    while (i < nalSize) {
        if (i < nalSize - 2 && nal[i] == 0x00 && nal[i + 1] == 0x00 && nal[i + 2] <= 0x03) {
            buffer[j++] = nal[i++];
            buffer[j++] = nal[i++];
            buffer[j++] = 0x03;
        } else {
            buffer[j++] = nal[i++];
        }
    }
    return j;
}

    uint8_t * escapedBuffer = (uint8_t *)calloc(1, data_size * 2);
    char xor_key[16]={53,50,112,111,106,105,101,119,115,98,104,103,109,100,104,104};
    for (unsigned int i=0; i<data_size; i++) {
        escapedBuffer[i] = data[i] ^ xor_key[i%16];
    }
    unsigned int escapedSize = nalUnescape(escapedBuffer, data_size,escapedBuffer);
    data_size = escapedSize;
    data = escapedBuffer;
    if (with_dts && (dts == pts)) {
       with_dts = false;
    }

可以看出来生成以后的文件,可以识别到pes头,但是负载全加密了

18.png
然后修改js中的parsePES函数

19.png

播放视频,可以看到和预期结果一样,视频也能正常播放。

20.png

5.2nal内容加密

上面已经提到了内置了一种帧加密模式所以直接在这基础上修改

首先把音频加密部分注释掉,不太熟悉音频

21.png
前面已经提到过了,加密nal时,只加密了1和5,那么简单看下为啥加密1和5,修改代码,分别只加密1或5

​                                                                                                 只加密1

5.gif

​                                                                                                   只加密5

1.gif

可以看出来,只加密5时,画面基本是完整的,但是并不连续,而只加密1时,画面基本不完整。具体涉及到了解码部分,主要是和i,p,b帧有关的,感兴趣可以去了解一下。这里只是展示一下有什么区别,方便快速定位是哪一帧出问题了。type=5就可以当做是i帧(idr),type=1就可以看做p或b帧。简单地讲,I 帧是一个完整的画面,而 P 帧和 B 帧记录的是相对于 I 帧的变化。

那么除了1和5,上面还提到了7,sps里面包含了分辨率以及解码器信息,尝试只加密一下7。这里需要修改一下加密长度,不然加密不了

AP4_UI08 nalu_type = nalu[nalu_length_size] & 0x1F;
if (nalu_length > 16 && (nalu_type==7)) {
    AP4_Size encrypted_size = 16*((nalu_length-1)/16);
    if ((nalu_length%16) == 0) {
        encrypted_size -= 16;
    }

然后就离谱了,居然还能正常播放。

22.png
然后用软件去分析,发现多了一个sps和pps,第一个能正常解析出分辨率,第二个不行

23.png
回想起写入文件的时候先写入了sps/pps,在写入了加密后的nal,说明这里写入的sps是加密前的,或者是直接生成的,那么最简单方法直接注释掉。

24.png
这次视频分辨率就识别不出来了,画面也没有

25.png
然后尝试用hls去播放

首先修改一下src/demux/sample-aes.ts->decryptAvcSamples函数

        if (
          curUnit.data.length <= 16 ||
          (curUnit.type !== 5||curUnit.type !== 1||curUnit.type !== 7)
        ) {
          continue;
        }

然后修改一下getAvcDecryptedUnit以及getAvcEncryptedData

26.png
然后,不出意外的出意外了,主要提示解码器不支持

27.png
之前的分析中,它是先解复用,在解密,而在解复用过程中就已经设置了解码器,所以在这里修改代码是不行的。

28.png

那就先修改c

if (nalu_length > 16 && (nalu_type ==5 ||nalu_type==7||nalu_type==1)) {
    AP4_Size encrypted_size = 16*((nalu_length-1)/16);
    if ((nalu_length%16) == 0) {
        encrypted_size -= 16;
    }
    if (nalu_type==5){
        m_StreamCipher->SetIV(m_IV);
        for (unsigned int i=0; i<encrypted_size; i += 10*16) {
            AP4_Size one_block_size = 16;
            m_StreamCipher->ProcessBuffer(nalu+nalu_length_size+1+i, one_block_size,
                                          nalu+nalu_length_size+1+i, &one_block_size);
        }
    }else if (nalu_type==7){
        encrypted_size=nalu_length-3;
        AP4_UI08 xor_mask[16]={53,50,112,111,106,105,101,119,115,98,104,103,109,100,104,104};
        for (unsigned int i=0; i<encrypted_size; i ++) {
            nalu[nalu_length_size+1+i] ^= xor_mask[i%16];
        }
    }

然后修改js src/demux/video/avc-video-parser.ts parseAVCPES

case 7:
.....
const stack = new Error().stack;
if (stack.includes('SampleAes')) {
  const xor_mask = new Uint8Array([53, 50, 112, 111, 106, 105, 101, 119, 115, 98, 104, 103, 109, 100, 104, 104]);
  const encrypted_size = sps.length - 3;
  for (let i = 0; i < encrypted_size; i++) {
    sps[i + 1] ^= xor_mask[i % 16];
  }
}
const expGolombDecoder = new ExpGolomb(sps);
..............

刷新网页,能正常播放视频了

29.png

六、补充知识

6.1.drm系统组成

看了上面内容,发现其实帧加密也不难,那么为什么drm那么难以破解,那么就不得不提整个流程了,下面是一张很完整的图,详细介绍在w3.org上

30.png

从图上可以看出来,我写的仅仅涉及了最右下角小小的一部分。而真正复杂的,生成请求信息,以及解密响应,以及它内部的解密帧算法,基本全是白盒算法,请求一般是rsa,解密帧是白盒的aes,而不是上面我们自己实现时,用的很明确的aes,在cdm中几乎不会直接出现key。而cdm分为软件和硬件层实现,又完全不是一个东西。所以任重而道远,我能力有限,只能写一点没什么用的东西。

6.2.sdt

在wv以及pr(谷歌和微软的drm),只需要key就能解密,因为他的iv隐藏在第一个文件中,而它们的格式是mp4,那么ts如果想要隐藏iv可以尝试放哪里。

在查看ts生成源码时发现,在写入pat,pmt之前,还有一个可能存在一个sdt表,但是无论是加密还是解密过程中,均没发现,说明没啥用,既然没啥用那我们就可以用一用

31.png

首先是sdt主要组成,可以看到字段很多,但没什么合适的,只有一个free_CA_mode可以来判断是否加密了

32.png
然后就是Descriptor,Descriptor也分很多种具体在官方文档可以找到,比较常见的是Service descriptor,下面就是其主要组成
33.png

而刚好从这张表的描述中可以看到,存在两个可以自定义长度的字符串,那么我们就可以把iv写入这里,前面又提到了这是写在ts开头的,那么还可以每个ts文件一个iv,还可以设置不同加密算法,cbc或者ctr模式切换用,除此之外加密的过程中,160字节只加密了,16字节,剩下的什么都不做太可惜了,还可以让他们和iv进行异或。甚至可以随机设置加密和异或块大小。当然上面只是吹牛,具体的可以尝试自行实现。

7、附件

附件没有代码,只是本地版文档,仅供备份。
https://nicaicai.lanzouo.com/i49kw1llmklc

免费评分

参与人数 8吾爱币 +19 热心值 +8 收起 理由
涛之雨 + 7 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
9lmr + 1 + 1 热心回复!
漁滒 + 3 + 1 我很赞同!
T4DNA + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
OVVO + 2 + 1 我很赞同!
willbe001 + 1 + 1 我很赞同!
iokeyz + 3 + 1 用心讨论,共获提升!
zhoushengzhi + 1 + 1 用心讨论,共获提升!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

wasm2023 发表于 2024-1-21 13:08
楼主yyds,不知道楼主有研究过某盾,某蜂的高强度加密不
xixicoco 发表于 2024-1-22 03:32
头像被屏蔽
tl;dr 发表于 2024-1-22 05:31
WXPDHR 发表于 2024-1-22 20:05
大佬牛牛牛,感谢分享,学到了
zhcxi 发表于 2024-1-30 19:58
学习,虽然目前看的不是很懂
hua111 发表于 2024-2-7 07:59
感谢分享,学到了
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则 警告:本版块禁止灌水或回复与主题无关内容,违者重罚!

快速回复 收藏帖子 返回列表 搜索

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-4-29 05:23

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表