由于大神推荐加上无聊,准备开坑学习一下miasm。网上相关资料并不算很多,而且很多脚本和api都是python2的,我用的python3没法直接跑那些脚本,有点难绷,所以开个博客记录一下各种问题,顺便留个档。miasm版本windows是0.1.5,Ubuntu是0.1.3.操作系统环境是windows和Ubuntu,ide是pycharm和vscode+remote。

初探

由于是初学,翻了一下官方的网站找到一个博客

https://miasm.re/blog/2016/01/27/re150.html

跟着学一学

首先用ida打开示例的程序

image-20240416222002291

image-20240416222025443

发现其start处的汇编代码是一个非常明显的smc。我们这里用miasm进行分析,不直接调试了。

注意,因为需要用到jit,而windows似乎用不了,所以此处转Ubuntu。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import os
from miasm.analysis.sandbox import Sandbox_Linux_x86_64

# Insert here user defined methods

# Parse arguments
parser = Sandbox_Linux_x86_64.parser(description="ELF sandboxer")
parser.add_argument("filename", help="ELF Filename")
options = parser.parse_args()
# Create sandbox
sb = Sandbox_Linux_x86_64(options.filename, options, globals())

# Run
sb.run()

用如下命令行执行,动态模拟smc的汇编代码

1
python3 t1.py -b reverseMe > log.txt

image-20240416223100137

可以看到跑到最后报错了,报错的原因如下

1
2
assert(self.get_exception() == 0)
AssertionError

可以看到是这里抛出了某个异常才中止了。我们可以看到log.txt里最后一行是INT 0x80,因此我们可以得到报错的原因就是这个INT 0x80。

因为这是动态模拟的,所以跑到报错的位置时内存应该是已经解密的状态,我们通过python加个-i参数,在报错后通过交互将内存dump出来分析。

1
python3 -i t1.py -b reverseMe

报错后在终端窗口交互,输入sb.jitter.vm

image-20240416224110890

可以看到程序加载的位置和size,直接dump出来

1
open("dump.bin", "wb").write(sb.jitter.vm.get_mem(0x8048000, 0x4000))

image-20240416224347338

确实解密了,但是代码完全看不得。

image-20240416224425868

我们拥有dump.bin后,可以用miasm进行静态分析。此时就可以使用windows来进行脚本编写和测试了。

(注意此处版本是0.1.5)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.locationdb import LocationDB

loc_db = LocationDB()

cont = Container.from_stream(open("dump.bin","rb"), loc_db=loc_db)
# Bin stream is a view on the mapped binary
bin_stream = cont.bin_stream

# 'cont.arch' is "x86_32", extracted from the ELF header
machine = Machine(cont.arch)
# Disassembly engine associated with the current binary
mdis = machine.dis_engine(bin_stream, loc_db=loc_db)
# Disassemble the main function
blocks = mdis.dis_multiblock(cont.entry_point)
# Get back the CFG as a dot file
open("cfg.dot", "w").write(blocks.dot())

运行完后用graphviz生成一下svg图

1
dot -Tsvg -Goverlap=prism cfg.dot -o output.svg -v

image-20240416225307996

接下来我们开始着手去除这一大坨smc的代码块,还原真实的有用的代码

识别smc基本块

通过观察可得知,这段smc基本块结构比较统一。首先是一个push开头的基本块将寄存器push进栈保存起来后,将需要smc的地址赋值给寄存器。随后就是xor开头的基本块,专门进行代码解密,而且有指向自己和下一个基本块的出度,也是一个需要注意的点。最后就是一个pop恢复寄存器,然后跳转的操作。由于模式基本是一样的,所以我们可以通过构造匹配这种模式的matcher来识别这类基本块并直接remove掉这种模式的基本块组。剩下的不就是真正有用的代码了吗?非常酷炫。

image-20240416225507084

这种匹配用到了miasm2.core.graph库,也是我学的时候感觉非常神奇好玩的一个库,如代码所示(经过修改)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59

from miasm.core.locationdb import LocationDB
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.graph import MatchGraphJoker, DiGraphSimplifier
from miasm.expression.expression import LocKey
from miasm.core.asmblock import AsmCFG

