查看原文
其他

用 S2E 和 Kaitai Struct 针对性地处理文件解析器

2017-11-13 fyb波 看雪学院





介绍



最近我一直在研究S2E中的文件解析器。这通常涉及调用s2ecmd symbfile 文件来使解析器的输入符号化,然后运行S2E来解析通过解析器的不同路径。但是,这是一个比较笨重的做法;它使整个输入文件产生一个非常大的符号化的块,这很快导致了路径爆炸。此外,我们可能只想探索行使特定功能的路径。

 

那么我们如何在基于文件的程序(如解析器)上实现更有针对性地实现符号执行呢?一种方法是编写一个自定义的S2E插件来处理onSymbolicVariableCreation事件,拦截s2ecmd symbfile文件。然后,您可以编写C++代码来迭代和具体调整符号化的数据内容。这种方法的缺点是显而易见的:编写C++代码是相当耗时且容易出错;它需要知道输入文件的格式;在处理不同的文件类型时还要重写,如何更好的实现呢?






KaitaiStruct




暂时抛开S2E不谈,看看 Kaitai Struct。 Kaitai Struct是开发二进制结构解析器的工具。它提供了一种类似YAML的语言,可以简洁地定义二进制结构。 Kaitai Struct 编译器(ksc)然后根据这个定义生成一个解析器。该解析器可以用多种语言生成,包括C ++,Python和Java。

 

以下是Kaitai Struct中的ELF文件格式的部分定义(取自格式库)。它由许多描述ELF文件的“属性”(例如magic,abi_version等字段)组成:


meta:

  id: elf

  title: Executable and Linkable Format

  application: SVR4 ABI and up, many *nix systems

  license: CC0-1.0

  ks-version: 0.8

seq:

  # e_ident[EI_MAG0]..e[EI_MAG3]

  - id: magic

    size: 4

    contents: [0x7f, "ELF"]

  # e_ident[EI_CLASS]

  - id: bits

    type: u1

    enum: bits

  # e_ident[EI_DATA]

  - id: endian

    type: u1

    enum: endian

  # e_ident[EI_VERSION]

  - id: ei_version

    type: u1

  # e_ident[EI_OSABI]

  - id: abi

    type: u1

    enum: os_abi

  - id: abi_version

    type: u1

  - id: pad

    size: 7

  - id: header

    type: endian_elf


强烈建议阅读Kaitai Struct文档以充分利用这篇文章,因为我跳过了大部分细节(主要是因为我自己并不擅长这方面)。 然而,有一个值得一提的功能是“处理规范”。

 

处理规范允许你以某种方式“处理”属性的自定义函数。 例如,可以对属性进行加密/编码。 处理规范可以在运行时对该属性进行解密/解码。

 

这与符号执行有关吗? 假设我们有一个s2e_make_symbolic的文件处理规范,并且通过将此规范应用于特定的属性,我们只会使输入文件的这些部分符号化。 这会使我们更好的控制S2E的状态空间,并可能减少路径爆炸问题。 只需要将S2E和Kaitai Struct结合起来就可以实现!






结合S2E和Kaitai Struct




我们将使用Lua编程语言来组合S2E和Kaitai Struct。使用Lua可以重用现有的组件--S2E包含一个嵌入式的Lua解释器(用于解析S2E配置文件,编写函数/指令注释),而ksc能够就生成Lua解析器。因此,我们可以使用ksc为我们的输入文件生成一个Lua解析器,并将该解析器嵌入到S2E配置文件中,使其可以被S2E访问。 (我们可以使用ksc来生成一个C++解析器,但这样的话,每次我们想要使用不同的文件格式时,都需要重新编译S2E)。通过在输入定义中选择性地应用s2e_make_symbolic处理规范,我们可以实现更有针对性的符号执行。

 

这篇文章剩余部分将介绍如何组合S2E和Kaitai Struct。我将使用ELF文件的定义(前面讨论过)和readelf来作为一个实例。

 

