基本汇编引擎

Keystone
Keystone 是一个开源的汇编器(assembler),用于生成二进制机器码。它支持多种 CPU 架构和操作模式,并且具有高效、轻量级、跨平台等特点,适合用在逆向工程、漏洞研究、二进制分析等领域。
keystone-engine
是 Keystone 项目的 Python 绑定,它允许开发者在 Python 环境中使用 Keystone 汇编器的强大功能。通过 keystone-engine
,你可以在 Python 中执行汇编代码,将汇编转换为机器码,或者将机器码反汇编成汇编代码。
Keystone 安装
keystone-engine
库已经发布到 PyPI(Python Package Index),所以你可以直接使用 pip
来安装它:
1 | pip install keystone-engine |
注意
这里安装的是 keystone-engine
而不是 keystone
,keystone
是另外一个模块。
基本使用
创建 Keystone 对象
要开始使用 Keystone,我们首先需要创建一个 Keystone 对象。这个对象将负责后续的汇编和反汇编工作。
1 | from keystone import Ks, KS_ARCH_X86, KS_MODE_32 |
Ks
构造函数的两个参数是 架构类型 和 模式类型。这两个参数定义了要汇编或反汇编的目标架构和模式。
架构类型指定了目标处理器的架构。Keystone 支持多种不同的处理器架构,包括:
KS_ARCH_ARM
:ARM 处理器架构KS_ARCH_ARM64
:ARM 64 位处理器架构KS_ARCH_MIPS
:MIPS 处理器架构KS_ARCH_X86
:x86 处理器架构KS_ARCH_PPC
:PowerPC 处理器架构KS_ARCH_SPARC
:SPARC 处理器架构KS_ARCH_SYSTEMZ
:SystemZ 架构(IBM Z 系列)KS_ARCH_HEXAGON
:Hexagon 架构(Qualcomm Hexagon DSP)KS_ARCH_EVM
:EVM 架构(TCS/EVM 虚拟机)
模式类型用于指定特定架构下的工作模式(如 32 位或 64 位模式),Keystone 在同一架构下支持多种模式。
提示
在 Keystone 中,如果你希望同时设置多个模式,可以通过 按位“或”操作 (
|
) 来组合不同的模式。这样可以在一个构造函数中同时指定多个模式类型。首先是最基本的大小端序,Keystone 默认是小端序。
KS_MODE_LITTLE_ENDIAN
:小端模式(默认)。KS_MODE_BIG_ENDIAN
:大端模式。
另外就是一些针对特定架构下的模式类型。
ARM 架构相关模式:
KS_MODE_ARM
:ARM 模式。KS_MODE_THUMB
:Thumb 模式(ARM 特有,表示压缩指令集模式)。KS_MODE_V8
:ARMv8 模式(用于支持 ARMv8 架构)。
MIPS 架构相关模式:
KS_MODE_MIPS3
:MIPS3 模式。KS_MODE_MIPS32R6
:MIPS32R6 模式。KS_MODE_MIPS32
:MIPS32 模式。KS_MODE_MIPS64
:MIPS64 模式。
x86 架构相关模式:
KS_MODE_16
:16 位模式。KS_MODE_32
:32 位模式。KS_MODE_64
:64 位模式。
PowerPC 架构相关模式:
KS_MODE_PPC32
:PowerPC 32 位模式。KS_MODE_PPC64
:PowerPC 64 位模式。KS_MODE_QPX
:QPX 模式(专门用于某些 PowerPC 处理器)。
SPARC 架构相关模式:
KS_MODE_SPARC32
:SPARC 32 位模式。KS_MODE_SPARC64
:SPARC 64 位模式。KS_MODE_V9
:SPARC V9 模式(64 位)。
这里给出我们在逆向辅助工具开发中常用的 Keystone 对象:
1 | from keystone import * |
汇编
Keystone 对象只有一个公开的成员函数 asm
,该函数定义如下:
1 | def asm(self, string: str, addr: int = 0, as_bytes: bool = False) -> tuple: |
参数:
string: str
:表示输入参数string
是一个字符串,包含汇编语言的代码。addr: int = 0
:addr
是一个整数类型的参数,表示机器码开始的内存地址,默认值为0
。as_bytes: bool = False
:as_bytes
是一个布尔值,表示是否返回机器码的字节表示。默认值为False
,即返回一个整数列表。
返回值:该函数返回一个元组,元组包含两个元素。
encoded
:机器码的字节表示。- 如果
as_bytes=True
,返回bytes
类型。 - 如果
as_bytes=False
,返回list[int]
类型,表示机器码的字节序列对应的整数。
- 如果
count
:生成的机器码指令的数量。
示例:
1 | from keystone import * |
提示
默认情况下,Keystone 使用 Intel 语法解析 x86 架构的汇编指令,如果要切换为 AT&T 语法,可以使用以下代码:
1 | ks = Ks(KS_ARCH_X86, KS_MODE_32) |
Capstone
Capstone 是一个流行的反汇编框架,它与 Keystone 密切相关,但其功能侧重点不同。Keystone 主要负责将汇编代码转换为机器码(即汇编器),而 Capstone 则是一个反汇编库,专注于将机器码转换为汇编代码(即反汇编器)。
Capstone 安装
Capstone 提供了 Python 绑定,可以通过 pip
安装:
1 | pip install capstone |
基本使用
创建 Capstone 对象
首先,你需要创建一个 Capstone 反汇编对象,并设置目标架构和模式。Cs
类用于创建 Capstone 对象,CS_ARCH_*
和 CS_MODE_*
常量用于指定架构和模式。
这里架构和模式定义和 Keystone 库的类似,但具体类别稍有不同。不过对于我们绝大多数逆向需求来说我们不需要关系两者的区别。
1 | from capstone import * |
反汇编
我们可以使用 Capstone 反汇编对象的 disasm
方法来进行反汇编,该函数定义如下:
1 | def disasm(self, code, offset, count=0) -> 'CsInsn': |
参数:
code: bytes
:表示输入参数code
是一个字节串(bytes
类型),包含要反汇编的机器码。这个字节串通常来自二进制文件或内存中的机器码表示。offset: int
:offset
是一个整数类型的参数,表示机器码开始的地址(通常是内存中的起始地址)。这个参数是必需的,用来确定反汇编指令的起始位置,帮助计算每条指令的地址。count: int = 0
:count
是一个整数类型的参数,表示最多反汇编的指令数。默认值为 0,表示反汇编尽可能多的指令,直到代码结束或者遇到无效指令。如果设置了非零的count
,则最多反汇编count
条指令。
返回值: 该函数返回一个生成器,每次迭代返回一个 CsInsn
对象,代表一条反汇编指令。CsInsn
对象包含以下信息:
address
:指令的地址,表示这条指令在内存中的位置(十六进制格式)。mnemonic
:指令的助记符(如:mov
、add
、push
等)。op_str
:指令的操作数(如:eax, ebx
或mem[rax]
)。size
:指令的字节大小,表示该指令在内存中占用了多少字节。
示例:
1 | from capstone import * |
提示
默认情况下,Capstone 使用 Intel 语法解析 x86 架构的汇编指令,如果要切换为 AT&T 语法,可以使用以下代码:
1 | cs = Cs(CS_ARCH_X86, CS_MODE_32) |
从 disasm
的函数定义我们发现,Capstone 不仅仅是简单的将机器码转为汇编指令,而是提供了一个描述汇编指令的对象 CsInsn
,从而对汇编指令提供更细粒度的描述。
因此在打印汇编的时候我们需要手动将 CsInsn.mnemonic
和 CsInsn.op_str
拼接成一条汇编。
1 | for insn in cs.disasm(machine_code, address): |
另外由于 disasm
返回的是一个迭代器,因此我们还可以作如下封装:
1 | def disasm(machine_code, address=0): |
另外除了上述四个成员外,如果我们开启 Cs.detail = True
选项时,CsInsn
还会多一个名为 operands
的字段,这个字段对于不同的架构是不同的。CsInsn
的 operands
在我们逆向辅助工具编写过程中十分重要,后面会有单独一节介绍。
当然如果我们将机器码反汇编得到 CsInsn
对象,尤其是 Cs.detail
选项开启时性能损耗将相当大。如果我们只需要基本数据,如地址、大小、助记符和操作数,我们可以使用更轻量的 API disasm_lite()
。
1 | def disasm_lite(self, code: bytes, offset: int, count: int = 0) -> Generator[Tuple[int, int, str, str], None, None]: |
从版本 2.1 开始,Python 绑定提供了这个新的 disasm_lite()
方法。与 disasm()
不同,disasm_lite()
只返回一个包含 (address, size, mnemonic, op_str)
的元组。基准测试显示,这个轻量级 API 比 disasm()
快最多 30%。
CsInsn 对象
CsInsn
是 Capstone 库中用于表示反汇编指令的对象。在反汇编过程中,CsInsn
对象提供了每条指令的详细信息。通过 CsInsn
,你可以访问与该指令相关的各种信息,如指令地址、助记符、操作数、寄存器访问、指令分组等。
提示
为了描述不同架构的指令,CsInsn
的成员比较多样,并且在的架构、指令不同时这成员也不同。因此这里给出一段辅助代码,可以方便在编写代码的时候快速查看 CsInsn
中的成员。
1 | # -*- coding: utf-8 -*- |
基本字段
CsInsn
对象提供了对反汇编指令的基本信息的访问,主要包括以下四个基本字段:
address
:指令的地址,表示这条指令在内存中的位置(十六进制格式)。mnemonic
:指令的助记符(如:mov
、add
、push
等)。op_str
:指令的操作数(如:eax, ebx
或mem[rax]
)。size
:指令的字节大小,表示该指令在内存中占用了多少字节。
寄存器读写
CsInsn
还提供了关于寄存器操作的详细信息。通过该信息可以知道指令涉及到哪些寄存器,并且是如何使用它们的:
regs_read()
:该方法返回一个指令中被读取的寄存器的集合,通常是一个整数列表,代表寄存器的编号。regs_write()
:该方法返回一个指令中被写入的寄存器的集合,通常也是一个整数列表,表示写入的寄存器编号。reg_read(reg_id)
:该方法用于检查特定的寄存器是否被指令读取。传入寄存器的 ID,如果该寄存器被读取,返回True
,否则返回False
。reg_write(reg_id)
:该方法用于检查特定的寄存器是否被指令写入。传入寄存器的 ID,如果该寄存器被写入,返回True
,否则返回False
。regs_access()
:返回一个元组,包含regs_read()
和regs_write()
两个方法的结果。
提示
这里得到的寄存器 ID 不是很直观,CsInsn
有一个 reg_name
方法可以将寄存器编号转换为寄存器的名称。
操作数字段
操作数结构
CsInsn
中的 operands
字段包含了与指令相关的操作数。由于一条指令可能涉及多个操作数,因此 operands
是一个数组其中每一个元素对应一个操作数,顺序和指令中操作数的顺序相同。
由于操作数是 Capstone 底层库获取到的,因此实际上这里 operands
中的元素是定义的 ctypes
结构体。对于不同的架构这个结构体的成员不同。
以 X86 架构为例,operands
中的元素是的类型 X86Op
定义如下:
1 | class X86OpMem(ctypes.Structure): |
从上述代码中可以看出,操作数可以是寄存器、立即数、内存地址等不同的类型。Capstone 通过操作数结构体中的 type
字段来描述操作数的类型,见的操作数类型有(当然除了这些之外还有一些各架构独有的操作数类型,这些类型的名称通常为 <架构>_OP_<类型>
,例如 ARM64_OP_CIMM
):
CS_OP_REG
:表示寄存器操作数(如eax
、ebx
)。CS_OP_IMM
:表示立即数操作数(如0x10
、42
)。CS_OP_MEM
:表示内存操作数(如mem[eax+4]
)。CS_OP_FP
:表示浮点数操作数。
对应不同类型的操作数,value
字段作为联合体有 reg
、imm
、mem
三种类型。我们可以根据操作数的类型从对应的结构中获取我们想要的值。
由于操作数对象提供了 imm
、reg
、mem
三种方法来封装 value
字段的访问,因此我们可以使用例如 insn.operands[1].mem.base
这种方式跳过 value
直接访问联合体中的成员。
内存操作数
因为会经常用到,这里要着重说明一下内存操作数的结构。内存操作数是指令中用于描述内存地址的操作数类型,通常在汇编指令中通过指定内存地址、基址寄存器、索引寄存器和偏移量来进行内存访问。
在 Capstone 库中,内存操作数的类型为 CS_OP_MEM
,并且通过 CsOperand
对象的 mem
字段来描述相关的内存信息。还是以前面的 X86OpMem
结构体举例:
1 | class X86OpMem(ctypes.Structure): |
segment
:内存段寄存器,在现代架构中通常为0
,表示直接访问内存,而不通过特定的段寄存器。如果涉及到段寻址(例如在 x86 16 位模式下),此字段可能会有所变化。base
:基址寄存器,是计算内存地址的基础寄存器。通过将基址寄存器的值与偏移量disp
相加,可以确定内存地址。例如,eax
或ebx
通常用于存储基地址。index
:索引寄存器,是一个可选的寄存器,通常用于数组、结构体等类型的数据访问。通过base + (index * scale) + disp
可以计算出最终的内存地址。index
通常用于访问内存中的多个元素,例如数组的每个元素,scale
会控制每次索引跳跃的大小。scale
:缩放因子,用于乘以索引寄存器的值,以调整访问的大小。对于数据结构的访问,这个字段决定了如何调整内存访问的步长。例如,访问一个int
类型数组时,scale
通常为4
(因为一个int
占 4 字节)。disp
:偏移量,表示相对于基址和索引寄存器的地址偏移量。它通常是一个常数,表示在基地址或索引基础上额外的偏移量。例如,[eax + 4]
中的偏移量就是4
。
x86 架构
在 x86-64 架构中,内存操作数常常包含基址寄存器、索引寄存器、缩放因子和偏移量,下面的汇编指令是 x86-64 中的一个常见例子:
1 | mov rax, [rbx + rcx*4 + 0x10] |
这条指令的操作数对应 Capstone 的内存操作数结构如下:
1 | # 对应 Capstone 解析的结构体 |
- segment :
0
(通常为 0,因为没有特别指定段寄存器) - base :
rbx
(基址寄存器) - index :
rcx
(索引寄存器) - scale :
4
(数组或数据元素大小,通常是4
) - disp :
0x10
(偏移量)
通常来说,X86 从全局变量读取数据的指令为:
1 | mov reg, qword ptr ds:[rip + disp] |
因此我们可以计算全局变量的地址:
1 | insn.operands[1].mem.disp + insn.address + insn.size |
ARM 架构
ARM 架构使用类似于 x86
的寻址模式,但是语法和寄存器命名方式不同。例如下面这条汇编指令是将 r1 + (r2 << 2)
计算出的内存地址中的值加载到 r0
寄存器:
1 | ldr r0, [r1, r2, lsl #2] |
这条指令的操作数对应 Capstone 的内存操作数结构如下:
1 | # 对应 Capstone 解析的结构体 |
- segment :
0
(默认段选择符) - base :
r1
(基址寄存器) - index :
r2
(索引寄存器) - scale :
4
(因为lsl #2
是左移 2 位,相当于乘 4) - disp :
0
(没有偏移量)
由于 ARM 架构的指令不能直接操作内存,因此无法直接根据全局变量的地址读取全局变量的数据,而是:
- 先将编译在函数后面的地址表中的全局变量的地址加载到寄存器中。
- 然后还可能跟
pc
寄存器相加顺便实现地址无关代码 - 最后再通过寄存器访问全局变量所在的地址读取数据。
也就是通常来说 ARM 架构从全局变量读取数据的指令为:
1 | ldr r1, [pc, #disp] ; 从 (pc + disp) 计算出的内存地址加载数据到 r1 |
由于编译器的针对 CPU 流水线的优化,这些指令之间可能还会穿插其它的指令。因此我们不能直接通过指令得出全局变量的地址。不过这里我们可以先获取指令序列 insn_list
,然后简单模拟一下这个从内存加载数据的过程来计算全局变量的地址。
1 | reg_value = {} |
AArch64 架构
AArch64 是 ARM 的 64 位版本,它使用与 ARM 相似的寻址模式。AArch64 允许更复杂的寻址模式,包括有符号立即数、寄存器和可选的缩放因子。例如下面这条汇编指令是将 x1 + (x2 << 3)
计算出的内存地址中的值加载到 x0
寄存器:
1 | ldr x0, [x1, x2, lsl #3] |
这条指令的操作数对应 Capstone 的内存操作数结构如下:
1 | # 对应 Capstone 解析的结构体 |
- segment :
0
(默认段选择符) - base :
x1
(基址寄存器) - index :
x2
(索引寄存器) - scale :
8
(因为lsl #3
是左移 3 位,相当于乘 8) - disp :
0
(没有偏移量)
AArch64 架构的指令同样不能直接操作内存,因此无法直接根据全局变量的地址读取全局变量的数据,而是:
- 通过
adrp
指令将全局变量地址所在内存页的基地址加载到寄存器中。由于 AArch64 是基于页面地址访问的,adrp
将会设置寄存器为地址的页对齐值。 - 将存有页基址的寄存器加上某个偏移量,使其指向全局变量。
- 通过
ldr
或ldp
等指令从该寄存器指向的内存中读入数据。
也就是通常来说 AArch64 架构从全局变量读取数据的指令为:
1 | adrp x2, #0x37ff000 ; 将页面的基地址加载到寄存器 x2 中 |
同样我们可以参考 ARM 架构的做法来计算全局变量的地址。
1 | reg_value = {} |
- Title: 基本汇编引擎
- Author: sky123
- Created at : 2025-08-18 23:59:54
- Updated at : 2025-08-25 01:10:09
- Link: https://skyi23.github.io/2025/08/18/基本汇编引擎/
- License: This work is licensed under CC BY-NC-SA 4.0.