miasm 使用笔记
# miasm
最开始接触到是在fcsc的keykoolol题目wp中, 使用miasm处理一个vm, 同时脚本中并没有出现对于255个opcode对应的设置,但是却像一个反汇编器一样直接打印出类汇编代码,似乎挺神奇, 而后在tigress挑战中的challenge0也在其官方blog有对应wp, 于是开始学习,
这个框架目前似乎还没有很完备的资料, 官方的blog简介了使用方向, 但是因为版本更新有些脚本甚至出现了问题,
翻到了一个这个, miasm - api (opens new window)但是比较老,这个框架更新还是比较频繁, 另外就是对照源码 (opens new window)中的注释进行学习, 注释倒是比较多, 而且代码质量感觉也比较不错。
这个框架的学习过程中对于源码的翻看更多了些, 没有一些相关资料, 使用起来的一些报错, 通过直接查找源码中的注释和回显信息等进行解决。
首先应该比较不同的是这个动态符号执行技术, 即混合使用符号执行和普通的输入确定值的调试,
主要思路大致是,因为符号执行会容易造成路径爆炸的问题, 于是提出, 先模拟程序的正常执行, 然后只在关键需要的位置插入符号并在另一端的对应位置取出, 由此通过输入的确定值使得运行过程是固定的, 不会有路径爆炸的情况,而同时使用符号记录运算处理过程, 标记路径, 并自动的通过内置的ir转化为类汇编的语言。
因此, 这个脚本写起来, 有点像ida python动调的脚本。
# 运行
首先是要保证程序在框架给出的环境内可以正常运行, 要达到的效果是, 运行脚本并指定对应程序, 除了miasm框架对应的提示信息,输入输出运算判断应该是和直接运行程序无二。
# sandbox
首先是一个运行环境的设置,miasm 中使用sandbox的概念,,miasm在/miasm/analysis/sandbox.py
预设好了以下几个sandbox:
details
- 对于linux系统:
- aarch64l
- armb_str
- arml_str
- arml
- armtl
- mips32b
- ppc32b
- x86_32
- x86_64
- 对于windows系统:
- x86_64
- x86_32
使用就如下:
from miasm.analysis.sandbox import Sandbox_Linux_x86_64
然后进行简单设置:
parser = Sandbox_Linux_x86_64.parser(description="ELF sandboxer")
parser.add_argument("filename", help="ELF Filename")
options = parser.parse_args()
sb = Sandbox_Linux_x86_64(options.filename, options, globals())
sb.run()
2
3
4
5
6
7
首先是获取parser对象, 其作用是处理运行脚本时的命令行参数,然后使用options获取到解析出命令行参数的namespace, 然后设置出sandbox对象,并开始运行即可。
# 数据和内存处理
主要通过sandbox.jitter, 这是一个jitter对象, 是运行时内置的一个jit engine, 通过这个可以访问和修改正在运行的程序的内存。 相关的函数在python中定义在/miasm/jitter/jitload.py
, 基本上全部会转入到jitter.vm对象, 这个对象主要是/miasm/jitter/vm_mngr.c
, /miasm/jitter/vm_mngr_py.c
中,
其中python中定义的jitter.get_c_str
和jitter.set_c_str
都会转到jitter.vm.get_mem
和jitter.vm.set_mem
, 这是最常用的四个, 一般用在对libc函数的实现。
另外对于内存页等的实现, 有jitter.vm.add_memory_page
进行内存页的设置, 一般出现canary的时候从fs[0x28]取值, 将会使用这个处理,不然程序无法访问到fs[0x28]的位置。
# 实现 libc
warning
和angr类似, 这个程序运行在python的miasm框架的环境内,如果他调用了libc函数,都无法找到, 因此我们需要在miasm环境中构造这个函数,在miasm中,我们可以直接在脚本中设置一个对应的函数即可,运行时会自动寻找和调用。
目前应该__libc_start_main
还有几个不需要,
可以直接运行, 看报错提示缺少, 再修复几个,
在miasm中已经实现了好几个,位置在/miasm/os_dep/linux_stdlib.py
, 可以import ×
进来, 但是可能也会因为函数名和提取参数啥的不对,
这里简单说明几个libc函数的构造,
首先传入函数的应该是jitter
, 这个我们前面说道的获取字符串或者内存啥的,也基本都是在这里使用的。
def xxx_....(jitter)
ret_ad, args = jitter.func_args_systemv([....])
....
return jitter.func_ret_systemv([ret_ad, ...])
2
3
4
一般一个模拟libc函数应该如上示,
关于函数对应的参数和返回值的获取和设置, 中间进行数据处理, 在一般情况下,也并不需要编写非常完备的函数功能, 只要符合当前程序应该运行的状态即可,
tip
比如在有些程序里, printf里从没有出现格式化字符串+参数的形式, 那就可以在实现的时候当作一个puts之类的, 简单实现其功能,
具体的例子可以翻看linux_stdlib.py
文件中,或者在实际操作中的脚本,
details
这里简单例子:
def xxx_fgets(jitter):
'''
原型: char *fgets(char *str, int n, FILE *stream)
在程序中stream一直为stdin, 因此直接用input了,
'''
ret_ad, args = jitter.func_args_systemv(["dest", "size", "stream"])
s = input()
jitter.vm.set_mem(args.dest, s.encode())
return jitter.func_ret_systemv(ret_ad, len(s))
def xxx___printf_chk(jitter):
'''
原型:int printf(const char *format, ...)
值得注意的是参数里的's', 必须加上就正确了, 但是不清楚啥原因。
'''
ret_ad, args = jitter.func_args_systemv(['s', "format", "arg"])
print(jitter.get_c_str(args.format))
return jitter.func_ret_systemv(ret_ad, 1)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 命令行参数
对于程序运行时需要命令行输入的参数,
options.mimic_env = Ture
这个变量是在Sandbox_Linux_x86_64对象初始化的时候进行判断,并获取命令行参数,压入栈中。在这里 (opens new window),
然后输入命令行参数:
options.command_line = ['....']
# 内存段
一般使用栈等都不会有太大问题, 但是当程序使用canary时, 将会导致问题, 于是我们要手动设置fs的内存和canary所取的 fs[0x28]位置的数据,
from miasm.jitter.csts import PAGE_READ
sb.jitter.ir_arch.do_all_segm = True
FS_0_ADDR = 0x7ff70000
sb.jitter.cpu.FS = 0x4
sb.jitter.cpu.set_segm_base(sb.jitter.cpu.FS, FS_0_ADDR)
sb.jitter.vm.add_memory_page(FS_0_ADDR + 0x28, PAGE_READ, b"\x42\x42\x42\x42\x42\x42\x42\x42", "Stack canary FS[0x28]")
2
3
4
5
6
7
8
在miasm.jitter.csts
中定义的都是一堆的关于权限控制等常量,
# dse 技术
当我们已经成功可以让程序正常的在miasm环境中运行的时候,可以开始考虑加入dse engine,
from miasm.analysis.dse import DSEEngine
# 初始化和设置dse
dse = DSEEngine(sb.machine)
dse.add_lib_handler(sb.libs, globals())
2
3
这个dse engine对象定义在/miasm/analysis/dse.py
, 初始化传入sandbox内对应的machine属性即可,
注意这个dse.add_lib_handler(sb.libs, globals())
方法, 作用是载入dse后, 所有的libc函数, 如函数{name}
, 设置到对应的{name}_symb
函数,如果没有查找到对应的{name}_symb
, 则会设置到default_func
函数 (opens new window), 提示这个函数不存在,需要定义。
注意这里设置的是handler, 最后add_lib_handler
会解析出名字以后调用add_handler
设置,
# dse attach到jitter
定义好dse对象以后,运行仍然是老样子,要想开始记录符号应该使用:
dse.attach(jitter)
这一句开始我们的dse对象才会投入使用,而且这以后的调用libc都会是调用对应的{name}_symb
函数,如果没有定义的话也会开始看到相关报错了。
关于这一句的位置,定义好dse以后就可以使用attach,但是一般使用在关键位置附近的libc函数设置一个, 在函数内使用可以加一个global dse
。
# instrumentation和handler相关
设置好dse和attach以后可以开始设置使用instrumentation和handler,这是dse对象内的两个字典:
# /miasm/analysis/dse.py
# class DSEEnigne
def __init__():
....
self.handler = {} # addr -> callback(DSEEngine instance)
self.instrumentation = {} # addr -> callback(DSEEngine instance)
def add_handler(self, addr, callback):
"""Add a @callback for address @addr before any state update.
The state IS NOT updated after returning from the callback
@addr: int
@callback: func(dse instance)"""
self.handler[addr] = callback
def add_instrumentation(self, addr, callback):
"""Add a @callback for address @addr before any state update.
The state IS updated after returning from the callback
@addr: int
@callback: func(dse instance)"""
self.instrumentation[addr] = callback
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
里面储存的是地址和对应调用的函数,我们可以在脚本中设置好一个函数, 然后使用dse.add_instrumentation
或dse.add_handler
设置地址和这个函数,
这两个字典的使用看起来基本类似,区别是handle不会刷新状态, instrumentation会刷新状态,
warning
占坑
目前不太清楚这个关于状态刷新的注释啥意思😅 😅😅
不过看到相关几个例子, 一般手动设置程序中的某些位置的时候似乎都是使用instrumentation,
当然,还有个update_state的方法, 但是看起来并不一样...
details
目前已经看到的, jitter中运行到对应一些位置应该是回去调用dse对象的callback函数 (opens new window),其中代码如下:
# /miasm/analysis/dse.py
# class DSEEinge
# def callback():
if cur_addr in self.handler:
self.handler[cur_addr](self)
return True
if cur_addr in self.instrumentation:
self.instrumentation[cur_addr](self)
2
3
4
5
6
7
8
9
这里是关于运行设置好的函数的位置, 因此可以看到我们设置的函数应该参数为dse对象,
另外一点就是在handler运行后直接return, 但是对于instrumentation运行后继续运行了后面的部分在最后进行返回,这应该就是两者的差别位置,也是关于状态刷新之类的。
# 符号化 和 状态刷新
类似z3中设置符号的方式, miasm中使用Expr
系列的对象,是miasm中的ir中间语言的表示,
一般这样一个对象表示一个符号化的变量,通过dse.update_state()
方法插入到程序中,通过dse.eval_expr()
方法获取输入后的已经记录了运算的一个带符号表达式,
注意在插入所有符号前, 应该在attach以后,使用dse.update_state_from_concrete()
, 从原本的由确定值运行的环境中载入到dse环境。
tip
最简单的一个示例是miasm-blog中的tigress0-challenge0的处理,在文章Playing with Dynamic symbolic execution (opens new window)
我下一步也将会在博客的wp区, tigress0-0中写上对应的wp
dse.update_state_from_concrete()
dse.update_state({....})
dse.exal_expr(...)
2
3
4
# 获取和设置寄存器
在加入dse以后,环境中一共存在两套数据, 一个是原本的具体值, 一个是符号化的数据。
具体值,通过dse.jitter.cpu.xx
访问寄存器, 或者sb.jitter.cpu.xx
这两个是等同的。 返回值是一个int类型,可以直接获取。赋值直接使用=
即可。
符号值, 通过1dse.ir_arch.arch.regs.xx
访问寄存器,赋值通过dse.update({..: ..})
赋值进去一个符号值。获取通过dse.eval_expr(xx)
, 即可获取对应带符号的表达式。
# bug
学习过程中发现的代码上的问题, 主要是错误回显信息没更新导致有几个小问题,
不过也不一定修没修, 这个框架更新还是比较频繁的,
# get_str_ansi
首先是jitter.get_str_ansi(args.nptr)
这个函数已经不再支持, 调用将会报错, 提示去使用另一个函数,源码中如下:
# miasm/jitter/jitload.py
def get_str_ansi(self, addr, max_char=None):
raise NotImplementedError("Deprecated: use os_dep.win_api_x86_32.get_win_str_a")
2
3
但是我们会发现,在miasm/os_dep/win_api_x86_32.py
中并没有这个函数, 他已经被转移到了miasm/os_dep/common.py
中, 源码:
# miasm/os_dep/common.py
def get_win_str_a(jitter, ad_str, max_char=None):
....
2
3
因此这个调用应该修改为:
from miasm.os_dep.common import get_win_str_a
....
content = get_win_str_a(jitter, args.nptr)
2
3
4
同样的get_str_unic函数也是一样:
# miasm/jitter/jitload.py
def get_str_unic(self, addr, max_char=None):
raise NotImplementedError("Deprecated: use os_dep.win_api_x86_32.get_win_str_a")
2
3