为了让其他人更容易地使用代码,我努力使它尽可能的独立。- 没有对S2E的核心引擎或ksc进行任何修改。然而,这意味着代码基本没有优化!代码由以下部件组成:

  • 在客户操作系统中执行的命令行工具(s2e_kaitai_cmd)。这个工具读取输入文件并且调用S2E插件,选择性地使文件符号化;

  • 一个S2E插件(KaitaiStruct),它调用Lua代码来运行由ksc生成的解析器;

  • 一小段Lua代码连接 S2E配置文件和由ksc生成的解析器。


这些部件中的每一个在下面描述。完整的代码在这儿。






s2e_kaitai_cmd工具




在这篇文章的开头,我提到我们通常会使用s2ecmd symbfile 来使输入文件的符号化。 symbfile命令使输入文件符号化:

  1. 以读/写模式打开输入文件

  2. 将输入文件读入缓冲区

  3. 在缓冲区上调用s2e_make_concolic

  4. 将(目前符号化的)缓冲区写回原始输入文件


我们将采取类似的方法,除了我们将步骤(3)修改为:

  • 调用KaitaiStruct插件来选择性地使缓冲区符号化


为此,我们将在S2E环境中添加以下目录/文件:

  • source/s2e/guest/common/s2e_kaitai_cmd/s2e_kaitai_cmd.c

  • source/s2e/guest/common/include/s2e/kaitai/commands.h


我会跳过步骤1,2和4,因为它们已经在s2ecmd中实现了。对于步骤3,我们会自己写一个自定义的S2E命令来调用一个插件(稍后描述),有选择地使输入的文件符号化。命令结构应放在source/s2e/guest/common/include/s2e/kaitai/commands.h中。它遵循从客户端调用S2E插件的标准方法:


enum S2E_KAITAI_COMMANDS {

    KAITAI_MAKE_SYMBOLIC,

};

 

struct S2E_KAITAI_COMMAND_MAKE_SYMBOLIC {

    // Pointer to guest memory where the symbolic file has been loaded

    uint64_t InputFile;

    // Size of the input file (in bytes)

    uint64_t FileSize;

    // 1 on success, 0 on failure

    uint64_t Result;

} __attribute__((packed));

 

struct S2E_KAITAI_COMMAND {

    enum S2E_KAITAI_COMMANDS Command;

    union {

        struct S2E_KAITAI_COMMAND_MAKE_SYMBOLIC MakeSymbolic;

    };

} __attribute__((packed))



然后我们可以将下面的函数添加到s2e_kaitai_cmd.c中。 这个函数包含指向文件内容(已经读入缓冲区)的指针和缓冲区的大小(由lseek确定),构造相关命令并将此命令发送到S2E。


static inline int s2e_kaitai_make_symbolic(const uint8_t *buffer, unsigned size) {

    struct S2E_KAITAI_COMMAND cmd = {0};

 

    cmd.Command = S2E_KAITAI_MAKE_SYMBOLIC;

    cmd.MakeSymbolic.InputFile = (uintptr_t) buffer;

    cmd.MakeSymbolic.FileSize = size;

    cmd.MakeSymbolic.Result = 0;

 

    s2e_invoke_plugin("KaitaiStruct", &cmd, sizeof(cmd));

 

    return (int) cmd.MakeSymbolic.Result;

}


现在我们需要一个S2E插件来处理这个命令。





KaitaiStruct插件



让我们从一个skeleton插件开始(不要忘了在source/s2e/libs2eplugins/src/CMakeLists.txt中向s2e/Plugins/KaitaiStruct.cpp添加add_library命令)。

 

头文件:


#ifndef S2E_PLUGINS_KAITAI_STRUCT_H

#define S2E_PLUGINS_KAITAI_STRUCT_H

 

#include <s2e/CorePlugin.h>

#include <s2e/Plugins/Core/BaseInstructions.h>

 

// Forward declare the S2E command from s2e_kaitai_cmd

struct S2E_KAITAI_COMMAND;

 

namespace s2e {

namespace plugins {

 

// In addition to extending the basic Plugin class, we must also implement the

// BaseInstructionsPluginInvokerInterface to handle custom S2E commands

class KaitaiStruct : public Plugin,

