文章首发于
https://mp.weixin.qq.com/s/7STPL-2nCUKC3LHozN6-zg
前言
CAJViewer是一个论文查看工具,主要用于查看caj文件格式的论文。本文介绍对该软件进行逆向分析和漏洞挖掘的过程。
代码地址
https://github.com/hac425xxx/cajviewer-fuzz-data
正文
逆向分析
首先分析的是CAJViewer的Windows版本,由于我们的目的是挖掘软件的漏洞,通过介绍我们知道CAJViewer本质上是一个文件解析程序,因此该软件的高危模块应该是软件中解析文件数据的部分,因此首先应该大概定义软件数据处理部分所在位置,Windows平台下可以使用 process monitor来进行初步的分析。
首先打开process monitor并开始捕获事件,然后使用CAJViewer打开一个caj文件,等文件解析完成后停止捕获事件。
然后我们可以过滤一下需要查看的事件,比如上图设置了只查看文件操作并且只查看对 input.caj
文件的操作,该文件就是之前让CAJViewer打开的文件。
然后我们可以找一下读文件的操作(ReadFile
),因为大部分文件解析逻辑应该读一部分文件内容解析一部分,因此通过查看读文件时的调用栈就可以大概定位解析数据的模块,然后双击就可以查看调用相应函数的调用栈。
通过查看多个数据读取的调用栈,可以发现ReaderEx.dll在调用栈中出现多次,因此大概可以猜测ReaderEx.dll应该主要负责处理文件数据。
逆向了一会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测试
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_OpenEx1和CAJFILE_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 函数,该函数中存在内存未初始化漏洞。
函数的流程如下
-
第8行,调用BaseStream::streamLength获取文件的大小。
-
第9行,调用FileStream::read从文件中读出14字节的文件头。
-
第10行,调用gmalloc分配内存用于存放文件的其他数据,这里实际上是直接调用malloc分配内存。
-
第12行,这里将分配的内存没有初始化直接传入FindDIBBits,该函数计算一个地址保存到this->DIBBits域。
-
第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函数进行解析:
其中重要函数的参数和作用如下:
- buf: 保存从文件中提取出的图片数据
- len: buf的长度
其中buf一开始是一个JbigInfo的结构,结构体的定义如下:
然后然后会进入CImage::CImage进行简单的文件解析。
首先使用JbigInfo中的字段计算一个sz, 然后使用 gmalloc分配内存,之后会使用memcpy 拷贝数据。
漏洞位于在计算sz时会导致整数溢出,进而导致会分配一个小于4LL * (1 << jbig_info->width2)的内存,然后在下面memcpy时会导致越界写。
此外整个过程没有校验jbig_info的长度,所以会导致越界读。
总结
本文介绍了如何分析一个软件并使用afl qemu模式来测试闭源二进制。