cajviewer逆向分析与漏洞挖掘

百家 作者:OPPO安全应急响应中心 2021-02-23 20:53:49





以下文章由作者【hackedbylh】的连载有赏投稿,详情可点击OSRC重金征集文稿!!!了解~~
温馨提示:建议投稿的朋友尽量用markdown格式,特别是包含大量代码的文章




前言

CAJViewer是一个论文查看工具,主要用于查看caj文件格式的论文。本文介绍对该软件进行逆向分析和漏洞挖掘的过程。


Fuzz测试

Windows版本

首先分析的是CAJViewer的Windows版本,由于我们的目的是挖掘软件的漏洞,通过介绍我们知道CAJViewer本质上是一个文件解析程序,因此该软件的高危模块应该是软件中解析文件数据的部分,因此首先应该大概定义软件数据处理部分所在位置,Windows平台下可以使用 process monitor来进行初步的分析。

首先打开process monitor并开始捕获事件,然后使用CAJViewer打开一个caj文件,等文件解析完成后停止捕获事件。



然后我们可以过滤一下需要查看的事件,比如上图设置了只查看文件操作并且只查看对?input.caj?文件的操作,该文件就是之前让CAJViewer打开的文件。

然后我们可以找一下读文件的操作(ReadFile),因为大部分文件解析逻辑应该读一部分文件内容解析一部分,因此通过查看读文件时的调用栈就可以大概定位解析数据的模块,然后双击就可以查看调用相应函数的调用栈



通过查看多个数据读取的调用栈,可以发现ReaderEx.dll在调用栈中出现多次,因此大概可以猜测ReaderEx.dll应该主要负责处理文件数据。


Linux版本

逆向了一会ReaderEx.dll后,发现CAJViewer今年还发布了Linux版本,于是下载下来分析了一下。下载下来后是一个可执行文件CAJViewer-x86_64-libc-2.24.AppImage,执行起来查看进程的maps发现其实软件会在tmp目录把打包好的二进制解压,然后去执行tmp目录下的二进制。



这里可以直接把/tmp/.mount_CAJVierjayBH/拷贝到一个目录,然后就可以直接执行?cajviewer了。

查看解压处理的二进制发现一个libreaderex_x64.so,看名字应该是ReaderEx.dll的Linux版本,然后使用IDA打开,发现比Windows版本的要好分析一点,信息也比ReaderEx.dll的多。于是接下来决定对Linux版本的二进制进行分析。

首先看看主程序cajviewer,查看main函数可以发现软件是用qt写的



之后翻了一下函数列表,发现了MainWindow::OpenFile,看名称应该是打开一个文件。

