首页 > 其他分享 >H264编码分析及隐写实践

H264编码分析及隐写实践

时间:2024-04-29 11:44:22浏览次数:24  
标签:编码 00 slice H264 data 隐写 type

H264编码分析及隐写实践

目录

CTF竞赛中经常出现图片隐写,视频作为更高量级的信息载体,应当有更大的隐写空间。本文就简单介绍一下H264编码以及一次校赛相关出题经历。

1 视频数据层级

平常我们生活中遇到的大部分是FLV、AVI等等的视频格式,但实际上这只是一种封装,实际的数据还是要看视频的编码,就比如H264。像我们平时在视频网站看到的视频就是通过HTTP协议传输的,直播则是RTMP协议,协议承载的一般是封装后的视频数据。

下图就很好的展示了视频数据的各个层级。

2 H264裸流

得到最原始的视频数据,需要提取H264裸流。

简单介绍一下ffmpeg的用法,不指定格式的情况下,ffmpeg会识别给定文件名的后缀自动进行转换,比如

ffmpeg input.flv output.mp4

就会自动转换为mp4

如何提取一个H264编码的视频裸流呢。

使用以下命令。

ffmpeg -vcodec copy -i input.flv output.h264

默认不加参数的情况,ffmpeg会把视频重新编码,视频数据会发生变化,所以要加上-vcodec copy,指示ffmpeg直接复制视频流,而不是重新编码。

这样得到的h264裸流就是封装格式中的原始数据。

有了H264裸流,可以使用H264Analyzer的工具查看裸流信息。

3 NALU

H.264裸流是由⼀个接⼀个NALU组成。H264NALU的封装有两种方式,一种是AnnexB,一种是 avcC

这里仅介绍AnnexB,对avcC感兴趣的可以看乔红大佬的文章

AnnexB的封装很简单,以00 00 00 01或者00 00 01开头作为一个新NALU的标志,为了防止竞争,即 NALU数据中出现00 00 00 01导致解码器误认为是一个新的NALU,所以采用了一些防竞争的策略。

00 00 00 => 00 00 03 00
00 00 01 => 00 00 03 01
00 00 02 => 00 00 03 02
00 00 03 => 00 00 03 03

看一眼下面的图就很清楚了。

那么也就是说当我们把数据从H264裸流中提取出来之后,还需要对防竞争字节进行还原。

这里的话对这些类型的数据有些定义,详细可以去看乔红老师的文章

  • NALU:去除00 00 00 01标志符的数据
  • EBSP:去除NALU header(通常是第一个字节)但未还原防竞争字节的数据
  • RBSP:将EBSP还原防竞争字节后的数据
一段AnnexB封装的NALU: 
 00 00 00 01  67 64 00 1F AC D9 40 50 05 BB 01 6A 02 02 02 80  00 00 03 00  80 00 00 1E 07 8C 18 CB
NALU: 
67 64 00 1F AC D9 40 50 05 BB 01 6A 02 02 02 80 00 00 03 00 80 00 00 1E 07 8C 18 CB
EBSP: 
64 00 1F AC D9 40 50 05 BB 01 6A 02 02 02 80  00 00 03 00  80 00 00 1E 07 8C 18 CB
RBSP: 
64 00 1F AC D9 40 50 05 BB 01 6A 02 02 02 80  00 00 00  80 00 00 1E 07 8C 18 CB

4 RBSP

现在有了NALU数据,我们就可以对着官方手册上的内容来一步步解码了。

直接看到手册7.3节,这里是表格式的语法,右边的Descriptor描述了数据的格式及占用的bit数,比如第一个f(1)表示1bit fixed-pattern bit string

可以在7.2节找到所有的Descriptor定义

还是拿之间的数据做例子

67 64 00 1F AC D9 40 50 05 BB 01 6A 02 02 02 80 00 00 00 80 00 00 1E 07 8C 18 CB

第一个字节为0b01100111(这部分称为NALU header),那么

forbidden_zero_bit= (byte >> 7) & 0x1 = 0
nal_ref_idc = (byte >> 5) & 0x3 = 3
nal_unit_type = byte & 0x1F = 7

