[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函数:
在这里插入图片描述
这个函数的逻辑是:

  1. 先对输入(flag去掉头尾和-之后转换成16进制数之后)进行md5
  2. 然后对flag进行某种变换(之后在分析)
  3. 对变换结果进行md5
  4. 分别取输入(flag去掉头尾和-之后转换成16进制数之后)、输入的md5、输入的变换的md5的依次4个字节转换成整数
  5. 将上述三个整数进入VM虚拟机进行计算,一共四轮
  6. 每一轮的计算结果(4字节)和输入的md5的4字节一起存入一个数组,然后和一个全局变量校验
  7. 返回校验结果,如果相同则通过,输出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}

在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章