__int64?__fastcall?MainWindow::OpenFile(MainWindow?*this,?const?QString?*a2)
{


??v2?=?this;
??QString::toUtf8_helper(&v16,?a2);
??memset(v19,?0,?sizeof(v19));
??*v19?=?0x2D8;
??*&v19[4]?=?256;
??*&v19[8]?=?CAJFILE_CreateErrorObject(&v20);
??v3?=?*&v19[8];
??if?(?*v16?>?1?||?(v5?=?*(v16?+?2),?v4?=?v16,?v5?!=?24)?)
??{
????QByteArray::reallocData(&v16,?v16[1]?+?1,?*(v16?+?11)?>>?31);
????v4?=?v16;
????v5?=?*(v16?+?2);
??}
??v6?=?CAJFILE_OpenEx1(v4?+?v5,?v19);???????????//?打开文件

这里对输入的QString进行一些处理后,调用了CAJFILE_OpenEx1函数,该函数位于libreaderex_x64.so


Fuzz CAJFILE_OpenEx1函数

函数代码如下


函数的第一个参数是要解析的文件路径,第二个参数是一块内存,这个参数的结构可以查看MainWindow::OpenFile调用点。


可以看到in_buf的结构如下

+0:?4个字节?in_buf的长度
+4:?4个字节?一个整形值
+8:?一个指针,?存放构造好的?ErrorObject

使用调试器在这个函数下个断点,然后打开一个文件就可以看到入参如下


之后有简单的翻了一些该函数的实现,以及使用该函数的位置可以大概确定CAJFILE_OpenEx1用于打开一个文件,并会对文件的内容进行解析,因此下面打算使用AFL Qemu模式Fuzz一下这个函数。

Fuzz之前需要写一点代码把so加载到内存,然后构造参数对目标函数进行测试。

首先需要把SO加载到内存中并获取目标函数的地址

void?my_init(void)?__attribute__((constructor));?//告诉gcc把这个函数扔到init?section
void?my_init(void)
{
????void?*handle;
????handle?=?dlopen("/home/hac425/cajviewer/cajviewer-bin/usr/lib/libreaderex_x64.so",?RTLD_LAZY);

????struct?link_map?*lm?=?(struct?link_map?*)handle;
????printf("%lx\n",?lm->l_addr);

????p_CAJFILE_OpenEx1?=?dlsym(handle,?"CAJFILE_OpenEx1");
????p_CAJFILE_CreateErrorObject?=?dlsym(handle,?"CAJFILE_CreateErrorObject");
}

my_init会在main函数之前执行,代码流程如下

  • 首先dlopen把so加载到内存,并把so在内存中的基地址打印到屏幕,便于后续测试。

  • 然后使用dlsym获取CAJFILE_OpenEx1CAJFILE_CreateErrorObject函数的地址。

然后在main函数中就会构造参数调用目标函数

int?main(int?argc,?char?**argv)
{
????char?buf[0x2D8];
????printf("main:%p\n",?main);

????memset(buf,?0,?0x2D8);
????*(unsigned?int?*)buf?=?0x2D8;
????//?*(unsigned?int?*)(buf?+?4)?=?256;

????//?*(char*?*)(buf?+?8)?=?p_CAJFILE_CreateErrorObject();

????char?*ret?=?p_CAJFILE_OpenEx1(argv[1],?buf);
????return?0;
}

代码逻辑很简单,首先构造CAJFILE_OpenEx1函数的第二个参数,然后把argv[1]作为文件路径传入函数。

然后编译一下

gcc?CAJFILE_OpenEx1.c?-o?test_CAJFILE_OpenEx1_dbg?-ldl?-lheapasan?-L?libheapasan/??-g

编译后执行一下,可以看到正常执行完了,并打印出so的基地址和main函数的地址。

harness$?./test_CAJFILE_OpenEx1_dbg?~/input.caj
string?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstringto?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?int
image?base:0x7f6d87bff000
p_CAJFILE_OpenEx1:0x7f6d881e486c
main:0x555ed124cb71

接下来再使用afl-qemu-trace执行一下,获取一些地址用于Fuzz,使用afl-qemu-trace执行一个可执行程序时,其进程的so的地址都是固定的。

harness$?~/AFLplusplus-2.66c/afl-qemu-trace?./test_CAJFILE_OpenEx1_dbg?~/input.caj
string?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstringto?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?intstring?to?int
image?base:0x400133e000
p_CAJFILE_OpenEx1:0x400192386c
main:0x4000000b71

可以看到libreaderex_x64.so的基地址为0x400133e000,?test_CAJFILE_OpenEx1_dbg?的main函数的地址为0x4000000b71

然后去IDA中查看libreaderex_x64.so中代码段的范围

所以可以得到afl-qemu-trace执行时libreaderex_x64.so中代码段的范围为

开始地址:?0x400133e000+0x3D4880?=?0x4001712880
结束地址:?0x400133e000+0x90984F =?0x4001c4784f

然后可以使用AFL进行测试了

export?AFL_CODE_START=0x4001712880
export?AFL_CODE_END=0x4001c4784f
export?AFL_ENTRYPOINT=0x4000000b71
/home/hac425/AFLplusplus-2.66c/afl-fuzz?-m?none?-Q?-t?20000?-i?in?-o?out?--?./test_CAJFILE_OpenEx1_dbg?@@

其中设置的环境变量的作用如下

AFL_CODE_START?和?AFL_CODE_END?表示需要统计覆盖率的范围
AFL_ENTRYPOINT?表示开启forkserver的位置


Fuzz UnCompressImage函数

在测试CAJFILE_OpenEx1时,去翻了一下libreaderex_x64.so里面的其他函数,在查看字符串时发现了一些源码路径。

拿路径去网上搜了一下,发现是用到了Kakadu_V2.2.3这个开源库,这个库很古老了(2008年的),用于解析jpeg2000格式,版本老往往表示存在漏洞几率较大,而且jpeg2000格式很复杂,在其他软件中也发现了很多漏洞,于是下面仔细的看了下。

下载到这个库的代码,然后一路回溯发现libreaderex_x64.so应该是在?jpeg2000.cpp里面实现了部分代码,最后一路跟到了DecodeJpeg2000函数,并基于开源代码把DecodeJpeg2000的参数基本弄清楚了。

继续往上跟DecodeJpeg2000,找到了UnCompressImage函数,这个函数应该是解析图片数据的统一接口了。

CAJViewer在解析CAJ等文件时,如果文件中嵌入了图片数据时,就会会使用libreaderex_x64.so中的UnCompressImage函数来对图片数据进行解析。


函数的参数信息如下:

buffer:?保存从文件中提取出的图片数据
type:?图片的类型
buffer_length:?图片数据的长度
剩下两个参数a4,a5:?个人猜测可能是需要将图片缩放的大小

然后编写代码,my_init的主要逻辑和?CAJFILE_OpenEx1函数的一致,只是需要hook一些函数,避免比Fuzz识别为crash,比如在代码里面有很多assert,如果直接执行到这个函数的话,会被afl识别为crash.



因此这里使用plt hook,把libreaderex_x64.so模块中的一些函数给hook了。


int?my_assert_fail()
{
????printf("my_assert_fail\n");
????exit(1);
????return?0;
}

int?my_cxa_throw()
{
????printf("my_cxa_throw\n");
????exit(1);
????return?0;
}

void?my_init(void)
{
?........................................
?........................................
????plt_hook_function("libreaderex_x64.so",?"__assert_fail",?my_assert_fail);
????plt_hook_function("libreaderex_x64.so",?"__cxa_throw",?my_cxa_throw);
}


然后再main函数中调用目标函数

int?main(int?argc,?char?**argv)
{
????printf("main:%p\n",?main);
????int?f_sz?=?0;
????char*?buffer?=?read_to_buf(argv[1],?&f_sz);
????char?*ret?=?p_UnCompressImage(buffer,?4,?f_sz,?100,?100);
????return?0;
}


然后其他的操作和Fuzz?CAJFILE_OpenEx1函数时一致,只是环境变量需要重新设置

/home/hac425/AFLplusplus-2.66c/afl-fuzz?-m?none?-Q?-t?20000?-i?image_fuzz/?-o?UnCompressImageOutput?--?./test_UnCompressImage?@@


部分漏洞分析

CImage::LoadBMP 内存为初始化漏洞

Cajviewer For Linux 在解析BMP图片时会进入 CImage::LoadBMP 函数,该函数中存在内存未初始化漏洞。


函数的流程如下

  1. 第8行,调用BaseStream::streamLength获取文件的大小。

  2. 第9行,调用FileStream::read从文件中读出14字节的文件头。

  3. 第10行,调用gmalloc分配内存用于存放文件的其他数据,这里实际上是直接调用malloc分配内存。

  4. 第12行,这里将分配的内存没有初始化直接传入FindDIBBits,该函数计算一个地址保存到this->DIBBits域。

  5. 第14行,这里会从this->DIBBits中读取数据,导致crash。

下面看看FindDIBBits的实现


这里取出a1的开始4个字节作为一个偏移值 v1,然后调用PaletteSize,这个函数的返回值的可以为0,128等数字值。

由于a1这个内存没有初始化,故v1有可能会很大,进而导致FindDIBBits会返回一个越界的地址

然后在CImage::CalibrateColor中就会去访问这个内存。

CImage::DecodeJbig 越界读写漏洞

CajViewer在解析CAJ等文件时,如果需要解析文件中嵌入的图片数据时,会使用libreaderex_x64.so中的函数来对图片进行解析,其中如果带解析的文件类型为Jbig文件时,会进入CImage::DecodeJbig函数进行解析:


其中重要函数的参数和作用如下:

  1. buf: 保存从文件中提取出的图片数据

  2. len: buf的长度

其中buf一开始是一个JbigInfo的结构,结构体的定义如下:


然后然后会进入CImage::CImage进行简单的文件解析。


首先使用JbigInfo中的字段计算一个sz, 然后使用 gmalloc分配内存,之后会使用memcpy 拷贝数据。

漏洞位于在计算sz时会导致整数溢出,进而导致会分配一个小于4LL * (1 << jbig_info->width2)的内存,然后在下面memcpy时会导致越界写。

此外整个过程没有校验jbig_info的长度,所以会导致越界读。


HN文件格式逆向

本节介绍对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;
};

