刚接触逆向的时候做过一两道虚拟机指令的题,记得有一道是 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