欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp

程序员文章站 2022-05-19 13:38:49
...

[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp


可以明显感觉到玄武组的题目难于其他三个组。这么复杂的一道题也好意思叫baby???

题目分析

拿到题目发现是一个64位windows程序,结合题目名称,怀疑是虚拟机逆向。
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
直接逆向分析:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
没有加壳,把所有函数的符号都去了,但字符串没有混淆,可以根据“Tell Me Your Flag:”字符串找到函数的主逻辑:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
向下分析可以的到flag的格式:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
flag长度42,uuid格式,并且由0-9,a-f组成,也就是小写16进制数。如:

flag{12345678-0123-5678-0123-567890123456}

接着将输入的函数去掉flag{}和-之后转换成16进制数,然后传给chkflag函数:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
注意中间的那些ifdebug函数出现了很多次,是用来检测调试的,如果调试的过程中经过了这些函数,就会退出,所以可以直接patch掉,但太多,也可以直接就断点避开这些函数。

然后查看chkflag函数:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
这个函数的逻辑是:

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

关于md5是怎么看出来的,首先点进这个函数能看到一堆跟hash有关的函数:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
百度一下就知道这是windows计算哈希用的一些东西,然后根据输入和输出的结果,首先,我们选择一个转换成16进制数之后是可显示的ascii嘛的输入作为输入,如:flag{78797878-7878-7878-7878-787878787878}

转换成16进制之后是:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
然后对比md5计算的结果:

927B94B9FE8EE835AB2A400FF4219644

可以搜到:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
注意上述步骤中的第六步,他VM计算的结果和我们输入flag的md5一起存在了result的数组里,每隔四个字节一个,那么我们是不是就知道了flag的md5了呢?没错确实我们知道了flag的md5,但并没啥用,查不出来,因为这个md5的输入并不是可见字符串,所以市面上的彩虹表都搜不出来。。。。

这是结果:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
这里每隔四个字节都是flag的md5的四个字节(转换成整数后),取出来flag的md5就是:

9036d8c5ea6281976ca9321969262204

根本搜不出来(24小时都是骗人的,我当天就没做出来,到现在写wp都快一周了):
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
老老实实的来分析一下VM吧

虚拟机分析

[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
首先将上文提到的三个数放在一个数组中,然后和opcode一起作为输入初始化虚拟机:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
整个虚拟机的虚拟处理器和内存结构如下上面所示,每一个寄存器是4个字节,也就是32位的。初始化完成之后将虚拟机结构体返回,在内存中看一下:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
值得注意的是寄存器0是用来存储当前执行到opcode第几个的:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
然后就是对opcode进行分析,找到各个操作码对应的操作。技巧在之前VM题目中已经提到过了,其实就是在执行一个opcode对应的函数之前和之后,记录并对比寄存器和栈的状态,大部分操作数都可以分析得出。可能有一些比如位移和异或不太容易看出,也可以直接进入到函数中,查看关键操作,比如这道题目里的0x11操作数,输入输出如下:

78787878,07 -> f0f0f0f0 

我最开始还以为是每个字节分别异或0x07,结果正好是0xf0f0f0f0,但后来我发现不对,进入这个函数之后发现了IDA的ROR4宏,也就是循环右移:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
所以这个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字节的四个字节对比:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp

解题

虽然VM部分分析完毕,但并不能直接就做出来,因为咋一看去,这个VM是三个输入,和一个输出,我们无法通过一个输出去反推三个输入,而且三个输入之间也没有线性关系(一个输入是flag,另外两个都是md5)。

但仔细分析其实并不是,因为输入的另一个内容我们已经知道了,那就是flag的md5。我们不知道的其实只有两个内容,flag变换后的md5和flag。

那么我们这时候分析一下那个变换:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
可以看到这个变换就是把输入的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

[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
然后生成一个对应的表:

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

成功:
[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp
flag:

flag{9e573902-0e31-4837-a337-32a475ca007c}

[re]复杂VM逆向:2020网鼎杯玄武组re baby_vm wp