buu [GKCTF2020]EzMachine wp

今天终于沉下心看了一道vm逆向题了...wp我尽量写的详细一点qaq

Part1

先拖进ida看一下,发现找不到什么关键函数,搜索字符串也搜不出什么。静态不大行我们就动调试试。

第一种

调试发现程序跳转到0x9B1595,和0x009B1594这一步对比发现0x009B1594会跑飞,所以初步判定它是混淆指令,到ida里把它nop掉。

其实nop掉的目的就是把0x9B1595这里创建函数然后反汇编,这样会更好分析一点。

直接nop肯定是不行的,会破坏掉0x9B1595这里的代码,要先ida动调然后专门修改0x9B1594这里的汇编。(我是这样一个思路)但是我的ida在local windows debugger的时候总是会崩溃,根本调不出...然后我就放弃了....

其实这一步做不做都可以的,在0x9B1595这里创建一个函数,然后反汇编会得到这样的结果,就是更直接的找到byte_4449A0也就是opcode...(不过搞不出的话看汇编慢慢摸索也行的啦,毕竟opcode很长,还是蛮好找的....比如下面的方法qaq)

第二种

当我们调试的时候发现主函数在0x009B15E5这里,

在这里打下断点发现,每次走到这里跳转的函数都不太一样,有0x009B1300,0x009B1000等等,所以我们可以初步判断这个虚拟机是有指令集的,然后我们去ida里(地址选择0x009B1300,到下一级调用)就可以发现一串字节码byte_4449A2。

指令集:(off_4448F4)

off_4448F4处保存各个opcode对应函数的地址,
byte_4449A0处为虚拟机代码起始地址,
dword_445BD8为ip(pc),
off_4427FC处保存有四个寄存器的地址,

dword_445BAC为栈的起始地址,
dword_445BC8为esp;

然后就是复现这些指令集,知道每一个是做什么的。进到每一个指令集f5观察具体的逻辑结构。


Part2

0 nop

1 mov

2 push

3 ??(pushr)

4 pop

5 caseprint(puts.cpp)

6 add

7 sub

8 mul

9 div

10 xor

11 jmp

12 subcmp

13 je

14 jne

15 jg(大于则转移)

16 jl(小于则转移)

17 input(gets.cpp)

18 ??(clr)

19 LoadStack //

20 LoadString //

0xFF quit / end

