VMP合集
(阶段性输出)代码虚拟化保护
题目清单
- DDCTF2018 黑盒破解
- RCTF2018 Simple vm
- RCTF2018 Magic
- 护网杯2018 rerere
原理介绍
基本原理
虚拟机保护技术是指,将程序代码转换为自定义的中间操作码(OpCode,当操作码只占一个字节可称作ByteCode),OpCode通过一种解释执行系统或者模拟器(Emulator)解释执行,实现代码基本功能。
逆向这种程序,一般需要对emulator结构进行逆向,结合opcode进行分析,得到各个操作码对应的基本操作,从而理解程序功能。(图片来自:https://mp.weixin.qq.com/s/4Nfso1OuHeQgCTGYv2IF5Q)
JVM虚拟机
JVM虚拟可以提供一种与平台无关的编程环境,这是虚拟化思想的一种成功应用。下图为JVM虚拟机的基本架构(图片来自:geeksforgeeks )
VMP虚拟机
VMP虚拟机保护技术指的是将基于x86汇编系统的可执行代码转换为字节码指令系统的代码,达到保护原有指令不被轻易逆向和修改的目的。从本质上来说,就是创建一套虚拟指令系统对原本的x86汇编指令系统进行一次封装,将原本的汇编指令转换为另一种表现形式。
虚拟指令有自己的机器码,但和原本的x86汇编机器码完全不一样,而且常常是一堆无意义的代码,他们只能由VM虚拟解释器(Dispatcher)来解释并执行。我们在逆向时看到的汇编代码其实不是x86汇编代码,而是字节码(伪指令),它是由指令执行系统定义的一套指令和数据组成的一串数据流,所以虚拟机的脱壳很难写出一个通用的脱壳机,原则上只要虚拟指令集一变动,原本的伪指令的解释就会发生变化。
要逆向被VM SDK保护起来的原始代码,只有手工分析这段虚拟指令,找到虚拟指令和原始汇编的对应关系,然后重写出原始程序的代码,完成算法的逆向和分析。
详情待施工……
题目
DDCTF2018 黑盒破解
解题的基本思路是通过分析二进制文件,得到opcode及其对应的基本操作,通过构造passcode,令程序输出binggo字样。
前期准备分析
拿到文件完成基本分析后加载到IDA。
首先通过字符串搜索,并交叉引用定位到主函数,分析程序的基本逻辑:输入password(即给的txt文件名中的数字),然后输入passcode,令程序输出Binggo。
main函数关键判断如下
对flag查找引用发现一个函数,但继续查找引用遇到障碍,所以开动态调试,看一下程序运行的情况。发现sub_401A48可能是一个重要函数,进入查看。
这段代码F5之后不是很友好,但还是看得到大概意思是:根据输入参数调用函数,也就是虚拟机的Dispatcher位置,被调用的各个函数就是虚拟机的各个Handler。下图的汇编更加直接的展示了这个意思,call eax表示调用Handler。
得到passcode与其对应的操作
在这段调用Handel的CFG块上方是判断是否会执行调用的关键代码。根据汇编代码推断算法,并写出idc脚本。在推断算法时要注意根据动态调试来确定各个值得变化,不然很容易出错。
idc脚本如下,要注意这个脚本中的值0x123E010在每一次执行过程中会发生变化,因为它是输入参数存放的地址,具体方法是动态调试执行到参数存放时得到地址值,再根据它修改脚本。爆破出各种可能的函数跳转。
1 |
|
运行结果如下
1324 $ 400dc1
9738 8 400e7a
1943 C 400f3a
774 t 401064
c730 0 4011c9
d545 E 40133d
c875 u 4012f3
1423 # 4014b9
现在要开始分析各个handler的作用,最为特殊的一个handler是sub_40133d,进入这个函数后发现代码如下:
这段代码有个地方比较坑,其实这个将flag置1并不是问题的关键,这个题目要求的是输出“Binggo”字样,所以这里的三个函数并不要去逆向分析,关键在于前面几句,将a1+0x120里的20个字节输出,我们要做的就是构造输出的字符串。在构造之前我们先要分析出其他handler的作用才能根据这些handler构造字符串。
各个handler的分析过程比较痛苦,但在分析时要有一个意识,虚拟机的堆栈或寄存器是创建在真实堆栈之上的,比如说在这个题目中有很多类似如下的代码。
1 | mov edx, [rax+120h] |
其实这当中的[rax+120h]只是模拟一个变量或者一个寄存器,在整个handler中一共用到了四个变量,下面是四个变量的枚举,以及经过分析得出的四变量的作用。
[rax+120h] –> 当前指针位置a[index]
[rax+298h] –> 当前字符参数的下一个字符passcode[next]
[rax+299h] –> 临时变量temp
最终得到下表
opcode | 功能 |
---|---|
$ | temp = a[index] |
8 | a[index] = temp |
C | temp += passcode[next] -33 |
t | temp -= passcode[next] +33 |
0 | index++ |
E | if(passcode[next]==’s’) print(a) |
u | index– |
# | temp = passcode[temp+passcode[next]-48]-49 |
构造passcode
根据上表构造passcode,这里要注意,由于每次取到的a[index]无法静态分析获得,只能每构造一个字符,再动态调试获得下一个a[index]。最后要注意由于输出handler是输出20个字符,而Binggo只有六个字符,所以要注意构造第七个字符加上’\x00’使其截断。构造方式有很多种,下面是一种从前向后的构造方法。
$ –> 取得a[index]的值为50h
t/ –> 将50h变为’B’
8 –> 将’B’放回到a[index]
0 –> index++
重复上述操作得到”Binggo”字符串,对应的passcode为’$t/80$C)80$CI80$CX80$Cg80$Cj80 ‘,然后再通过’#J1’构造’\x00’最后将index移回字符串开头并输出对应passcode为’uuuuuuuEs ‘。
passcode:
$t/80$C)80$CI80$CX80$Cg80$Cj80#J1uuuuuuuEs
RCTF2018 Simple vm
方法一
这道题目给出了Emulator和虚拟机的opcode,通过逆向分析Emulator中各个handler的作用以及opcode的意义推断正确输入从而得到flag
分析
运行vm_rel看一下程序要求,发现是要猜输入
把vm_rel拖入IDA里面进行字符串搜索,并未发现“Input Flag”等字符串。进入主函数看代码流程,猜测vm_rel其实是虚拟机emulator,p.bin才是真正的函数。
把p.bin拖到hexwin里面,发现字符串“Input Flag”、“Wrong”、“Right”,证实上述猜测。
动态调试对p.bin的行为进行分析
注意输出”Input Flag“和接受输入部分的代码可以直接在Pseudocode窗口调试,分别将断点下在putchar()和getchar()处,通过观察寄存器edx的值来推测循环次数。但是在调试输入字符串处理和判断结果并输出部分,最好在汇编窗口调试,观察值的变化,便于找到比较字符串。
1 | //输出“Input Flag”部分的代码 |
求解
理清楚上述逻辑之后就编写程序求解。
1 |
|
09a71bf084a93df7ce3def3ab1bd61f6
方法二
直接用angr解
待施工……
RCTF2018 Magic
这道题涉及到寻找程序入口、打补丁、vm保护、rc4等知识点,基本思路是首先找到程序入口,发现存在一个时间验证和一个输入字符串验证,时间验证的算法十分复杂,可以直接逆也可以调用源程序算法爆破,爆破出时间之后,给程序打好补丁继续调试发现还有一个输入验证,这个输入验证首先对输入进行rc4编码,然后用vm进行加密,最后进行对比,在破解时,先破解虚拟机emulator,然后根据它的opcode推断出vm保护代码,得到rc4加密后的字符串,最后利用rc4加解密算法一致的方式,直接利用源程序得到解密就可以得到部分flag,将着部分flag输入patch过的程序得到另一半flag。
寻找入口函数
运行程序发现如下信息
1 | C:\CTF\tmp\VM>magic.exe |
拖入IDA中,查找字符串,返现与上述输出相关的信息,但是交叉引用跳转到的main函数并无相关输出。在main函数上下断点,动态调试发现在执行main函数之前已经输出,所以判断这个main函数并不是真正的程序入口。交叉引用找到调用main的函数sub_4011B0,在sub_4011B0开始设一个断点,在main函数开始设一个断点,然后利用IDA函数跟踪功能找到这两个断点之间调用puts函数的的函数sub_402218。往上看可以看到函数sub_402357调用了sub_402218,再往上没有找到sub_402357的调用信息。初步判定这里就是入口函数。
时间验证
打开sub_402357发现如下关键判断:
对dword_4099D0[0]交叉引用找到如下函数,将其命名为check_time。要想执行main函数必须使得dword_402357的值为1,进入check_time函数,发现代码逻辑为:首先返回在(0x5B028A8F, 0x5AFFE78F]之间的的时间戳,以时间戳为种子取随机数对Table表做异或运算,再通过sub_4027ED对Table表进行变化,并返回v4和v3。如果想达到前文阐述的目的,只有让v4 == 0x7000 & v3 == 0条件成立。
进入sub_4027E0函数发现反编译代码十分奇怪,可以选择直接看汇编,但我在https://www.52pojie.cn/thread-742361-1-1.html这篇文章中,发现了一种调用原程序的方法,暴力求解time的方法。这个方法十分巧妙的借用源程序中的算法和数据,得到满足条件的time,程序如下:
下面代码还存在问题,待施工……
1 | typedef unsigned int(*test)(); |
得到满足条件的time为:0x5b00e398,对程序打个补丁,跳过if时间戳合法判断,将srand的参数修改为我们爆破出来的time,汇编代码修改如下,将补丁应用到原有的二进制文件上的步骤为Edit->Patch Program/Apply patchs to input file…
给程序打好补丁之后,在这个函数结束的地方打上断点,继续观察程序的运行情况。
输入验证
调用check_time的函数sub_402357在执行完成之后回到函数sub_4032A0,经过几次跳转来到函数sub_403180,发现onexit()函数,这个函数的作用是注册一个函数,使得程序在exit()的时候自动调用这个被注册的函数。
执行完这个函数之后,程序终于开始执行main函数,main函数里面没有什么特别重要的内容,两个条件跳转都不会发生,那么肯定是执行了前面onexit()注册的函数,继续F8,跳转到一个有用函数sub_403260,这个函数中的call rax调用了一个关键函数sub_4023B1如下
有些变量名称已经根据其作用有些修改,这个check_rc4其实就是一个简单的rc4加密的过程,它首先生成s盒然后对输入input进行加密,在经过一次rc4后有一个if判断,这个if判断里的函数其实是虚拟机入口,这个虚拟机实现的时候用到了setjmp/longjmp,相关知识在这里:http://www.cs.cmu.edu/afs/cs/academic/class/15213-s02/www/applications/recitation/recitation7/B/r07-B.pdf
在利用setjmp/longjmp实现vm保护时,有一个地方特别巧妙,一开始vm_run函数首先将处理过的输入input复制到Count中,然后用signal注册一个函数。
它注册的函数如下
而在各个handler中可以看到如下一个操作
可以看到这里面有除法操作,而除数为0的时候会引起异常,这个时候就会转到signal注册的函数signfunc中去执行操作,执行完之后再返回到循环中去,取下一步操作。
其他handler作用的分析比较简单这里不赘述,0x405340处存储的是要执行的opcode,以下是对op凑得的分析
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 > AB 03 00 reg[3] = 0
> AB 04 1A reg[4] = 0x1A
> AB 00 66 reg[0] = 0x66
> AA 05 02 reg[5] = reg[2] ;input_rc4ed
> A9 53 reg[5] += reg[3]
> A0 05 reg[5] = *(byte)reg[5] ;input_rc4ed[i]
> AB 06 CC reg[6] = 0xCC
> A9 56 reg[5] += reg[6]
> AB 06 FF reg[6] = 0xFF
> AC 56 reg[5] &= reg[6]
> AE 50 reg[5] ^= reg[0]
> AD 00 reg[0] = ~reg[0]
> AA 06 05 reg[6] = reg[5]
> AA 05 01 reg[5] = reg[1] ;cmpstr
> A9 53 reg[5] += reg[3]
> A0 05 reg[5] = *(byte)reg[5] ;cmpstr[i]
> AF 56 00 reg[5] = (reg[5] == reg[6])
> A7 01 if (reg[5] == 1) vip += 1
> CC
> A9 35 reg[3] += reg[5]
> AA 05 03 reg[5] = reg[3]
> AF 54 00 reg[5] = (reg[5] == reg[4])
> A6 D1 if (reg[5] == 0) vip +=0xD1
> CC
>
根据上述分析得到的opcode写出爆破求解程序如下:
1 |
|
得到的结果如下
1 | 0x23 0x8c 0xbe 0xfd 0x25 0xd7 0x65 0xf4 0xb6 0xb3 0xb6 0xf 0xe1 0x74 0xa2 0xef 0xfc 0x38 0x4e 0xd2 0x1a 0x4a 0xb1 0x10 0x96 0xa5 |
由于rc4加解密过程一样,所以把上述字符串作为输入,利用源程序的rc4的密钥求解,把上述字符串作为输入可以通过脚本修改,脚本来自http://ahageek.com/blog/rctf2018-magic-writeup/
1 | from idaapi import * |
最后得到字符串如下
@ck_For_fun_02508iO2_2iOR}
在打过补丁的程序中输入上述flag,得到另一半flag:rctf{h
综上整个flag为:rctf{h@ck_For_fun_02508iO2_2iOR}
护网杯2018 rerere
程序流程分析
通过字符串搜索定位到主函数,发现如下代码。
上述代码是对输入参数长度的判断,长度必须要大于0x30,于是输入参数
在主函数中存在两个结构体函数
1 | (*(void (__stdcall **)(_DWORD *, int))(*(_DWORD *)Memorya + 104))(v7, v4); |
动态调试进入这两个函数,发现第二个函数结构如下
1 | int __thiscall VMDispatcher(unsigned __int8 **this) |
非常像dispatcher,初步判断这是一个vm。
VM结构分析
下图为VM初始化,this[1]~this[5]为寄存器,初始值为0;this[6]为输出参数字符串;this[9]为opcode。
然后是各个handler的分析,结果如下
操作码(操作数) | 功能 | 说明 |
---|---|---|
0x43 | exit() | 退出循环 |
0x53(a) | this[a2] += this[a1] | |
0x59(a) | this[a2] -= this[a1] | |
0x58(a) | this[a2] *= this[a1] | |
0x45(a) | this[a2] /= this[a1] | |
0x50(a) | this[a2] = this[a1] + 1 | |
0x4E(a) | this[a2] = this[a1] - 1 | |
0x47(a) | this[a2] ^= this[a1] | |
0x4A(a) | this[a2] &= this[a1] | |
0x52(a) | this[8] = this[a1] | |
0x4F(a,b,c,d) | this[8] = abcd | |
0x54(a) | this[a2] = this[8] | |
0x51(a) | result = this[a2] | |
0x46(a) | this[a2] = this[6] | |
0x57(a) | ||
0x55(a) | if(this[4]!=0) {this[4]–;this[9] - a;} | 跳转指令 |
0x48(a) | if(v1==v2){this[5]=0;}if(v1<v2){this[5]=-1;}if(v1>v2){this[5]=1;} | v1=this[a1];v2=this[a2] |
0x44(a) | if(this[5]==-1){this[9] + a;} | 跳转指令 |
0x4D(a) | if(this[5]==1){this[9] + a;} | 跳转指令 |
0x4B(a) | if(this[5]==0){this[9] + a;} | 跳转指令 |
0x49() | this[6]++ | |
0x56() | this[6]– | |
0x4C() |
tips:
a1 = (a&0xF)+1 ; a2 = (a>>4)+1
this[9]指向下一条要执行的语句
最后对opcode进行分析,得到vm保护部分的代码含义。
第一段代码的含义是限制输入为[0~9][A~F]。
后几段是将48位输入分为6段,每段8byte,但是要注意是从后往前进行比较的。下面是以第二段为例的具体分析。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23 > code2:
> 55 40 ;if(this[4]!=0){this[4]--;this[9]-0x40;}
> 4F 00 00 00 07 54 30 ;this[4]=0x7
> 47 11 ;this[2]^=this[2]
> t_code1:
> 56 ;index--
> 46 00 ;this[1]=input[index]
> 4F 00 00 00 30 54 20 ;this[3]=0x30
> 59 02 ;this[1]-=this[3]
> 4F 00 00 00 0A 54 20 ;this[3]=0xa
> 48 02 44 09 ;if(this[3]<this[1])jmp t_code2
> 4F 00 00 00 07 54 20 ;this[3]=0x7
> 59 02 ;this[1]-=this[3]
> t_code2:
> 4F 00 00 00 10 54 20 ;this[3]=0x10
> 58 12 ;this[2]*=this[3]
> 53 10 ;this[2]+=this[1]
> 55 2B ;if(this[4]!=0){this[4]--;jmp t_code1;}
> 4F 33 B4 88 AC 54 20 ;this[3]=0x33B488AC
> 48 12 47 00 4B 03 ;if(this[3]==this[2])jmp code3;this[1]^=this[1]
> 50 00 ;this[1]++
> 43 ;exit()
>
解密得flag
最后用python编写解密代码得到输入
1 | C:\CTF\Challenge\Binary_sec\Reverse>task_huwang-refinal-4.exe E25BD838D2B62FE1B1579293CECDC5C7F349B0A4CA884B33 |
flag如下
flag{E25BD838D2B62FE1B1579293CECDC5C7F349B0A4CA884B33}
参考内容
https://mp.weixin.qq.com/s/4Nfso1OuHeQgCTGYv2IF5Q
https://www.anquanke.com/post/id/145553
https://blog.csdn.net/liutianshx2012/article/details/48466327?locationNum=7&fps=1
http://ahageek.com/blog/rctf2018-magic-writeup/
https://www.52pojie.cn/thread-742361-1-1.html
https://www.xctf.org.cn/media/infoattach/b805a17b85c54091975aab3709b7a5bb.pdf