                     public BaseInstructionsPluginInvokerInterface {

    S2E_PLUGIN

 

public:

    KaitaiStruct(S2E *s2e) : Plugin(s2e) { }

 

    void initialize();

 

    // The method from BaseInstructionsPluginInvokerInterface that we must

    // implement to respond to a custom command. This method takes the current

    // S2E state, a pointer to the custom command object and the size of the

    // custom command object

    virtual void handleOpcodeInvocation(S2EExecutionState *state,

                                        uint64_t guestDataPtr,

                                        uint64_t guestDataSize);

 

private:

    // The name of the Lua function that will run the Kaitai Struct parser

    std::string m_kaitaiParserFunc;

 

    // handleOpcodeInvocation will call this method to actually invoke the Lua

    // function

    bool handleMakeSymbolic(S2EExecutionState *state,

                            const S2E_KAITAI_COMMAND &command);

}

 

} // namespace plugins

} // namespace s2e

 

#endif


cpp 文件: 


// From source/s2e/guest/common/include

#include <s2e/kaitai/commands.h>

 

#include <s2e/ConfigFile.h>

#include <s2e/S2E.h>

#include <s2e/Utils.h>

 

#include "KaitaiStruct.h"

 

namespace s2e {

namespace plugins {

 

S2E_DEFINE_PLUGIN(KaitaiStruct,

                  "Combine S2E and Kaitai Struct",

                  "",

                  // Dependencies

                  "LuaBindings"); // Reuse the existing Lua binding code from

                                  // the function/instruction annotation

                                  // plugins

 

void KaitaiStruct::initialize() {

    m_kaitaiParserFunc = s2e()->getConfig()->getString(getConfigKey() +

                                                       ".parser");

}

 

bool KaitaiStruct::handleMakeSymbolic(S2EExecutionState *state,

                                      const S2E_KAITAI_COMMAND &command) {

    // We'll finish this later

    return true;

}

 

void KaitaiStruct::handleOpcodeInvocation(S2EExecutionState *state,

                                          uint64_t guestDataPtr,

                                          uint64_t guestDataSize) {

    S2E_KAITAI_COMMAND cmd;

 

    // 1. Validate the received command

    if (guestDataSize != sizeof(cmd)) {

        getWarningsStream(state) << "S2E_KAITAI_COMMAND: Mismatched command "

                                 << "structure size " << guestDataSize << "\n";

        exit(1);

    }

 

    // 2. Read the command

    if (!state->mem()->readMemoryConcrete(guestDataPtr, &cmd, guestDataSize)) {

        getWarningsStream(state) << "S2E_KAITAI_COMMAND: Failed to read "

                                 << "command\n";

        exit(1);

    }

 

    // 3. Handle the command

    switch (cmd.Command) {

        case KAITAI_MAKE_SYMBOLIC: {

            bool success = handleMakeSymbolic(state, cmd);

            cmd.MakeSymbolic.Result = success ? 0 : 1;

 

            // Write the result back to the guest

            if (!state->mem()->writeMemory(guestDataPtr, cmd)) {

                getWarningsStream(State) << "S2E_KAITAI_COMMAND: Failed to "

                                         << " write result to guest\n";

                exit(1);

            }

        } break;

 

        default: {

            getWarningsStream(state) << "S2E_KAITAI_COMMAND: Invalid command "

                                     << hexval(cmd.Command) << "\n";

            exit(1);

        }

    }

}

 

} // namespace plugins

} // namespace s2e


我们的插件只有一个依赖关系:LuaBindings插件。这个插件配置了S2E的Lua解释器,并允许我们在S2E配置文件中调用Lua代码。

 

handleOpcodeInvocation方法遵循和其他插件类似的方法,实现了BaseInstructionsPluginInvokerInterface接口(例如FunctionModels和LinuxMonitor):

  1. 通过检查它的大小来验证接收的命令。

  2. 读取命令。由于该命令是由客户机发出的,因此它驻留在客户机内存中。我们的命令都不是符号化的(记住它只包含输入文件的起始地址和大小),所以我们可以详细地读取这个内存内容。

  3. 处理命令。在这种情况下,我们调用另一个函数(我们将在稍后讨论)来调用Lua解释器解析输入文件。

  4. 显示客户机的成功/失败。我们通过在命令结构中设置“返回值”并将命令写回到客户端内存中。


