在编写汇编代码的过程中,有时我们需要深入观察编译器内部的执行细节。尤其是在调试 shellcode 时,更需要细致、谨慎地逐条运行指令,以便准确捕捉问题所在。
本文将介绍如何在基于 x86_64 架构的 Ubuntu 系统上模拟执行 32 位 ARM 架构的 shellcode。由于大多数开发设备仍采用 x86_64 架构,并不原生支持 ARM 指令集,因此我们必须借助仿真手段来运行非本机架构的代码。此外,原始的 shellcode 通常仅为裸二进制数据,不具备标准可执行文件格式(如 ELF),无法被常规工具直接加载运行,这就要求我们采用特殊的执行方式。
为此,我们将使用 Radare2 —— 一个功能强大的命令行驱动逆向分析框架,集成了多种用于二进制分析的实用工具。它既支持脚本化操作,也提供交互式命令行界面,便于灵活操控。在 Ubuntu 系统中安装和配置 Radare2 只需执行几个简单的命令即可完成。
mkdir?~/github
cd?~/github
git?clone?https://github.com/radareorg/radare2.git
cd?radare2
sys/install.sh
若你已安装 Radare2,请务必确认当前版本为最新。该项目持续维护并频繁更新,旧版本可能存在影响实验结果的缺陷。特别是 2022 年 6 月之前的某些版本存在关键性错误,可能导致本次仿真任务无法顺利完成。
cd?~/github/radare2
git?pull
sys/install.sh
r2?-V
要获取本文所用的 shellcode 二进制文件,可在 bash 终端中执行以下命令进行复制:
nemo@hammerhead:~$?echo?-n?-e?'\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x78\x46\x0c\x30\xc0\x46\x01\x90\x49\x1a\x92\x1a\x0b\x27\x01\xdf\x2f\x62\x69\x6e\x2f\x73\x68\x00'?>?shellcode-696.bin
nemo@hammerhead:~$?md5sum?shellcode-696.bin?
42ba1c77446594cac3508b940926575d??shellcode-696.bin
ESIL 简介
ESIL(可评估字符串中间语言)是 Radare2 中实现的一种抽象指令表示方法,能够脱离具体硬件平台来“执行”机器码指令。这种机制特别适用于跨架构仿真场景,使得在 x86_64 主机上运行 ARM 指令成为可能。
利用 ESIL 执行 ARM shellcode 需完成以下几个步骤:
- 加载目标 shellcode 二进制文件
- 配置 Radare2 以正确解析该二进制内容
- 初始化 ESIL 仿真环境
- 根据需要设置相关寄存器状态
- 逐步执行并验证汇编指令的行为
使用 ESIL 执行 ARM shellcode
1. 加载 shellcode 二进制文件
当我们对 shellcode 文件执行 file 命令时,系统通常无法识别其格式。同样,Radare2 初始也无法判断其类型。
nemo@hammerhead:~/labs/shellcode/asm$?file?shellcode-696.bin
shellcode-696.bin:?data
由于该文件仅包含原始二进制代码,无任何文件头信息,因此需要手动告知 Radare2 如何解析。我们需调整部分分析参数,以确保能正确识别 ARM 架构指令:
nemo@hammerhead:~/labs/shellcode/asm$?r2?shellcode-696.bin
[0x00000000]>?e?anal.arch?=?arm
[0x00000000]>?e?asm.arch?=?arm
[0x00000000]>?e?asm.bits?=?32
[0x00000000]>?e?anal.armthumb=true
2. 配置 Radare2 解析规则
接下来的关键是明确哪些部分属于 ARM 指令,哪些属于 THUMB 指令。实践中发现,通过定义函数边界可以有效控制指令解码模式。本例中的 shellcode 会在 ARM 与 THUMB 状态之间切换。
在以下命令片段中,我们对地址 0 处创建一个函数,尽管此处并非真正的函数起点,但此举可帮助 Radare2 正确识别指令集类型。pdf 命令用于打印该函数的反汇编结果,其中包含 add 和 bx 指令。
[0x00000000]>?af
[0x00000000]>?pdf
┌8:?fcn.00000000();
│?rg:0(vars?0,?args?0)
│?bp:0(vars?0,?args?0)
│?sp:0(vars?0,?args?0)
│0x0000000001308fe2???????add?r3,?pc,1
└0x0000000413ff2fe1???????bx?r3
<p>[0x00000000]>?s?8
[0x00000008]>?af
[0x00000008]>?pdf
┌24:?fcn.00000008(int32_t?arg1,int32_t?arg2);
│;?arg?int32_t?arg1?@?r0
│;?arg?int32_t?arg2?@?r1
│0x0000000878460c30???????andlo?r4,?ip,?r8,?ror?r6
│0x0000000c??????c0460190???????andls?r4,?r1,?r0,?asr?13;?arg2
│┌─<0x00000010491a921a???????bne?0xfe48693c
││0x000000140b2701df???????svcle?0x1270b
││0x000000182f62696e???????cdpvs?p2,6,?c6,?c9,?c15,1
└│0x0000001c2f736800???????rsbeq?r7,?r8,?pc,?lsr?6
[0x00000008]>?afB?16</p>
从地址 8 开始的后续指令属于 THUMB 模式。s 8 命令将位置跳转至第 8 字节处,随后定义新的函数区域。使用 af 创建函数后,若直接调用 pdf 查看,显示结果可能异常,因为默认仍按 ARM 模式解析。
为解决此问题,可通过设置 asm.bits=16 来指定该函数使用 THUMB 指令集。这一设置仅作用于当前函数范围。在完整的 ARM 可执行文件中,Radare2 通常能自动识别模式切换,但在纯 shellcode 场景下,必须手动干预。
注意:我们可以选择移除前两条 ARM 指令,改用全 THUMB 编写的 shellcode。此时,在打开文件时设置 e asm.bits=16 即可全局启用 16 位模式,无需再单独定义函数。当然,保留两种模式混合的情况有助于展示更复杂的控制流程。
Radare2 还提供了便捷命令 izz,可用于快速提取二进制文件中嵌入的所有字符串信息。
>?izz
[Strings]
nth?paddr??????vaddr??????len?size?section?type??string
―――――――――――――――――――――――――――――――――――――――――――――――――――――――
0???0x00000008?0x00000008?4???5????????????ascii?xF\f0
1???0x00000018?0x00000018?7???8????????????ascii?/bin/sh</p>
至此,shellcode 已被正确加载并解析。
3. 初始化 ESIL 仿真环境
如前所述,Radare2 支持通过在命令后添加 ? 来查看其子命令列表。ae? 可列出所有与 ESIL 及仿真相关的操作命令。
[0x00000000]>?ae?
Usage:?ae[idesr?][arg]??ESIL?code?emulation
|?ae?[expr]????????????????evaluate?ESIL?expression
|?ae???????????????????????show?this?help
|?ae???????????????????????show?ESIL?help
|?aea[f][count]???????????analyse?n?esil?instructions?accesses?(regs,?mem..)
|?aeA[f][count]???????????analyse?n?bytes?for?their?esil?accesses?(regs,?mem..)
|?aeb?([addr])?????????????emulate?block?in?current?or?given?address
|?aeC[arg0?arg1..]@?addr??appcall?in?esil
|?aec[?]continueuntil^C
|?aef?[addr]???????????????emulate?function
|?aefa?[addr]??????????????emulate?function?to?find?out?args?in?given?or?current?offset
|?aeg?[expr]???????????????esil?data?flow?graph
|?aegf?[expr][register]???esil?data?flow?graph?filter
|?aei[?]???????????????????initialize?ESIL?VM?state?(aei-?to?deinitialize)
|?aek[?][query]???????????perform?sdb?query?on?ESIL.info
|?aeL??????????????????????list?ESIL?plugins
|?aep[?][addr]????????????manage?esil?pin?hooks?(see?“e?cmd.esil.pin”)
|?aepc?[addr]??????????????change?esil?PC?to?this?address
|?aer[?][..]??????????????handle?ESIL?registers?like?“ar”or“dr”?does
|?aes[?]???????????????????perform?emulated?debugger?step
|?aets[?]??????????????????esil?Trace?session
|?aev?[esil]????????visual?esil?debuggerfor?the?given?expression?or?current?instruction
|?aex?[hex]????????????????evaluate?opcode?expression
首先,使用 aei 命令初始化 ESIL 引擎。接着需建立堆栈空间。Radare2 会自动分配默认堆栈地址,也可通过 aeim 命令手动指定特定内存位置作为堆栈基址。
[0x00000008]>?aei
[0x00000008]>?aeim
4. 设置寄存器状态
由于 shellcode 从地址 0 开始执行,必须将程序计数器(PC)设置为 0。可通过 aepc 0 命令完成。若希望从其他偏移地址开始执行,则可使用 aepc [address] 指定起始位置。
[0x00000008]>?aepc?0
在我们的shellcode中,有一条指令("subs r1, r1, r1")会将寄存器r1的值减去自身并存回r1,从而使其归零。由于该寄存器初始状态通常已是0,为了更清晰地观察执行过程中的变化,我们可以先将其设置为0xffff。实现这一操作需要使用“aer”命令来修改寄存器状态。
[0x00000008]>?aer?r1?=?0xffff
1. 验证配置是否生效
完成上述设置后,接下来可以切换至可视化调试界面。进入调试器的视觉模式后,系统提供了多个面板视图,因此需连续按两次“p”键以切换到正确的显示面板。若需退出当前模式,直接按下Esc键即可。此外,随时可按下“?”查看当前环境下支持的所有操作指令。
[0x00000008]>?V
(hit?“p”?twice?to?get?to?the?debugger?panel)
进入面板后,可在顶部区域看到一组当前寄存器的状态信息,其布局如下所示:
值得注意的是,在视觉模式下同样可以执行常规r2命令。例如,若要打印位于偏移地址0x18处的字符串内容,只需输入相应的命令即可实现。
#?Hit?“:”?while?in?visual?mode.
>?ps?@0x18
/bin/sh
>?#?Hit?enter?on?a?blank?line?to?return?to?visual?mode.
通过按“s”键,我们可以逐条执行汇编指令。在逐步执行过程中,顶部的寄存器值和堆栈数据会实时更新(堆栈起始地址如第一张图所示为0x00178000)。同时,程序计数器(PC)所指向的下一条待执行指令会在汇编代码区被高亮标识出来(如第一幅图中地址0x00000010所示)。
从上图可见,此时r1寄存器的值为0x0000ffff。紧接着即将执行的指令是“subs r1, r1, r1”,该指令将r1与自身相减,并将结果写回r1,最终使其值变为0。
再次按下“s”键,进入下一条指令的执行阶段。
此时已准备就绪,即将通过“svc 1”指令触发系统调用。当前我们正在执行的是execve系统调用,因此需要确保r7寄存器中存放了对应的系统调用号0xb。而r0寄存器作为第一个参数,应包含指向目标二进制文件路径的指针。当前r0的值为0x18,我们可通过以下命令验证其所指向的具体内容:
#?Hit?“:”?while?at?the?“svc?1”?instruction?in?visual?mode.
>?ps?@r0
/bin/sh
>
由于本次调用并未向"/bin/sh"传递额外参数,也未设置环境变量,因此r1和r2寄存器均被置为0。
需要注意的是,由于当前并非运行在真实的ARM架构系统上,因此无法真正完成守护进程调用(即“svc 1”指令的功能)。这一点在测试复杂shellcode时尤为重要,必须加以注意。
总结
无论是调试自行编写的shellcode,还是分析静态提取的机器码片段,有时都必须深入观察每条指令的实际行为。Radare2提供了一种强大的能力:可以从非标准格式文件(如裸shellcode二进制或固件镜像)中加载汇编代码,并对其进行逐指令的动态执行分析,帮助我们准确理解其运行机制。