行业新闻

虚拟机保护逆向入门

虚拟机保护逆向入门

虚拟机保护逆向入门

本文原创作者:b1ngo

原创投稿详情:

远程也是可以通过的:


640.jpg

RCTF 2018 Simple vm

这个题是今年RCTF2018的一个入门题吧,感谢这位师傅的出题和分享,学

到了很多。题如其名,这是一个虚拟机程序,题目本身给了一个ELF64位的可执行程序,给了一个bin文件,这是opcode文件。

vm_run在sub_400896函数,进去之后发现没有代码加密或者混淆,直接可

以反编译和生成CFG图:


640.png

从结构可以看出虚拟机一般的switch/case结构,所以loc_4008A2可以理解为vm_dispatcher,各个case为vm_handler。赛后做这个题目,我们就用最傻的方法,一步步调试这个vm,结合opcode,理解各个handler。在IDA的反编译结果中,出现了两个比较关键的全局变量:dword_6010A4和c。我们可以将之认为是用于临时存放opcode指令和代码的变量。

给出p.bin文件的解析:

“””

01 # 读取addr赋值给指针

30000000  # 带读取的addr

15 # 读取字符串所在addr赋值给vm.c

00010000 # Inputflag字符串长度地址,后面就是字符串

#dword_6010A4是字符串的ASCII,c是字符串所在的地址

# 这部分完成了打印字符串

0E # c++

12 # dword_6010A4 = p[c]

0B # putchar(dword_6010A4);

0C # 循环

00010000 # 循环次数所在地址(也就是字符串的长度)

35000000 # 每次循环开始的指针

# 这部分完成了循环

15 c = READ_INT();

10010000 # 要读入的字符串长度地址

0E # c

0A # dword_6010A4 = getchar();

66 # nop

16 # p[c] = dword_6010A4存放到0x111位置处

0C # 循环

10010000 # 循环的次数

47000000 # 每次循环开始的指针

03 #取0x140偏移处的内容0x20赋值给dword_6010A4

40010000

10 # c = dword_6010A4;

11 # dword_6010A4 += val;

F1000000 # val

13 # dword_6010A4 =p[dword_6010A4]; 到这个位置,就将输入的ASCII赋值到dword_6010A4中了

04 # p[dst] = dword_6010A4;

43010000 # dst 0x143

#开始对输入做变化,0x141~0x144可以理解为变化临时存放为位置

08 # dword_6010A4 =~(dword_6010A4

18 # OP_JNE

60010000

0C # 循环

46010000

B6000000

“””

这就是这个程序的执行流,调试过程十分麻烦,vm实在是太磨人了。理解

上述过程,其实就是对输入做异或,并与p.bin文件部分内容进行比较的过程,然后写出逆向算法,解出flag:

“””

# -*- coding:utf-8 -*-

a ="1018431415474017101D4B121F49481853540157515305565A08585F0A0C5809"

a = a.decode("hex")

a = [ord(i) for i in a]

print a

f = ""

for i in range(len(a)):

f += chr(a[i]^(i+32))

print f

“””

其实这种方法算是最麻烦的,该方法是从汇编层面调试二进制程序,我还看

到更为简单的一种方法是通过IDA反编译结果(https://expend20.github.io/2018/05/24/RCTF-simple-vm.html),稍微改改之后,重新编译,然后就可以进行源码级调试。这里的修改基本不用动IDA的反编译结果,就是需要加些IDA的宏定义,在每个case后面都使用printf输出当前指针位置,就可以自动的分析出程序的执行流,十分方便。

从赛后复盘的角度是可以慢慢调试虚拟机的,搞清每个字节码的意义,然后

完整的理解虚拟机解释执行过程。不过随着虚拟机的复杂,这种方法将会非常费时,应该学习一些自动化的处理方法。

RCTF 2018 magic

又是今年RCTF的一个Re题,题目也出很好,我感觉还挺难的,赛后看了一

些其他师傅的WP,感觉收获很多。这里前面爆破time的第一次检查不再赘述(吾爱有个师傅的处理方法很不错https://www.52pojie.cn/thread-742361-1-1.html),只说说第二个对输入的检查。当第一次对时间检查通过之后,程序执行流来到了函数sub_4023B1位置:

640.jpg

scanf之后,对输入进行加密,加密后的数据作为虚拟机输入。这个地方是利用setjmp/longjmp实现的一个虚拟机,虚拟机opcode位于

0x405340处。地址409040是虚拟机用于数据处理的区域,我们可以将之定义为dword型数组。该虚拟机值得主要的是这个地方,调试的过程中需要注意一下,这里是通过异常处理完成了一个Handler:


640.png

异常处理在这个signal回调函数中:


640.png

对于虚拟机的逆向,往往都是通过首先逆出来各个Handler,然后结合opcode

搞清楚程序算法,在目前这个阶段,这个算法一般不会很难。如果算法很难,opcode很长,这种情况题目就算题出的很难的。接下来给出opcode的解释:

“””

con[1] = cmp

con[2] = input

AB 03 00 mov con[3],0

AB 04 1A mov con[4],0x1A

AB 00 66 mov con[0],0x66

AA 05 02 mov con[5],con[2] #input

A9 53 con[5] += con[3]  

A0 05 mov con[5],*(byte)con[5]    

AB 06 CC mov con[6],0xCC

A9 56 con[5] += con[6]

AB 06 FF mov con[6],0xFF

AC 56 con[5] &= con[6]

AE 50 con[5] ^= con[0]

AD 00 not con[0]

AA 06 05 mov con[6],con[5]

AA 05 01 mov con[5],con[1]

A9 53 con[5] += con[3]

A0 05 mov con[5],*(byte)con[5]

AF 56 00 if con[5]==con[6]

算法总结:

key = 0x66

((input[i] + 0xCC ) &0xFF) ^key == cmp[i]

~key

“””

直接爆破就可以得到需要输入值:

“””

comp ="89C1EC50973A5759E4E6E442CBD90822AE9D7C07808F1B4504E8"

comp = [ord(i) for i incomp.decode("hex")]

print comp

'''

key = 0x66

((input[i] + 0xCC ) &0xFF) ^key == cmp[i]

~key

'''

key = 0x66

f = []

for i in range(len(comp)):

for j in range(256):

if ((j + 0xCC) &0xFF ) ^ key == comp[i]:

f.append(j)

key = (~key) & 0xFF

print f

“””

值得注意的是,这个只是vm_run的输入值,需要利用程序中的解密算法解

密后得到输入值,这个可以在vm_run成功后,在地址0x040249D下断点,在内存将要解密的值进行替换,最后解密后如下所示:


640.png

这个只是其中一部分的flag,将之输入后会得到一个图案,图案为rctf{h:


640.jpg

拼接之后即为flag。

(注:本文属于合天原创投稿奖励,未经允许,禁止转载!)

关闭