block_list = []
loc_db = LocationDB()
cont = Container.from_stream(open("dump.bin", "rb"),loc_db=loc_db)
bin_stream = cont.bin_stream
machine = Machine(cont.arch)

# Pattern we want to match:
# |
# +----v----+
# | (dad) |
# | PUSH |
# | MOV |
# +----+----+
# |
# +----v----+
# | (middle)|<---+
# +----+--+-+ |
# | +------+
# +----v----+
# | (end) |
# +----+----+
# |
# v

def filt_func(graph: AsmCFG, node: LocKey):
global block_list
blocks = graph.blocks
for block in blocks:
if len(block.lines) == 2 and block.lines[0].name == "PUSH" and block.lines[1].name == "MOV":
if block not in block_list:
block_list.append(block)
return block_list


dad = MatchGraphJoker(name="dad", restrict_in=False,
filt=filt_func)

middle = MatchGraphJoker(name="middle")
end = MatchGraphJoker(name="end", restrict_out=False)

matcher = dad >> middle >> middle >> end
print(matcher)
open("to_match.dot", "w").write(matcher.dot())

# dgs = DiGraphSimplifier()
# dgs.enable_passes([block_merge])
# block_after = dgs(blocks)
# # print(block_after)
# open("cfg_after.dot", "w").write(block_after.dot())


直接看代码可能会奇怪这个matcher是怎么个事儿,我们看看这个类的介绍

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MatchGraphJoker(object):

"""MatchGraphJoker are joker nodes of MatchGraph, that is to say nodes which
stand for any node. Restrictions can be added to jokers.

If j1, j2 and j3 are MatchGraphJoker, one can quickly build a matcher for
the pattern:
|
+----v----+
| (j1) |
+----+----+
|
+----v----+
| (j2) |<---+
+----+--+-+ |
| +------+
+----v----+
| (j3) |
+----+----+
|
v
Using:
>>> matcher = j1 >> j2 >> j3
>>> matcher += j2 >> j2
Or:
>>> matcher = j1 >> j2 >> j2 >> j3

"""

是不是非常的一目了然。这就得夸夸miasm的源码了,至少源码的注释还是比较全的。

这个刚好和咱们的这个smc的基本块的模式很相像。首先我们初始化一个dad基本块,叫啥都行,原文是dad。可以看到这个类的参数如下

1
2
3
4
5
6
7
8
9
10
11
def __init__(self, restrict_in=True, restrict_out=True, filt=None,
name=None):
"""Instantiate a MatchGraphJoker, with restrictions
@restrict_in: (optional) if set, the number of predecessors of the
matched node must be the same than the joker node in the
associated MatchGraph
@restrict_out: (optional) counterpart of @restrict_in for successors
@filt: (optional) function(graph, node) -> boolean for filtering
candidate node
@name: (optional) helper for displaying the current joker
"""

restrict_in限制的是当前的joker节点的匹配的前驱数量,如果这个参数设置为True,则匹配节点的前驱数量必须与关联的Joker节点在MatchGragh中的前驱数量相同。

假设是上面的情况,dad >> middle >> middle >> end,如果middle的该参数设置为True(事实也确实如此),则匹配的节点的中间的这个milddle的入度必须是2才会匹配上。而由于dad和end,前者对入度没要求,后者对出度没要求,所以需要手动设置为False。

随后就是加入更多的匹配条件。在dad的参数filt中加入约束函数filt_func。匹配的就是dad的第一个和第二个汇编的类型和dad的总汇编行数(虽然感觉这个没必要,如果之后用的话)

最后将match的dot打印出来并生成svg

image-20240416232429938

非常帅的生成了。一模一样。

去除smc基本块