有了nal_unit_type,可以在7.4节的Table 7-1找到对应的类型和对RBSP数据的解析。

4.1 指数哥伦布熵编码

在Descriptor中有以下几种特殊的编码

  • 无符号指数哥伦布熵编码 ue(v)
  • 有符号指数哥伦布熵编码 se(v)
  • 映射指数哥伦布熵编码 me(v)
  • 截断指数哥伦布熵编码 te(v)

这部分建议跟着乔红老师的视频来自己复现一下。

5 NALU种类

NALU种类有很多,简单介绍几个重要的

  • SPS(Sequence Paramater Set):序列参数集, 包括一个图像序列的所有信息,如图像尺寸、视频格式等。
  • PPS(Picture Paramater Set):图像参数集,包括一个图像的所有分片的所有相关信息,包括图像类型、序列号等。

在传输视频流之前,必须要传输这两类参数,不然无法解码。为了保证容错性,每一个 I 帧前面,都会传一遍这两个参数集合。

一个流由多个帧序列组成,一个序列由以下三种帧组成。

  • I帧(Intra-coded picture帧内编码图像帧):不参考其他图像帧,只利⽤本帧的信息进⾏编码。
  • P帧(Predictive-codedPicture预测编码图像帧):利⽤之前的I帧或P帧,采⽤运动预测的⽅式进⾏帧间预测编码。
  • B帧(Bidirectionallypredicted picture双向预测编码图像帧):提供最⾼的压缩⽐,它既需要之前的图像帧(I帧或P帧),也需要后来的图像帧(P帧),采⽤运动预测的⽅式进⾏帧间双向预测编码。

这些个帧组成一个序列,每个序列的第一个帧是IDR帧

  • IDR(Instantaneous Decoding Refresh,即时解码刷新):⼀个序列的第⼀个图像叫做 IDR 图像(⽴即刷新图像),IDR 图像都是 I 帧图像。

IDR帧必须是I帧,但是I帧可以不是IDR帧

IDR帧的信息,slice_type写了I slice only

其他

  • SEI(Supplemental Enhancement Information辅助增强信息):SEI是H264标准中一个重要的技术,主要起补充和增强的作用。 SEI没有图像数据信息,只是对图像数据信息或者视频流的补充,有些内容可能对解码有帮助.

6 实践

BUAACTF2024中出了一道H264编码的视频题,思路如下。

首先有一个正常的带flag的视频

希望把视频损坏,但是是可修复的损坏。

首先用ffmpeg重新编码一下,不然太清晰裸流的文件大小很大

os.system('ffmpeg -i  flag.mp4  -c:v libx264 -crf 18 -preset medium -c:a aac -b:a 128k  encode-origin.h264 ')

并生成一个H264裸流文件,接下来就是对H264裸流进行操作。

python中操作H264裸流可以用h26xparser

H26xParser = H26xParser(ORIGIN_H264, verbose=False)
H26xParser.parse()
nalu_list = H26xParser.nalu_pos 

nalu_pos方法 返回的是一个元组列表,前两个表示的是nalu数据的开始字节和结束字节

然后获取rbsp数据,用getRSBP方法,这个方法返回的数据是包含NALU头部的。

for tu in nalu_list:
    start, end, _, _, _, _ = tu
    rbsp = bytes(H26xParser.getRSBP(start, end))
    nalu_header = rbsp[0]
    nal_unit_type = nalu_header & 0x1F
    nalu_body = rbsp[1:]

6.1 修改IDR帧类型

前面提到,IDR帧的类型必须是I帧,所以可以将他的类型进行改变。改变IDR帧的帧类型

if nal_unit_type == 5:
    origin_data[start + 1] = origin_data[start + 1] | 0x4  # 把关键帧slice_type改为11

nal_unit_type == 5意味着这是一个IDR帧,然后看IDR的解析语法

找到slice_layer_without_partitioning_rbsp()

找到slice_header()

ue(v)就是我们前面提到的无符号指数哥伦布编码

