Lingze's blog Lingze's blog
timeline
about
friends
categories
tags

lingze

bin不是垃圾桶的意思!
timeline
about
friends
categories
tags
  • book
  • reverse
  • vm和符号执行
lingze
2020-07-16
目录

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
1

然后进行简单设置:

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()
1
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, ...])
1
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 命令行参数

对于程序运行时需要命令行输入的参数,

options.mimic_env = Ture
1

这个变量是在Sandbox_Linux_x86_64对象初始化的时候进行判断,并获取命令行参数,压入栈中。在这里 (opens new window),

然后输入命令行参数:

options.command_line = ['....']
1

# 内存段

一般使用栈等都不会有太大问题, 但是当程序使用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]")
1
2
3
4
5
6
7
8

在miasm.jitter.csts中定义的都是一堆的关于权限控制等常量,

# dse 技术

当我们已经成功可以让程序正常的在miasm环境中运行的时候,可以开始考虑加入dse engine,

from miasm.analysis.dse import DSEEngine
1

# 初始化和设置dse

dse = DSEEngine(sb.machine)

dse.add_lib_handler(sb.libs, globals())
1
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)
1

这一句开始我们的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
1
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)
1
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(...)
1
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")
1
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):
    ....
1
2
3

因此这个调用应该修改为:

from miasm.os_dep.common  import get_win_str_a

....
content = get_win_str_a(jitter, args.nptr)
1
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")
1
2
3
#miasm#dse
上次更新: 6/24/2025, 5:07:55 AM
Theme by Vdoing | Copyright © 2019-2025 lingze | MIT License
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式