CTF中的Gif

部分参考:https://www.jianshu.com/p/4fabac6b15b3,讲解很详细

原创文章,未经许可严禁转载

Author:ex@m1ne2


Gif

文件格式

Gif图像整体上来看由11个块组成:

其中有8个块是必备的:

Header (文件头)、Logical Screen Descriptor(逻辑屏幕描述符)、Image Descriptor(图像描述符)、Image Data(图像数据流)、Plain Text Extension(文本扩展)、Application Extension(应用扩展)、Comment Extension(注释扩展)、Trailer(尾部标记)

此外还有可选的3个块:Global Color Table(全局颜色表)、Graphic Control Extension(图形控制扩展)、Local Color Table(本地颜色表)

我们暂时只学习一些常见的块,随便借用一张Gif:

链接:https://pan.baidu.com/s/1m-LcmVGzfMfPKFmLdZDE0g
提取码:frd3


文件头 Header

Gif的文件头有两种:

  • 47 49 46 38 37 61,dump为 GIF87a
  • 47 49 46 38 39 61,dump为 GIF89a

之所以有两种,是因为Gif的文件头中,前3 Bytes才是真正的签名(signature)、后3 Bytes表示版本:

目前有 87a 和 89a 两个版本


Logical Screen Descriptor

文件头有6 Bytes,而Logical Screen Descriptor也才只有7 Bytes

它负责告诉Gif解码器一些关于Gif图像的格式信息,包括:

  • 宽度(2 Bytes,小端序)

  • 高度(2 Bytes,小端序)

  • 包装字段(Packed Field)(1 Byte)

    将包装字段的1 Byte展开成8 Bits,下标分别为[7, 0],分别有:

    • 最高位,[7],Global Color Table Flag,块开启标志

      前面有提 Global Color Table 这个块在Gif中是可选的,如果这里的 Global Color Table 为 1,则表示存在 Global Color Table 这个块;为 0 则表示不存在

    • [6, 4],Color Resolution,色彩分辨率

      这3位构成的数值如果是 s,则表示图片中最多有 $2^{s+1}$ 种颜色

      更确切的概念是,该Gif的像素的颜色最多只需用 $s+1$ Bits来表示

    • [3],Sort Flag,排序标志

      如果为 1,表示 Global Color Table 中的颜色会按照出现频率进行排序,由高到低;为 0 则表示不排序

    • [2, 0],Size Of Global Color Table,颜色表中颜色的数目

      如果其值为 $s$,则表示有 $2^{s+1}$ 种颜色

  • 背景色索引(Background Color Index)

    Gif图的背景色在Global Color Table中的索引

  • Pixel Aspect Ratio

    通常为 0,不作了解


以给定的Gif图为例:

可以计算得到:

  • 宽度 = 高度 = 0x0140 = 320,所以图像分辨率为 320 × 320
  • 将包装字段 F6 展开成 11110110,最高位的 1 表示存在Global Color Table;然后的 111 值为 7,则表示每种颜色可以由8 Bits来表示;接下来的 0 表示Global Color Table中的颜色不排序;最后的 110 值为 6,表示Global Color Table中有 $2^{6+1} = 128$ 种颜色
  • 0x49 = 73,Gif图的背景色的下标为 73

Global Color Table

全局颜色表,如果存在的话,Gif中的每一种颜色都会表示成RGB模式下的3 Bytes

例如,从前面的 Size Of Global Color Table 可知该Gif有128种颜色,所以应该占据 128 × 3 = 384 Bytes,而从010 Editor中可以看到,Global Color Table的大小的确是384 Bytes:

并且从010 Editor的Gif模板中可以看到,Global Color Table中的数据都是以RGB三元组为基本单位的:


前面都都是一张Gif比较全局的配置信息,接下来就进入了详细的数据部分


帧(Frame)

再次借助010 Editor,可以看到:

除了最前面的 Application Extension 外,数据部分又可以划分为一个个的子块,其中每个子块的内容有:

  • Graphic Control Extension[x]
  • Image Descriptor[x]
  • Image Data[x]

我们知道,普通的视频就是一连串快速切换的图像,它利用人眼的视觉暂留现象,将静态的多张图画转变成动态的视频;而Gif图在这一方面与视频是一样的

视觉暂留现象

指人眼看到的影像消失后,仍能继续保留其影像0.1 - 0.5秒左右

