文章首发于
https://mp.weixin.qq.com/s/7STPL-2nCUKC3LHozN6-zg
概述
本文介绍对cajviewer中对HN文件格式的逆向分析并介绍如何编写相应的010editor模板,最后介绍通过分析如何构造POC,触发cajviewer在解析HN文件中的图片时的漏洞。HN文件是cajviewer支持的其中一种文件格式,这个文件类似于PDF,可以包含文字、图片等,下图是一个HN文件应用模板后的截图,具体的分析过程请看正文部分。
样例文件和010模板
https://github.com/hac425xxx/cajviewer-fuzz-data
https://github.com/hac425xxx/cajviewer-fuzz-data/releases/download/2020-8-2/sample.7z
正文
解析文件头
基于上文的分析,我们知道cajviewer
使用CAJFILE_OpenEx1
函数来打开和解析一个文件,因此这个函数就是我们的分析入口.
CCAJReaderStruct *__fastcall CAJFILE_OpenEx1(char *fpath, char *a2)
{
file_type = CAJFILE_GetDocTypeEx1(fpath, a2, 0LL);// 获取文档类型
switch ( file_type )
{
case 1u:
case 2u:
case 8u:
case 0xAu:
case 0x1Bu:
ccaj_reader = operator new(0x210uLL);
a2 = v12;
CCAJReader::CCAJReader(ccaj_reader, v12); // 根据文件类型,构造Reader对象
函数首先调用CAJFILE_GetDocTypeEx1
根据文件头和文件名返回一个表示文档类型的int值,对于样本文件来说会进入CCAJReader::CCAJReader
构造文档对象用于后续的解析。
通过分析类的构造函数可以大概了解对象的内存布局,比如通过new函数的参数可以知道 CCAJReader::CCAJReader
对象的大小为 0x210
字节,下面看看类的构造函数
首先赋值虚表为 vtable for CCAJReader + 2
,其实就是0xB19B0
。
我们可以把这个抠出来,作为一个结构体以便后续分析
struct CCAJReaderVtableStruct
{
void *_ZN10CCAJReaderD2Ev;
void *_ZN10CCAJReaderD0Ev;
....................................
....................................
void *_ZN7CReader16InternalFileOpenEPKc;
void *_ZN7CReader18InternalFileLengthEPv;
void *_ZN7CReader16InternalFileSeekEPvll;
void *_ZN7CReader16InternalFileReadEPvS0_l;
void *_ZN7CReader17InternalFileCloseEPv;
void *_ZN7CReader19InternalFileIsReadyEPKcijj;
};
然后设置CCAJReaderStruct
的vtbl
的类型为CCAJReaderVtableStruct*
,这样再看虚函数调用时就可以很方便的定位到目标函数,其他用到的类也用这种方式逆向即可,继续往下看
这里调用CCAJReader::Open
对文件进行初步解析,该函数实际会进入CAJDoc::Open
读取文件内容并解析
首先这里调用BaseStream::getStream来创建一个stream对象,在cajviewer里面通过stream对象来从各种来源读取数据,比如网络、文件、内存等。
就我们这个例子实际构建的对象为FileStream
,创建完后就会调用FileStream::open
和FileStream::seek
打开文件并把文件指针重定向到文件开头。
然后会进入 CAJDoc::OpenNHCAJFile
进行具体的解析,第二个参数为0,在该函数里面首先会调用FileStream::read
读取文件开头的0x88
字节,并进行简单的判断
校验了前0x88字节的部分数据后,会再次读取 0x50字节的数据(0x10+0x40)
其中buffer_0x10.page_count
表示文件中包含的页面数,这个通过观察下面的引用来推测,继续往下
这里首先校验buffer_0x10.field_0
是否大于 0x18f
,如果大于0x18f
就会再次读取一些内容作为元数据,然后会根据这个值设置item_size
。
首先cajdoc->current_offset
在前面读取内容时会进行调整,从cajdoc->current_offset
开始就是表示CAJPage
的信息数组,数组中每一项的大小为 cajdoc->item_size
,类的构造函数的最重要的参数是第三个参数,表示该CAJPage
在文件中的偏移,后面解析时会用到这些。
至此我们可以得到文件开头的格式为
0x88字节的hn_header
0x10字节的buffer_0x10;
0x40字节的buffer_0x40;
如果buffer_0x10.field_0 > 0x18F,后面还会跟一个 0x84字节的buffer_0x84 和 308 * buffer_0x84.count 字节的内存
然后是buffer_0x10.page_count个page_info结构,每个结构的大小item_size为12或者20,item_size 根据buffer_0x10.field_0来判断
此时我们可以写一个简单的010editor模板,来解析文件头的数据
typedef struct{
ubyte data[0x88];
}HN_FILE_HEADER;
typedef struct{
uint32 field_0;
uint32 field_4;
uint32 page_count;
uint32 field_0xc;
}BUFFER_0X10;
typedef struct{
ubyte gap[12];
uint16 w1;
uint16 w2;
uint32 unknown_dword;
uint32 dword_20;
ubyte data[40];
}BUFFER_0X40;
typedef struct{
ubyte data[0x80];
uint32 count;
}BUFFER_0X84;
local uint32 item_size = 12;
HN_FILE_HEADER hn_header;
BUFFER_0X10 buffer_0x10;
BUFFER_0X40 buffer_0x40;
local uint64 page_info_offset = FTell();
if(buffer_0x10.field_0 > 0x18F)
{
BUFFER_0X84 buffer_0x84;
local uint64 cur_pos = FTell();
page_info_offset = 308 * buffer_0x84.count + cur_pos;
}
if(buffer_0x10.field_0 <= 0xC7)
{
item_size = 12;
}
else
{
item_size = 20;
}
FSeek(page_info_offset);
这里有几个关键的点,在010editor的模板中类型定义和local开头的局部变量不会导致文件指针的移动,当直接定义结构体变量时就会导致010editor读取文件内容并进行解析。
HN_FILE_HEADER hn_header;
比如这个代表010editor
会读取0x88
字节到hn_header
并会移动文件指针,最后会使用FSeek(page_info_offset)
把文件指针移动到page_info
开始的位置,详细的教程和语法可以看下面的链接
https://bbs.pediy.com/thread-257797.htm
解析页面数据
解析完文件头的数据后会调用CAJPage::LoadPageInfo
解析具体的页面信息
函数逻辑比较简单,就是FileStream::seek
到指定的文件偏移,然后读取item_size
数据用于page_info
,然后会把page_info
的数据保存到当前page对应的结构体里面, page_info
的结构如下
struct page_info
{
int file_offset; // page数据在文件中的偏移
int size; // page数据的大小
__int16 pic_count; // page中的图片个数
__int16 field_A;
__int64 field_C;
};
然后会跳到page_info.file_offset
,读取page
数据的前0x20个字节,然后从里面解析了一些数据,用途不明。
加载完page_info
后会调用CAJPage::LoadPage
加载页面的文本数据
这里首先跳转到page
数据所在的文件偏移,然后把页面的数据读出来
这里对文件内容解析,首先从头8个字节里面解析出当前page
的heigh
和width
,然后后面是具体的文本数据,然后判断文本数据开头是否有COMPRESSTEXT
,如果是表示文本数据是压缩过的会使用UnCompress
对文本数据进行解压。
解析完page
的文本数据后会把page
的图片数据在文件的起始偏移记录在page->pic_info_foffset
里面,解析完之后会进入CAJPage::LoadPicInfo
加载图片的元数据
这里会根据page->page_info.pic_count
创建CAJ_FILE_PICINFO
数组,数组中的每个元素为pic_info
结构,结构体定义如下
struct pic_info_struct
{
int type; // 图像类型
int offset; // 图像数据在文件中的偏移
int size; // 图像数据的大小
};
通过这个函数每个page
的图片信息会保存到page->caj_picinfo_list
里面,然后会在CAJPage::LoadImage
里面对页面的某个图片数据进行解析
函数的流程也简单,首先根据图片的索引在cajpage->caj_picinfo_list
里面找到图片的picinfo
结构,然后根据该结构读取图片的数据并使用UnCompressImage
对图片数据进行解析。
至此我们可以得到page数据的组织方式如下
首先在文件头后面是buffer_0x10.page_count
个page_info
结构,page_info
结构里面记录了页面的数据所在的文件偏移、内容的大小以及页面包含图片的个数,然后根据这些信息可以得到页面的文本数据和图片数据(图片数据紧跟在文本数据的后面)。
这部分的010模板如下
typedef struct{
uint32 type;
uint32 file_offset;
uint32 size;
local uint64 backup_offset = FTell();
FSeek(file_offset); // move to data offset
ubyte pic_data[size]; // page_data
FSeek(backup_offset); // move back
}PICINFO;
typedef struct (uint32 size){
PAGE_CONENT_HEADER page_hdr;
local char tmp[12];
ReadBytes(tmp, FTell(), 12);
if(Memcmp(tmp, "COMPRESSTEXT", 12) == 0)
{
char compress_sig[12];
uint32 decompressed_size;
char compressed_data[size - 12 - 4 - sizeof(PAGE_CONENT_HEADER)];
}
else
{
ubyte page_text_content[size - sizeof(PAGE_CONENT_HEADER)]; // page_data
}
}PAGE_CONTENT;
typedef struct _PAGE_INFO_ITEM{
uint32 file_offset;
uint32 size;
uint16 pic_count;
uint16 field_A;
if(item_size==20)
{
uint64 field_C;
}
local uint64 backup_offset = FTell();
FSeek(file_offset); // move to data offset
PAGE_CONTENT page_content(size);
local uint32 i = 0;
while(i < pic_count)
{
PICINFO pic_info;
i++;
}
FSeek(backup_offset); // move back
}PAGE_INFO_ITEM;
解析完后的效果图如下:
构造POC的技巧
通过前面的分析可知UnCompress
会对页面文本数据进行解压,简单的看下UnCompress
的实现我们可以知道该函数调用了zlib 1.1.3
版本解压文本数据,这个版本有很多漏洞,如果我们想触发UnCompress
的漏洞就可以把文件中压缩文本数据替换成zlib的poc数据即可
如果是要触发解析图片的的漏洞时也是一样的思路,替换掉正常文件中的某个图片数据即可