橄榄球世界杯_1990世界杯阿根廷 - liuweiqing95511.com

橄榄球世界杯_1990世界杯阿根廷 - liuweiqing95511.com

H264编码分析及隐写实践

Home 2025-10-20 19:24:14 H264编码分析及隐写实践

H264编码分析及隐写实践

H264编码分析及隐写实践 目录 1 视频数据层级 2 H264裸流 3 NALU 4 RBSP 4.1 指数哥伦布熵编码 5 NALU种类 6 实践 6.1 修改IDR帧类型 6.2 修改其他帧类型 6.3

  • admin 男篮世界杯名单
  • 2025-10-20 19:24:14

H264编码分析及隐写实践

目录

1 视频数据层级

2 H264裸流

3 NALU

4 RBSP

4.1 指数哥伦布熵编码

5 NALU种类

6 实践

6.1 修改IDR帧类型

6.2 修改其他帧类型

6.3 重新封装

6.4 修复

7 总结

8 参考

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组成。H264对NALU的封装有两种方式,一种是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帧。

其他

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的无符号指数哥伦布熵编码是0b1,7的无符号指数哥伦布熵编码是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

  • 梓元奇门基础之天时——九星
Copyright © 2088 橄榄球世界杯_1990世界杯阿根廷 - liuweiqing95511.com All Rights Reserved.
友情链接