来看看如何使用无符号指数哥伦布进行编码:

  • 先把要编码的数字加 1,假设我们要编码的数字是 4,那么我们先对 4 加 1,就是 5。
  • 将加 1 后的数字 5 先转换成二进制,就是: 101。
  • 转化成二进制之后,我们看转化成的二进制有多少位,然后在前面补位数减一个 0 。例如,101 有 3 位,那么我们应该在前面补两个 0。

最后,4 进行无符号指数哥伦布编码之后得到的二进制码流就是 0 0 1 0 1。

而前面的first_mb_in_slice表示该slice的第一个宏块在图像中的位置,涉及到一些更深入的知识,但是这里不用关心,因为我们的情况中first_mb_in_slice始终为0。

slice_type就是我们的帧类型,同样在7.4节给出了不同类型对应的值。

观察我们正常的h264裸流,这个slice_type的值都是被设置为7。

所以从RBSP的第一个字节开始,0的无符号指数哥伦布熵编码是0b17的无符号指数哥伦布熵编码是0b0001000,比特流应当是

0b 1 0001000 xxxxxxx

找一个IDR帧的数据来验证一下

00 00 01 65 88 84 00 6F F9 C3 AB 0F 3B E0 BC 1E 03 54 39 CD 48 64 95 22 F4 6E AA 45 2F E6 8A 4F A2 1D 61 88 5C B2 0F 61 41 11 81 69 27 E5 93 DE D3 15 0D A2 97 F7 9A 41 E7 DF D5 B0 BD 50 57 D9 30 65 42 D9

RBSP为

88 84 00 .....

88恰好对应0b10001000

所以我直接对这个字节byte | 0x4,让这个字节变成0b10001100,于是slice_type就变成了11。这里主要是为了好处理数据,所以直接用二进制运算,实际上slice_type想改多少都可以。

修改后IDR的信息如下

6.2 修改其他帧类型

关于其他帧类型的修改,题目是将所有帧类型都改为B帧,然后记录下原来的帧类型,存放在每个IDR帧之后的SEI帧里,供后续修复。

if nal_unit_type == 1:  #修改slice
    slice_type = extract_slice_type(nalu_body)
    origin_slice_type_list.append(SLICE_TYPES[slice_type % 5])
    print(SLICE_TYPES[slice_type % 5], end=' ')
    origin_data[start + 1] = origin_data[start + 1] | 0x4  # 非关键帧全部变为B帧

效果如下

SEI内容

6.3 重新封装

由于ffmpeg的转换会重新编码,所以还是一样要加上-vcodec copy参数,使其不重新编码,而是只做封装。

os.system(f"ffmpeg -i {OUTPUT_H264} -vcodec copy {OUTPUT_MP4}")

最后的视频成了这样

放出完整的出题脚本,只需要修改FLAG_VIDEO就可以生成。

import os

from h26x_extractor.h26x_parser import H26xParser
from uuid import uuid4

SLICE_TYPES = {0: 'P', 1: 'B', 2: 'I', 3: 'SP', 4: 'SI'}
FLAG_VIDEO = 'flag.mp4'
ORIGIN_H264='encode-origin.h264'
OUTPUT_H264='encode-new.h264'
OUTPUT_MP4='encode-output.mp4'
OUTPUT_FLV='encode-output.flv'
class BitStream:
    def __init__(self, buf):
        self.buffer = buf
        self.bit_pos = 0
        self.byte_pos = 0

    def ue(self):
        count = 0
        while self.u(1) == 0:
            count += 1
        res = ((1 << count) | self.u(count)) - 1
        return res

    def u1(self):
        self.bit_pos += 1
        res = self.buffer[self.byte_pos] >> (8 - self.bit_pos) & 0x01
        if self.bit_pos == 8:
            self.byte_pos += 1
            self.bit_pos = 0
        return res

    def u(self, n: int):
        res = 0
        for i in range(n):
            res <<= 1
            res |= self.u1()
        return res


def extract_slice_type(nalu_body):
    body = BitStream(nalu_body)
    #print(nalu_body[:3])
    first_mb_in_slice = body.ue()
    slice_type = body.ue()
    return slice_type


def generate_sequence_data(origin_slice_type_list: list):
    sei_data = b'\x00\x00\x01\x06\x05'
    sei_payload_len = len(origin_slice_type_list) + 16
    uuid = uuid4().bytes
    while sei_payload_len > 255:
        sei_payload_len -= 255
        sei_data += b'\xFF'
    sei_payload = uuid + ''.join(origin_slice_type_list).encode()
    sei_data += int.to_bytes(sei_payload_len, 1, 'big')
    sei_data += sei_payload
    sei_data += b'\x80'
    return sei_data


if __name__ == '__main__':
    os.system(f'ffmpeg -i {FLAG_VIDEO} -c:v libx264 -crf 18 -preset medium -c:a aac -b:a 128k {ORIGIN_H264}')
    f = open(ORIGIN_H264, 'rb')
    origin_data = list(f.read())
    f.close()
    # 进行加密
    H26xParser = H26xParser(ORIGIN_H264, verbose=False)
    H26xParser.parse()
    nalu_list = H26xParser.nalu_pos
    print(nalu_list)
    data = H26xParser.byte_stream
    origin_slice_type_list = []
    sei_data_list = []
    for tu in nalu_list:
        start, end, _, _, _, _ = tu
        rbsp = bytes(H26xParser.getRSBP(start, end))
        nalu_header = rbsp[0]
        nal_unit_type = nalu_header & 0x1F
        nalu_body = rbsp[1:]
        if nal_unit_type == 1:  #修改slice
            slice_type = extract_slice_type(nalu_body)
            origin_slice_type_list.append(SLICE_TYPES[slice_type % 5])
            print(SLICE_TYPES[slice_type % 5], end=' ')
            origin_data[start + 1] = origin_data[start + 1] | 0x4  # 非关键帧全部变为B帧
        elif nal_unit_type == 5:
            origin_data[start + 1] = origin_data[start + 1] | 0x4  # 把关键帧slice_type改为11
        elif nal_unit_type == 7 and origin_slice_type_list:
            sei_data_list.append(generate_sequence_data(origin_slice_type_list))
            origin_slice_type_list = []
    sei_data_list.append(generate_sequence_data(origin_slice_type_list))
    # 构造新数据
    origin_slice_type_list = []
    new_data = b''
    start_pos = 0
    count = 0
    for start, end, _, _, _, _ in nalu_list:
        rbsp = bytes(H26xParser.getRSBP(start, end))
        nalu_header = rbsp[0]
        nal_unit_type = nalu_header & 0x1F
        if nal_unit_type == 5:
            new_data += bytes(origin_data[start_pos:end]) + sei_data_list[count]
            count += 1
            start_pos = end
    new_data += bytes(origin_data[start_pos:])
    # 输出
    f = open(OUTPUT_H264, 'wb')
    f.write(bytes(new_data))
    f.close()
    # 封装
    os.system(f"ffmpeg -i {OUTPUT_H264} -vcodec copy {OUTPUT_MP4}")
    os.system(f"ffmpeg -i {OUTPUT_MP4} -vcodec copy {OUTPUT_FLV}")

6.4 修复

理解了出题思路,解题就比较简单。将EXTRACT_VIDEO修改为损坏的视频即可。

import os

from h26x_extractor.h26x_parser import H26xParser
from uuid import uuid4

SLICE_TYPES = {0: 'P', 1: 'B', 2: 'I', 3: 'SP', 4: 'SI'}
EXTRACT_VIDEO = 'final/extract.flv'
ORIGIN_H264 = 'decode-extract.h264'
OUTPUT_H264 = 'decode-origin.h264'
OUTPUT_MP4 = 'decode-origin.mp4'


class BitStream:
    def __init__(self, buf):
        self.buffer = buf
        self.bit_pos = 0
        self.byte_pos = 0

    def ue(self):
        count = 0
        while self.u(1) == 0:
            count += 1
        res = ((1 << count) | self.u(count)) - 1
        return res

    def u1(self):
        self.bit_pos += 1
        res = self.buffer[self.byte_pos] >> (8 - self.bit_pos) & 0x01
        if self.bit_pos == 8:
            self.byte_pos += 1
            self.bit_pos = 0
        return res

    def u(self, n: int):
        res = 0
        for i in range(n):
            res <<= 1
            res |= self.u1()
        return res


