查看: 107|回复: 0

[Reverse] 从做题到出题再到做题三部曲-VM

[复制链接]

22

主题

23

帖子

0

精华

VIP

Rank: 16

学币
10
荣耀
0
rank
0
违规
0

VIP

    发表于 2020-6-15 01:00:30 | 显示全部楼层 |阅读模式

    前言  VM的题目相信对于CTF老鸟来说已经不陌生了,基本上每场比赛都会出现,通常会作为签到题或者简单题。
      对于刚接触的小白来说,确实会感到十分棘手,不过一旦理解什么是VM,那么这种题就是体力活了(汗!)
      为此我想从做题者和出题者的角度来理解VM,同时自己也做一个总结。
      由于代码拿去出题了,不便公开,所以网上找了个简单的VM实现
    基本介绍
      参考自《加密与解密》
      VMP:也就是虚拟机保护技术,它是将基于x86汇编系统的可执行代码转换为字节码指令系统的代码,以达到保护原有指令不被轻易逆向和篡改的目的。这种指令执行系统和Intel的x86指令系统不在同一个层次中。
      字节码(Bytecode):是由指令执行系统定义的一套指令和数据组成的一串数据流,由于每个系统设计的字节码都是供自己使用的,因此他们之间的字节码并不通用。
      虚拟机执行时的情况:

    从做题到出题再到做题三部曲-VM

    从做题到出题再到做题三部曲-VM
      VStartVM将真实环境压入栈后会生成一个VMDispather标签,当Handler执行完毕后会跳回这里,形成一个循环,所以VStratVM,也叫做dispather
      网上有篇别人写的笔记可以参考,这里不再赘述
    做题  护网杯的refinal我之前做过较为详细的分析,可以参考此文,因此这里就不在分析了。
      分析VM题的一般套路:
    • 提取出bytecode
    • 根据op代入函数
    • 转化成伪汇编代码
    • 转化成高级语言代码(C/C++/Python)
    • 逆向算法,写出解密脚本
    出题  这里主要还是讲一下出题。
      首先一条指令由操作符、操作地址和操作数组成。为简单起见,操作符可以如下定义:
    typedef enum {
    NOOP    = 0,
    IADD    = 1,   // int add
    ISUB    = 2,
    IMUL    = 3,
    ILT     = 4,   // int less than
    IEQ     = 5,   // int equal
    BR      = 6,   // branch
    BRT     = 7,   // branch if true
    BRF     = 8,   // branch if false
    ICONST  = 9,   // push constant integer
    LOAD    = 10,  // load from local context
    GLOAD   = 11,  // load from global memory
    STORE   = 12,  // store in local context
    GSTORE  = 13,  // store in global memory
    PRINT   = 14,  // print stack top
    POP     = 15,  // throw away top of stack
    HALT    = 16    //over
    } VM_CODE;
      然后我们需要编写一个VStartVM,也就是VM的入口函数,将所有的寄存器压栈,设置VM的ip,sp,fp,定义出全局变量的大小,并为stack分配空间,初始化完成后,便进入了VMDispatcher循环,根据一些特殊的条件退出循环。
      为了简单起见,我们一条指令只有两字节操作数+操作地址将操作数压入栈中,通过操作地址来获取。
      定义一个常量的代码可以如下实现:
    case ICONST:
    stack[++sp] = code[ip++];//将操作符后的数据压入vm的stack中
    break;
      将常量压入stack后,还需要将其存储到相对vm是固定位置的地方去,因此需要使用fp+offset的方法。
    存储一个常量的代码可以如下实现:
    case STORE:
    offset = code[ip++];
    stack[fp+offset] = stack[sp--];
    break;
      因此定义一个常量并存储的伪指令可以如下:
    ICONST,10,
    STORE,0
      加减运算可以如下实现:
    case IADD:
    b = stack[sp--];           // 2nd opnd at top of stack
    a = stack[sp--];           // 1st opnd 1 below top
    stack[++sp] = a + b;       // push result
    break;
      由于此时的数据通常在fp+offset中,我们需要加载到当前的栈上进行运算,加载变量的实现可以如下:
    case GLOAD: // load from global memory
    addr = code[ip++];
    stack[++sp] = globals[addr];
    break;
      因此加法运算的伪指令可以如下:
    加载需要进行运算的两个操作数
    GLOAD,0,        // 假设 0 为 I
    ICONST,1,       // 引入常量 1
    IADD,           // I+1
    STORE,1         // I = I + 1
      接下来还需要实现判断以及跳转,其实这两者是联系在一起的,所以结合起来考虑,跳转指令只需要实现三种方式即可,其余均靠比较条件实现。
    直接跳转
    TRUE 跳转
    FALSE 跳转
      首先是将判断的结果保留在栈顶。
      相等和小于的判断可以如下实现:
    case ILT:
    b = stack[sp--];
    a = stack[sp--];
    stack[++sp] = (a < b) ? TRUE : FALSE;
    break;
    case IEQ:
    b = stack[sp--];
    a = stack[sp--];
    stack[++sp] = (a == b) ? TRUE : FALSE;
    break;
      跳转指令可以如下实现:
    直接跳转:
    
    case BR:
    ip = code[ip];
    break;
    
    当前栈顶为TRUE则跳转:
    
    case BRT:
    addr = code[ip++];
    if (stack[sp--] == TRUE) ip = addr;
    break;
    
    当前栈顶为FALSE则跳转:
    
    case BRF:
    addr = code[ip++];
    if (stack[sp--] == FALSE) ip = addr;
    break;
      因此一个简单的条件转移的伪指令可以如下:
    GLOAD, 1,              // 12
    GLOAD, 0,              // 14
    ILT,                   // 16 A是否小于B
    BRT, 35,                    //如果 A<B 则跳转到addr 35 处
      至此我们已经实现了一个VM的基本功能,从代码上来看,并不复杂,理解起来也十分的简单。
    做题  结合着源代码一起逆向分析VM
    分析  通常来说,正常的VM题,第一步需要找到VM的入口函数,在其入口处参数往往能暴露出很多信息。

    从做题到出题再到做题三部曲-VM

    从做题到出题再到做题三部曲-VM
      loop2是如下的数据

    从做题到出题再到做题三部曲-VM

    从做题到出题再到做题三部曲-VM
      这就是伪代码,也就是Bytecode,接下来就需要将Bytecode逐步的根据opcode转化为伪汇编代码,最后转换为高级语言,进而写出解密脚本。这是一般套路,不过在这里用不到。
      VM的题一般不会去静态分析opcode的功能,通常采用动态调试的方法,搭好环境我们开始。
      当进入while循环时,读取第一个opcode

    从做题到出题再到做题三部曲-VM

    从做题到出题再到做题三部曲-VM
      程序根据opcode执行到对应的handler,此时我们不能继续关注当前的stack,而是应该关注VM的stack。

    从做题到出题再到做题三部曲-VM

    从做题到出题再到做题三部曲-VM
      注意到此时的VMStack在esp+0x50的位置,并且将code[1]传递给了stack[sp]

    从做题到出题再到做题三部曲-VM

    从做题到出题再到做题三部曲-VM
      _sp指向的便是VM的栈顶,由于事先预设了stack的大小为1000,因此程序的栈并不会将VM的栈覆盖,这一点在设计VM的时候需要注意。
      每次运算完成后,将结果保存在栈顶,通常的VM会取用到栈顶元素,对其进行操作
      所以09 03相当于是r1 = 03,为了便于观察,我们将stack固定住,使其反应出VM的stack,设置也很简单,将stack同步取消即可。

    从做题到出题再到做题三部曲-VM

    从做题到出题再到做题三部曲-VM
      继续走,程序会逐个识别Bytecode,并执行相应的功能,通常我会这样来组织,并将中间过程如下记录。
      bytecode.txt
    bytecode
    
    opcode              伪代码                          addr                           高级语言
    
    0x09,0x03           r0 = 3                          0                             // init
    0x0D,0x00           d0 = r1 =3                      2                               i = 0
    0x09,0x00           r0 = 0                          4                               N = 3
    0x0D,0x01           d1 = r0 = 0                     6                               sum = 0
    0x09,0x00           r0 = 0                          8
    0x0D,0x02           d2 = r0 = 0                     10
    
    0x0B,0x01           r0 = d1 = 0                     12                           while(i<N)
    0x0B,0x00           r1 = d0 = 3                     14                          
    0x04,               JL                              16                          
    0x08,0x23           jmp code-offset 0x23(35)        17                          
    0x0B,0x01,          r0 = d1 = 0                     19                           
    0x09,0x01,          r1 = 1                          21  
    0x01,               r0 = r0+r1 = 1                  23                          i = i + 1
    0x0D,0x01,          d1 = r0 = 1                     24
    0x0B,0x02,          r0 = d2 = 0                     26
    0x0B,0x01,          r1 = d1 = 1                     28
    0x01,               r0 = r0 + r1 = 1                30                          sum = sum + i
    0x0D,0x02,          d2 = r0 = 1                     31
    0x06,0x0C,          jmp 0xc(12)                     33  
    
    0x0B,0x02,          r0 = d2 = ?                     35                          print sum
    0x0E,               print r0                        37
    0x10,               exit                            38
      当然如果觉得有必要为了简化手动分析的工作量,我们可以写一个interpreter来翻译伪指令,使其便于我们的分析。
      以下代码仅仅作为参考,因为就像之前所说,VM是没有通用的interpreter的,因此第一步还是需要分析出各个handler的功能,然后进行后续的分析。通常只有在遇到代码量较大的bytecode时才会选择去编写一个针对性的interpreter。
    #-*-coding:utf-8-*-
    code=[0x09,0x03,0x0D,0x00,0x09,0x00,0x0D,0x01,0x09,0x00,0x0D,0x02,0x0B,0x01,0x0B,0x00,0x04,0x08,0x23,0x0B,0x01,0x09,0x01,0x01,0x0D,0x01,0x0B,0x02,0x0B,0x01,0x01,0x0D,0x02,0x06,0x0C,0x0B,0x02,0x0E,0x10]
    
    def interpreter():
    inst = {
    0: "noop",
    1: "iadd",
    2: "isub",
    3: "imul",
    4: "ilt",
    5: "ieq",
    6: "br",
    7: "brt",
    8: "brf",
    9: "iconst",
    10: "load",
    11: "gload",
    12: "store",
    13: "gstore",
    14: "print",
    15: "pop",
    16: "halt",
    }
    
    index = 0
    while 1:
    op = code[index]
    if inst[op] == "halt" :
    print "%s: exit" % (inst[op])
    break
    if inst[op] == "iadd" :
    print "%s: r0 = r0 + r1" % (inst[op])
    index = index + 1
    continue
    if inst[op] == "print" :
    print "%s: " % (inst[op])
    index = index + 1
    continue
    if inst[op] == "gstore" :
    d_index = code[index+1]
    print "%s: d%d = r0" % (inst[op],d_index)
    index = index + 2
    continue
    if inst[op] == "ilt" :
    print "%s: JL" % (inst[op])
    index = index + 1
    continue
    if inst[op] == "brf" :
    d_index = code[index+1]
    print "%s: jmp %d" % (inst[op],d_index)
    index = index + 2
    continue
    if inst[op] == "br" :
    d_index = code[index+1]
    print "%s: jmp %d" % (inst[op],d_index)
    index = index + 2
    continue
    if inst[op] == "gload" :
    d_index = code[index+1]
    print "%s: r0 = d%d " % (inst[op],d_index)
    index = index + 2
    continue
    if inst[op] == "iconst" :
    d_index = code[index+1]
    print "%s: r0 = %d " % (inst[op],d_index)
    index = index + 2
    continue
    index = index + 1
    
    interpreter()

      运行结果如下:
    总结  如果理解了VM,那么完全可以自己开发一个VM,用自己写的VM来保护核心代码,这真的是棒极了!
    题目代码  vm.h
    #ifndef VM_H_
    #define VM_H_
    
    #ifdef __cplusplus
    extern "C" {
    #endif
    
    typedef enum {
    NOOP    = 0,
    IADD    = 1,   // int add
    ISUB    = 2,
    IMUL    = 3,
    ILT     = 4,   // int less than
    IEQ     = 5,   // int equal
    BR      = 6,   // branch
    BRT     = 7,   // branch if true
    BRF     = 8,   // branch if false
    ICONST  = 9,   // push constant integer
    LOAD    = 10,  // load from local context
    GLOAD   = 11,  // load from global memory
    STORE   = 12,  // store in local context
    GSTORE  = 13,  // store in global memory
    PRINT   = 14,  // print stack top
    POP     = 15,  // throw away top of stack
    HALT    = 16    //over
    } VM_CODE;
    
    void vm_exec(int *code, int count, int startip, int nglobals, int trace);
    
    #ifdef __cplusplus
    }
    #endif
    
    #endif
      vm.c
    #include <stdio.h>
    #include <stdlib.h>
    
    #include "vm.h"
    
    #define DEFAULT_STACK_SIZE 1000
    #define FALSE 0
    #define TRUE 1
    
    typedef struct {
    char name[8];
    int nargs;
    } VM_INSTRUCTION;
    
    VM_INSTRUCTION vm_instructions[] = {
    { "noop",   0 },
    { "iadd",   0 },
    { "isub",   0 },
    { "imul",   0 },
    { "ilt",    0 },
    { "ieq",    0 },
    { "ret",    0 },
    { "br",     1 },
    { "brt",    1 },
    { "brf",    1 },
    { "iconst", 1 },
    { "load",   1 },
    { "gload",  1 },
    { "store",  1 },
    { "gstore", 1 },
    { "print",  0 },
    { "pop",    0 },
    { "halt",   0 }
    };
    
    static void vm_print_instr(int *code, int ip);
    static void vm_print_stack(int *stack, int count);
    static void vm_print_data(int *globals, int count);
    
    void vm_exec(int *code, int count, int startip, int nglobals, int trace)
    {
    // registers
    int ip = 0;         // instruction pointer register
    int sp = -1;          // stack pointer register
    int fp = -1;        // frame pointer register
    
    int opcode = code[ip];
    int a = 0;
    int b = 0;
    int addr = 0;
    int offset = 0;
    
    // global variable space
    int globals[nglobals];
    
    // Operand stack, grows upwards
    int stack[DEFAULT_STACK_SIZE];
    
    while (opcode != HALT && ip < count) {
    if (trace) vm_print_instr(code, ip);
    ip++; //jump to next instruction or to operand
    switch (opcode) {
    case IADD:
    b = stack[sp--];           // 2nd opnd at top of stack
    a = stack[sp--];           // 1st opnd 1 below top
    stack[++sp] = a + b;       // push result
    break;
    case ISUB:
    b = stack[sp--];
    a = stack[sp--];
    stack[++sp] = a - b;
    break;
    case IMUL:
    b = stack[sp--];
    a = stack[sp--];
    stack[++sp] = a * b;
    break;
    case ILT:
    b = stack[sp--];
    a = stack[sp--];
    stack[++sp] = (a < b) ? TRUE : FALSE;
    break;
    case IEQ:
    b = stack[sp--];
    a = stack[sp--];
    stack[++sp] = (a == b) ? TRUE : FALSE;
    break;
    case BR:
    ip = code[ip];
    break;
    case BRT:
    addr = code[ip++];
    if (stack[sp--] == TRUE) ip = addr;
    break;
    case BRF:
    addr = code[ip++];
    if (stack[sp--] == FALSE) ip = addr;
    break;
    case ICONST:
    stack[++sp] = code[ip++];  // push operand
    break;
    case LOAD: // load local or arg; 1st local is fp+1, args are fp-3, fp-4, fp-5, ...
    offset = code[ip++];
    stack[++sp] = stack[fp+offset];
    break;
    case GLOAD: // load from global memory
    addr = code[ip++];
    stack[++sp] = globals[addr];
    break;
    case STORE:
    offset = code[ip++];
    stack[fp+offset] = stack[sp--];
    break;
    case GSTORE:
    addr = code[ip++];
    globals[addr] = stack[sp--];
    break;
    case PRINT:
    printf("%x\n", stack[sp--]);
    break;
    case POP:
    --sp;
    break;
    default:
    printf("invalid opcode: %d at ip=%d\n", opcode, (ip - 1));
    exit(1);
    }
    if (trace) vm_print_stack(stack, sp);
    opcode = code[ip];
    }
    if (trace) vm_print_instr(code, ip);
    if (trace) vm_print_stack(stack, sp);
    if (trace) vm_print_data(globals, nglobals);
    }
    
    static void vm_print_instr(int *code, int ip)
    {
    int opcode = code[ip];
    VM_INSTRUCTION *inst = &vm_instructions[opcode];
    switch (inst->nargs) {
    case 0:
    printf("%04d:  %-20s", ip, inst->name);
    break;
    
    case 1:
    printf("%04d:  %-10s%-10d", ip, inst->name, code[ip + 1]);
    break;
    }
    }
    
    static void vm_print_stack(int *stack, int count)
    {
    printf("stack=[");
    for (int i = 0; i <= count; i++) {
    printf(" %d", stack[i]);
    }
    printf(" ]\n");
    }
    
    static void vm_print_data(int *globals, int count)
    {
    printf("Data memory:\n");
    for (int i = 0; i < count; i++) {
    printf("%04d: %d\n", i, globals[i]);
    }
    }
      vmtest.c
    #include <stdio.h>
    #include <time.h>
    #include "vm.h"
    
    int loop2[] = {
    // .GLOBALS 2; N, I
    // N = 10                      ADDRESS
    ICONST, 3,            // 0
    GSTORE, 0,             // 2
    // I = 0
    ICONST, 0,             // 4
    GSTORE, 1,             // 6
    // SUM = 0
    ICONST,0,               //8
    GSTORE,2,               //10
    // WHILE I<N:
    // START (8):
    GLOAD, 1,              // 12
    GLOAD, 0,              // 14
    ILT,                   // 16
    BRF, 35,               // 17
    //     I = I + 1
    GLOAD, 1,              // 19
    ICONST, 1,             // 21
    IADD,                  // 23
    GSTORE, 1,             // 24
    //sum = sum +i
    GLOAD,2,                //26
    GLOAD,1,                //28
    IADD,                   //30
    GSTORE,2,               //31
    
    BR, 12,                 // 33
    GLOAD,2,                //35
    PRINT,                  //37
    // DONE (24):
    // PRINT "LOOPED "+N+" TIMES."
    HALT                   // 38
    };
    
    int main(int argc, char *argv[])
    {
    //     vm_exec(hello, sizeof(hello), 0, 0, 1);
    vm_exec(loop2, sizeof(loop2), 0, 2, 0);
    return 0;
    }



    温馨提示:
    1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
    2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
    3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
    学逆向论坛-免费的逆向学习论坛
    微信公众号
    快速回复 返回顶部 返回列表