题目要求
VMP下载链接:https://cloud.icespite.top/s/o2S4
分析ELF文件VMP:
1) 在IDA中观察,会发现程序的主体特别简单:一个大switch语句。稍加分析你就会发现,switch语句的每一个case好像都在处理一条“语句”。结合进一步的上网搜索,你会了解到这是一种虚拟机加壳手段。请分析该虚拟机的指令集(10%)
2) 了解了虚拟机的指令集后,你会发现,你还是无法直接阅读二进制的“代码”。那么一种比较好的方式或许是为它开发一个“反汇编器”,反汇编到你定义的可读格式。请为该指令格式设计一个反汇编器(40%)
3) 在完成了2之后,你终于可以得到一份可读的“代码”了。但是,如果能够运行并调试这段代码,那么就更好了。稍加思索你会发现,既然我们已经有了一个反汇编器,那么稍加修改我们就可以将它变成一个解释器/调试器,唯一的区别只在于:维护“运行时”的状态来进行分支选择,还是静态扫描,遇到分支的时候分别进行反汇编。修改你的反汇编器,设计一个解释器/调试器(40%)
4) 现在有了强大的的工具的帮助,你可以很方便地进行“程序”的调试与分析了。请分析出使用什么输入可以让程序输出Congratulations!(10%)
解题思路
格式文档
v14:flag标志位
v11:程序计数器
byte_804C0C0[v10]:模拟堆栈
v9:模拟内存
byte_804C0C0: 控制命令,相当于代码文件
funcs_8049C54:包含了取值和打印等函数
0:下一条指令
1:弹出堆栈,v11 = byte_804C0C0[v10](堆栈内下一个变量),猜测通过这条指令可以模拟实现jmp到指定地址
2:add指令
3:sub指令
4:imul指令
5:v9[byte_804C0C0[v11 + 2]] = (v16 >> 32) / v17
v9[v16] = (v16 >> 32) % v17
v14 = (v16 >> 32) / v17
6:v9[byte_804C0C0[v11 + 2]] = v14 = v17 ^ v16
7:v9[byte_804C0C0[v11 + 6]] = v14 = - v9[byte_804C0C0[v11 + 2]]
8:v9[byte_804C0C0[v11 + 6]] = v14 = ~ v9[byte_804C0C0[v11 + 2]]
9:v9[byte_804C0C0[v11 + 2]] = v14 = v17 & v16
A:v9[byte_804C0C0[v11 + 2]] = v14 = v17 | v16
B:v9[byte_804C0C0[v11 + 6]] = v14 = ( v16 == 0 )
C:v9[byte_804C0C0[v11 + 2]] = v14 = v16 << v17
D:v9[byte_804C0C0[v11 + 2]] = v14 = v16 >> v17
E:v11=byte_804C0C0[v11 + 1]
F:声明变量;变量值为 v11 + 5
10:jz指令;如果v14不等于0:v11 = v11 + 5,否则v11 = byte_804C0C0[v11+1]
11:js指令;如果v14小于0:v11 = byte_804C0C0[v11+1],否则v11 = v11 + 5
12:jle指令;如果v14小于等于0:v11 = v11 + 5,否则v11 = byte_804C0C0[v11+1]
13:jg指令;如果v14大于0:v11 = v11 + 5,否则v11 = byte_804C0C0[v11+1]
14:jns指令;如果v14大于等于0:v11 = v11 + 5,否则v11 = byte_804C0C0[v11+1]
15:jnz指令;如果v14等于0:v11 = v11 + 5,否则v11 = byte_804C0C0[v11+1]
16:v14 = v16 & v17
17:v14 = v16 - v17
18:mov指令; v9[byte_804C0C0[v11 + 2]] = byte_804C0C0[v11+3];
19:++v9[byte_804C0C0[v11 + 1]];
1A:--v9[byte_804C0C0[v11 + 1]];
1B:v9[byte_804C0C0[v11 + 4]] = byte_804C0C0[v9[byte_804C0C0[v11 + 5]]]
v14 = v9[byte_804C0C0[v11 + 4]];
1C:byte_804C0C0[v9[byte_804C0C0[v11 + 4]]] = v9[byte_804C0C0[v11 + 5]];
v14 = v9[byte_804C0C0[v11 + 5]]
1E:声明变量
1F:取变量;v9[byte_804C0C0[v11 + 1]] = byte_804C0C0[v10];
20:调用函数
default:下一条指令;v11++
分析
第一问
结合第一题题目要求,switch语句的每一个case好像都在处理一条“语句”,可以推测每一个switch语句应该是完成了一小部分功能。
把ida反汇编出的代码复制出来。
switch的判断语句是v11 + 0x804C0C0
,也就是byte_804C0C0[v11]
,并且switch语句中大量的出现判断byte_804C0C0[v11 + 1]
的值来进行不同的操作,所以推测byte_804C0C0
就是实际的控制数组,也就是所谓的“源代码”。
以 case 4
为例:
灰色的部分多次重复出现,且按照上面的分析应该是完成指令的解析工作,而在下方则是一个简单的v16*v17
,所以推测该条完成了乘法的功能。并且结合完成判断功能的case语句,如下图:
可以推断 v14
是flag标志位。
对byte_804C0C0
取值的不仅仅有v11
,还有v10
,而v11
和v10
的初始值相差非常之大,
而可以看出只对v10
进行了+4或-4的操作,
可以推测,byte_804C0C0[v10]
模拟了堆栈,而结合动态调试的信息,推测用v9
模拟内存、用funcs_8049C54
完成了取值和打印等操作。
funcs_8049C54
:
第二三问
第一问解决之后就是控制流平坦化的展开问题,这样控制流平坦化之后很难看出程序原有的逻辑,而有些语句中对v11
的值是经过其他部分计算得到的,无法仅仅依赖解析byte_804C0C0
来完成逻辑的还原,因而用脚本模拟这些case
语句,还原执行逻辑。
由于byte_804C0C0
和v9
可能在进入switch
语句之前被其他函数进行过操作,所以利用动态调试,在执行到switch
时dump出二者的内存,并在脚本中加载,帮助恢复执行。
并且可以在模拟测试时对比值和case流程比对来判断模拟过程是否正确。
第四问
将模拟得到的过程语句提出来进行分析。
上图是提示输入密码
上图则说明程序是逐个字符进行输入处理
提取程序的共同的部分,对比程序差异部分,可以看到程序会逐个字符判断是不是回车,然后在得到回车后判断输入长度是不是30
可以整理出对比的逻辑:逐个字符对输入进行异或,并和某个值比对,如果有一条没有比对成功,赋值v9[1]
等于1,如果最后v9[1]
不等于1则输入正确,否则失败。
那么可以根据输出的逻辑,全局搜索^
,提取出每个字符要异或和要比对的值。
得到脚本:
xors = [[253, 14, 99, 79 ] for i in range(8)]
xorss = []
for i in xors:
for j in i:
xorss.append(j)
xorss = xorss[:30]
simple = [141, 111, 0, 36, 152,124,16,16,156,96,7,16,139,99,16,16,156,96,7,16,133,97,17,60,162,97,11,16,144,119]
for i in range(30):
index = ord("0")
while index < ord("z"):
if index ^ xorss[i] == simple[i]:
print(chr(index),end="")
break
index += 1
# result: packers_and_vms_and_xors_oh_my
验证无误: