前言
漏洞复现参考于《捉虫日记》第二章,作者讲述了通过静态分析找漏洞的方法,通过vlc-0.9.4这个例子,演示了从程序分析到漏洞挖掘的整个过程。
vlc-0.9.4源码(看书里面有)
复现环境
- 系统 : win11
- 调试器 : windbg、immdbg
- 反汇编 : IDA pro
漏洞分析
vlc是一款音频视频流播放器,支持相当多的文件格式,这个洞是在对TiVo 媒体文件(后缀为ty或ty+)进行解析时出现的栈溢出漏洞(当然文件解析不是通过后缀进行的)。
源码分析
- 重要数据结构
include/vlc_demux.h
1 | struct demux_t |
底下的stream_t *s
是输入数据流,可以理解为以二进制格式用fopen打开一个文件,对应的数据流。
- 漏洞点
modules/demux/ty.c line 1623
1 | static void parse_master(demux_t *p_demux) |
底下的stream_Read(p_demux->s, mst_buf, 8 + i_map_size);
是漏洞出现的地方
steam_Read(src, dest, size);
是他自己实现的函数,从src中复制size大小的数据给dest,内部是通过memcpy实现的。
这里mst_buf
是大小为32字节的局部变量
i_map_size
来自于这里19、20行
/* parse header info */
19 stream_Read(p_demux->s, mst_buf, 32);
从输入流(数据块头部)中读入32个字节进入mst_buf
20 i_map_size = U32_AT(&mst_buf[20]);
从mst_buf[20]
开始将之后的4个字节转成unsigned int类型,赋值给i_map_size
- 到达漏洞点的程序执行路径
Demux( demux_t *p_demux )
-> \
get_chunk_header(demux_t *p_demux)
-> \
parse_master(demux_t *p_demux)
从宏观的角度分析下这条路径在做啥。
Demux
是在从块中读取一条记录,并进行解析(参见源码注释 modules/demux/ty.c line 386),音视频流在播放的时候是通过一个块一个块进行解析的。(在进行漏洞复现时,我将会造成程序栈溢出崩溃的视频文件放入程序播放时,程序播放到一半,然后崩溃,这同样证明程序在解析到某个位置的数据时,造成的崩溃)
get_chunk_header
从名字上看,此函数会读取块的头部,对头部进行解析,分析这个函数可以帮助我们定位块的头部特征
modules/demux/ty.c line 1839
1 | static int get_chunk_header(demux_t *p_demux) |
程序首先首先判断当前块是否为最后的填充(为了保证对齐),如果是的就跳过进入进入下一个块,否则当前位置就是数据块头。
stream_Peek
将p_peek
指向块头地址,然后检查块头标准位U32_AT(&p_peek[ 0 ] )
4字节。如果标志位为0,设置错误信息,并退出。
关注底部,当程序满足U32_AT( &p_peek[ 0 ] ) == TIVO_PES_FILEID
会执行parse_master(p_demux);
也就是漏洞发生的地方。
而TIVO_PES_FILEID
正是Tivo文件的标志,之后实际进行利用复现阶段也是通过这个标志位来定位到可利用数据块的块头。
#define TIVO_PES_FILEID (0xf5467abd)
源码中是这样定义的(modules/demux/ty.c line 112)
看看ida 确实是
崩溃实验
用的是捉虫的样本来做的实验
这个样本是一个正常的Tivo文件,捉虫上也是推荐通过修改正常的文件来进行利用分析更适合,因为我们并不知道具体的文件格式,正常文件,能保证程序能解析到我们想要的漏洞利用路径。
将文件放入Imhex,搜索标志位
0xf5467abd
我们可以知道从0x300000开始,是一个新的块,前面的00用于块对齐
定位
i_map_size
,并修改
uint8_t mst_buf[32];
每个单位是一字节;
stream_Read(p_demux->s, mst_buf, 32);
从头部开始读入32字节;
i_map_size = U32_AT(&mst_buf[20]);
20代表,从头部开始的第20个字节;也就是
0x14
,U32_AT
表示将后面四个字节给i_map_size
将其改为
0xff
这里长度我试了下
0x1ff
和0x500
不能引起崩溃,stream_read函数内部实现对输入长度有检查,猜测原因是这样的。immdbg调试程序,将修改后的文件放入其中程序崩溃EIP为
0x02003000
,左边调试器没有显示,说明eip这个地址是一个和程序正常运行无关的值,下次看到一定要很兴奋,有机会控制。查找这个eip的值,因为x86地址是以小端存放,所以搜索地址应该为
0x00000320
将其改为修改为如下
重新用Immdbg进行调试
可以控制返回地址和栈
至此复现完了捉虫日记第二章。但是我想到如果我在fuzz用遇到了栈溢出的崩溃,我该怎么去调试定位呢?
调试器、反汇编 找漏洞点
我现在有源码,有二进制,但是当程序运行崩溃时,由于eip已经发生了改变,我不知道程序在什么地方崩溃的。
- 静态分析和动态调试初步分析下程序的运行过程
程序通过vlc.exe作为引导,他调用了libvlc.dll中几个函数来加载分别是libvlc_new
、libvlc_add_intf
、libvlc_playlist_play
、libvlc_wait
前两个函数主要负责基础库的加载,后两个执行完就出来播放器的框了,wait是等待程序终止,清理下空间之后就退出程序。libvlc.dll
这几个函数的实现从libvlccore.dll
导入了大量函数,播放器的主要函数应该是由core来实现的。
播放器目录下dll的数量很多,而且有很多插件式的dll,这里陷入了僵局。
(读代码能力仍需提升)
- 我考虑从调试器角度入手
Immdbg里面的view->log模块,记录了程序开始到运行结束加载的dll。
当将文件丢进播放器运行,到程序运行崩溃的过程中,我看到了一个dll有被加载
libty_plugin.dll
这里的ty让我感觉熟悉。放入ida中,先看看导出函数表
并没有熟悉的函数,如Demux、get_chunk_head之类的
函数名的字符表也被取消了,但是看看函数内容时,有了发现
有字符串,用字符串在源码中搜索一番,发现正是get_chunk_head函数
之后对漏洞点终于可以进行定位了
调试器定位漏洞
这个程序他自己的dll在加载时使用的是静态地址。我先用了Immdbg,尝试对找到的漏洞点用bp
命令下断点,但是只有libty_plugin.dll
被加载,对应的地址才能被下断点,因为bp断点本质是将对应的地址机器码修改成中断的码。而dll被加载时,程序正在解析文件,那时我手动中断,程序会被卡死,不知道为啥。
所以我选择用windbg来进行调试。
windbg可以用bu
在dll没被加载时,就对dll的某个函数下断点,当dll被加载执行时,检查函数名是否相同来判断中断。
我对
libty_plugin.dll
导出的所有函数下了断点,因为不知道那个函数是入口。g
运行程序,将文件放入播放器,程序被中断
bp 0x61401D5A
对漏洞点下断点,g
运行程序
程序开始播放了起来(windbg效率比Immdbg<-高,<-他都播放不了)
播放到一半,windbg断了下来
用
dd esp esp+100
和kb
看看栈空间的情况
p
步过,在看看栈空间情况
bp 6140213C
定位到返回地址
返回地址被控制