今天终于沉下心看了一道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!