对VM逆向的分析 | 一个经典的虚拟机逆向CTF题
2021-06-12 18:59:00 Author: mp.weixin.qq.com(查看原文) 阅读量:119 收藏

本文为看雪论坛精华文章
看雪论坛作者ID:SYJ-Re
简单的vm-re框架
 
虚拟机就是要去模仿一个机器,让机器去执行一个文件(类似用win10去执行一个文件)
 
首先它需要一些在CPU中的寄存器和内存中的堆栈,这样去模拟一个CPU不断的去读取指令。主要是以循环的形式进行读取。
 
1. 在全局变量中分配如下内容:
这里就构成了CPU+内存。
 
2. 模拟一个CPU读取指令的形式(dispatcher)去写这样一个主程序。
 
主程序:其实就是一个循环,这个循环不断的去读取指令(伪机器码opcode)这个会存在于内存中或文件中,然后执行指令opcode所对应的一些函数(其实这些函数可以理解为伪汇编的执行过程,比如就像一些add,sub,pop,push,jmp等操作),这样下来就可以与真实的程序执行相差无几。

一  WriteUp

拿到程序之后先查壳,发现无壳之后运行一下查看,只有一个plz input:
 
我们将其直接拖入ida中进行查看,左侧函数窗口CTRL+F查找main函数。
 
跳转到main函数之后发现有个花指令:
%2007d4e31ff53d45308e7309126226c678/L0__6X1NGME7LX43BB.png)
将地址0x401594处的0xB8换成0x90(nop的硬编码)即可,然后P一下main函数按Tab进行反汇编。
 