最终实现MakeSymbolic。为了编写Lua代码,需要添加一些头文件:

#include <vector>

 

#include <s2e/Plugins/Lua/Lua.h>

#include <s2e/Plugins/Lua/LuaS2EExecutionState.h>


最终实现的函数:  


bool KaitaiStruct::handleMakeSymbolic(S2EExecutionState *state,

                                      const S2E_KAITAI_COMMAND &command) {

    uint64_t addr = command.MakeSymbolic.InputFile;

    uint64_t size = command.MakeSymbolic.FileSize;

    std::vector<uint8_t> data(size);

 

    // Read the input file's contents from guest memory

    if (!state->mem()->readMemoryConcrete(addr, data.data(),

                                          sizeof(uint8_t) * size)) {

        return false;

    }

 

    // Get the Lua interpreter's state

    lua_State *L = s2e()->getConfig()->getState();

 

    // Wrap the current S2E execution state

    LuaS2EExecutionState luaS2EState(state);

 

    // Turn the input file into a Lua string

    luaL_Buffer luaBuff;

    luaL_buffinit(L, &luaBuff);

    luaL_addlstring(&luaBuff, (char*) data.data(), sizeof(uint8_t) * size);

 

    // Set up our function call on Lua's virtual stack

    lua_getglobal(L, m_kaitaiParserFunc.c_str());

    Lunar<LuaS2EExecutionState>::push(L, &luaS2EState);

    lua_pushinteger(L, addr);

    luaL_pushresult(&luaBuff);

 

    // Call our Kaitai Struct parser function

    lua_call(L, 3, 0);

 

    return true;

}


希望这比较容易理解(参见这里有关Lua语言的C API的更多信息)。首先,我们将输入文件读入Kaitai Struct解析器的Lua字符串。然后,我们调用Kaitai Struct解析器函数(我们将在下一部分中定义)。

 

我们必须设置解析器函数的参数才能调用它。用栈把值传递给Lua函数。函数名首先入栈。解析器函数在Lua的全局命名空间中定义(为了简单起见),因此我们可以使用lua_getglobal从S2E配置文件中检索该函数,并将其压入栈中。然后依次入栈:

  1. 当前S2E执行状态;

  2. 输入文件的起始地址(在客户机内存中);

  3. 输入文件的内容(作为字符串)。

现在要做的就是在S2E配置文件中实现这个解析器。





Lua脚本



首先,我们需要将Kaitai Struct格式的定义编译成Lua解析器。既然我们是用readelf做实验,现在让我们创建一个readelf项目,并从Kaitai Struct Gallery

获取ELF定义:


# Create the S2E project

s2e new_project -n readelf_kaitai readelf -h @@

cd projects/readelf_kaitai

 

# Get the ELF Kaitai Struct definition and compile it

wget https://raw.githubusercontent.com/kaitai-io/kaitai_struct_formats/master/executable/elf.ksy

ksc -t lua elf.ksy


这将会产生elf.lua。 让我们用AFL的例子测试下。 如果您还没有安装它,您还需要Kaitai Struct的的Lua runtime:


# Get Kaitai Struct's Lua runtime

git clone https://github.com/kaitai-io/kaitai_struct_lua_runtime lua_runtime

 

# Get the ELF testcase

wget https://raw.githubusercontent.com/mirrorer/afl/master/testcases/others/elf/small_exec.elf

 

# Parse the testcase

lua5.3 - << EOF

package.path = package.path .. ";./lua_runtime/?.lua"

require("elf")

 

inp = assert(io.open("small_exec.elf", "rb"))

testcase = Elf(KaitaiStream(inp))

print("testcase e_ehsize: " .. testcase.header.e_ehsize)

EOF


