-
-
[翻译] 用 S2E 和 Kaitai Struct 针对性地处理文件解析器
-
2017-10-29 14:55 4420
-
用 S2E 和 Kaitai Struct 针对性地处理文件解析器
介绍
最近我一直在研究S2E中的文件解析器。这通常涉及调用s2ecmd symbfile
文件来使解析器的输入符号化,然后运行S2E来解析通过解析器的不同路径。但是,这是一个比较笨重的做法;它使整个输入文件产生一个非常大的符号化的块,这很快导致了路径爆炸。此外,我们可能只想探索行使特定功能的路径。
那么我们如何在基于文件的程序(如解析器)上实现更有针对性地实现符号执行呢?一种方法是编写一个自定义的S2E插件来处理onSymbolicVariableCreation
事件,拦截s2ecmd symbfile
文件。然后,您可以编写C++代码来迭代和具体调整符号化的数据内容。这种方法的缺点是显而易见的:编写C++代码是相当耗时且容易出错;它需要知道输入文件的格式;在处理不同的文件类型时还要重写,如何更好的实现呢?
Kaitai Struct
暂时抛开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
命令使输入文件符号化:
- 以读/写模式打开输入文件
- 将输入文件读入缓冲区
- 在缓冲区上调用
s2e_make_concolic
- 将(目前符号化的)缓冲区写回原始输入文件
我们将采取类似的方法,除了我们将步骤(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):
- 通过检查它的大小来验证接收的命令。
- 读取命令。由于该命令是由客户机发出的,因此它驻留在客户机内存中。我们的命令都不是符号化的(记住它只包含输入文件的起始地址和大小),所以我们可以详细地读取这个内存内容。
- 处理命令。在这种情况下,我们调用另一个函数(我们将在稍后讨论)来调用Lua解释器解析输入文件。
- 显示客户机的成功/失败。我们通过在命令结构中设置“返回值”并将命令写回到客户端内存中。
最终实现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配置文件中检索该函数,并将其压入栈中。然后依次入栈:
- 当前S2E执行状态;
- 输入文件的起始地址(在客户机内存中);
- 输入文件的内容(作为字符串)。
现在要做的就是在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
方法:
构造器包含以下参数:
- 当前S2E的执行状态;
- 输入文件的起始地址(在客户机内存中);
- 解析器的当前位置。这个地址加上起始地址可以计算出符号化的内存地址;
- 符号变量的名称。
当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", }
完成了!
用readelf实验下
我们终于可以开始readelf部分的实验了。 在我们开始之前,请修改S2E配置文件,仅启用以下的插件:
- BaseInstructions
- HostFiles
- VMI
- TranslationBlockCoverage
- ModuleExecutionDetector
- ForkLimiter
- ProcessExecutionDetector
- LinuxMonitor
我们还必须修改bootstrap.sh
。 在${S2EGET} “readelf”
下添加$ {S2EGET}“small_exec.elf”
以便将测试用例复制到客户机。为了使用我们的测试用例,在prepare_inputs
函数中,将truncate -s 256 $ {SYMB_FILE}
替换为cp small_exec.elf $ {SYMB_FILE}
。 还不用替换symbfile
命令; 让我们先来看一下readelf如何在一个完全符号化的文件上执行。
运行S2E一分钟左右,然后结束进程。 你应该看到很多分叉的情况(我这里是136种情况)。 让我们生成代码覆盖信息:
# The actual disassembler isn't important s2e coverage basic_block --disassembler=binaryninja readelf_kaitai
这些分支情况发生在哪? 由于readelf调用在符号化数据时调用了printf
,所以libc中有很多。 readelf 自身的分支呢? 下面的图片显示了readelf中的两个函数的片段:process_section_headers
和init_dwarf_regnames
。 绿色的部分表示由S2E执行的块。 分支节点受到的约束已由注释说明(KLEE中的KQuery格式):
readelf's process_section_headers 代码覆盖
readelf's init_dwarf_regnames 代码覆盖
当检查到下列情况也会发生分叉:
- 如果输入文件是一份存档
- 数据编码(小端或大端字节序)
- section header 表的文件偏移量
- 如果每个部分的
sh_link
和sh_info
值都是有效的
还有许多其他的地方!眼下只对留下那些与ELF头部的e_machine
字段有关的程序路径。编辑bootstrap.sh
并用./s2e_kaitai_cmd ${SYMB_FILE}
替换${S2ECMD} symbfile ${SYMB_FILE}
。现在重新运行S2E一分钟。在运行期间,分支情况仅限于get_machine_name
和init_dwarf_regnames
函数,这两个函数都是取决于e_machine
的值的switch语句。成功了!
让我们尝试在ELF文件中换一个不同的字段 -section header 的sh_type
字段。不像e_machine
字段,只会在ELF文件中出现一次。sh_type
可以在整个文件中出现多次(取决于ELF文件中section的数量)。
我们必须将S2E执行状态和输入文件的起始地址传播到ELF声明中的相对应的属性中。这次我们必须将params spec添加到section_header
类型中。 type
属性定义为无符号的4字节枚举类型,因此我们必须将其更改为4字节的数组类型,以便我们可以使用s2e_make_symbolic
:
# Elf(32|64)_Shdr section_header: params: - id: s2e_state - id: start_addr seq: # sh_name - id: name_offset type: u4 # sh_type - id: type size: 4 process: s2e_make_symbolic(s2e_state, start_addr, _io.pos, "sh_type") # ...
我们还必须确保将这两个参数传递给SectionHeader
的构造函数。 section头可以在section_headers
实例下找到:
# The original section_headers section_headers: pos: section_header_offset repeat: expr repeat-expr: qty_section_header size: section_header_entry_size type: section_header # Redefined for symbolic execution section_headers: pos: section_header_offset repeat: expr repeat-expr: qty_section_header size: section_header_entry_size type: section_header(s2e_state, start_addr)
注意section_headers
被声明为“实例规范”。 这意味着section_headers
只能根据需要将要解析section头部的函数编译为一个函数。 因此,我们必须访问section_headers
以强制解析它们。 为此,我们必须修改s2e-config.lua
中的make_elf_symbolic
函数:
function make_symbolic_elf(state, start_addr, buffer) -- ... -- This will kick-start the parser. However, now we do care about the final -- result, because we must access the section headers to force them to be -- parsed local elf_file = Elf(state, start_addr, KaitaiStream(ss)) -- This will kick-start the section header parser _ = elf_file.header.section_headers end
运行ksc
再次重新生成elf.lua
。 在我们重新运行S2E之前,我们来看下elf.lua
。 特别是在section_headers
中的get方法中解析的section头部:
function Elf.EndianElf.property.section_headers:get() -- ... for i = 1, self.qty_section_header do self._raw__m_section_headers[i] = \ self._io:read_bytes(self.section_header_entry_size) local _io = KaitaiStream(stringstream(self._raw__m_section_headers[i])) self._m_section_headers[i] = Elf.EndianElf.SectionHeader(self.s2e_state, self.start_addr, _io, self, self._root, self._is_le) end -- ... end
注意到ksc
创建一个局部变量_io
,它被传递给SectionHeader
构造函数。 这个_io
变量包含最终将被转换成SectionHeader
对象的原始数据。 不幸的是,这会导致s2e_make_symbolic
出现处理规范方面的问题。
回想一下,解析器的当前位置(_io.pos
)被传递给s2e_make_symbolic
处理规范。 但是糟糕的是当创建本地_io
流时,这个地址将清零,因此符号化的时候使用这个地址会造成错误的内存地址。 不过,我们可以通过对稍微修改下Lua代码来解决这个问题:
for i = 1, self.qty_section_header do -- Get the absolute start address of the section header before it is parsed local _sec_hdr_start_addr = self.start_addr + self._io:pos() self._raw__m_section_headers[i] = \ self._io:read_bytes(self.section_header_entry_size) local _io = KaitaiStream(stringstream(self._raw__m_section_headers[i])) -- Use the section header's start address instead of the ELF's start address self._m_section_headers[i] = Elf.EndianElf.SectionHeader(self.s2e_state, _sec_hdr_start_addr, _io, self, self._root, self._is_le) end
是的,修改生成的Lua代码是令人厌恶的。但是,它确保了符号化时的内存地址是正确的。当我重新编译S2E时,分支被限制在process_section_headers
函数中的sh_type
比较部分。
结论和未来的工作
在这篇文章中,探讨了如何更有针对性的执行文件解析器的符号执行问题。我们可以使用Kaitai Struct来定位输入文件的特定部分来进行符号化,而非给解析器一个完全符号的输入文件(这会很快导致路径爆炸问题)。这种方法似乎奏效,但还是有些问题。
首先,首先,它依赖于用户有一个有效的样例文件来执行符号执行。
。这个样例文件还必须包含我们希望运行的解析器部分的数据。比如,假设我们想将此技术应用于PNG解析器。如果我们拿这个PNG文件的定义,并希望看到当bkgd_truecolor
属性符号化时发生了什么,我们的PNG文件也必须包含一个背景颜色块。否则我们的解析器将没有符号化的东西。
由于类似的原因,我们不能仅仅使用S2E引导脚本创建的“空”的符号文件。为当Kaitai Struct解析器执行时,它运行在文件中的具体数据上。 S2E创建的默认符号文件用NULL
字符填充,因此解析器无法解析。如果我们可以凭空创造出文件,是不是会很酷?
其他问题取决于我们如何使用Kaitai Struct。这不是Kaitai Struct的错误;实际上,Kaitai Struct FAQ明确指出,生成的解析器本来就不是为了“基于事件”的解析模型而设计的。我们可以修改ksc
来生成基本不需要手动修改的代码(例如,自动生成参数规范,使用非延迟的实例规范,始终跟踪解析器的绝对路径等等),但是为了简单起见不去考虑Kaitai Struct “原本的样子”。
不是基于文件的符号执行怎么办?例如,在我之前的帖子中,我展示了如何使用S2E来解决使用命令行字符串作为输入的CTF挑战。这篇文章中描述的方法对解决这个CTF的挑战是没有帮助的。同样我们可以扩展KaitaiStruct
插件来处理命令行字符串。例如,我们可以在Kaitai Struct中定义CTF挑战的输入字符串如下:
meta: id: ctf-input title: Google CTF input format ks-version: 0.8 seq: - id: prefix size: 4 contents: "CTF{" - id: to_solve size: 63 # total length of 67 bytes minus the 4 byte prefix process: s2e_make_symbolic(s2e_state, start_addr, _io.pos, "to_solve") params: - id: s2e_state - id: start_addr
加上一些额外的代码,我们可以在输入字符串上的运行此解析器,只将最后63个字节符号化。 这将允许我们从S2E插件中删除onSymbolicVariableCreation
方法。
尽管出现了这些问题,但是把S2E和Kaitai
Struct组合起来似乎对我目前正在做的工作(尽管你的目的可能会有所不同)还是很有帮助的。 我们可以通过更多的工作(更多的代码)来解决这些问题。 所以,我想我会把那作为一个未来的帖子:)
2017年10月23日
原文链接
本文由看雪翻译小组fyb波翻译。
阿里云助力开发者!2核2G 3M带宽不限流量!6.18限时价,开 发者可享99元/年,续费同价!