首页
社区
课程
招聘
[翻译]消失的字节:对 MS Office RTF 解析器的逆向工程
2018-3-30 15:21 4165

[翻译]消失的字节:对 MS Office RTF 解析器的逆向工程

2018-3-30 15:21
4165

消失的字节:对 MS Office RTF 解析器的逆向工程

微软的Office是2017年的众多攻击目标之一,除了发现的大量漏洞和发布的POC之外,恶意软件作者认为有必要防止由防病毒软件检测到“one-day”和“old-day”漏洞。很明显,使用RTF解析功能和特性已不足以有效逃避检测。随着对MS Office利用的兴起,我们遇到了大量的RTF被用作攻击容器的情况,这些样本“利用”Microsoft Word RTF解析器的实现来混淆所有其他第三方的RTF解析器,包括那些在AV软件中的解析器。

 

为了还原MS Office中的解析,我们需要对其进行逆向工程。

 

我决定首先看看较早版本的MS Office 2010,可以先研究一下早期的实现方法。然后,再与新版本中的相关实现方法进行比较。

 

一个RTF解析器包含一个具有37个状态的状态机,其中的22个状态是唯一的:

 

 

我们先看下最重要的状态以及那些对解析 \objdata(包含对象数据的目标控制字)有影响的那些状态。Microsoft OLE链接,Microsoft OLE嵌入对象和Macintosh版本管理器订阅服务器对象在RTF中用对象表示。以下是所有的情况:

enum

{
       PARSER_BEGIN = 0,
       PARSER_CHECK_CONTROL_WORD = 2,
       PARSER_PARSE_CONTROL_WORD = 3,
       PARSER_PARSE_CONTROL_WORD_NUM_PARAMETER = 4,
       PARSER_PARSE_HEX_DATA = 5,
       PARSER_PARSE_HEX_NUM_MSB = 7,
       PARSER_PARSE_HEX_NUM_LSB = 8,
       PARSER_PROCESS_CMD = 0xE,
       PARSER_END_GROUP = 0x10,
       // …
};

由于没有Microsoft Office的调试符号,所以无法恢复其原始的状态名称。不过,我已根据其基本功能选择了合适的名称。

 

在打开的RTF文件上执行的第一个状态是PARSER_BEGIN。在大多数情况下,它也是在处理完一个控制字之后执行的。此状态的主要目标是根据遇到的char,目标以及存储在'this'结构中并由控制字处理器设置的其他值来确定下一个状态。默认情况下,下一个状态是PARSER_CHECK_CONTROL_WORD

case PARSER_BEGIN:

       // … – 检查是否满足一些条件

        while (data.pos != data.end)
        {
                byte = *(uint8_t*)data.pos;
                data.pos++;
                if (this->bin_size > 0)
                {
                        goto unexpected_char;
                }
                // …
                if (byte == 9)
                {
                        // …
                        continue;
                }
                if (byte == 0xA || byte == 0xD)
                {
                        // …
                        break;
                }
                if (byte == ‘\\’)
                {
                        uint8_t byte1 = *(uint8_t*)data.pos;
                        if (byte1 == ‘\”)
                        {
                               if (this->destination == listname ||
                                       this->destination == fonttbl ||
                                       this->destination == revtbl ||
                                       this->destination == falt ||
                                       this->destination == leveltext ||
                                       this->destination == levelnumbers ||
                                       this->destination == liststylename ||
                                       this->destination == protusertbl ||
                                       this->destination == lsdlockedexcept)
                                       goto unexpected_char;
                               state = PARSER_CHECK_CONTROL_WORD;
                               // …
                               break;
                        }
                        if (byte1 == ‘u’)
                        {
                               // …
                               break;
                        }
                        state = PARSER_CHECK_CONTROL_WORD;
                        // …
                        break;
                }
                if (byte == ‘{‘)
                {
                        create_new_group();
                        // …
                        break;
                }
                if (byte == ‘}’)
                {
                        state = PARSER_END_GROUP;
                        break;
                }
        unexpected_char:
                // 它会根据我们的目的地设置下一个状态 / 或者跳转到 `unexpected_cmd ` 检查额外的东西。
                // …
                if (this->destination == pict ||
                        this->destination == objdata ||
                        this->destination == objalias ||
                        this->destination == objsect ||
                        this->destination == datafield ||
                        this->destination == fontemb ||
                        this->destination == svb ||
                        this->destination == macro ||
                        this->destination == tci ||
                        this->destination == datastore ||
                        this->destination == mmconnectstrdata ||
                        this->destination == mmodsoudldata ||
                        this->destination == macrosig)
                {
                        state = PARSER_PARSE_HEX_DATA;
                        data.pos–;
                        break;
                }
                // …
                break;
        }
        break;

