zoukankan      html  css  js  c++  java
  • 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

  • 相关阅读:
    namespace std 定义的位置
    [Struts]学习日记3 在页面中显示条目列表
    [Hibernate]关于ID的一个容易混淆的地方
    [Struts]"Cannot find bean in any scope"之一解
    [Struts]HibernatePlugIn for Struts(转贴)
    日志搬家了!
    [Struts]学习日记2 增加一些验证
    实验室的项目 讨论
    Struts常见异常信息和解决方法
    参加婚礼
  • 原文地址:https://www.cnblogs.com/hac425/p/14437749.html
Copyright © 2011-2022 走看看