[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
可以明显感觉到玄武组的题目难于其他三个组。这么复杂的一道题也好意思叫baby???
题目分析
拿到题目发现是一个64位windows程序,结合题目名称,怀疑是虚拟机逆向。
直接逆向分析:
没有加壳,把所有函数的符号都去了,但字符串没有混淆,可以根据“Tell Me Your Flag:”字符串找到函数的主逻辑:
向下分析可以的到flag的格式:
flag长度42,uuid格式,并且由0-9,a-f组成,也就是小写16进制数。如:
flag{12345678-0123-5678-0123-567890123456}
接着将输入的函数去掉flag{}和-之后转换成16进制数,然后传给chkflag函数:
注意中间的那些ifdebug函数出现了很多次,是用来检测调试的,如果调试的过程中经过了这些函数,就会退出,所以可以直接patch掉,但太多,也可以直接就断点避开这些函数。
然后查看chkflag函数:
这个函数的逻辑是:
- 先对输入(flag去掉头尾和-之后转换成16进制数之后)进行md5
- 然后对flag进行某种变换(之后在分析)
- 对变换结果进行md5
- 分别取输入(flag去掉头尾和-之后转换成16进制数之后)、输入的md5、输入的变换的md5的依次4个字节转换成整数
- 将上述三个整数进入VM虚拟机进行计算,一共四轮
- 每一轮的计算结果(4字节)和输入的md5的4字节一起存入一个数组,然后和一个全局变量校验
- 返回校验结果,如果相同则通过,输出flag正确
关于md5是怎么看出来的,首先点进这个函数能看到一堆跟hash有关的函数:
百度一下就知道这是windows计算哈希用的一些东西,然后根据输入和输出的结果,首先,我们选择一个转换成16进制数之后是可显示的ascii嘛的输入作为输入,如:flag{78797878-7878-7878-7878-787878787878}
转换成16进制之后是:
然后对比md5计算的结果:
927B94B9FE8EE835AB2A400FF4219644
可以搜到:
注意上述步骤中的第六步,他VM计算的结果和我们输入flag的md5一起存在了result的数组里,每隔四个字节一个,那么我们是不是就知道了flag的md5了呢?没错确实我们知道了flag的md5,但并没啥用,查不出来,因为这个md5的输入并不是可见字符串,所以市面上的彩虹表都搜不出来。。。。
这是结果:
这里每隔四个字节都是flag的md5的四个字节(转换成整数后),取出来flag的md5就是:
9036d8c5ea6281976ca9321969262204
根本搜不出来(24小时都是骗人的,我当天就没做出来,到现在写wp都快一周了):
老老实实的来分析一下VM吧
虚拟机分析
首先将上文提到的三个数放在一个数组中,然后和opcode一起作为输入初始化虚拟机:
整个虚拟机的虚拟处理器和内存结构如下上面所示,每一个寄存器是4个字节,也就是32位的。初始化完成之后将虚拟机结构体返回,在内存中看一下:
值得注意的是寄存器0是用来存储当前执行到opcode第几个的:
然后就是对opcode进行分析,找到各个操作码对应的操作。技巧在之前VM题目中已经提到过了,其实就是在执行一个opcode对应的函数之前和之后,记录并对比寄存器和栈的状态,大部分操作数都可以分析得出。可能有一些比如位移和异或不太容易看出,也可以直接进入到函数中,查看关键操作,比如这道题目里的0x11操作数,输入输出如下:
78787878,07 -> f0f0f0f0
我最开始还以为是每个字节分别异或0x07,结果正好是0xf0f0f0f0,但后来我发现不对,进入这个函数之后发现了IDA的ROR4宏,也就是循环右移:
所以这个0x11操作数就是循环右移x位。也就是说,简单的操作数对比输入输出,复杂的操作数进去看关键操作指令,剩下的就是熟练度,熟练度越高,分析出虚拟机行为越快。
将操作数导出:
D0 00 00 00 00 02 01 0D 01 02 04 01 10 00 00 00
02 01 01 00 00 00 00 02 03 01 01 00 00 00 02 02
09 01 02 0D 01 02 05 01 08 00 00 00 02 06 14 05
06 11 04 05 01 B9 79 37 9E 02 05 0C 04 05 0D 04
03 AD 00 00 00 0C 04 01 02 04 0E 01 03 07 06 19
00 00 00 D0 01 00 00 00 D0 02 00 00 00 02 02 02
03 0D 04 02 05 10 05 02 10 05 03 0D 05 0D 04 02
05 0F 05 02 0F 05 03 0D 05 0D 04 02 05 0F 05 02
0D 05 0D 04 02 05 0F 05 03 0D 05 0D 02 02 05 0F
05 03 02 06 0C 05 06 02 06 0C 05 06 02 06 0C 05
06 02 06 0C 05 06 0D 05 02 01 13 01 FF 00 08 00
00 00 01 01 03 00 00 00 02 02 08 01 02 E0 08 00
00 00 01 00 09 00 00 00 02 01 06 00 00 00 02 03
12 02 03 E0 09 00 00 00 02 04 00 00 00 00 00 00
90 36 D8 C5 CC 02 79 1F EA 62 81 97 15 3D AE 2F
6C A9 32 19 91 FE EB CE 69 26 22 04 42 AF F6 AF
然后根据操作数的顺序,来一个一个的分析,遇到相同的就跳过,分析完毕之后没出现的操作数也不用分析了。我分析的虚拟机操作数含义如下:
opcode:
00 xx 00 00 00 0x: 把栈偏移xx mov 给0x寄存器
E0 xx 00 00 00 0x: 把第x个寄存器mov到栈偏移xx处
01 xx xx xx xx:push xxxxxxxx 立即数
0d 0x : push xreg
02 0x : pop reg0x
03 AD 00 00 00 0C 04 01: call AD
04 : 返回
06 19 00 00 00 : jmp 19
07 : falg=~flag
08 0x 0y : regx+=regy
09 0x 0y : regx-=regy
0c 0x 0y : regx^=regy
0f 0x 0y : regx&=regy
10 0x 0y : regx|=regy
11 0x 0y : regx 循环右移regy
12 0x 0y : regx 循环左移regy
13 0x : 0x寄存器取反
14 0x 0y : regx 取余 regy.
D0 0x 00 00 00 : 缓冲区 第x部分(输入)拿到栈上
ff : 退出
比较麻烦的就是0x03 call操作数和0x04 ret操作数,这两个操作数居然还会保存寄存器状态和回退寄存器状态,很强。
然后整理一下,整个虚拟机的操作,翻译成python就是:
temp=input1
temp2=input2
temp3=input3
for i in range(16):
x=(15-i)%8
temp=((temp>>x)&0xffffffff|(temp<<(32-x))&0xffffffff)&0xffffffff
temp=temp^0x9e3779b9
temp=((temp<<6)&0xffffffff|(temp>>(32-6))&0xffffffff)&0xffffffff
newtemp6=(temp3&temp2)^(temp & temp2)^(temp & temp3)^(temp & temp3& temp2)^(temp | temp3| temp2)
newtemp6=newtemp6^0xffffffff
input1 input2 input3分别是输入的flag转换成16进制之后的4个字节、flag的md5转换成16进制的4个字节和flag变换后md5后的4个字节。
可以看到对输入的这三个内容进行了一些列的逻辑运算,然后得到的结果(newtemp6)就是要返回去对比的。
需要跟上文提到的result每隔4字节的四个字节对比:
解题
虽然VM部分分析完毕,但并不能直接就做出来,因为咋一看去,这个VM是三个输入,和一个输出,我们无法通过一个输出去反推三个输入,而且三个输入之间也没有线性关系(一个输入是flag,另外两个都是md5)。
但仔细分析其实并不是,因为输入的另一个内容我们已经知道了,那就是flag的md5。我们不知道的其实只有两个内容,flag变换后的md5和flag。
那么我们这时候分析一下那个变换:
可以看到这个变换就是把输入的16字节的flag进行逐字节的累加,然后对0x64取余,然后根据取余结果生成100字节的输出。接下来就是对输出进行md5了,那么也就是说最后生成的内容是一个对0x64取余的操作,那么一共也就0x64种可能啊。所以,整理一下思路:
三个输入,一个结果
一个结果和一个输入已知,一个输入有100中可能,一个输入未知并且是要求的输入
所以此题可解,我们**100种情况,然后分别反推这个输入。并且我们知道输入的内容的md5,进而可以从100种结果之中筛选出正确的结果。
还有一个难点就是,逆推这个逻辑关系:
newtemp6=(temp3&temp2)^(temp & temp2)^(temp & temp3)^(temp & temp3& temp2)^(temp | temp3| temp2)
已知newtemp6,temp2,temp3来求temp。这里我直接通过查表来进行复原,将所有可能打印出来,这里我忘记打印带取反结果的了,懒得改了,后文写代码的时候直接结果输入之前取反:
a=[1,0]
for temp in a:
for temp2 in a:
for temp3 in a:
print temp2,temp3,(temp3&temp2)^(temp & temp2)^(temp & temp3)^(temp & temp3& temp2)^(temp | temp3| temp2),temp
然后生成一个对应的表:
vmRetable={
"111":"1",
"100":"1",
"010":"1",
"001":"1",
"110":"0",
"101":"0",
"011":"0",
"000":"0"
}
对查到的结果拼接在一起之后就是前半段计算的temp:
for i in range(16):
x=(15-i)%8
temp=((temp>>x)&0xffffffff|(temp<<(32-x))&0xffffffff)&0xffffffff
temp=temp^0x9e3779b9
temp=((temp<<6)&0xffffffff|(temp>>(32-6))&0xffffffff)&0xffffffff
然后再逆这段即可。完整代码如下:
import hashlib
import binascii
result=[0xC5D83690, 0x1F7902CC, 0x978162EA, 0x2FAE3D15, 0x1932A96C,0xCEEBFE91, 0x04222669, 0xAFF6AF42] #结果数组
flagmd5='9036d8c5ea6281976ca9321969262204' #flag的md5
vmRetable={
"111":"1",
"100":"1",
"010":"1",
"001":"1",
"110":"0",
"101":"0",
"011":"0",
"000":"0"
}
def transform(input): #变换函数
output=""
for k in range(100):
a=0
input^=0xC3
output+=chr(input&0xff)
for j in range(8):
a^=((input&0xff)>>j)&1
input=(a|2*input)&0xff
m = hashlib.md5()
m.update(output)
output=m.hexdigest()
return output
def getinputbit(a,b,c): #查表复原最后胡的逻辑运算
return vmRetable[a+b+c]
def movere(temp): #复原逻辑运算前的循环位移
i=15
while i>=0:
x=(15-i)%8
temp=((temp>>6)&0xffffffff|(temp<<(32-6))&0xffffffff)&0xffffffff
temp=temp^0x9e3779b9
temp=((temp<<x)&0xffffffff|(temp>>(32-x))&0xffffffff)&0xffffffff
i-=1
return temp
def big2small(data): #大小端转换
return binascii.hexlify(binascii.unhexlify(data)[::-1])
for i in range(0x64): #循环生成变换内容
transoutput=transform(i)
flag=""
for i in range(4):
md5bin=bin(int(result[2*i]))[2:].rjust(32,'0')
transbin=bin(int(big2small(transoutput[8*i:8*i+8]),16))[2:].rjust(32,'0')
resultbin=bin(result[2*i+1]^0xffffffff)[2:].rjust(32,'0')
flagbin="" #先将三个输入转换成二进制
for j in range(32): #对二进制内容查表复原flag的位移后二进制形式
flagbin+=getinputbit(md5bin[j],transbin[j],resultbin[j])
flagtemp=movere(int(flagbin,2)) #复原flag的一部分
flagpart=""
for j in range(4): #将flag从整型转换成字符串型(好md5)
flagpart+=chr((flagtemp>>(j*8))&0xff)
flag+=flagpart
m = hashlib.md5()
m.update(flag)
flagmd5_=m.hexdigest() #对flag求md5
flagstr="flag{"
if(flagmd5_==flagmd5): #如果和真flag的md5相同则计算正确,输出
for i in range(len(flag)):
if(i==4 or i==6 or i==8 or i==10):
flagstr+='-'
flagstr+=hex(ord(flag[i]))[2:].rjust(2,'0')
flagstr+='}'
print flagstr
成功:
flag:
flag{9e573902-0e31-4837-a337-32a475ca007c}
上一篇: 【Procedure】VS开发环境 + 应用 + 组件
下一篇: .NET中Redis的使用