Rizin项目中的ESIL中间语言详解
什么是ESIL?
ESIL(Evaluable Strings Intermediate Language)是Rizin逆向工程框架中采用的一种可评估字符串中间语言。它采用类似Forth语言的表示方法来描述每个操作码的行为,这种表示方法可以被评估和执行,从而实现代码模拟的功能。
ESIL虚拟机工作原理
ESIL虚拟机的工作流程可以简化为以下伪代码:
while ((word=haveCommand())) {
if (word.isKeyword()) {
esilCommands[word](esil);
} else {
esil.push(evaluateToNumber(word));
}
nextCommand();
}
ESIL命令是从栈中弹出值、执行计算操作并将结果(如果有)压回栈的操作。这些命令旨在覆盖CPU执行的所有常见操作,包括二进制运算、内存访问、系统调用等。
基本语法和使用
启用ESIL显示
要在反汇编时显示ESIL表达式,可以使用以下命令:
e asm.esil = true
表达式语法
操作码被翻译成逗号分隔的ESIL表达式列表:
-
寄存器操作示例:
xor eax, eax → 0,eax,=,1,zf,=
-
内存访问使用方括号表示:
mov eax, [0x80480] → 0x80480,[],eax,=
-
指定操作大小(默认为目标操作数大小):
movb $0, 0x80480 → 0,0x80480,=[1]
条件表达式
条件表达式以'?'字符开头,检查表达式结果是否为0,并根据结果决定是否跳过下一个表达式:
cmp eax, 123 → 123,eax,==,$z,zf,=
jz eax → zf,?{,eax,eip,=,}
对于多表达式条件,需要这样写:
zf,?{,eip,esp,=[],eax,eip,=,$r,esp,-=,}
特殊指令处理
系统调用
系统调用以'$'开头,后面可以跟指定系统调用号的数值:
0x80,$
陷阱指令
陷阱指令用于抛出异常(如无效指令、除零错误、内存读取错误等):
<trap>,<code>,$$
快速分析技巧
通过检查ESIL字符串可以快速获取信息:
indexOf('[')
:包含内存引用indexOf("=[")
:内存写入操作indexOf("pc,=")
:修改程序计数器(分支、跳转、调用)indexOf("sp,=")
:修改栈指针indexOf("?{")
:条件指令equalsTo("")
:空字符串表示NOP指令
CPU标志和ESIL标志
CPU标志
CPU标志通常定义为RReg配置文件中的1位寄存器,有时归类为'flag'寄存器类型。
ESIL内部标志
ESIL虚拟机有内部状态标志(只读),可用于将值导出到底层CPU标志:
$z
:零标志(结果为0时设置)$b
:借位标志(需指定位数,如$b4)$c
:进位标志(需指定位数,如$c7)$p
:奇偶标志$r
:寄存器大小(asm.bits/8)
变量和运算
变量特性
- 没有预定义的位宽(可轻松扩展至128、256和512位)
- 数量无限(兼容SSA形式)
- 寄存器名称没有特定语法
- 数字可以使用RzNum支持的任何进制表示
- 每个ESIL后端应有相关的RReg配置文件
算术运算
- 加法:"+"
- 乘法:"*"
- 减法:"-"
- 除法:"/"
- 取模:"%"
- 幂运算:"**"
位运算
- 与运算:"&"
- 或运算:"|"
- 异或:"^"
- 左移:"<<"
- 右移:">>"
- 循环左移:"<<<"
- 循环右移:">>>"
- 取反:"!"
控制流指令
ESIL规定解析控制流命令使用大写字母:
3,SKIP
:跳过N条指令(用于相对向前跳转)3,GOTO
:跳转到指令3LOOP
:0,GOTO的别名BREAK
:停止评估表达式STACK
:将栈内容输出到屏幕CLEAR
:清空栈
x86 REP前缀示例
rep cmpsb → cx,!,?{,BREAK,},esi,[1],edi,[1],^,!,?{,BREAK,},esi,++,edi,++,cx,--,LOOP
未实现指令处理
未实现/未处理的指令用'TODO'命令表示,它会像'BREAK'一样停止执行,但会显示警告信息:
fmulp ST(1), ST(0) → TODO,fmulp ST(1),ST(0)
实际应用示例
AVR架构分析示例
AVR微控制器的ESIL分析实现示例展示了如何将指令表示为ESIL:
static int avr_op(RzAnalysis *analysis, RzAnalysisOp *op, ut64 addr, const ut8 *buf, int len) {
short ofst;
int d, r, k;
(...)
变量d、r和k分别表示"目标"、"寄存器"和"常数"。例如LDI(立即数加载)指令:
r_strbuf_setf(&op->esil, "0x%x,r%d,=", k, d);
反汇编显示:
0x00000080 30e0 0x0,r19,= ; LDI Rd,K. load immediate
内省和API钩子
内省表达式
为了简化ESIL解析,我们需要一种表达内省表达式的方法来提取所需数据。例如获取jmp指令的目标地址:
ao~esil,opcode
opcode: jmp 0x10000465a
esil: 0x10000465a,rip,=
我们需要能够检索'rip'的数值。更复杂的情况包括:
- 操作码类型
- 跳转目标
- 条件依赖
- 所有修改的寄存器(写)
- 所有访问的寄存器(读)
API钩子
在解析器中设置钩子对于仿真非常重要,这样我们就可以扩展解析器来实现分析功能,而无需重复编写解析器。例如:
esil.on('regset', function(){..
esil.on('syscall', function(){esil.regset('rip'
现有钩子包括:
hook_flag_read()
hook_execute()
hook_mem_read()
返回true可以覆盖回调的默认操作(如使内存区域只读),返回false或0可以跟踪ESIL表达式解析(仿真)。
总结
ESIL作为Rizin中的中间语言,为代码分析和仿真提供了强大而灵活的基础。通过理解ESIL的工作原理和语法,逆向工程师可以更深入地分析二进制代码的行为,并构建自定义的分析工具。从简单的算术运算到复杂的控制流和系统调用,ESIL能够精确描述处理器指令的语义,为二进制分析开辟了新的可能性。
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考