realworldctf 2022 hso writeup与nso iMessage 0click漏洞分析
欢迎大家关注公众号”天问记事簿”,以及加入天问之路知识星球,一起做技术分享,一起学习,happy hack。
简介
本题的考点可能来源于Project Zero的A deep dive into an NSO zero-click iMessage exploit: Remote Code Execution一文,这篇文章介绍了一个图片渲染库的整数溢出漏洞,以及如何通过这个漏洞来利用这个解析库原有的处理像素数据的与或非功能,构建了一个图灵完备的小型计算机,从而完成后续的漏洞利用。
但由于Linux平台相比,缓解机制并不完善,以及我们不需要对接一个sandbox escape漏洞来逃逸imessage沙箱,所以只需要简单的构建一个全加器就可以实现整个漏洞利用,体验到神奇的乐趣。
这里是复盘 RWCTF2022 中 hso groupie
题时所写下的一些笔记,在做题的过程中,我们大量阅读了fcd14492标准文档,如果你在做题或者阅读本文的过程中感觉难以理解,请参考文档的第0章/第7章和第6章等,想必会有所收获,感谢Riatre师傅提供的有趣题目。
整体的做题思路主要由作者 exploit 中所推导出,换句话说,这里的笔记主要是对 作者 exploit 的解释说明。
由于这题同样也较为复杂,因此需要单独开一个博文来记录。
一、小叙
1 | Help check how secure our latest PaaS (Pdftohtml-as-a-Service) is! |
这题是 clone-and-pwn,源码没有做任何改变,就是通过查看最近提交的漏洞修复记录来发掘并利用漏洞。
二、环境搭建
1. 本地环境搭建
这一题是在 debian 下编译的,因此对于 debian 系统来说,有些系统可以直接跑 exp(例如我的 XD)。
1 | wget https://dl.xpdfreader.com/xpdf-4.03.tar.gz |
启动方式:
1 | xpdf/pdftohtml <pdf-path> -- |
2. exploit 调试环境搭建
去 题目环境 这里下载 dockerfile 等题目环境,之后给 dockerfile 打 patch:
1 | --- a/Dockerfile |
修改目的主要是把 gdbserver 放进镜像里,以及让入口点停在 /bin/sh
,而不直接启动 pdftohtml。
这里要注意 COPY 命令的源路径,这里是直接使用相对路径。
执行 build.sh
,执行完成后可以检查一下镜像
1 | ➜ chall git:(master) docker image ls |
启动 docker 镜像
1 | docker run -itd -p 1234:1234 -v sakura_volume:/tmp/chall --cap-add=SYS_PTRACE --security-opt seccomp=unconfined --name hsogroupie hsogroupie/pdftohtml |
该命令非常长,解构如下:
1 | docker run --help |
这里挂载数据卷需要额外说明(参考这篇文章)
1 | docker volume create sakura_volume // 创建一个自定义容器卷 |
然后我们对 /var/lib/docker/volumes/sakura_volume/_data
的修改就会映射到容器的 /tmp/chall
里,传输文件就比较方便。
启动完了之后我们可以 docker ps
一下看看有没有问题
1 | ➜ chall git:(master) docker ps -a |
生成 exp pdf,注意要对 submodule 初始化,不然没有 jbig2enc 库
1 | git clone https://github.com/Riatre/hso-groupie.git |
然后我们进入 docker 容器里对应数据卷的 exploit 目录下,应该要 install 这些安装包,要是少了就自己补一下:
1 | apt-get update |
调试 exp
1 | docker exec -it 15f265c337c0 bash |
进入容器的 bash 环境,然后启动 gdbserver
1 | rm -rf output && /usr/bin/gdbserver :1234 /usr/local/bin/pdftohtml /tmp/chall/exploit/sploit.pdf output |
这里的 output 是随便给一个文件夹名就行了,这是 pdftohtml 必须的启动参数,它会创建这个文件夹,并输出一个结果到这个文件夹里,并且它不能是已经存在的文件夹,而 sploit.pdf 就是我们生成出来的 exp pdf 文件。
然后在宿主机也启动 gdb,然后 target remote:1234
,然后随便下个断点看看效果,注意因为 docker 里的源码路径和我宿主机的源码路径并不一致,所以要用 substitute-path
做个转换,建议写个 gdb 脚本完成这个事情,后面就不用一直自己敲了。
1 | target remote :1234 |
现在我们就完成了整个调试环境的搭建。
三、漏洞点
这题预期的解法是使用这篇 google project zero 的 iMessage exploit 中的漏洞。漏洞点位于 JBIG2Stream
:
1 | void JBIG2Stream::readTextRegionSeg(Guint segNum, GBool imm, |
由于恶意构造的 refSegs
中,一些 seg->getSize()
值很大(4GB),因此如果全部写进则肯定会触发 crash。所以在实际的漏洞利用中,会尝试先做做堆风水:
看图,exploit 需要将 segments GList 的后备存储,放置在刚刚创建的溢出堆块的高地址处。这样触发堆溢出时,就能在执行前几个正常 size 的写入操作时,将后备存储中的那个超大 size 所对应的 segment 指针,替换成非 JBIG2SymbolDict 类型的 segment 指针(即 JBIG2Bitmap 类型)。之后当程序检索这个 segment 指针时,就会跳过该指针的检索。
四、漏洞利用前置知识
1. JBIG2Decode
漏洞点位于 JBIG2Stream ,而 JBIG2Stream 又怎么存在于 pdf 中呢?
pdf 文件结构本质上是一个树状图,这里给出一个使用 JBIG2Stream 的 pdf 片段:
1 | 4 0 obj |
pdf 文件中,4 0 obj、5 0 obj 都是表示一个特定的 pdf object。
其中,4 0 obj
标识了下面中的 MyStream1
,其参数 /Filter /FlateDecode
表示该流是使用 zlib 压缩。
继续往下看可以看到: 5 0 obj
中,/DecodeParms
引用了 4 0 obj
中的 stream 流,即 MyStream1
;同时参数 /Filter /JBIG2Decode
指定了接下来的流 MyStream2
使用的解码方式是 JBIG2Decode
。
因此从上文可以得知,MyStream2
使用 JBIG2Decode 进行解码,其解码参数为上面引用的这个 4 0 obj
,即 MyStream1
使用 FlateDecode
所解码后的流,而该参数的键为 JBIG2Globals
。
而我们要做的,就是精心构建 MyStream1
和 MyStream2
(这两个流都是 JBIG2Stream),使其在解析这两个 Stream 时能触发漏洞,从而 get shell。
构建好这两个流后,可以使用 jbig2enc/pdf.py 来创建出 pdf。
2. Segments 小叙
注,这一节中,每个 segment 所对应的代码最好亲自阅读一下。
当 xpdf 对 JBIG2Stream 解码时,正如上节中所示,JBIG2Decode 需要一个参数 JBIG2Globals
。因此在解析时,会先解析 JBIG2Globals
的 stream,之后再解析下面的 main stream。以下代码说明了 stream 的解析过程:
1 | void JBIG2Stream::reset() |
这里我们可以了解到,JBIG2Stream 是由多个 Segment 组成的,Segment 种类较多。这里我们只关注几个有用到的 Segment。
a. EOFSeg
该 Segment 的解析标志了完成了全部 segment 的读取,没有其他用途。
b. SymbolDictSeg
SymbolDict 主要存放了一个指向 Bitmap 的指针数组。Bitmap 可以用于存放数据,在实际漏洞利用中将起到类似内存的作用。
对于每个 symbol dict 中的 Bitmap,规范中将其称为一个 instance。
解析 SymbolDictSeg 时,将会从 stream 中读取并创建出每一个 Bitmap。
1 | GBool JBIG2Stream::readSymbolDictSeg(Guint segNum, Guint length, |
c. PageInfoSeg
对于每个 Page 来说,需要有一个 Bitmap 来表示当前页面渲染的数据。而在解析 PageInfoSeg 时,程序会创建一个流内全局 Bitmap:pageBitmap。
1 | void JBIG2Stream::readPageInfoSeg(Guint length) |
需要注意的是,pageBitmap 很关键,它表示了一个 Page 的 bitmap。我们将使用堆溢出来覆写 pageBitmap 的 Width 和 Height,进而达到越界读写的目的。
同时 PageInfoSeg 还可用于绕过一个 sanity check,下文中会提到。
d. GenericRegionSeg
GenericRegionSeg 的解析将会从流中读取一个 Bitmap,并与当前的 pageBitmap 的特定区域进行运算:
需要注意的是,JBIG2Globals Stream 中的 Segment 不允许引用任何 Segment,因此 GenericRegionSeg 不能存放在 JBIG2Globals 流中。
1 | void JBIG2Stream::readGenericRegionSeg(Guint segNum, GBool imm, |
其中,从流中读取 Bitmap 的操作位于 readGenericBitmap
函数中,读取的操作需要使用到编码器。
而与 pageBitmap 的运算主要是使用 JBIG2Bitmap::combine
方法,该方法中有五种运算方式,分别是 与、或、异或和替换:
1 | switch (combOp) |
我们可以将外部的立即数,通过利用该段的解析过程,将其传入 pageBitmap 中等待进一步的运算。
e. GenericRefinementRegionSeg
GenericRefinementRegionSeg 的解析过程,组合起来可以对 pageBitmap 上的部分数据进行位运算。我们可以利用这里的位运算来构建加法器:
1 | void JBIG2Stream::readGenericRefinementRegionSeg(Guint segNum, GBool imm, |
当 GenericRefinementRegionSeg 不引用任何段时,变量 nRefSegs 为 0,此时 refBitmap 为 pageBitmap 上指定 x、y、w、h 属性的一块数据空间。
由于函数
readGenericRefinementRegion
只会受到 refBitmap 的影响,因此我们可以认定传出的bitmap 变量等价于 pageBitmap 上特定区域的数据。接下来,若我们指定 imm 为 false,那么这块等价于 pageBitmap 上特定区域的数据,将被存储进 segments 数组中。
若下一次解析 GenericRefinementRegionSeg 时引用了第一步创建的段,那么此时 refBitmap 为第一步创建的 Bitmap。这样当 imm 为 true 时,第一步创建的 Bitmap 将会和 pageBitmap 上指定的位置进行 combine 操作,即位运算。
由于第一步创建的 bitmap 是和 pageBitmap 相关,因此整个过程就等价于
从 pageBitmap 上特定位置1取下一块数据,并保存至 segments 上
从 segments 上取下这块数据,并将其与 pageBitmap 上特定位置2进行位运算。
1
2
3
4
5
6
7
8+----------------------> x-axis
|
| .(2)
|
| .(1)
|
V
y-axis
如此,便达到了让 pageBitmap 上指定两个位置的数据进行位运算的操作。我们将使用该操作来一步步构建位运算原语、乃至加法器。
f. TextRegionSeg
TextRegionSeg 可以引用指定的 SymbolDictSeg,并对其中的任意 instance 进行操作。
需要注意的是,JBIG2Globals Stream 中的 Segment 不允许引用任何 Segment,因此 TextRegionSeg 不能存放在 JBIG2Globals 流中。
整体流程大致如下:
1 | void JBIG2Stream::readTextRegionSeg(Guint segNum, GBool imm, |
3. JBIG2Encode
a. encode Bitmap
通过阅读上面关于 Segments 的源代码,我们可以很容易的得知:在诸如 readGenericBitmap
等读入 bitmap 的函数中,hso 会尝试从外部 JBIG2Stream 流中,使用某种解码器来对读入的 bitmap 进行解码(例如代码中多次出现 arithDecoder->decodeInt
等调用)。
因此,作为提供外部 JBIG2Stream 流的我们,需要对写入至 pdf 中的 bitmap 做对应的编码操作。
从最上面的 JBIG2Stream::reset
函数中可以得知,一共由三种解码器:
- JArithmeticDecoder
- JBIG2HuffmanDecoder
- JBIG2MMRDecoder
而这些解码器的内部算法,如果要让我们徒手撸一个的话 ,那么做题效率就会非常低。因此,我们可以使用 jbig2enc
库来帮助我们完成数据编码操作,该库已经实现了 JArithmeticDecoder 状态机的编码算法,故我们无需了解内部细节即可完成对 bitmap 的编码过程。
1 | git clone git@github.com:agl/jbig2enc.git |
但是,该库是使用 C++ 编写的,若 exploit 也全部使用 C++ 完成,则工作量较高。因此,我们可以使用 pybind11 来暴露 jbig2enc 中的部分接口给 python,这样编写 exploit 时可以使用 python 语言来完成。
1 | sudo apt-get install pybind11-dev |
最后需要注意的是,由于 jbig2enc
的接口会使用到大量的指针,而将指针暴露给 python 接口调用是一个非常不明智的选择(因为如果让 python 来调用需要指针的接口,则会降低开发速度和提高触发 bug 的几率),因此我们最好根据当前的需求,即:
将 bitmap 数据以 JArithmeticDecoder 方式来进行编码。
来额外编写一个 wrapper C++ 代码,实现三个封装好的结构体/枚举:
ArithEncoder
:调用 jbig2enc 对 bitmap 进行编码的类Bitmap
:待被编码的 bitmap 数据ArithEncoder::Proc
:ArithEncoder
编码器的状态枚举
最后将这三个结构体/枚举 暴露给 python 调用,避免让 python 直接操作指针。
这一小节所实现的代码,正对应于 exp 中的以下几个文件:
hso-groupie/exploit/jbig2arith.[cc,h]
hso-groupie/exploit/jbjbarith.[cc,h]
b. encode segments
hso 在 read segments 时,首先会读取出每个当前 segment 的 段号 segNum、segFlags、refFlags 等一系列字段和标志,之后才是进行(可能的) bitmap 读取。
这些字段和标志同样是需要我们手动放进 JBIG2Stream 中。由于这里的字段和标志不需要使用解码器进行解码,因此可以手动编写代码将字段一个个放置进流中。
这一步的操作位于 exp 中的 hso-groupie/exploit/jbig2.py
,该脚本为所有用到的 segment 都编写了一个对应的 python 结构转 JBIG2Stream 字节流的操作;同时,上一节中暴露给 python 所调用的 bitmap encoder 接口,也是在该脚本中所使用。
这样,当我们使用 python 设计好一个个特定的 segments 后,我们便可以将这些 segments 快速转换成 JBIG2Stream 流数据,方便快捷。
五、漏洞利用流程
1. 堆风水
a. 创建堆空洞
先放上这张镇楼图:
为了利用这个堆溢出漏洞,我们需要充分发动堆风水,将指定的结构放至对应的堆块。这里,我们的堆风水需要完成以下几个目标:
让 pdf 在解析 TextRegionSeg 时,其创建的 syms 指针数组位于
undersized syms buffer
处让内含存放超多指针的 JBIG2SymbolDict 结构体的 segment 放置在
segments GList backing buffer
处这里,我们打算让 JBIG2SymbolDict 结构体存放至 global segment 中,因为 SymbolDictSegment 不依赖与任何的 Segments,但是后续的 TextRegionSegment 会依赖这些 SymbolDictSegment。
让 pageBitmap 结构体占据图中
JBIG2Bitmap
那块内存,并让其 data 占据图中上面bitmap backing buffer
那块内存。通读代码,我们可以得知绝大多数 segments 在解析时,都可以让其 bitmap 与 pageBitmap 进行运算,并将结果保存在 pageBitmap 上。因此让 pageBitmap 拥有越界读写的能力是最好的选择。
我们先尝试在 global segment 中分配三个不同 Bitmap 大小的 SymbolDict 出来。这里分配不同大小的 SymbolDict 是为了后续在 TextRegionSeg 中,排列组合 size 至溢出,因此这三个堆块的位置不需要关心:
1 | # global segment |
其中 size_to_overflow 为上图中 overflow 的字节数,具体计算过程稍后介绍。
此时我们看看分配完这三个 SymbolDict 后的 bins 是什么情况,可以看到有大量的碎片堆块:
1 | pwndbg> bins |
这些碎片堆块对于接下来的堆风水是相当不利的,因此需要将其全部分配掉。这里使用的是 PageInfoSeg
来分配内存,因为通读代码可以发现 JBIG2Stream::readPageInfoSeg
函数除了分配一个堆块以外,没有产生其他任何影响:
1 | def DummyAlloc(size): |
分配后的 bin 如下所示,可以看到清爽了不少:
1 | pwndbg> bins |
那么接下来的问题是,如何设计堆风水?exploit 给了一个清晰明了的做法:
利用 global segment GList 满则扩增的特性创建堆空洞,进而让其他结构体来占据这些内存空洞,完成堆风水。
什么意思呢?我们看看 GList 的一些类方法:
1 | GList::GList() { |
可以看到,初始时 GList size 为 8。当 GList 中元素个数超过容量时,GList 容量将会双倍扩增。也就是说,初始时的 size 为 8,下次扩增后的 size 是 16,再下次扩增后的 size 为 32,再下下次的 size 为 64(单位,个指针)。
扩增所使用的堆函数为 realloc
,即当 GList 容量扩增后,原先那个堆块将被释放。同时又因为上面已经将其余全部小堆块全都分配出去了,因此 GList 容量扩增所分配的新堆块,一定来自于 top chunk,这就能保证每次 GList 容量扩张时,新堆块的分配顺序一定是从低地址向高地址分配。
因此尝试让 global segment GList 多次扩展,从 8 扩展至我们所需要的最终大小 64:
代码中的 glist_capacity == 32。个人认为这个数表示的是第几次 append global GList 时会扩充 GList size 至 64。
1 | global_file = [ |
global segment 的堆风水执行结束后,其堆布局大致如下:
注意 segNum 从 3 开始的 Symbol Dict,其结构体所分配的堆块(chunk size = 0x40)也是直接来自于 top chunk 。
1 | // low address -------------------------------------------- |
接下来,只需分别
- 让 pageBitmap backing store 占据 size=16 的 Glist 堆空洞
- 让解析 TextRegion 时创建的 syms 指针数组占据 size=32 的 Glist 堆空洞
即可完成堆布局。
pageBitmap 的 JBIG2Bitmap 结构体堆位置在下文中将会说明。
最后贴个 gdb script,可以使用该 gdbscript 辅助观察内存布局:
1 | file ../../xpdf-4.03/build/xpdf/pdftohtml |
b. 占据堆空洞
global stream 中的解析操作是为了创建堆空洞,那 main stream 的解析操作就是为了占据堆空洞。
承接上文,接下来我们试着分配一个全新的 pageBitmap 结构,并让其 backing store 占据 size=16 的 Glist 空洞:
代码中的 GLIST_DATA_SIZE = 0x200,表示 size=64 时 global glist data 占据的字节数。
1 | page0 = [ |
此时堆布局如下:
1 | // low address -------------------------------------------- |
这里简单说一下 pageBitmap 结构本身的堆块分配(JBIG2Bitmap),由于其 size 0x20 在堆链上找不到可分配的堆块,因此将仍然从 top chunk 中分配,故其地址位于 size=64 的 Glist 位置的高地址处,满足堆风水要求。
接下来需要在解析 TextRegion 时继续占用 size=32 的 Glist 堆空洞。因此 TextRegion 中创建的用户内存大小必须是 syms_size = GLIST_DATA_SIZE // 2
,正好对应到 size=32 的 Glist 堆空洞大小。
但在做进一步的利用之前,我们需要绕过一个比较有趣的 sanity check:
1 | // sanity check: if the w/h/x/y values are way out of range, it likely |
xpdf-4.03/xpdf/JBIG2Stream.cc
中多次出现上面的这种 sanity check,判断当前正在处理的 w是否越过了当前的 pageW 和 pageH(两个 JBIG2Stream 类的成员变量,用于表示当前 page 的宽度和高度),如果越界则说明当前解析过程可能存在问题,那么则立即停止解析当前 segment。
看上去好像这个 sanity check 没啥问题……
但实际上,我们回过头看看 readPageInfoSeg
函数的代码:
1 | void JBIG2Stream::readPageInfoSeg(Guint length) |
我们可以非常容易的发现, 即便 readPageInfoSeg
函数中检测到了 pageW
和 pageH
的异常,但也只是简单的退出掉当前 seg 的解析,保留了畸形 pageW
和 pageH
的值在 JBIG2Stream 类成员中。
这样,我们可以尝试插入一个超大 pageW 和 pageH 的 PageInfoSeg,从而污染这两个字段为超大值,bypass 后续所有新增加的 sanity check:
1 | page0 = [ |
bypass 掉这个 sanity check 后,接下来就可以尝试创建 TextRegionSeg 来进行堆溢出了。承接上面所说的,这里所创建的 TextRegionSeg 需要满足几种要求:
- 其内部创建的 syms 大小必须是 syms_size(这个值上面已经说明了)
- 向堆块写入的数据大小为
size_to_overflow
个字节,即实际写size_to_overflow // 8
个指针
因此接下来在 main stream 中,需要合理组合 TextRegion 所引用的 Symbol Dict 大小:
1 | # Trigger the out-of-bound write. |
上面代码的组合中,
sizetooverflow/8 + {0x10000 + (symssize − sizetooverflow)/8} + 0xffff0000 = 0x*100000000 + *symssize/8,即刚好分配 syms_size 个字节。
又因为先 ref 的那个 Symbol Dict 的大小为 size_to_overflow // 8
个指针。因此当 readTextRegion 解析第一个 ref 的 Symbol Dict 时,刚好向 syms 堆块中写入 size_to_overflow
个字节,直接溢出至 pageBitmap JBIG2Bitmap 结构体头部位置,如此便能达到溢出的目的。
这里说明一下 size_to_overflow 是怎么得出的,先上堆布局:
1 | // low address -------------------------------------------- |
根据堆布局可得知:
1 | size_to_overflow = ( |
之后,将 readTextRegionSeg 中刚刚被释放掉的那个 syms_size 大小的堆块再次分配回来,防止在后续的利用中出现可能的崩溃。
1 | # Take back the free-d syms, hold it to prevent potential crash. |
由于越界写入 pageBitmap JBIG2Bitmap 结构体头部位置的是指针值,可以越界读写的数据有限,因此我们需要根据这个有限的 pageBitmap 越界读写原语,来自己修改自己的 JBIG2Bitmap 结构体头,将其中的 w修改的更大,扩展自己的读写范围。根据上面的堆布局,同样可以得出 page_bitmap_buf
至 pageBitmap JBIG2Bitmap
的距离:
1 | page_bitmap_buf_to_class_offset = ( |
之后将其 w分别更改为 w = 227、h = 224、line = 224:
imm 为 true 表示即时渲染,即立即修改 pageBitmap 上的指定位置。
1 | # Overwrite pageBitmap->w, h and line |
修改后的 pageBitmap 的二维空间构造:
1 | +------------------> w=2^27 bit |
最后创建带有 16 个 Bitmap 的 SymbolDict ,以备接下来的利用所使用:
1 | # 16 "variables". Since we can only do bitwise operations relative to page bitmap |
这些 SymbolDict 将用于地址解引用原语中,具体在下面会详细介绍。
整体的堆风水布局大体如上所示。完成堆溢出后,pageBitmap 具备了大偏移读写的功能,因此接下来就要开始写原语利用了。
2. 位运算原语
还记得先前介绍的 GenericRefinementRegionSeg
么(不记得就翻到上面看看),接下来我们需要利用这个 seg 的特性来编写任意位的位运算器。
exploit 中实现的位运算器如下所示:
1 | class BitSeg: |
原语 bitop 的 oa、ob 两个参数的单位为 bit,op 有 5 种。
bitop 原语初始时将一维偏移量 oa、ob 分别映射至 bitmap 的二维偏移量 xy1、xy2,之后在解析 ob 对应的 RefinementRegionSeg 时,从 pageBitmap 中取出对应 xy2 的数据,并将其存入 segments 中。
一维偏移量向二维偏移量映射时,为什么使用的是 2^27 作为除数/模数呢?因为这是上面所修改后的 width 的大小。
接下来当 hso 解析 oa 对应的 RefinementRegionSeg 时,hso 会重新读入先前存入的 ob 对应的 RefinementRegion,并将其与 pageBitmap 特定 xy1 位置进行位运算,达到指定 pageBitmap 上任意两位之间进行位运算的目的。
这里需要注意的是,findSegment 查找算法的核心,是依次遍历 segments 列表的元素并比对 segNum 来进行查找。因此每次添加进 segment 的 RefinementRegion,其 segNum 一定不能与之前 append 进去的 segments 相同!
当位运算原语 binop
可用后,接下来就可以构建其他原语:
1 | bitwise_mov = lambda a, b: bitop(a, b, CombOp.Replace) |
这里的 op_q_q
原语,其 oa、ob 参数的单位为字节(注意和 binop 的单位并不相同)。
op_q_q
原语的目的,是对给定 oa
和 ob
的相对一维偏移字节所对应的两个位置,做一次8字节位运算。
举个例子,原语 and_q_q(0, 8)
,执行的操作为:
- 将偏移量为 0字节 的位置上的八字节(即 0-7 这8个字节),与 偏移量为 8字节 的位置上的 八字节(即 8-15 这8字节),进行一次一一对应的 and 运算。
- 将运算结果放置在偏移量为 0字节 的位置上的八字节(即 0-7 这8个字节)上。
这个原语其实很好理解,只是用文字记录下来感觉不太好记录,也可能是我文笔不太好。
之后便是通过位运算来构建8字节全加器,可以先看看这篇文章再看看代码:
1 | # Don't worry, Libra won't hu^W^W^W Xpdf allocates 1 more byte |
其全加器结构如下所示:
3. 立即数运算原语
除了上面所介绍的位运算原语以外,还有加载外部立即数计算的原语。
1 | def op_q_imm(offset, imm, op): |
readGenericRegionSeg 方法可从外部 JBIG2Stream 流中读入一个 bitmap 并将其与 pageBitmap 上的特定位置进行运算,因此 GenericRegionSeg 可用于此处的立即数运算原语。
4. 地址解引用原语
当我们有了某个指针的绝对地址后,我们如何将这个指针从该绝对地址中读取出来呢?这就需要用到地址解引用操作。这里,exploit 准备了两个原语:
rebase_variable_q
:将 pageBitmap 中一维偏移为addr_page_offset
处的 8 字节数据,复制进堆风水中最后一步所创建的带有 16 个 Bitmap 的 SymbolDict 中,第 idx 个 JBIG2Bitmap 的 data 字段上:注意,是直接将值覆盖在 JBIG2Bitmap 的 data 字段上,而不是写进 data 指针所指向的内存上。
1
2
3
4
5def rebase_variable_q(idx, addr_page_offset):
mov_q_q(
variable_bitmap_offset + idx * ptmalloc_chunk_size(0x20) + 0x18,
addr_page_offset,
)load_variable
:读取最后一个 Symbol Dict 中,第 idx 个 JBIG2Bitmap backing store 里的(即 data 指针解引用后的内存上) 的第一个 8 字节数据,至 pageBitmap 中一维偏移为to_page_offset
处的 8 字节内存位置。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16def load_variable(to_page_offset, idx):
to_page_offset *= 8
x, y = to_page_offset % 2 ** 27, to_page_offset // 2 ** 27
page0.append(
TextRegion(
233,
x=x,
y=y,
w=64,
h=1,
imm=True,
instances=[idx],
ref_symbol_cnt=16,
ref_segs=[105],
)
)
这两个原语一结合,就能达到地址解引用的目的。
5. 整体利用流程
各类原语已经都准备好了,接下来便是结合这些原语覆写 free_hook 为 libc_system 的地址。
首先,我们需要 leak 一个地址出来(这个地址自然不能是堆地址),通过查看堆布局:
1 | // low address ..... |
可以看到紧临着 pageBitmap 的便是 SymbolDict,因此我们可以尝试读取其虚表指针。
1 | # vtbl of a JBIG2SymbolDict adajacent to page bitmap buffer |
之后从外部读取一个相对偏移至 pageBitmap data + 8 的位置:
1 | # 计算出-vtbl_offset + free_got_offset |
然后再简单做个加法,就能得到 free 条目在 GOT 表上的绝对地址,放到 +0 处:
1 | # 计算vtbl地址+(-vtbl_offset + free_got_offset)得到free_got的地址,放到+0处 |
接下来,尝试对该 free.got
地址进行解引用,获取 free.libc
地址:
1 | # 从+0处取出free_got的地址,放到第0个"变量"data 指针处 |
在获取到 free.libc
地址后,读入一个相对偏移并做个加法,经过简单几步,我们便能得到 free_hook
和 libc_system
的绝对地址:
1 | # 把LIBC_FREE_OFFSET这个立即数的值放到+0处 |
注意,此时 pageBitmap->data
上的数据为:
1 | +0: free_hook_address +8: libc_system_address |
接下来便是计算 pageBitmap->data + 8
的地址,即存放着这个 libc_system_address
值的内存地址:
1 | # 取出pagebitmap的data指针,放到+24处 |
计算出这个内存地址的用处是什么呢?继续向下看,注意重头戏快到了:
1 | # 取出pagebitmap的data指针的值放到第0个变量的 data 字段 |
这样,此时的 free hook 便被改写成了 libc_system 的地址,接下来便是尝试执行命令。
这里再 append 一个 带有待执行命令的 bitmap:
1 | page0.append( |
这样当 readGenericRegionSeg
函数结束时,新创建的 bitmap(即带有命令的 bitmap)将会被 free 掉,这样就可以触发 system(command)
:
1 | void JBIG2Stream::readGenericRegionSeg(Guint segNum, GBool imm, |
但有两点需要注意:
imm 必须为 true,这样才能触发 delete 操作。
创建的 GenericRegionSeg,其二维偏移 xy 映射至一维偏移后的偏移量,不能小于 64(即 8 字节)
这是因为代码中会先执行
pageBitmap->combine
再执行delete bitmap
操作。此时的pageBitmap->data
为 free hook address,如果执行 combine 时修改了pageBitmap->data
最低的8个字节,那么 free 时就无法调用到 libc_system,因为保存在 free_hook 上面的 libc_system 地址被破坏了。