Gif动图由许多图像组成,每个图像的快速播放构成了Gif图的「动态」,而Gif图中的每一张静态图都被称之为帧(Frame)

回到上面,我们可以看到数据区域被划分为许多拥有共同属性的子块,其实每个子块就对应一,在数据区域中,每一帧的数据都是对某张图片的描述

其中,Application Extension 就跟表示注释Comment Extension 一样,并不会对Gif图本身的渲染产生任何影响,所以这里不理会


Graphic Control Extension

我们前面提到了 Plain Text Extension(文本扩展)、Application Extension(应用扩展)、Comment Extension(注释扩展)、Graphic Control Extension(图形控制扩展)

它们都被称之为扩展,为了区分它们,Gif在格式中引入了Extension Introducer(扩展入口)的概念,它类似于文件头,用不同的数值去标识每个扩展:

扩展名称 标识(Label)
Plain Text Extension 21 01
Application Extension 21 FF
Comment Extension 21 FE
Graphic Control Extension 21 F9

Extension Introducer的值就是 21,它标识接下来的一段数据是某个扩展的数据;21 的下一个字节是该扩展的标识(Label)

此外,每个扩展的数据除了用长度去表示自身的数据范围外,扩展的最后一个数据字节一定是 00Block Terminator


而Graphic Control Extension算上前两个字节 21 F9,总共有8 Bytes:

最后1 Byte 00 是Graphic Control Extension的Block Terminator,下面来介绍下中间的5 Bytes的实际意义:

  • 第3 Byte表示扩展大小(Block Size)

    从上图可以看到,Block Size04,而刚好截止到后面的Block Terminator,共有4 Bytes的数据

  • 第4 Byte又是一个包装字段(Packed Field)

    照常将包装字段展开为8 Bits,下标为[7, 0],分别有:

    • [7, 5] 保留,暂不做使用

    • [4, 2] 表示展示方法(Display Method)

      我们需要记住的是,无论是视频还是Gif,都是由许许多多的图片帧组成;而在实际存储的时候,是不会完整地存储每一图片帧的完整数据的,因为视频和Gif可能在某时刻变动的范围不大

      上一帧和下一帧只有细微地改变时,往往是存储下一帧的改变量,然后在渲染时,直接在上一帧的数据中进行更改,节省空间

      Display Method表示在进行逐帧渲染时,对前一帧留下的图形如何处理,其中:

      • 0:不做任何处理
      • 1:保留前一帧,在前一帧的图像的基础上进行渲染
      • 2:渲染前将图像设置为背景色
      • 3:下一帧直接覆盖掉

      通常的Display Method值为 1,因为这样最节省空间;当Gif图像下一刻发生大幅度地改变时,才会用到其它Display Method

      而且暂时不知道为什么Display Method只有4种,却预留了3 Bits的空间,可能以后会扩展

    • [1] 是用户输入标识(User Input Flag)

      如果此标识置 1,那么在渲染到这一帧时,需要得到用户的输入才能进行下一帧的渲染

      很少见,具体的「用户输入」指什么,要视应用而定

    • [0] 为透明标识(Transparent Flag)

      取值为 01,作用下面谈

    上图中,包装字段的值为 04,展开为 00000100

    由上面的分析可知,Display Method001,也就是 1,表示在上一帧的基础上进行渲染;而User Input FlagTransparent Flag都是 0,表示关闭

  • 第5、6 Bytes表示下一帧之间的间隔时间(Delay Time)

    Delay Time由2 Bytes组成,意味着最大取值为 0xFFFF = 65535,而它的单位是百分之一秒;也就是说在Gif动态图中,上一帧和下一帧的间隔最多是655.35秒,也就是约11分钟

    此外,当Delay Time为 0 时,时间间隔将会由Gif的解码器决定

    上图中,Delay Time的值为 0x000A = 10,所以0.1秒后进行下一帧的渲染

  • 第7 Byte为透明颜色索引(Transparent Color Index)

    只有上面包装字段中的Transparent Flag1 时,Transparent Color Index才生效,它表示在Global Color Index中对应索引的颜色会被当作透明色来处理

    Transparent Flag0 时,该字节的数值无效

    上图中的Transparent Color Index取了一个较大的值 0xFF,明显超出了Global Color Table的大小,可能是由于Transparet Flag0,就随意取了一个值

总的Graphic Control Extension可以概括为:

