请注意,本文编写于 1387 天前,最后修改于 1387 天前,其中某些信息可能已经过时。
这是一道VM虚拟机的题目,我花了三天的晚上终于解决掉了。这周还有操作系统的考试,还没有怎么复习呢,祝我好运吧!昨天,准确的来说是前天,子洋师傅给了我一道Creakme,是今年看雪Creakme大赛上的题目。具体是关于js下混淆加密的exe文件的破解。下一个目标应该就是这个了。说回题目,这道题目与网鼎杯的那道题目相比还是稍微逊色一点的,但这不妨碍恶心了我三天的晚上。今天下午计算计网络课上,生动的给群里的各位大老们演示了一下啥叫和老师的udp不可靠传输(指趴在桌子上睡死了),最关键的还被丢到群里面公开处刑了,awsl(American wrestling superstars live)。又跑题了,我准备将这道题目分成三段来叙述,每一段对应的都是个不眠夜哈!
我们拿到题目,运行一下,出现input输入,拖入IDA分析,没有正确的定位到主函数,因为太乱了,并且没有找到有用的字符串。
静态不行我们就上动态调试,32位程序我们直接上OD。由于字符串很乱,没有办法通过字符串定位到相应的位置,所以我们采用单步的方法,遇到call函数就步过,如果程序跑飞了我们打下断点,下次在此处步入。通过两三次的跳转我们成功的定位到了一个拥有很多case的函数内部。
打下断点,控制跳转,通过每个case,发现输出了“plz input”“wrong”“hack”等字符串,由于知道这是个虚拟机的题目,所以很快就能反应过来这应该是一个指令,用于打印的指令。所以调用这个指令的地方应该就是整个虚拟机的主函数。返回上级代码,我们看见整个call的结构。
在这个call处打下断点,运行,每次击中断点的时候,call的函数都不一样如0x931000等,所以初步判断这个虚拟机可定是拥有一个指令集的,并且不会像入门的vm那么简单,他一定拥有寄存器等部件。毕竟指令流程还是蛮复杂的,步入第一个调用函数。
仔细看看,观察所有的内存地址,与数值的存取内存地址。我们很容易地找到了一串字节码,很长。
unsigned char ida_chars[] =
{
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
};
IDA导出以做备用,这个不出意外就是我们的code。在OD中确定位置,IDA中静态结合寻找地址,我们可以确定寄存器的位置。
并且在这个指令中,edx被取出来之后进行了add操作,并且执行完之后被放入了原地址。在后续分析流程控制的时候每个函数基本上都对这个地址上的数字进行了操作,并且放回了原位置。所以这应该是一个pc程序计数器,负责存取指令,在IDA中做出标记。
回到函数,在IDA中进行x交叉引用的查找,找到了一个list,程序的对照表。
到这里,整个vm大体的样貌就已经描绘出来了,首先这是一个有寄存器,pc指令计数器,自己专有指令集的虚拟机。并且在虚拟机中自己有一个程序(code字节码)。我们输入flag后,虚拟机会调用code,进行加密解密,最终输出相应的结果。
由于整体逻辑我在第一天已经掌握了,所以第二天的任务就是复现指令集。进入list,对每个指令进行静态分析(F5看不出来的时候建议配合OD动态调试梳理逻辑)由于指令还是蛮多的,我这里只主要做几个指令,其余的原理基本相同。
如果仔细观察的话,在code码处有两个名字,他们之间的偏移量为1,所以这里的的add指令是三字节指令,将两个寄存器相加,送入到第一个寄存器当中。
逻辑清晰,将第二个操作码所指的寄存器里面的值放入第一个操作码中。
这是一个跳转函数,使用if控制流程,对pc进行加减操作。
有一说一所有指令中居然有四五个跳转指令,就很有趣。其实仔细想想这应该算是降低了难度,使用一个指令代替了好几个指令,让程序好理解了很多。其余的函数原理差不多,耐心看看就明白了。
写出脚本,拿自己的伪汇编替换难读的字节码。
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=0
C=1
print(str(C)+": ",end="")
for i in code:
#print(hex(i)+" ",end="")
if B==0:
#print(i)
op=opcodekey[i]
print(op,end=' ')
else:
print(i,end=" ")
B=B+1
if B==3:
B=0
C=C+1
print()
print(str(C)+": ",end="")
这里就很简单了,对着我们逆向出来的字节码分析分析。结合我写的注释可以稍微看一下。
1: mov 3 3
2: caseprint 0 0
3: input 0 0 #输入falg
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 #《《------------循环终止判断
14: LoadString 0 2
15: mov 1 97 #将ASCII码97(a)放入寄存器【1】
16: subcmp 0 1 #寄存器【0】中的值与a比较
17: jedx4orhight 26 0 #如果小于 jmp 26
18: mov 1 122 #‘z’
19: subcmp 0 1
20: jedx4orlow 26 0 #大于z跳转
21: mov 1 71 #如果输入在‘a’<[i]<'z' 将71放入寄存器【1】
22: xor 0 1 #将[i]^71
23: mov 1 1 #
24: add 0 1 #+1
25: jmp 36 0 #跳转36
#此处为小写字符第一次加密,与71相与并且加一
26: mov 1 65 #A
27: subcmp 0 1 #此处判断是否属于大写字符
28: jedx4orhight 36 0
29: mov 1 90 #Z
30: subcmp 0 1
31: jedx4orlow 36 0
32: mov 1 75 #大写字符相与75相与
33: xor 0 1
34: mov 1 1
35: sub1 0 1 #-1
#此处为大写字符第一次加密,与75相与并且减一
36: mov 1 16 #寄存器【1】放入16
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 #应该是falg加密后的代码
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 #==》》此处为一个jmp的循环
88: mov 3 0
89: caseprint 0 0
90: end 0 0
91: mov 3 1
92: caseprint 0 0
93: end 0 0
整个程序大概是这样的,首先判断你输入的是不是17位,不是直接跳转报错。是的话进行判断,如果是小写字母,那就xor71+1,最后除以16,将商和余数压栈。如果是大写字母,那就xor75-1,最后除以16,将商和余数压栈。其余的直接除16压栈。然后压入对照数据,最后弹栈比较。
A=[7,13,0,5,1,12,1,0,0,13,5,15,0,9,5,15,3,0,2,5,3,3,1,7,7,11,2,1,2,7,2,12,2,2]
B=0
C=A[::-1]
sum1=0
for i in C:
if B==1:
sum1=sum1+i*16
print(sum1,end=",")
sum1=0
B=0
else:
sum1=sum1+i
B=B+1
D=[34,44,39,33,123,23,51,37,48,95,9,95,13,16,28,5,125]
print()
for i in D:
if ord('a')<=((i-1)^71) and ord('z')>=((i-1)^71):
print(chr((i-1)^71),end="")
elif ord('A')<=((i+1)^75) and ord('Z')>=((i+1)^75):
print(chr((i+1)^75),end="")
else:
print(chr(i),end="")
全部评论 (共 2 条评论)