刚接触逆向的时候做过一两道虚拟机指令的题,记得有一道是 RCTF 的 magic,当时还跟着无名大佬的 wp 做了很久。但是那个时候对 VM 的理解不是很深,然后这两天看到南邮的两道题,仔细做了一下,发现很适合入门 VM 这类型的题目,所以记录一下。

WxyVM1

拿到文件先 file 一下:

AssassinQ@MacBook-Air  ~/Downloads  file WxyVM1
WxyVM1: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=0391bf87f6f7a11b4d23e29eb39330a762aff5b4, stripped

然后拿到虚拟机下运行一下看看什么样:

[Desktop] ./WxyVM1                                                     3:25:21
[WxyVM 0.0.1]
input your flag:
nctf{123456}
wrong

没看出啥东西,基本判断就是程序应该是一个对 flag 的加密。然后拖进 ida 里分析:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char v4; // [rsp+Bh] [rbp-5h]
  signed int i; // [rsp+Ch] [rbp-4h]

  puts("[WxyVM 0.0.1]");
  puts("input your flag:");
  scanf("%s", &input);
  v4 = 1;
  vm_start();
  if ( strlen(&input) != 24 )
    v4 = 0;
  for ( i = 0; i <= 23; ++i )
  {
    if ( *(&input + i) != enc[i] )
      v4 = 0;
  }
  if ( v4 )
    puts("correct");
  else
    puts("wrong");
  return 0LL;
}

main 函数中输入一个 flag,然后一个 vm 加密函数,再将加密过后的 flag 与存放在 data 段中的 enc 比较,如果相等那么输出 correct。所以基本思路应该是通过 enc 逆出 flag。然后进到 vm_start 函数中看看:

__int64 vm_start()
{
  unsigned int v0; // ST04_4
  __int64 result; // rax
  signed int i; // [rsp+0h] [rbp-10h]
  char v3; // [rsp+8h] [rbp-8h]

  for ( i = 0; i <= 14999; i += 3 )
  {
    v0 = byte_6010C0[i];
    v3 = byte_6010C0[i + 2];
    result = v0;
    switch ( v0 )
    {
      case 1u:
        result = byte_6010C0[i + 1];
        *(&input + result) += v3;
        break;
      case 2u:
        result = byte_6010C0[i + 1];
        *(&input + result) -= v3;
        break;
      case 3u:
        result = byte_6010C0[i + 1];
        *(&input + result) ^= v3;
        break;
      case 4u:
        result = byte_6010C0[i + 1];
        *(&input + result) *= v3;
        break;
      case 5u:
        result = byte_6010C0[i + 1];
        *(&input + result) ^= *(&input + byte_6010C0[i + 2]);
        break;
      default:
        continue;
    }
  }
  return result;
}

这里发现在 data 段中还有一个 byte 数组。总共有 15000 个数,每三个数一组。第一个数作为需要执行的指令,第二个数为输入 flag 的下标,第三个数为与其进行操作的数据。到这里基本已经清楚了,把数据都 dump 下来,写个脚本逆一下就 ok 了。然后还需要注意的是,这里的运算是以 byte 为单位,可能会产生溢出,所以应该每次操作之后模一下 256。

看到网上大多数 wp 都是用 idc 脚本 patch,因为数据确实太多了,连 lazyida 都 dump 不出来。我是选择手动复制出来所有的数据,然后再用 python 正则匹配一下,提取出来。

最后的脚本:

import re
f = open('WxyVM1.txt', 'r')
enc = [0xFFFFFFC4, 0x00000034, 0x00000022, 0xFFFFFFB1, 0xFFFFFFD3, 0x00000011, 0xFFFFFF97, 0x00000007, 0xFFFFFFDB, 0x00000037, 0xFFFFFFC4, 0x00000006, 0x0000001D, 0xFFFFFFFC, 0x0000005B, 0xFFFFFFED, 0xFFFFFF98, 0xFFFFFFDF, 0xFFFFFF94, 0xFFFFFFD8, 0xFFFFFFB3, 0xFFFFFF84, 0xFFFFFFCC, 0x00000008]
text = f.read()
f.close()
pat = re.compile(r'db.{5}')
find_pat = pat.findall(text)
nums = []
for n in find_pat:
    n = n[2:].strip()
    if n.endswith('h'):
        n = int(n[:-1], 16)
    else:
        n = int(n)
    nums.append(n)

def cal(v0, v3, index):
    if v0 == 1:
        enc[index] = (enc[index] - v3) % 256
    elif v0 == 2:
        enc[index] = (enc[index] + v3) % 256
    elif v0 == 3:
        enc[index] = (enc[index] ^ v3) % 256
    elif v0 == 4:
        enc[index] = (enc[index] / v3) % 256
    elif v0 == 5:
        enc[index] = (enc[index] ^ enc[v3]) % 256

for i in range(5000):
    t = 5000 - i
    v0 = nums[3 * t - 3]
    v3 = nums[3 * t - 1]
    res = nums[3 * t - 2]
    cal(v0, v3, res)
flag = ''
for i in range(len(enc)):
    flag += chr(enc[i])
print flag

WxyVM2

file 一下:

AssassinQ@MacBook-Air  ~/Downloads  file WxyVM2
WxyVM2: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=e57d1a1b70ac3d843afa30523dbbbc53c4ff341f, stripped

运行一下,发现和上一题 VM 应该基本是同一个类型:

[Desktop] ./WxyVM2                                                     3:25:36
[WxyVM 0.0.2]
input your flag:
nctf{123456}
wrong

然后拖进 ida 里,只有一个 main 函数。f5 反编译发现提示说函数太大,无法反编译。这个时候需要先修改一下 ida 的配置文件 hexrays.cfg具体操作。修改完后看一下 main 函数的情况:

__int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
  char v4; // [rsp+Bh] [rbp-5h]
  signed int i; // [rsp+Ch] [rbp-4h]

  puts("[WxyVM 0.0.2]");
  puts("input your flag:");
  scanf("%s", &input);
  v4 = 1;
  if ( strlen(&input) != 25 )
    v4 = 0;
  [......]
  for ( i = 0; i <= 24; ++i )
  {
    if ( *(&input + i) != enc[i] )
      v4 = 0;
  }
  if ( v4 )
    puts("correct");
  else
    puts("wrong");
  return 0LL;
}

头和尾是基本一样的,主要是中间的部分,是一段又臭又长的对数据的加密:

我们输入的 input 应该都是 byte,而这么多 dword 的操作其实都是对加密部分的混淆。然后这里的话我是把 main 函数提出来,然后筛选出 byte 开头的语句,并且通过一系列切片简化语句。然后把数据段里被加密的 flag 即 enc 数组 dump 出来,将提取出来的语句进行逆向的实现,就能输出 flag。

其他的一些注意实现和前一题一样。最后的实现脚本:

f = open('WxyVM2.txt', 'r')
text = f.read()
f.close()
enc = [0xFFFFFFC0, 0xFFFFFF85, 0xFFFFFFF9, 0x0000006C, 0xFFFFFFE2, 0x00000014, 0xFFFFFFBB, 0xFFFFFFE4, 0x0000000D, 0x00000059, 0x0000001C, 0x00000023, 0xFFFFFF88, 0x0000006E, 0xFFFFFF9B, 0xFFFFFFCA, 0xFFFFFFBA, 0x0000005C, 0x00000037, 0xFFFFFFFF, 0x00000048, 0xFFFFFFD8, 0x0000001F, 0xFFFFFFAB, 0xFFFFFFA5]
ori = text.split(';\n')
ops = []
for s in ori:
    if s.startswith('d'):
        continue
    elif s.startswith('b'):
        t = s[:1] + s[9:11] + s[12:14] + s[15:]
        ops.append(t)
    elif s.startswith('--'):
        t = s[2:3] + s[-2:] + '-=1'
        ops.append(t)
    elif s.startswith('++'):
        t = s[2:3] + s[-2:] + '+=1'
        ops.append(t)
    else:
        continue
ops = ops[::-1]

def getPart(op):
    index = int(op[1:3], 16)
    symbol = op[3:4]
    num = op[5:]
    if num.endswith('u'):
        num = num[:-1]
    if num.startswith('0x'):
        num = int(num, 16)
    else:
        num = int(num)
    return index, symbol, num

def cal(index, symbol, num):
    if symbol == '+':
        enc[index] = (enc[index] - num) % 256
    elif symbol == '-':
        enc[index] = (enc[index] + num) % 256
    elif symbol == '^':
        enc[index] = (enc[index] ^ num) % 256
    else:
        print 'error'

for op in ops:
    index, symbol, num = getPart(op)
    # print 'enc[', index, ']', symbol, num
    cal(index, symbol, num)

flag = ''
for i in range(len(enc)):
    flag += chr(enc[i])
print flag

ctf wp re

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

简单压缩壳脱壳指南
合天网安实验室-逆向部分