PARSER_CHECK_CONTROL_WORD将检查下一个字符是控制字的开始还是控制符号,并相应地设置下一个状态。

case PARSER_CHECK_CONTROL_WORD:
        byte = *(uint8_t*)data.pos;
        if ((byte >= ‘a’ && byte <= ‘z’) || (byte == ‘ ‘) || (byte >= ‘A’ && byte <= ‘Z’))
        {
                state = PARSER_PARSE_CONTROL_WORD;
                this->cmd_len = 0;
        }
        else
        {
                data.pos++;
                this->temp[0] = 1;
                this->temp[1] = byte;
                this->temp[2] = 0;
                state = PARSER_PROCESS_CMD;
                this->cmd_len = 1;
                break;
        }

PARSER_PARSE_CONTROL_WORDPARSER_PARSE_CONTROL_WORD_NUM_PARAMETER 两种状态下, 由ASCII 字母组成的以Null结尾的控制字和以非Null 结尾的数字参数(如果有的话)会被存储在一个固定大小的缓冲区里。

case PARSER_PARSE_CONTROL_WORD:
        pos = this->temp + 1;
        parsed = this->temp + 1;
        while (data.pos != data.end)
        {
                byte = *(uint8_t*)data.pos;
                // length of null-terminated strings cmd + num should be <= 0xFF
                if ((byte == ‘-‘) || (byte >= ‘0’ && byte <= ‘9’))
                {
                        //if parsed == temp_end
                        // goto raise_exception
                        *parsed = 0;
                        parsed++;
                        pos = parsed;
                        if (parsed >= temp_end)
                        {
                               parsed = temp_end – 1;
                               *parsed = 0;
                               state = PARSER_PROCESS_CMD;
                               this->cmd_len = pos – (this->temp + 1);
                               break;
                        }
                        data.pos++;
                        this->cmd_len = pos – (this->temp + 1);
                        *parsed = byte;
                        parsed++;
                        pos = parsed;
                        state = PARSER_PARSE_CONTROL_WORD_NUM_PARAMETER;
                        break;
                }
                if (byte == ‘ ‘)
                {
                        data.pos++;
                        if (parsed >= temp_end)
                        {
                               parsed = temp_end – 1;
                        }
                        *parsed = 0;
                        state = PARSER_PROCESS_CMD;
                        this->cmd_len = pos – (this->temp + 1);
                        break;
                }
                if ((byte >= ‘a’ && byte <= ‘z’) || (byte >= ‘A’ && byte <= ‘Z’))
                {
                        if (parsed – this->temp >= 0xFF)
                        {
                               if (parsed >= temp_end)
                               {
                                       parsed = temp_end – 1;
                               }
                               *parsed = 0;
                               state = PARSER_PROCESS_CMD;
                               this->cmd_len = pos – (this->temp + 1);
                               break;
                        }
                        //if parsed == temp_end
                        // goto raise_exception
                        *parsed = byte;
                        parsed++;
                        pos = parsed;
                        data.pos++;
                }
                else
                {
                        if (parsed >= temp_end)
                        {
                               parsed = temp_end – 1;
                        }
                        *parsed = 0;
                        state = PARSER_PROCESS_CMD;
                        this->cmd_len = pos – (this->temp + 1);
                        break;
                }
        }
        break;