如下:
int __cdecl main(int argc, const char **argv, const char **envp){  unsigned int i; // [esp+10h] [ebp-8h]   sub_6914F0();  while ( 1 )  {LABEL_2:    for ( i = 0; ; i += 2 )    {      if ( i >= 0x58 )        goto LABEL_2;      if ( dword_6D48F0[4 * i] == opcode_team[ei_p] )// opcode_team存在于内存中的伪机器码(指令)        break;    }    dispatcher[i]();    // 模拟一个CPU不断的去执行指令(其实就是根据opcode去执行一些模拟汇编的一些函数)  }}

dispatcher就是去实现模拟CPU读取指令。opcode_team就是存在于内存中(也可存在于文件中)的opcode 伪机器码。
 
点开dispatcher,依次进行分析如下(这里就是一些模拟汇编的伪汇编函数)
 
分析之后的:

分析过程:(不一一举例,举几个例子)
什么也没做,就只是++了eip,说明该函数模拟的是NOP。

这里点击过去发现了连续的一个数组和一些字符串数据,说明模拟的是内存和CPU中的寄存器。
这里模拟的就是mov reg, data,又比如sub_691070。

这个模拟的就是push data,同时我们也在内存中找到了模拟栈。

 
以此类推,将全部伪汇编的函数分析出来,然后写出解析脚本:
opcode_team = [0x01, 0x03, 0x03, 0x05, 0x00, 0x00, 0x11, 0x00, 0x00, 0x01, 0x01, 0x11, 0x0C, 0x00, 0x01, 0x0D, 0x0A, 0x00, 0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x02, 0x00, 0x01, 0x00, 0x11, 0x0C, 0x00, 0x02, 0x0D, 0x2B, 0x00, 0x14, 0x00, 0x02, 0x01, 0x01, 0x61, 0x0C, 0x00, 0x01, 0x10, 0x1A, 0x00, 0x01, 0x01, 0x7A, 0x0C, 0x00, 0x01, 0x0F, 0x1A, 0x00, 0x01, 0x01, 0x47, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x06, 0x00, 0x01, 0x0B, 0x24, 0x00, 0x01, 0x01, 0x41, 0x0C, 0x00, 0x01, 0x10, 0x24, 0x00, 0x01, 0x01, 0x5A, 0x0C, 0x00, 0x01, 0x0F, 0x24, 0x00, 0x01, 0x01, 0x4B, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x07, 0x00, 0x01, 0x01, 0x01, 0x10, 0x09, 0x00, 0x01, 0x03, 0x01, 0x00, 0x03, 0x00, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x0B, 0x00, 0x02, 0x07, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x00, 0x00, 0x02, 0x05, 0x00, 0x02, 0x01, 0x00, 0x02, 0x0C, 0x00, 0x02, 0x01, 0x00, 0x02, 0x00, 0x00, 0x02, 0x00, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x05, 0x00, 0x02, 0x0F, 0x00, 0x02, 0x00, 0x00, 0x02, 0x09, 0x00, 0x02, 0x05, 0x00, 0x02, 0x0F, 0x00, 0x02, 0x03, 0x00, 0x02, 0x00, 0x00, 0x02, 0x02, 0x00, 0x02, 0x05, 0x00, 0x02, 0x03, 0x00, 0x02, 0x03, 0x00, 0x02, 0x01, 0x00, 0x02, 0x07, 0x00, 0x02, 0x07, 0x00, 0x02, 0x0B, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, 0x02, 0x02, 0x00, 0x02, 0x07, 0x00, 0x02, 0x02, 0x00, 0x02, 0x0C, 0x00, 0x02, 0x02, 0x00, 0x02, 0x02, 0x00, 0x01, 0x02, 0x01, 0x13, 0x01, 0x02, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x01, 0x0E, 0x5B, 0x00, 0x01, 0x01, 0x22, 0x0C, 0x02, 0x01, 0x0D, 0x59, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x4E, 0x00, 0x01, 0x03, 0x00, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00]opcode_key = {    0: 'nop',    1: 'mov reg data',    2: 'push data',    3: 'push_reg',    4: 'pop_reg',    5:  'printf',    6:  'add_reg_reg1',    7:  'sub_reg_reg1',    8:  'mul',    9:  'div',    10: 'xor',    11: 'jmp',    12: 'cmp',    13: 'je',    14: 'jne',    15: 'jg',    16: 'jl',    17: 'scanf_strlen',    18: 'mem_init',    19: 'stack_to_reg',    20: 'load_input',    0xff: 'exit'}count = 0code_index = 1for x in opcode_team:   # 取每个opcode    if count % 3 == 0:  # 每3个opcode是一条指令,每个指令的第一个是操作码        print(str(code_index)+':', end='')        print(opcode_key[x], end=' ')        code_index += 1    elif count % 3 == 1:        print(str(x)+',', end=' ')    else:        print(str(x))    count += 1
//打印出来的结果如下1:mov reg data 3, 32:printf 0, 03:scanf_strlen 0, 04:mov reg data 1, 175:cmp 0, 16:je 10, 07:mov reg data 3, 18:printf 0, 09:exit 0, 010:mov reg data 2, 011:mov reg data 0, 1712:cmp 0, 213:je 43, 014:load_input 0, 215:mov reg data 1, 9716:cmp 0, 117:jl 26, 018:mov reg data 1, 12219:cmp 0, 120:jg 26, 021:mov reg data 1, 7122:xor 0, 123:mov reg data 1, 124:add_reg_reg1 0, 125:jmp 36, 026:mov reg data 1, 6527:cmp 0, 128:jl 36, 029:mov reg data 1, 9030:cmp 0, 131:jg 36, 032:mov reg data 1, 7533:xor 0, 134:mov reg data 1, 135:sub_reg_reg1 0, 136:mov reg data 1, 1637:div 0, 138:push_reg 1, 039:push_reg 0, 040:mov reg data 1, 141:add_reg_reg1 2, 142:jmp 11, 043:push data 7, 044:push data 13, 045:push data 0, 046:push data 5, 047:push data 1, 048:push data 12, 049:push data 1, 050:push data 0, 051:push data 0, 052:push data 13, 053:push data 5, 054:push data 15, 055:push data 0, 056:push data 9, 057:push data 5, 058:push data 15, 059:push data 3, 060:push data 0, 061:push data 2, 062:push data 5, 063:push data 3, 064:push data 3, 065:push data 1, 066:push data 7, 067:push data 7, 068:push data 11, 069:push data 2, 070:push data 1, 071:push data 2, 072:push data 7, 073:push data 2, 074:push data 12, 075:push data 2, 076:push data 2, 077:mov reg data 2, 178:stack_to_reg 1, 279:pop_reg 0, 080:cmp 0, 181:jne 91, 082:mov reg data 1, 3483:cmp 2, 184:je 89, 085:mov reg data 1, 186:add_reg_reg1 2, 187:jmp 78, 088:mov reg data 3, 089:printf 0, 090:exit 0, 091:mov reg data 3, 192:printf 0, 093:exit 0, 094:nop

分析和注释:

mov edx, 3printf(...)scanf(our_input)mov eax, strlen(our_input)mov ebx, 17cmp eax, ebxje (10)     //相等才跳转到position0mov edx, 1printf(...)exit(0)          //不相等就退出了(10)mov ecx, 0mov eax, 17cmp eax, ecxje (43)                   //直至计数器等于17eax = load_input[ecx]     //循环读取我们的inputmov ebx, 'a'cmp eax, ebxjl (26)                   //该字符小于'a'则跳转到26mov ebx, 'z'cmp eax, ebxjg (26)                  //该字符大于'z'则跳转到26mov ebx, 71xor eax, ebx            mov ebx, 1add eax, ebx             //否则将该字符(char^71+1)jmp (36)(26)mov ebx, 'A'cmp eax, ebxjl (36)                 //该字符比'A'小则跳转到36mov ebx, 'Z'cmp eax, ebxjg (36)                //该字符比'Z'大则跳转到36mov ebx, 75           xor eax, ebxmov ebx, 1sub eax, ebx          //否则将该字符(char^75-1) (36)mov ebx, 16div eax, ebx          //将该字符%16push ebx             push eax              //先压入整除的数字,再压入余数mov ebx, 1mov ecx, 1jmp(11)(43)push         //压入一连串的数据,......(76)push 2(77)mov ecx, 1(78)stack_to_reg 1, 2pop eaxcmp eax, ebxjne (91)mov ebx, 34       //17拆成34个(整数和余数)cmp ecx, ebxje (89)mov ebx, 1mov ecx, 1jmp(78)mov edx, 0        //设置返回值为0(89)printf(..)    //打印失败字符串exit(..)          //退出程序(91)mov edx, 1    //设置返回值为1printf(..)        //打印成功字符串# exit(..)        //退出程序nop

最后理解程序的逻辑之后写出解题脚本:

data = [0x7,0xd,0x0,0x5,0x1,0xc,0x1,0x0,0x0,0xd,0x5,0xf,0x0,0x9,0x5,0xf,0x3,0x0,0x2,0x5,0x3,0x3,0x1,0x7,0x7,0xb,0x2,0x1,0x2,0x7,0x2,0xc,0x2,0x2,]data = data[::-1]flag = ''for i in range(0, 34, 2):    temp = data[i] + data[i+1]*16    x = ((temp+1) ^ 75)    y = ((temp-1) ^ 71)    if x>=65 and x<=90:    # 'A'-'Z'        flag += chr(x)    elif y>=97 and y<=122:   # 'a' - 'z'        flag += chr(y)    else:        flag += chr(temp)    # 没有处于'a'-'z''A'-'Z'之间print(flag)
得到flag值为:
flag{Such_A_EZVM}

二  总结

再遇见vm-re的题目,先查找到opcode(伪机器码),然后找到dispatcher(就是模拟CPU读取指令的分发器),然后边分析那些伪汇编函数(就是模仿汇编指令的函数)边查找模拟的CPU的栈,寄存器,全局变量(多是字符串)等。
 
参考资料:
  • https://www.bilibili.com/video/BV1Nv411k7HJ

  • https://blog.csdn.net/weixin_43876357/article/details/108488762

 

看雪ID:SYJ-Re

https://bbs.pediy.com/user-home-921830.htm

  *本文由看雪论坛 SYJ-Re 原创,转载请注明来自看雪社区。

《安卓高级研修班》
2021年6月班火热招生中!

# 往期推荐

公众号ID:ikanxue
官方微博:看雪安全
商务合作:[email protected]

球分享

球点赞

球在看

点击“阅读原文”,了解更多!


文章来源: http://mp.weixin.qq.com/s?__biz=MjM5NTc2MDYxMw==&mid=2458386737&idx=1&sn=30c9c8f1d6e2a6f09b0f85d9d5f087f7&chksm=b18f31bb86f8b8ad3d25493efc5620d5529b96fb8996ced1f2742c8bf5d5c3f2a72d0d532268#rd
如有侵权请联系:admin#unsafe.sh