想去看看昆仑雪山

cve-2008-4654_vlc_0.9.4

前言

漏洞复现参考于《捉虫日记》第二章,作者讲述了通过静态分析找漏洞的方法,通过vlc-0.9.4这个例子,演示了从程序分析到漏洞挖掘的整个过程。

vlc-0.9.4源码(看书里面有)

vlc-0.9.4 win32 binary

复现环境

  • 系统 : win11
  • 调试器 : windbg、immdbg
  • 反汇编 : IDA pro

漏洞分析

vlc是一款音频视频流播放器,支持相当多的文件格式,这个洞是在对TiVo 媒体文件(后缀为ty或ty+)进行解析时出现的栈溢出漏洞(当然文件解析不是通过后缀进行的)。

源码分析

  • 重要数据结构

include/vlc_demux.h

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct demux_t
{
VLC_COMMON_MEMBERS

/* Module properties */
module_t *p_module;

/* eg informative but needed (we can have access+demux) */
char *psz_access;
char *psz_demux;
char *psz_path;

/* input stream */
stream_t *s; /* NULL in case of a access+demux in one */

...
}

底下的stream_t *s​ 是输入数据流,可以理解为以二进制格式用fopen打开一个文件,对应的数据流。

  • 漏洞点

modules/demux/ty.c line 1623

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static void parse_master(demux_t *p_demux)
{
demux_sys_t *p_sys = p_demux->p_sys;
uint8_t mst_buf[32];
int i, i_map_size;
int64_t i_save_pos = stream_Tell(p_demux->s);
int64_t i_pts_secs;

/* Note that the entries in the SEQ table in the stream may have
different sizes depending on the bits per entry. We store them
all in the same size structure, so we have to parse them out one
by one. If we had a dynamic structure, we could simply read the
entire table directly from the stream into memory in place. */

/* clear the SEQ table */
free(p_sys->seq_table);

/* parse header info */
stream_Read(p_demux->s, mst_buf, 32);
i_map_size = U32_AT(&mst_buf[20]); /* size of bitmask, in bytes */
p_sys->i_bits_per_seq_entry = i_map_size * 8;
i = U32_AT(&mst_buf[28]); /* size of SEQ table, in bytes */
p_sys->i_seq_table_size = i / (8 + i_map_size);

/* parse all the entries */
p_sys->seq_table = malloc(p_sys->i_seq_table_size * sizeof(ty_seq_table_t));
for (i=0; i<p_sys->i_seq_table_size; i++) {
stream_Read(p_demux->s, mst_buf, 8 + i_map_size);

...
}

底下的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
static int get_chunk_header(demux_t *p_demux)
{
int i_readSize, i_num_recs;
uint8_t *p_hdr_buf;
const uint8_t *p_peek;
demux_sys_t *p_sys = p_demux->p_sys;
int i_payload_size; /* sum of all records' sizes */
msg_Dbg(p_demux, "parsing ty chunk #%d", p_sys->i_cur_chunk );
/* if we have left-over filler space from the last chunk, get that */
if (p_sys->i_stuff_cnt > 0) {
stream_Read( p_demux->s, NULL, p_sys->i_stuff_cnt);
p_sys->i_stuff_cnt = 0;
}
/* read the TY packet header */
i_readSize = stream_Peek( p_demux->s, &p_peek, 4 );
p_sys->i_cur_chunk++;
if ( (i_readSize < 4) || ( U32_AT(&p_peek[ 0 ] ) == 0 ))
{
/* EOF */
p_sys->eof = 1;
return 0;
}
/* check if it's a PART Header */
if( U32_AT( &p_peek[ 0 ] ) == TIVO_PES_FILEID )
{
/* parse master chunk */
parse_master(p_demux);
return get_chunk_header(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)

image看看ida 确实是

崩溃实验

用的是捉虫的样本来做的实验

这个样本是一个正常的Tivo文件,捉虫上也是推荐通过修改正常的文件来进行利用分析更适合,因为我们并不知道具体的文件格式,正常文件,能保证程序能解析到我们想要的漏洞利用路径。

  1. 将文件放入Imhex,搜索标志位 ​0xf5467abdimage

    我们可以知道从0x300000开始,是一个新的块,前面的00用于块对齐

  2. 定位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

    image

    将其改为0xff

    ​​image

    这里长度我试了下0x1ff​和0x500​不能引起崩溃,stream_read函数内部实现对输入长度有检查,猜测原因是这样的。

  3. immdbg调试程序,将修改后的文件放入其中程序崩溃​image​EIP为0x02003000​,左边调试器没有显示,说明eip这个地址是一个和程序正常运行无关的值,下次看到一定要很兴奋,有机会控制。

  4. 查找这个eip的值,因为x86地址是以小端存放,所以搜索地址应该为0x00000320

    image

  5. 将其改为修改为如下

    image

  6. 重新用Immdbg进行调试

    imageimage

    可以控制返回地址和栈

至此复现完了捉虫日记第二章。但是我想到如果我在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有被加载

image

libty_plugin.dll​ 这里的ty让我感觉熟悉。放入ida中,先看看导出函数表

image

并没有熟悉的函数,如Demux、get_chunk_head之类的

函数名的字符表也被取消了,但是看看函数内容时,有了发现​image

有字符串,用字符串在源码中搜索一番,发现正是get_chunk_head函数

image

之后对漏洞点终于可以进行定位了

image

image

调试器定位漏洞

这个程序他自己的dll在加载时使用的是静态地址。我先用了Immdbg,尝试对找到的漏洞点用bp​命令下断点,但是只有libty_plugin.dll​被加载,对应的地址才能被下断点,因为bp断点本质是将对应的地址机器码修改成中断的码。而dll被加载时,程序正在解析文件,那时我手动中断,程序会被卡死,不知道为啥。

所以我选择用windbg来进行调试。

windbg可以用bu​在dll没被加载时,就对dll的某个函数下断点,当dll被加载执行时,检查函数名是否相同来判断中断。

  1. 我对 libty_plugin.dll​ 导出的所有函数下了断点,因为不知道那个函数是入口。​image

  2. g​ 运行程序,将文件放入播放器,程序被中断​image

  3. bp 0x61401D5A​对漏洞点下断点,g​运行程序

    image

    程序开始播放了起来(windbg效率比Immdbg<-高,<-他都播放不了)

    image

    播放到一半,windbg断了下来

  4. dd esp esp+100​ 和 kb​ 看看栈空间的情况

    image

    image

  5. p​ 步过,在看看栈空间情况

    image

    image

  6. bp 6140213C​ 定位到返回地址

    image

  7. 返回地址被控制

    image