cajviewer逆向分析-HN文件格式分析和010editor模板开发

文章首发于

https://mp.weixin.qq.com/s/7STPL-2nCUKC3LHozN6-zg

概述

本文介绍对cajviewer中对HN文件格式的逆向分析并介绍如何编写相应的010editor模板,最后介绍通过分析如何构造POC,触发cajviewer在解析HN文件中的图片时的漏洞。HN文件是cajviewer支持的其中一种文件格式,这个文件类似于PDF,可以包含文字、图片等,下图是一个HN文件应用模板后的截图,具体的分析过程请看正文部分。

image.png

样例文件和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字节,下面看看类的构造函数

image.png

首先赋值虚表为 vtable for CCAJReader + 2,其实就是0xB19B0

image.png

我们可以把这个抠出来,作为一个结构体以便后续分析

struct CCAJReaderVtableStruct
{
  void *_ZN10CCAJReaderD2Ev;
  void *_ZN10CCAJReaderD0Ev;
  ....................................
  ....................................
  void *_ZN7CReader16InternalFileOpenEPKc;
  void *_ZN7CReader18InternalFileLengthEPv;
  void *_ZN7CReader16InternalFileSeekEPvll;
  void *_ZN7CReader16InternalFileReadEPvS0_l;
  void *_ZN7CReader17InternalFileCloseEPv;
  void *_ZN7CReader19InternalFileIsReadyEPKcijj;
};

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

image.png

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

image.png

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

image.png

就我们这个例子实际构建的对象为FileStream,创建完后就会调用FileStream::openFileStream::seek打开文件并把文件指针重定向到文件开头。

image.png

然后会进入 CAJDoc::OpenNHCAJFile 进行具体的解析,第二个参数为0,在该函数里面首先会调用FileStream::read读取文件开头的0x88字节,并进行简单的判断

image.png

校验了前0x88字节的部分数据后,会再次读取 0x50字节的数据(0x10+0x40)

image.png

其中buffer_0x10.page_count表示文件中包含的页面数,这个通过观察下面的引用来推测,继续往下

image.png

这里首先校验buffer_0x10.field_0是否大于 0x18f ,如果大于0x18f就会再次读取一些内容作为元数据,然后会根据这个值设置item_size

image.png

首先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解析具体的页面信息

image.png

函数逻辑比较简单,就是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加载页面的文本数据

image.png

这里首先跳转到page数据所在的文件偏移,然后把页面的数据读出来

image.png

这里对文件内容解析,首先从头8个字节里面解析出当前pageheighwidth,然后后面是具体的文本数据,然后判断文本数据开头是否有COMPRESSTEXT,如果是表示文本数据是压缩过的会使用UnCompress对文本数据进行解压。

image.png

解析完page的文本数据后会把page的图片数据在文件的起始偏移记录在page->pic_info_foffset里面,解析完之后会进入CAJPage::LoadPicInfo加载图片的元数据

image.png

这里会根据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里面对页面的某个图片数据进行解析

image.png

函数的流程也简单,首先根据图片的索引在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;

解析完后的效果图如下:

image.png

构造POC的技巧

通过前面的分析可知UnCompress会对页面文本数据进行解压,简单的看下UnCompress的实现我们可以知道该函数调用了zlib 1.1.3版本解压文本数据,这个版本有很多漏洞,如果我们想触发UnCompress的漏洞就可以把文件中压缩文本数据替换成zlib的poc数据即可

image.png

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

image.png

posted @ 2021-02-23 19:28  hac425  阅读(785)  评论(0编辑  收藏  举报