def extract_slice_type(nalu_body):
    body = BitStream(nalu_body)
    #print(nalu_body[:3])
    first_mb_in_slice = body.ue()
    slice_type = body.ue()
    return slice_type


def read_sei(nalu_body):
    payload_type = nalu_body[0]
    payload_size = 0
    i = 1
    while nalu_body[i] == 0xff:
        payload_size+=255
        i+=1
    payload_size += nalu_body[i]
    return [chr(i) for i in nalu_body[i+1+16:i+1+payload_size]]


if __name__ == '__main__':
    os.system(f'ffmpeg -i {EXTRACT_VIDEO} -vcodec copy {ORIGIN_H264}')
    f = open(ORIGIN_H264, 'rb')
    origin_data = list(f.read())
    f.close()
    # 进行解密
    H26xParser = H26xParser(ORIGIN_H264, verbose=False)
    H26xParser.parse()
    nalu_list = H26xParser.nalu_pos
    data = H26xParser.byte_stream
    origin_slice_type_list = []
    prev_unit_type = 0
    count=0
    for tu in nalu_list:
        start, end, _, _, _, _ = tu
        rbsp = bytes(H26xParser.getRSBP(start, end))
        nalu_header = rbsp[0]
        nal_unit_type = nalu_header & 0x1F
        nalu_body = rbsp[1:]
        if nal_unit_type == 1:  #修改slice
            if origin_slice_type_list[count]=='P':
                origin_data[start + 1] = origin_data[start + 1] ^ 0x4
        elif nal_unit_type == 5:
            origin_data[start + 1] = origin_data[start + 1] ^ 0x4
        elif nal_unit_type == 6 and prev_unit_type == 5:
            count=0
            print(read_sei(nalu_body))
            origin_slice_type_list = read_sei(nalu_body)
        prev_unit_type = nal_unit_type
    new_data = bytes(origin_data)
    # 输出
    f = open(OUTPUT_H264, 'wb')
    f.write(bytes(new_data))
    f.close()
    # 封装
    os.system(f"ffmpeg -i {OUTPUT_H264} -vcodec copy {OUTPUT_MP4}")

7 总结

关于视频编码的隐写还有很多待发掘的地方,本文仅抛砖引玉,比如YUV像素信息就可以尝试LSB隐写。希望对你有些启发。

8 参考

H264之NALU解析! - 知乎 前言: 大家晚上好,今天给大家分享一篇技术文章,废话不多说,咋们直接“起飞”吧,哈哈哈! 一、H264简介: H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准⾥称 为H.264,在MPEG… https://zhuanlan.zhihu.com/p/409527359

21、H264 NALU分析 - 知乎 视频编码在流媒体和⽹络领域占有重要地位;流媒体编解码流程⼤致如下图所示: 1、H264简介 H.264从1999年开始,到2003年形成草案,最后在2007年定稿有待核实。在ITU的标准称为H.264,在MPEG的标准⾥是MPEG-4的一个… https://zhuanlan.zhihu.com/p/419901787?utm_id=0

https://github.com/yistLin/H264-Encoder/blob/master/doc/ITU-T%20H.264.pdf

https://www.itu.int/rec/T-REC-H.264-202108-I/en

什么是 NALU-ZigZagSin 解释什么是 NALU https://www.zzsin.com/article/avc_0_5_what_is_nalu.html

GitHub - slhck/h26x-extractor: Extracts NAL units from H.264 bitstreams and decodes their type and content Extracts NAL units from H.264 bitstreams and decodes their type and content - slhck/h26x-extractor https://github.com/slhck/h26x-extractor

标签:编码,00,slice,H264,data,隐写,type
From: https://www.cnblogs.com/Joooook/p/18165347

相关文章

  • 开源相机管理库Aravis学习——PixelFormat编码规则
    目录前言前置知识PixelFormatBpp编码规则源码分析分类标准补充ARV_PIXEL_FORMAT_BIT_PER_PIXEL参考文章前言在学习Aravis官方例程的时候,有这么一个函数:arv_camera_get_pixel_format,它的返回类型是ArvPixelFormat(本质是个32位无符号整数)。这意味着对于每个图像数据格式,都有自己......
  • pycharm更换编辑器默认编码方式
    Pycharm运行py文件,出现SyntaxError:Non-UTF-8codestartingwith'\xb5'infileF:\桌面\python\tk_learning\01.pyonline7,butnoencodingdeclared;seehttps://python.org/dev/peps/pep-0263/fordetails错误这个错误通常意味着你的Python源代码文件中包含了非UTF......
  • 根据不同的编码格式读取txt文件内容
    参考:https://blog.csdn.net/chiwang1984/article/details/8593240importlombok.extern.slf4j.Slf4j;importjava.io.BufferedReader;importjava.io.DataInputStream;importjava.io.FileInputStream;importjava.io.IOException;importjava.io.InputStreamReader;imp......
  • 编码技巧C++
    编码技巧C++非零都是true在c++环境下不等于0的数值都被认为是true在判断一个值是否为0时以下代码是等效的,但第一种效率更高inti=123;if(i)cout<<"i不为0";if(i!=0)cout<<"i不为0";不需要用到下标的计数循环可以不用for语句intn;cin>>n;while(n--){......
  • 「白嫖」开源的后果就是供应链攻击么?| 编码人声
      「编码人声」是由「RTE开发者社区」策划的一档播客节目,关注行业发展变革、开发者职涯发展、技术突破以及创业创新,由开发者来分享开发者眼中的工作与生活。 面对网络安全威胁日益严重的今天,软件供应链安全已经成为开发者领域无法避免的焦点。从令人瞠目的ApacheLog4j......
  • Qt/C++音视频开发71-指定mjpeg/h264格式采集本地摄像头/存储文件到mp4/设备推流/采集
    一、前言用ffmpeg采集本地摄像头,如果不指定格式的话,默认小分辨率比如640x480使用rawvideo格式,大分辨率比如1280x720使用mjpeg格式,当然前提是这个摄像头设备要支持这些格式。目前市面上有一些厂家做的本地设备支持264格式,这个压缩率极高,由于采集到的就是264格式的裸流,所以不用编码......
  • 西安站开营!AI 编码助手通义灵码帮大学生“整活儿”
    如何更好地与AI为伴,做时代的先进开发者?4月17日,阿里云推出的AI编程助手通义灵码与云工开物“高校训练营”走进西安多所高校开启实操培训,结合AI辅助编程的发展背景、通义灵码的具体能力和应用实操,帮助在校大学生了解人工智能技术在编程领域的发展,利用AI辅助编码,提升学习......
  • 西安站开营!AI 编码助手通义灵码帮大学生“整活儿”
    如何更好地与AI为伴,做时代的先进开发者?4月17日,阿里云推出的AI编程助手通义灵码与云工开物“高校训练营”走进西安多所高校开启实操培训,结合AI辅助编程的发展背景、通义灵码的具体能力和应用实操,帮助在校大学生了解人工智能技术在编程领域的发展,利用AI辅助编码,提升学习......
  • 前端面试题·讲一讲什么是URL编码?
    前端面试题·讲一讲什么是URL编码?Url编码通常也被称为百分号编码(UrlEncoding),是因为它的编码方式非常简单,使用%百分号加上两位的字符——代表一个字节的十六进制形式。例如a在US-ASCII码中对应的字节是0x61,那么Url编码之后得到的就是%61。Url编码默认使用的字符集是US-ASCII。......
  • visual studio连接linux编码
    该操作最好是在root用户下进行,请确保你的linux里已经修改过root用户的权限修改ubuntu里root用户权限Linux安装远程调试ubuntu下安装如下:sudoapt-getinstallopenssh-serverg++gdbgdbserverLinux设置远程调试ssh允许root登录默认情况安装完SSH服务并开启,root是不允......