case PARSER_PARSE_CONTROL_WORD_NUM_PARAMETER:
        while (data.pos != data.end)
        {
                byte = *(uint8_t*)data.pos;
                // length of null-terminated strings cmd + num should be <= 0xFF
                if (byte == ‘ ‘)
                {
                        data.pos++;
                        if (parsed >= temp_end)
                        {
                               parsed = temp_end – 1;
                        }
                        *parsed = 0;
                        state = PARSER_PROCESS_CMD;
                        break;
                }
                if (byte >= ‘0’ && byte <= ‘9’)
                {
                        if (parsed – this->temp >= 0xFF)
                        {
                               if (parsed >= temp_end)
                               {
                                       parsed = temp_end – 1;
                               }
                               *parsed = 0;
                               state = PARSER_PROCESS_CMD;
                               break;
                        }
                        //if parsed == temp_end
                        // goto raise_exception
                        *parsed = byte;
                        *parsed++;
                        data.pos++;
                }
                else
                {
                        if (parsed >= temp_end)
                        {
                               parsed = temp_end – 1;
                        }
                        *parsed = 0;
                        state = PARSER_PROCESS_CMD;
                        break;
                }
        }
        break;
case PARSER_PROCESS_CMD:
case PARSER_SKIP_DATA:
case PARSER_END_GROUP:
case PARSER_SKIP_DATA_CHECK_B:
case PARSER_SKIP_DATA_CHECK_I:
case PARSER_SKIP_DATA_CHECK_N:
case PARSER_SKIP_DATA_GET_BIN_VAL:
case PARSER_SKIP_DATA_INNER_DATA:
        this->state = state;
        cmd_parser(&data);
        state = this->state;
        break;

然后在 PARSE_PROCESS_CMD状态下调用另一个函数来处理 控制字和控制标志。它根据当前状态,相应地设置下一个状态。

 

有很多种状态来负责解析十六进制数据,我们最感兴趣的是PARSER_PARSE_HEX_DATA状态。正如你所看到的,如果设置了objdata的目的地的话,就会在PARSER_BEGIN中设置该字段。

case PARSER_PARSE_HEX_DATA:
        parsed_data = this->temp;
        if (this->bin_size <= 0)
        {
                while (data.pos != data.end)
                {
                        byte = *(uint8_t*)data.pos;
                        if (byte == ‘{‘ || byte == ‘}’ || byte == ‘\\’)
                        {
                               state = PARSER_BEGIN;
                               if (parsed_data != this->temp)
                               {
                                       push_data(parsed_data – this->temp);
                                       parsed_data = this->temp;
                               }
                               break;
                        }
                        if (this->flag & 0x4000)
                        {
                               data.pos++;
                               continue;
                        }
                        if (byte >= ‘0’ && byte <= ‘9’)
                        {
                               val = byte – 0x30;
                        }
                        else if (byte >= ‘a’ && byte <= ‘f’)
                        {
                               val = byte – 0x57;
                        }
                        else if (byte >= ‘A’ && byte <= ‘F’)
                        {
                               val = byte – 0x37;
                        }
                        else if (byte == 9 || byte == 0xA || byte == 0xD || byte == 0x20)
                        {
                               data.pos++;
                               continue;
                        }
                        else
                        {
                               // show message that there are not enough memory
                               this->flag |= 0x4000;
                               data.pos++;
                               continue;
                        }
                        if (this->flag & 0x8000)
                        {
                               this->hex_data_byte = val << 4;
                               this->flag &= 0x7FFF;
                        }
                        else
                        {
                               if (parsed_data == temp_end)
                               {
                                       push_data(sizeof(this->temp));
                                       parsed_data = this->temp;
                               }
                               this->hex_data_byte |= val;
                               *parsed_data = this->hex_data_byte;
                               parsed_data++;
                               this->flag |= 0x8000;
                        }
                        data.pos++;
                }
        }
        else
        {
                if (this->flag & 0x4000)
                {
                        uint32_t size;
                        if (this->bin_size <= data.end – data.pos)
                        {
                               size = this->bin_size;
                        }
                        else
                        {
                               size = data.end – data.pos;
                        }
                        this->bin_size -= size;
                        data.pos += size;
                }
                else
                {
                        while (this->bin_size > 0)
                        {
                               if (parsed_data == temp_end)
                               {
                                       push_data(sizeof(this->temp));
                                       parsed_data = this->temp;
                               }
                               byte = *(uint8_t*)data.pos;
                               *parsed_data = byte;
                               parsed_data++;
                               data.pos++;
                               this->bin_size–;
                        }
                }
        }
        if (parsed_data != this->temp)
        {
                push_data(parsed_data – this->temp);
                parsed_data = this->temp;
        }
        break;

