thumbnail
恶意软件分析VMP题分析

题目要求

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,而v11v10的初始值相差非常之大,

而可以看出只对v10进行了+4或-4的操作,

可以推测,byte_804C0C0[v10]模拟了堆栈,而结合动态调试的信息,推测用v9模拟内存、用funcs_8049C54完成了取值和打印等操作。

funcs_8049C54:

第二三问

第一问解决之后就是控制流平坦化的展开问题,这样控制流平坦化之后很难看出程序原有的逻辑,而有些语句中对v11的值是经过其他部分计算得到的,无法仅仅依赖解析byte_804C0C0来完成逻辑的还原,因而用脚本模拟这些case语句,还原执行逻辑。

由于byte_804C0C0v9可能在进入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

验证无误:

暂无评论

发送评论 编辑评论


|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