基本概念

这里的虚拟机当然并不是指VMWare或者VirtualBox之类的虚拟机,而是指的意思是一种解释执行系统或者模拟器(Emulator)。

所以虚拟机保护技术,是将程序可执行代码转化为自定义的中间操作码(OperationCode,如果操作码是一个字节,一般可以称为Bytecode),用以保护源程序不被逆向和篡改,opcode通过emulator解释执行,实现程序原来的功能。

在这种情况下,如果要逆向程序,就需要对整个emulator结构进行逆向,理解程序功能,还需要结合opcode进行分析,整个程序逆向工程将会十分繁琐。

这种虚拟化的思想,广泛用于计算机科学其他领域。从某种程度上来说,解释执行的脚本语言都需要有一个虚拟机,例如Java通过JVM实现了平台无关性

虚拟机保护也有其缺点,就是程序运行速度会受到影响。在商用的一些虚拟机保护软件中,可以提供SDK,在编程的时候可以直接添加标记,只保护关键算法。

在目前的CTF比赛中,虚拟机题目往往有两种:

  • 给可执行程序和opcode,逆向emulator,结合opcode文件,推出flag

  • 只给可执行程序,逆向emulator,构造opcode,读取flag

DDCTF 2018黑盒破解

这个题目属于第二种,需要构造opcode,使其输出flag

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
puts("---------------------[Welcome To ReverseMe!]---------------------");
puts("\n\nPlease Input Your Passcode,If You See print the \"Binggo\" string,Congratulations,You Win. Good luck!");
v4 = operator new(0xAA0uLL);
sub_401E98(v4);
if ( (unsigned int)sub_4016BD(v4) == 0 )
{
printf("Error code:%x\n", (unsigned int)dword_6038E0);
exit(0);
}
fgets((char *)(v4 + 16), 100, stdin);
if ( memcmp("exit", (const void *)(v4 + 16), 4uLL) )
{
sub_401A48(v4);
if ( byte_603F00 )
printf("Success!\nYour flag is %s\n", &ptr);
else
puts("Failed!");
}

可以看到我们输入v4后到check前调用了sub_401A48函数,通过查看byte_603F00的引用发现这个函数并没有对这个值的引用,可能伪代码看起来比较困难,我们直接查看cfg图

发现一堆check和循环后call rax

大概可以猜测出来这个函数进行了dispatch,并且rax就是handler

1
2
3
4
5
6
7
8
9
10
11
12
b = []
c = []
for j in range(9):
a = a1[4*(j+72)+8]+408
b.append(a1[a])
print(b)

for i in b:
for j,index in zip(ida,range(len(ida))):
if i == j:
c.append(chr(index))
print(c)

得到opcode:[‘$’, ‘8’, ‘C’, ‘t’, ‘0’, ‘E’, ‘u’, ‘#’, ‘;’]

通过动态调试我们可以找到9个handler的地址

1
2
3
4
5
6
7
8
9
.data:0000000000603840 off_603840      dq offset sub_400DC1    ; DATA XREF: sub_4016BD+239↑r
.data:0000000000603848 dq offset sub_400E7A
.data:0000000000603850 dq offset sub_400F3A
.data:0000000000603858 dq offset sub_401064
.data:0000000000603860 dq offset sub_4011C9
.data:0000000000603868 dq offset sub_40133D
.data:0000000000603870 dq offset sub_4012F3
.data:0000000000603878 dq offset sub_4014B9
.data:0000000000603880 dq offset sub_400CF1

回到main函数的check,可以发现第6个handler引用了这个变量,根据main函数中的提示和3个check可以知道

我们的目标是将 *(_BYTE *)(*(_QWORD *)(a1 + 8) + *(_DWORD *)(a1 + 288) 处的字符串改为Binggo

大概写一下这9个函数的操作

  • $ 665 = str[index]

  • 8 str[index] = 665

  • C 665 = 665 + input[i+1] - 33

  • t 665 = 665 - input[i+1] + 33 如果665==0 665++

  • 0 index++

  • E check

  • u index–

  • # *(&input[0] + index + input[i+1] - 48) - 49 赋值给str[index] check:input[i+1]<=0x59

  • ; 没用到

可以看到C和t都对665处进行了赋值,8将665赋值给了我们要改变的字符串

根据str原来的内容PaF0!&Prv}H{ojDQ#7v=我们可以将前六个改为Binggo,但是t对\x00进行了检查,这时候就要使用#函数了

使用pwntool输入opcode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from pwn import*
p = process("./ReverseMe.elf")
context.log_level = "debug"
p.sendlineafter("word!\n","48ee204317")
gdb.attach(p,"bp 0x40155F")
pause()

payload = "Cc80"
payload += "tcC\x8980"
payload += "t\x8aC\x8e80"
payload += "t\x8fC\x8780"
payload += "80"
payload += "t\x88C\x8f80"
payload += "1#\x48"
payload += "uuuuuuEs"

p.sendlineafter("luck!\n",payload)
p.interactive()

2018RCTF simplevm

首先看main函数,前面读入了题目给的p.bin文件,然后调用了sub_400896函数,p.bin文件里有input flag right wrong 字符串

sub_400896应该就是dispatch函数,里面有一堆case对应一个个handler

为了方便看到伪代码,使用ida远程动态调试

可以看到有两个case分别调用了getchar()和putchar(),程序在输出Input Flag之后会调用0x20次getchar()来读取输入并储存在v1[0x111-0x130]之间。

大概程序流程就是对输入进行加密然后与p.bin文件中的0x20个常量进行比较

case 0xc是用来判断是否已经读入了0x20个字符,在对输入进行加密和判断时也是如此。

加密后的字符也会被放在v1[0x111-0x130]也就是原来的位置

大概看一下加密流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
a   #index
140 #index
143 = c #临时储存input
c = ~(a & c)
141 = c#保存第一次加密
a = c
c = 140
c = ~(a & c)
142 = c#保存第二次加密
c = 141
c = 143
c = ~(a & c)
a = c
c = 142
c = ~(a & c) #C

case 0x18是一个check,这个c是从case 0x17得出来的,可以想到如果c == 0 ,程序就会输出Right,否则输出Wrong

1
2
3
4
5
6
7
8
9
10
11
12
case 0x17:                                // c = c - a 
v14 = c - dword_6010A4;
label_c_v14:
c = v14;
break;
case 0x18: // check
if ( c )
LABEL_35:
v2 = *(_DWORD *)&v1[v2];
else
v2 = v0 + 5;
break;

注意这里判断是从最后一个字符开始的,那么他是如何判断的呢

大概写一下判断的过程

1
2
3
4
5
6
7
c = 146  #index--->0
c+= v1[0xbc] #c+=5
c = v1[c] #获取常量
a = c
c = 146 #index---->0
c+= v1[c8] #+=111 定值
c = v1[c] #获取加密后的数据并和a比较

通过这个我们得到要判断的常量

[0x10, 0x18, 0x43, 0x14, 0x15, 0x47, 0x40, 0x17, 0x10, 0x1D, 0x4B, 0x12, 0x1F, 0x49,
0x48, 0x18, 0x53, 0x54, 0x01, 0x57, 0x51, 0x53, 0x05, 0x56,
0x5A, 0x08, 0x58, 0x5F, 0x0A, 0x0C, 0x58, 0x09]

然后就可以写脚本爆破了,脚本写的很烂,体谅一下。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import ctypes 
cmp = [0x10, 0x18, 0x43, 0x14, 0x15, 0x47, 0x40, 0x17, 0x10, 0x1D, 0x4B, 0x12, 0x1F, 0x49,
0x48, 0x18, 0x53, 0x54, 0x01, 0x57, 0x51, 0x53, 0x05, 0x56,
0x5A, 0x08, 0x58, 0x5F, 0x0A, 0x0C, 0x58, 0x09]
index = 0
flag = []
al = [i for i in range(ord('a'),ord('z')+1)]
for i in range(10):
al.append(i+ord("0"))
for i in range(26):
al.append(i+ord("A"))
while(1):

for c in al:
if index >= 0x20:
#print(len(flag))
print("".join(flag))
exit()
_140 = index + 0x20
a = index + 0x20
_143 = c
c = ctypes.c_ulong(~(a & c))
a = c.value
c = _140
c = ctypes.c_ulong(~(a & c)).value
_142 = c#保存第二次加密
c = _143
c = ctypes.c_ulong(~(a & c)).value
a = c
c = _142
c = (~(a & c)) + 4294967295 + 1
if c == cmp[index]:
#print(_143)
flag.append(chr(_143))
index+=1
_140+=1
a+=1
print(flag)

https://expend20.github.io/2018/05/24/RCTF-simple-vm.html

附一个国外大佬的方法

我这种方法可能更适合我这种菜鸡

referen

https://zhuanlan.zhihu.com/p/39049784

https://zszcr.github.io/2019/01/13/2019-11-13-%E9%80%86%E5%90%91%E8%99%9A%E6%8B%9F%E6%9C%BA%E4%BF%9D%E6%8A%A4%E5%AD%A6%E4%B9%A0/#

https://expend20.github.io/2018/05/24/RCTF-simple-vm.html