如果设置,该状态就会解析十六进制数据和二进制数据。

 

PARSER_PARSE_HEX_NUM_MSBPARSER_PARSE_HEX_NUM_LSB状态用来解析十六进制值(\ panose控制字和\'控制符号的数据)。

case PARSER_PARSE_HEX_NUM_MSB:
        this->flag |= 0x8000;
        this->hex_num_byte = 0;
        state = PARSER_PARSE_HEX_NUM_LSB;
case PARSER_PARSE_HEX_NUM_LSB:
        //  …
        byte = *(uint8_t*)data.pos;
        data.pos++;
        val = 0;
        if (byte – ‘0’ <= 9)
        {
                val = byte – 0x30;
        }
        else if (byte – ‘a’ <= 5)
        {
                val = byte – 0x57;
        }
        else if (byte – ‘A’ <= 5)
        {
                val = byte – 0x37;
        }
        this->hex_num_byte |= val << ((this->flag >> 0xF) << 2);
        this->flag = ((~this->flag  ^ this->flag) & 0x7FFF) ^ ~this->flag;
        if (this->flag & 0x8000)
        {
                // …
                state = PARSER_BEGIN;
        }
        else
        {
                break;
        }
        break;

状态重置

看下PARSER_PARSE_HEX_NUM_MSBPARSER_PARSE_HEX_NUM_LSBPARSER_PARSE_HEX_DATA这三种状态,很容易发现其中的错误。即使它们使用不同的变量来存储解码后的十六进制值,但它们还是使用了相同的位来确定哪个半字节现在被解码了 - 高位(最高有效位或MSB)或低位(低位有效位或LSB)。并且,PARSER_PARSE_HEX_NUM_MSB状态总是将此位重置为MSB。

 

因此,通过改变PARSER_PARSE_HEX_NUM_MSB的状态,就可以使PARSER_PARSE_HEX_DATA状态中的上下文里的一些字节消失。

 

为了达到我们的目的,需要在\objdata控制字之后的数据中加入\'XX。在这种情况下,当解析器在PARSER_PARSE_HEX_DATA状态中遇到\时,它就会返回到状态PARSER_BEGIN,之后将进入状态PARSER_PROCESS_CMD\'控制符号的处理程序不会改变最终的目的地,但会将下一个状态更改为PARSER_PARSE_HEX_NUM_MSBPARSER_PARSE_HEX_NUM_MSBPARSER_PARSE_HEX_NUM_LSB控制权转移回PARSER_BEGIN后,最终转换为PARSER_PARSE_HEX_DATA,因为最终的目的地仍然是objdata。在那之后,下一个字节将被解码为高半字节。

 

值得注意的是,PARSER_PARSE_HEX_NUM_LSB不检查提供的值是否为有效的十六进制数;因此,\'后面可以是任意的两个字节。

 

比如说,下面的这个例子:

 

图片描述

 

最终结果会删除"f\'cc"

 

当控制首次转到PARSER_PARSE_HEX_DATA状态时,处理\ objdata控制字后,MSB位已经设置。我们来详细地看下这个过程的处理细节:
图片描述

 

在逆向了关键字处理函数后,我找到了所有控制字及其相应结构的列表:

 

图片描述

 

有了这些信息,我们可以找到并查看objdata构造函数:

 

图片描述

 

可以看到它设置了MSB位,分配一个新的缓冲区并用新的指针替换旧的指针。因此,在两个\ objdata控制字之间解码的数据从来没有用到过。

 

图片描述

 

最终结果会删除掉"d0cf11e0a1b11ae1"

最终目的地

我们知道如果\\ objdata被放入数据中,会改变输出结果。但如果是其他控制字和控制符号呢?这里有超过1500个,但几乎没有!

 

由于某些控制字表示目的地,因此不能使用它们 - 它们自己更改objdata目标,并解码需要objdata目标的对象。其他的一些控制字并不影响objdata的目的地。

 

在不丢失先前解码的数据的情况下返回到objdata目的地的唯一方法是使用特殊符号 -{},这些符号表示组的开始和结束。

 

当解析器遇到PARSER_BEGIN状态的组末尾时,将在组开始之前设置的目标将被恢复。因此,通过在\objdata之后放置{\aftncn FF},FF不会作为解码数据,因为FF现在应用于目标aftncn并将根据此目标进行处理。但是,通过使用{\aftnnalc FF},FF将进入解码数据,因为目标仍然等于objdata。还值得注意的是{\ objdata FF}仍然不能使用,因为缓冲区不会被恢复。所有目标控制字的准确列表都是用简单的fuzzer创建的。

固定大小的缓冲区

在查看RTF解析器的代码时想到课另一种混淆技术(与'MSB'bug无关),但也可用于从十六进制流中移除字节。 该技术与临时缓冲区大小以及在PARSER_PARSE_CONTROL_WORDPARSER_PARSE_CONTROL_WORD_NUM_PARAMETER状态下控制字和数值参数如何解析有关。 在下面的截图中,你可以看到实际样例:

 

图片描述

 

在此示例中,将使用以下公式计算将那部分被作为数字参数删除的数据的大小:

0xFF(临时缓冲区的大小) - 0xB(“oldlinewrap”的大小) - 2(空终止符字符)= 0xF2 。

不必要的数据

尽管上述技术与通常的RTF解析相关,但某些特定关键字的处理隐藏了一些进一步的混淆措施。

 

根据规范所称,如果发现了\*,但在查找表中却找不到之后的控制字或控制符号,它将被视为一个未知的目标组,并且直到右括号之前的所有数据都应该被丢弃。 而MS Office中的查找表包含规范中不存在的控制字,使人们担忧它将来会改变,从而影响不同版本的MS Office上的同一文档的解析过程。 当负责处理关键字的函数遇到这种情况或某个特定控制字(如\comment\generator\nonshppict等)时,它会将状态PARSER_SKIP_DATA和遇到的{设置为1。

enum
{
       // …
       PARSER_SKIP_DATA = 0xF,
       // …
       PARSER_SKIP_DATA_CHECK_B = 0x13,
       PARSER_SKIP_DATA_CHECK_I = 0x14,
       PARSER_SKIP_DATA_CHECK_N = 0x15,
       PARSER_SKIP_DATA_GET_BIN_VAL = 0x16,
       PARSER_SKIP_DATA_INNER_DATA = 0x17,
       // …
};

magic的种类

在分析PARSER_SKIP_DATA *状态的过程中,我发现不仅与规范相反,而且还与解析器代码的其余部分相反。

 

在查找\bin控制字时,此状态将跳过数据,更改所遇到{}大括号的数量,直到该数字等于零。隐藏的问题在于数字参数处理的方式。

 

首先,数字参数的最大允许长度增加到0xFF--它的计算没有考虑控制字的长度。

 

第二个问题是数字参数不再是数字了! 解析器不仅允许使用十进制字符,而且还允许传递拉丁字符。 然后将此参数传递给自定义strtol,从而可以指定应该跳过的数据的长度,而不用将{}视为十六进制数字。

 

目前来看,还尚未在野外遇到使用这两种情况的混淆。

结论

逆向工程是建立一个解析器的最有效方式,然而对于RTF,却有可能无法达到预期的行为。

 

精确的解析依赖于小的实现细节和算法错误,而不是一个可能令人困惑的规范或陈述不真实的规范。

 

原文链接:https://securelist.com/disappearing-bytes/84017/

 

本文由看雪翻译小组 fyb波 编译


[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界

最后于 2019-2-1 20:24 被admin编辑 ,原因:
收藏
点赞1
打赏
分享
最新回复 (2)
雪    币: 8863
活跃值: (2369)
能力值: ( LV12,RANK:760 )
在线值:
发帖
回帖
粉丝
cvcvxk 10 2018-3-30 15:21
2
0
Cool  Job
雪    币: 10230
活跃值: (2260)
能力值: ( LV5,RANK:71 )
在线值:
发帖
回帖
粉丝
joker陈 2018-3-30 18:59
3
0
看着就很牛逼,代码逆的如此详细。
游客
登录 | 注册 方可回帖
返回