综上,Graphic Control Extension存储的更多是当前图片帧的一些控制信息,如「如何处理上一帧留下的图像」、「是否有用户输入中断」、「是否有透明色」、「当前图片帧保留多长时间后开启下一帧的渲染」


Image Descriptor

前面的Graphic Control Extension是当前图片帧的控制信息,而接下来的Image Descriptor则是图片帧的一些属性了

与「扩展」拥有标识符 21 一样,Image Descriptor也有自己的标识符:2C;包括标识符 2CImage Descriptor一般有10 Bytes的空间:

标识符 2C 之后分别是:

  • Image Left(图片距离画布左边的距离)、Image Top(图片距离画布上边的距离)、Image Width(图片的宽度)、Image Height(图片的高度),每个占2 Bytes,共8 Bytes

    一般图像的建系都是在左上角设置原点的,而采用 [图像左上角横坐标, 图像左上角纵坐标, 图像宽度, 图像高度] 这一四元组就能够确定一张图像的位置;往往也是采用这种方式表示,而不是分别给出左上角的坐标和右下角的坐标

    事实上,图片的宽度和高度在前面的Logical Screen Descriptor已经定义过了,之所以再次给出每个图片帧的宽和高,就是因为Gif并不是存储每一帧的完整数据的,就像前面Display Method1 时,下一帧是在前一帧的图像上进行处理的,所以存储的只是下一帧相当于上一帧的改变量

    如果Gif图中只有中间某个区域是动的、四周是静止不变的,那么就只需要存储中间那部分的数据即可;而Image Descriptor的4个属性值,就是存储该改变图像的位置的

  • 再一个包装字段(Packed Field),仍然是1 Byte的空间

    展开为[7, 0]的8 Bits,有:

    • [7],Local Color Table Flag

      Local Color Table 也是前面提到的Gif的11个块中,3个可选块的其中一个,表示局部独立的颜色表

      如果该Flag为 1,则会在后面开辟一个 Local Color Table,对这一帧图像的渲染将不使用Global Color Table,而是使用 Local Color Table

    • [6],Interlace Flag,隔行扫描标识

      1 标识需要进行隔行扫描

    • [5],Sort Flag

      与之前的一样,如果启动Local Color Table的话,当中的RGB值是否需要按照出现频率来排序

    • [4, 3],闲置保留位

    • [2, 0],Size Of Local Color Table

      与Global Color Table一样,值为 $s$ 表示Local Color Table中有 $2^{s+1}$ 种颜色

    总的来看,这一Packed Field就是为可选的Local Color Table服务的,而前面也有一个Packed Field也是为Global Color Table服务的

所以Image Descriptor总共有10 Bytes,首字节是其标识符,固定为 2C;然后是当前图片帧的位置信息,占8 Bytes;最后是Local Color Table的包装字段


Image Data

既有了图片的控制信息(Graphic Control Extension),也有了图片的相关属性(Image Descriptor)后,就到了一张图片的真正数据部分

Gif文件的图像数据采用了可变长度的LZW压缩算法(Variable-Length LZW Compression),该算法从LZW算法(Lempel Ziv Welch Compression)压缩算法演变而来

在这里并不学习具体的算法原理,也正是因为这个原因,我们只需将Image Data看作是一个整体,它就代表Gif中某一帧图片的数据

此外,Image Data也把 00 作为结束符(Terminator)


Trailer

这个其实就是Gif格式的文件尾,并且这个文件尾很简洁,只有1 Byte:3B,它的dump是 ;

因此它可以产生类似 .jpg 图种的效果,也就是在Gif后面附加其它文件数据


格式概述

总体来看,Gif的文件格式由以下部分组成:

  1. Header

    固定6 Bytes,为 47 49 46 38 37 61GIF87a)或 47 49 46 38 39 61GIF89a

  2. Logical Screen Descriptor

    固定7 Bytes,包含一些提供给解码器的Gif图信息,如分辨率、背景色等

  3. Global Color Table

    可选的全局颜色表,以{R, G, B}三元组的形式存储每一种颜色

  4. Frame

    Gif中每一张图片帧对应的数据

    • Graphic Control Extension

      图片帧的控制信息,包含如何「处理上一帧留下的图像」、「是否有用户输入中断」、「是否有透明色」、「当前图片帧保留多长时间后开启下一帧的渲染」

    • Image Descriptor

      基于“不完整存储每一帧的图像数据,只存储改变部分”的思想,这里的数据包含当前图片帧的位置(由 ImageLeftImageTopWidthHeight 给出),以及Local Color Table的设置

    • Image Data

      对图片帧的数据信息进行可变长的LZW压缩算法压缩

  5. Trailer

    文件尾,固定是 3B;