然后设置CCAJReaderStructvtbl的类型为CCAJReaderVtableStruct*,这样再看虚函数调用时就可以很方便的定位到目标函数,其他用到的类也用这种方式逆向即可,继续往下看


这里调用CCAJReader::Open对文件进行初步解析,该函数实际会进入CAJDoc::Open读取文件内容并解析


首先这里调用BaseStream::getStream来创建一个stream对象,在cajviewer里面通过stream对象来从各种来源读取数据,比如网络、文件、内存等。


就我们这个例子实际构建的对象为FileStream,创建完后就会调用FileStream::openFileStream::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个字节里面解析出当前pageheighwidth,然后后面是具体的文本数据,然后判断文本数据开头是否有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_countpage_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数据即可



如果是要触发解析图片的的漏洞时也是一样的思路,替换掉正常文件中的某个图片数据即可



总结

本文介绍了如何分析一个软件并使用afl qemu模式来测试闭源二进制,并分析了其中的一些漏洞,最后介绍了逆向HN文件格式的过程,并介绍如何编写010editor模板来辅助poc构造。

附录

https://github.com/hac425xxx/cajviewer-fuzz-data


? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? ?

最新动态
2020年OPPO安全大事记
年度奖励 | 期待已久的年终奖出炉啦~
CVE-2020-16040: Chromium V8引擎整数溢出漏洞分析
揭秘QUIC的性能与安全
OSRC 2周年第二弹——第五次奖励升级
OPPO互联网DevSecOps实践
OPPO安全最新招聘信息



关注公众号:拾黑(shiheibook)了解更多

[广告]赞助链接:

四季很好,只要有你,文娱排行榜:https://www.yaopaiming.com/
让资讯触达的更精准有趣:https://www.0xu.cn/

公众号 关注网络尖刀微信公众号
随时掌握互联网精彩
赞助链接