直接上最后的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
import os
from miasm.analysis.sandbox import Sandbox_Linux_x86_32
from miasm.core.locationdb import LocationDB
from miasm.core.types import Str, set_allocator
from miasm.os_dep.common import heap
from miasm.jitter.csts import PAGE_READ, PAGE_WRITE, PAGE_EXEC
from miasm.analysis.binary import Container
from miasm.analysis.machine import Machine
from miasm.core.graph import MatchGraphJoker, DiGraphSimplifier
from miasm.expression.expression import LocKey
from miasm.core.asmblock import AsmCFG
from miasm.analysis.simplifier import IRCFGSimplifierSSA

block_list = []
if_print = False
loc_db = LocationDB()
cont = Container.from_stream(open("dump.bin", "rb"),loc_db=loc_db)
bin_stream = cont.bin_stream
machine = Machine(cont.arch)

mdis = machine.dis_engine(bin_stream, dont_dis=[0x8049215],loc_db=loc_db)
blocks = mdis.dis_multiblock(cont.entry_point)
open("cfg.dot", "w").write(blocks.dot())

def delete_items_by_value(d, value):
return {k: v for k, v in d.items() if v != value}

def block_merge(dgs, graph: AsmCFG):
global block_list
for sol in matcher.match(graph):
print(sol)
successors = graph.successors(sol[end])
for pred in graph.predecessors(sol[dad]):
for succ in successors:
graph.add_edge(pred, succ, graph.edges2constraint[(pred, sol[dad])])
for node in sol.values():
graph.del_node(node)


def filt_func(graph: AsmCFG, node: LocKey):
global if_print, block_list
# print(f"node ==> {node}")
blocks = graph.blocks
for block in blocks:
if len(block.lines) == 2 and block.lines[0].name == "PUSH" and block.lines[1].name == "MOV":
if block not in block_list:
block_list.append(block)
return block_list

dad = MatchGraphJoker(name="dad", restrict_in=False,
filt=filt_func)

middle = MatchGraphJoker(name="middle")
end = MatchGraphJoker(name="end", restrict_out=False)

matcher = dad >> middle >> middle >> end

open("to_match.dot", "w").write(matcher.dot())

dgs = DiGraphSimplifier()
dgs.enable_passes([block_merge])
block_after = dgs(blocks)
# print(block_after)
open("cfg_after.dot", "w").write(block_after.dot())

主要解释的就是这个block_merge和DiGraphSimplifier。

首先是DiGraphSimplifier,这个如名字所示,就是Graph Simplifier。我们要优化刚刚的cfg图的smc的基本块模式就需要用这个类,这个类的enable_passes方法会将cfg graph用enable_passes的参数的函数进行过滤。大概就这样。

然后是block_merge,首先matcher.match(graph)识别所有的smc模式的基本块,然后把它们一坨一坨的选出来(就是下面的一坨),把dad的入度的基本块的出度指向end的出度对应的基本块,然后把这一坨节点删除了。简单点说就是把dad的上一个基本块指向end的下一个基本块。再简单点说就是把这个模式全删掉。

image-20240416233117678

这里遇到了一个麻烦的地方。就是enable_passes会跑三次这个apply_simp.当只跑第一次时可以成功将node删除掉,但是它跑完一轮后new_graph = graph.copy()会把node还原,第二次第三次又不会进入for sol in matcher.match(graph):的循环内。所以相当于我这个删除操作没啥用。不知道是这个copy的逻辑问题还是我的问题,我直接改源码让它第一次break就正常了。如果有知道的大佬跪求联系我指导一下(

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def enable_passes(self, passes):
"""Add @passes to passes to applied
@passes: sequence of function (DiGraphSimplifier, DiGraph) -> None
"""
self.passes += passes

def apply_simp(self, graph):
"""Apply enabled simplifications on graph @graph
@graph: DiGraph instance
"""
while True:
new_graph = graph.copy()
for simp_func in self.passes:
simp_func(self, new_graph)

if new_graph == graph:
break
graph = new_graph
return new_graph

def __call__(self, graph):
"""Wrapper on 'apply_simp'"""
return self.apply_simp(graph)

总而言之,最后跑出来是这样的

image-20240416233730343

可以看出,这里的比较就是和输入比较,直接提取出来就是flag了。这里就不继续提取了。

总结

似乎有点bug,但是总体还是很好用的。之后分析点别的