CTF应用


文件头修复

这种题目故意将Gif的文件头 GIF87aGIF89a 删除,使得无法检测出这是一个 .gif 文件

举例,BUUCTF中的Misc题 鸡你太美

解答

下载后解压,得到 篮球.gif篮球副本.gif

首先检查 篮球.gif,用010 Editor检查数据,并没有发现什么问题;然后检查 篮球副本.gif,发现它丢失了部分Gif的文件头:

在最前面插入 47 49 46 38 即可修复文件头,正常显示Gif,获得flag


宽度隐藏、高度隐藏

Logical Screen Descriptor中记录了Gif图的宽度和高度,倘若我们将宽度和高度减小,那么在打开的Gif图中会只显示部分

链接:https://pan.baidu.com/s/19sVIvle3BS6VqybKY_561A
提取码:fojv

下载得到 hhh.gif,只要将宽度和高度都修改到500就可以显示出flag了

留意到,打开 hhh.gif 有明显的图片缩小的过程,这是因为Logical Screen Descriptor中的宽度和高度是Gif显示软件显示的宽度和高度,我们修改它只是将显示的范围改小了,但是每一帧的图片仍然是原始大小

而且略缩图是取自第一帧的图像的,上面的 hhh.gif 的flag是隐藏在第4帧,因此修改完后查看flag,flag会一闪一闪;如果flag写入的是第1帧,那么在略缩图的时候就会显现

这就要求我们如果要修改Gif的宽度、高度,并且第一帧有flag的话,就必须修改第一帧图片的宽度和高度(而且第一帧图片的Display Method一定是 0 而不是 1,因为它没有“前一帧”的基础可以去修改)

然后又引申出一个问题,就像我们直接修改 .jpg.png 图片的宽度和高度一样,我们只能修改高度,而不能修改宽度;因为在解析这些图片的数据时,都是一个一个像素逐个显示的,如果修改了宽度,那每一行减少的像素就会堆积到下一行,导致整张图片的毁坏;而如果修改高度,每一行的像素还是正常的,只是最后几行的像素不会被解析

基于此,如果我们是出题人,在Gif的「修改宽度、高度」的题型中,有两种:

  • flag在单独的某一帧宽度或高度缩减的部分

    这时可以只修改Logical Screen Descriptor的宽度和高度;但是由于第一帧未修改,可能从略缩图中观察到一些被修改了宽高的线索;而在打开Gif时会有明显的图片缩小的痕迹

  • flag出现在第一帧 or flag出现在所有帧

    同时修改Logical Screen Descriptor第一帧,并且只能修改高度

事实上,这种修改宽度、高度进行隐写的方法还没在CTF中出现过,我写在这里只是提供一种出题和解题的可能性,毕竟这种「修改宽高」的套路在 .jpg.png 也出现过

而且修改了宽高的Gif只是在普通的显示软件中会缩小显示范围,但是在比较专业的显示软件中还是会直接显示出原分辨率


Gif播放速度

极快

一般的Gif中,每一帧的停留时间只有0.1秒,便切换到下一帧

在CTF中,通常会将flag隐藏在 .gif 的某一帧中,而在播放时会极快地闪过去,导致难以捕捉到flag

通常只需将 .gif 用特定的工具打开,逐帧浏览即可;我一般使用的是StegSolveFrame Browser功能,它就能提供逐帧浏览的功能

链接:https://pan.baidu.com/s/1In2sn7Ty2Ztk4AhGqwx_Rw
提取码:2yih

解答

下载得到 lovely_dog.gif,打开发现flag一闪而逝,用StegSolve打开逐帧浏览,在最后一帧中出现flag

此外还可以借助网上在线的Gif分解网站,把Gif中的所有图片帧提取出来,观察每一张图片即可

可以借助Linux终端的图片处理工具convert将Gif分离;安装为:

1
sudo apt-get install imagemagick

ImageMagick是图片处理的工具集和开发包,其中包含了convert这个子命令;而子命令convert是可以直接作为单独的命令使用的,安装成功后查看其版本号:

1
convert -version

只需1条指令就能将Gif所有的图片帧分离:

1
convert temp.gif *.png

极慢

在每一帧的Graphic Control Extension中有Delay Time这一属性,来决定当前帧延迟多长时间后才进行下一帧的渲染

由于Delay Time占据2 Bytes,最大取值为 0xFFFF = 65535,而Delay Time的单位是百分之一秒,所以单帧最长的停留时间可以达到655.35秒,也就是约11分钟

在CTF中,可以人为地将某一帧的Delay Time修改为较大值,使得Gif图呈现「卡住」的现象,需要等待较长时间才能看到下一帧;这种题目也是用工具直接浏览每一帧、或者将Gif的所有图片帧进行分离即可

链接:https://pan.baidu.com/s/1VDRM_lCqxwbvwoaWM14-2A
提取码:0pi1

解答

下载得到的 lovely_dog2.gif 中,flag在最后一帧的图片上,但是倒数第二帧的图片的Delay Time被修改为 0xFFFF,用StegSolve可以直接查看最后一帧


时间规律

这种题目是将信息隐写到Gif的每一帧的时间间隔上,由于时间间隔通常在0.1 - 0.2秒,所以稍微修改可以不会引起发觉

首先介绍同属于ImageMagick子命令的identify,与convert的「处理图片」的功能不同,它的主要功能是「检测图片」

identify

对于一张图片,使用identify去检测:

可以得到一张图片的「名称」、「图片格式」、「分辨率」、「位深度」、「色彩模式」、「大小」等信息

这是identify最直接的用法,还可以添加 -verbose 参数查看更详细的信息:

其它具体参数可以通过 man identify 查看

除了 -verbose,比较常用的还有 -format 参数,它能够以自定义的输出方式将图像的信息输出出来

例如,直接使用identify获得一张图片的信息:

如果我们想只获取图片的宽度和高度,并且输出形式是 Height=xx, Weight=xx,那么可以键入指令:

1
identify -format "Height=%h, Weight=%w\n" temp.png

其中的 %h%w 等格式化字符串可以在官方文档 https://imagemagick.org/script/escape.php 中查询学习


例题

比较有代表性的题目是2017年XMAN选拔赛的Misc题:[SimpleGif],但是搜寻了一番实在没有找到原文件,因此这里就不贴解法了,可以参考别人的Writeup:https://www.sqlsec.com/2018/01/ctfimg.html#toc-heading-1

然后自己出了一道题目:

链接:https://pan.baidu.com/s/1GfgmRQbkDWmivJQ0uFf-1Q
提取码:73ug

解答

打开下载得到的 heart.gif,发现动图有明显的卡顿,怀疑修改了帧与帧之间的Delay Time,于是用identify提取出每一帧图片的耗时:

1
2
identify -format "%T " heart.gif
# 0 102 108 97 103 123 101 120 64 109 109 49 110 101 95 71 49 102 95 116 49 109 99 125 0 0 0 0 0 0 0 0

发现有奇怪的数值,将所有的非0数值转换为对应的ASCII码,获得flag:

1
2
3
4
5
s = "102 108 97 103 123 101 120 64 109 109 49 110 101 95 71 49 102 95 116 49 109 99 125"
s = s.split(" ")
for i in s:
print(chr(int(i)), end="")
# flag{ex@mm1ne_G1f_t1mc}

由于是自己随便出的题,所以没考虑到修改后的Delay Time较大,直接影响到了Gif动图的播放效果;如果像上面提到的那题[SimpleGif],把每个字符转化成由「10」和「20」构成的序列,这样每帧播放流畅、效果就很好,也很难找到flag

从这里也可以知道,identify%T 参数对于Gif而言,其实就是直接提取帧与帧之间的Delay Time


拆分、拼接

参考TWCTF的Misc题:[glance]

链接:https://pan.baidu.com/s/13_ItWC76dP1eisHgdjQS7w
提取码:n9d2

解答

下载得到的 glance.gif 很窄、宽度很小,直接尝试将它分离:(在 glance.gif 同目录下新建一个 temp 文件夹,用来存放分离出的图片,避免太多太乱)

1
convert glance.gif temp/*.png

分解得到201张图片,然后由于它们的宽度都很小,尝试将它们横向拼接起来:

1
convert temp/*.png +append result.png

参数 +append 表示横向拼接、-append 表示纵向拼接,更多convert的用法参考网上教程

拼接后的图片中出现flag


end