你应该看到一个52字节大小的header(你可以运行readelf -h small_exec.elf来确认)。

 

我原先说过我们会用Kaitai Struct的处理规范来定位特定的文件属性来使其符号化。 我们在lua_runtime/s2e_make_symbolic.lua中定义这个处理规范:


local class = require("class")

 

S2eMakeSymbolic = class.class()

 

function S2eMakeSymbolic:_init(s2e_state, start_addr, curr_pos, name)

    self._state = s2e_state

    self._addr = start_addr + curr_pos

    self._name = name

end

 

function S2eMakeSymbolic:decode(data)

    local mem = self._state:mem()

    local size = data:len()

 

    -- The decode routine is called after the data has already been read, so we

    -- must return to the start of the data in order to make it symbolic

    local addr = self._addr - size

 

    mem:makeConcolic(addr, size, self._name)

 

    -- Return the data unchanged

    return data

end


目前已经定义了一个新的类S2eMakeSymbolic和一个构造函数(_init),一个decode方法:
构造器包含以下参数:

  1. 当前S2E的执行状态;

  2. 输入文件的起始地址(在客户机内存中);

  3. 解析器的当前位置。这个地址加上起始地址可以计算出符号化的内存地址;

  4. 符号变量的名称。


当ELF解析器遇到应用s2e_make_symbolic处理规范的属性时,将自动调用decode。 然而,在从输入文件中读取数据之后才调用decode方法,所以使数据符号化(通过减去刚刚读取的存储器区域的大小)时,必须对此进行弥补。

 

让我们做一些符号化的东西。 我们现在将选择一些简单的部分 - ELF头部的e_machine字段。 在elf.ksy中,e_machine字段在endian_elf类型下定义:


# The original definition of the e_machine field

- id: machine

  type: u2

  enum: machine


处理规范只能应用于字节数组,所以我们必须用字节数组的size字段来替换type字段。 因为原始数据类型是无符号的双字节数,所以我们可以将该机器简单地视为一个大小为2字节的数组。我们还必须删除枚举映射,否则当它尝试将枚举类型应用到一个字节的数组时,ksc会引发编译错误。


# Redefinition of the e_machine field to make it symbolic

- id: machine

  size: 2

  process: s2e_make_symbolic(s2e_state, start_addr, _io.pos, "machine")


最后,我们必须从解析器的构造函数传递另外两个参数--S2E执行状态和输入文件的起始地址--从解析器的构造器传到s2e_make_symbolic。 我们用“params spec”来实现。 machine属性嵌套在endian_elf和顶级elf类型下,因此下面的参数规范必须被定义。


params:

  - id: s2e_state

  - id: start_addr


我们还必须将header的类型从endian_elf修改为endian_elf(s2e_state,start_addr)。 这确保两个参数传递给endian_elf的构造函数。 (如果还有点困惑,看下这里的源代码)。


# The original header's type

- id: header

  type: endian_elf

 

# Redefined to propagate the S2E execution state and input file's start address

# to the endian_elf type

- id: header

  type: endian_elf(s2e_state, start_addr)


现在重新编译elf.ksy。 如果打开elf.lua,你应该看到,构造函数(Elf:_init)的前两个参数为s2e_state和start_addr。 这些参数被保存下来,并通过Elf.EndianElf构造函数传播到S2eMakeSymbolic构造函数。


剩下要做的就是在我们的S2E配置文件中写一个小的函数来实例化并运行我们的解析器。 该功能由KaitaiStruct插件中的handleMakeSymbolic方法调用。


package.path = package.path .. ";./lua_runtime/?.lua"

local stringstream = require("string_stream")

require("elf")

 

function make_symbolic_elf(state, start_addr, buffer)

    local ss = stringstream(buffer)

 

    -- This will kick-start the parser. We don't care about the final result

    Elf(state, start_addr, KaitaiStream(ss))

end

 

-- Enable and configure the necessary plugins

add_plugin("LuaBindings")

add_plugin("KaitaiStruct")

pluginsConfig.KaitaiStruct = {

    parser = "make_symbolic_elf",

}


完成了!

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存