(有些逻辑差不多的就不贴代码上来了,??是我也不太清楚的,//是不算太理解的)
(温馨提示:如果f5看不懂可以动调看看逻辑)


以上就是我们分析的指令集了,然后写一个脚本将难懂的字节码换成刚刚分析出的伪汇编:
(内附详细注释)

#字节码
code =[
  0x01, 0x03, 0x03, 0x05, 0x00, 0x00, 0x11, 0x00, 0x00, 0x01, 
  0x01, 0x11, 0x0C, 0x00, 0x01, 0x0D, 0x0A, 0x00, 0x01, 0x03, 
  0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x01, 0x02, 0x00, 
  0x01, 0x00, 0x11, 0x0C, 0x00, 0x02, 0x0D, 0x2B, 0x00, 0x14, 
  0x00, 0x02, 0x01, 0x01, 0x61, 0x0C, 0x00, 0x01, 0x10, 0x1A, 
  0x00, 0x01, 0x01, 0x7A, 0x0C, 0x00, 0x01, 0x0F, 0x1A, 0x00, 
  0x01, 0x01, 0x47, 0x0A, 0x00, 0x01, 0x01, 0x01, 0x01, 0x06, 
  0x00, 0x01, 0x0B, 0x24, 0x00, 0x01, 0x01, 0x41, 0x0C, 0x00, 
  0x01, 0x10, 0x24, 0x00, 0x01, 0x01, 0x5A, 0x0C, 0x00, 0x01, 
  0x0F, 0x24, 0x00, 0x01, 0x01, 0x4B, 0x0A, 0x00, 0x01, 0x01, 
  0x01, 0x01, 0x07, 0x00, 0x01, 0x01, 0x01, 0x10, 0x09, 0x00, 
  0x01, 0x03, 0x01, 0x00, 0x03, 0x00, 0x00, 0x01, 0x01, 0x01, 
  0x06, 0x02, 0x01, 0x0B, 0x0B, 0x00, 0x02, 0x07, 0x00, 0x02, 
  0x0D, 0x00, 0x02, 0x00, 0x00, 0x02, 0x05, 0x00, 0x02, 0x01, 
  0x00, 0x02, 0x0C, 0x00, 0x02, 0x01, 0x00, 0x02, 0x00, 0x00, 
  0x02, 0x00, 0x00, 0x02, 0x0D, 0x00, 0x02, 0x05, 0x00, 0x02, 
  0x0F, 0x00, 0x02, 0x00, 0x00, 0x02, 0x09, 0x00, 0x02, 0x05, 
  0x00, 0x02, 0x0F, 0x00, 0x02, 0x03, 0x00, 0x02, 0x00, 0x00, 
  0x02, 0x02, 0x00, 0x02, 0x05, 0x00, 0x02, 0x03, 0x00, 0x02, 
  0x03, 0x00, 0x02, 0x01, 0x00, 0x02, 0x07, 0x00, 0x02, 0x07, 
  0x00, 0x02, 0x0B, 0x00, 0x02, 0x02, 0x00, 0x02, 0x01, 0x00, 
  0x02, 0x02, 0x00, 0x02, 0x07, 0x00, 0x02, 0x02, 0x00, 0x02, 
  0x0C, 0x00, 0x02, 0x02, 0x00, 0x02, 0x02, 0x00, 0x01, 0x02, 
  0x01, 0x13, 0x01, 0x02, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x01, 
  0x0E, 0x5B, 0x00, 0x01, 0x01, 0x22, 0x0C, 0x02, 0x01, 0x0D, 
  0x59, 0x00, 0x01, 0x01, 0x01, 0x06, 0x02, 0x01, 0x0B, 0x4E, 
  0x00, 0x01, 0x03, 0x00, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 
  0x01, 0x03, 0x01, 0x05, 0x00, 0x00, 0xFF, 0x00, 0x00, 0x00
]
#指令集
opcodekey = {0:'pcadd',1:'mov',2:'push',3:'?',4:'pop',5:'caseprint',6:'add',7:'sub1',8:'mul',9:'div',10:'xor',11:'jmp',12:'subcmp',13:'jedx4',14:'jnedx4',15:'jedx4orlow',16:'jedx4orhight',17:'input',18:'?2',19:'LoadStack',20:'LoadString',0xFF:'end'}

#B为字节码的下标
B=0
#C为每一行的序号
C=1
print(str(C)+": ",end="")
#字节码三个一组,每三个的第一个是指令,后两个为操作数
#i为字节码的遍历
for i in code:
   #print(hex(i)+" ",end="")
    if B==0:
        #print(i)
        op=opcodekey[i]
        #opcodekey【每三个的第一个字节码】所对应的指令
        print(op,end=' ')
    else:
        print(i,end=" ")
    B=B+1
    #每三个转下一行,也就是下一个汇编
    if B==3:
        B=0
        C=C+1
        print()
        print(str(C)+": ",end="")

Part3

输出的伪汇编:
(详细注释,#是已经理解的,//是还不算太懂的)

1: mov 3 3 
2: caseprint 0 0 
3: input 0 0            #输入flag
4: mov 1 17             #将17放进寄存器【1】中
5: subcmp 0 1           #比较flag长度是否为17  
6: jedx4 10 0           #跳转至10:执行  
7: mov 3 1 
8: caseprint 0 0        #不等于17则报错且退出 
9: end 0 0 
10: mov 2 0 
11: mov 0 17 
12: subcmp 0 2 
13: jedx4 43 0          #压栈操作(转到43:)
14: LoadString 0 2 
15: mov 1 97            #将ASCII97('a')放入寄存器【1】
16: subcmp 0 1          #寄存器【0】与'a'比较   
17: jedx4orhight 26 0   #如果小于则jmp26:
18: mov 1 122           #将ASCII122('z')放入寄存器【1】
19: subcmp 0 1 
20: jedx4orlow 26 0     #如果大于则jmp26:
21: mov 1 71            #如果输入在'a'-'z'  将71存入寄存器【1】
22: xor 0 1             #将输入[i] ^ 71
23: mov 1 1             
24: add 0 1             # +1 
25: jmp 36 0            #跳转到36:
#以上为小写字符第一次加密,异或71然后+1

26: mov 1 65            #将ASCII65('A')放入寄存器【1】
27: subcmp 0 1          #判断是否为大写字符
28: jedx4orhight 36 0   #小于则跳转36:
29: mov 1 90            #将ASCII97('Z')放入寄存器【1】
30: subcmp 0 1          #判断是否为大写字符  
31: jedx4orlow 36 0     #大于则跳转36:
32: mov 1 75            #如果是'A'-'Z'  将75存入寄存器【1】
33: xor 0 1             #将输入[i] ^ 75
34: mov 1 1 
35: sub1 0 1            # -1
#以上为大写字符第一次加密,异或75然后-1

36: mov 1 16            #将16存入寄存器【1】中
37: div 0 1             #将处理好的输入 /16
38: ?push2 1 0          //       
39: ?push2 0 0          //这两步是将个位和十位分别放入堆栈2
40: mov 1 1 
41: add 2 1             //ebx【2】负责计数并且指向二号堆栈顶
42: jmp 11 0 

#压栈操作
43: push 7 0 
44: push 13 0 
45: push 0 0 
46: push 5 0 
47: push 1 0 
48: push 12 0 
49: push 1 0 
50: push 0 0 
51: push 0 0 
52: push 13 0 
53: push 5 0 
54: push 15 0 
55: push 0 0 
56: push 9 0 
57: push 5 0 
58: push 15 0 
59: push 3 0 
60: push 0 0 
61: push 2 0 
62: push 5 0 
63: push 3 0 
64: push 3 0 
65: push 1 0 
66: push 7 0 
67: push 7 0 
68: push 11 0 
69: push 2 0 
70: push 1 0 
71: push 2 0 
72: push 7 0 
73: push 2 0 
74: push 12 0 
75: push 2 0 
76: push 2 0             #flag加密后的代码

77: mov 2 1 
78: LoadStack 1 2        //加载堆栈 一次弹出两个数值 放入eax与ebx
79: pop 0 0 
80: subcmp 0 1           #【0】寄存器与【1】寄存器不相等  则跳转
81: jnedx4 91 0          //此处为关键判断,判断失败则跳出循环
82: mov 1 34             #将34存入寄存器【1】
83: subcmp 2 1           //控制跳转,观察ebx2是否已经弹出34个值  
84: jedx4 89 0           //如果是则跳转成功,不是则继续循环
85: mov 1 1              //eax1 = 1
86: add 2 1              //ebx2 + 1
87: jmp 78 0             #jump循环
88: mov 3 0 
89: caseprint 0 0 
90: end 0 0 
91: mov 3 1 
92: caseprint 0 0 
93: end 0 0 
94: pcadd 

整个程序的逻辑大概是这样的:(开了两个栈)
首先判断你输入的是不是17位,不是直接跳转报错,是的话进行判断。
如果是小写字母,那就xor71+1,最后除以16,将商和余数压栈。(即十位和个位)
如果是大写字母,那就xor75-1,最后除以16,将商和余数压栈。(即十位和个位)
其余的直接除16压栈。
然后压入对照数据,最后弹栈比较。

Final exp

上最终脚本(内附详细注释):

array = [0x7,0xd,0x0,0x5,0x1,0xc,0x1,0x0,0x0,0xd,0x5,0xf,0x0,0x9,0x5,0xf,0x3,0x0,0x2,0x5,0x3,0x3,0x1,0x7,0x7,0xb,0x2,0x1,0x2,0x7,0x2,0xc,0x2,0x2,]
#注意是压栈,先进后出,所以要[::-1]
array = array[::-1] 
#先输出的是个位然后是十位,两个一组
for i in range(0, len(array), 2): 
    c = array[i] + array[i+1]*16 
    tmp = (c-1) ^ 71 
    #如果是小写字母
    if tmp >= ord('a') and tmp <= ord('z'): 
        print(chr(tmp), end = "") 
        continue 
    tmp = (c+1) ^ 75 
    如果是大写字母
    if tmp >= ord('A') and tmp <= ord('Z'): 
        print(chr(tmp), end = "") 
        continue
    #都不是 
    print(chr(c), end = "") 

GET FLAG

最后拿到flag!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值