[置顶] 泰晓 RISC-V 实验箱,配套 30+ 讲嵌入式 Linux 系统开发公开课
实例讲解支持多种架构指令集编解码的 pwntools 工具
By Falcon of TinyLab.org Dec 09, 2019
背景简介
之前在群里发起了一个讨论:
不用 gcc 如何实现代码的编解码,类似 MIPS Linux 内核中用到的 UASM。
大家的回复总结如下:
- GDB compile command, Debugging with GDB: Compiling and Injecting Code
- MIPS uasm,arch/mips/include/asm/uasm.h
- X86 Encoder/Decoder, X86 Encoder Decoder: X86 Encoder Decoder User Guid…
- luajit backend, Hello, JIT World: The Joy of Simple JITs
- ebpf, GitHub - iovisor/bcc: BCC - Tools for BPF-based Li… , eBPF - IO Visor Project
- unicorn, Unicorn – The ultimate CPU emulator
而来自龙芯的张老师提到了一个 Python 的库,这个库是:
- utds3lab/multiverse: https://github.com/utds3lab/multiverse
安装的过程中,发现它依赖 pwntools。
如果直接用 pip3 安装 multiverse,在新版 python3 中运行得很不友好,很多语法不兼容了。
然后看了一下,multiverse 的 asm 部分实际是用的 pwntools,而且 pwntools 支持的架构更多,所以转而直接用 pwntools 就好了:
初试 pwntools
安装
这个工具的稳定版本仅支持到 python 2.7,可以参考以下文档安装:Installation — pwntools 3.12.1 documentation
下面介绍在 Ubuntu 16.04 下安装最新 dev 版本的过程,这里用国内某位同学在码云上做的镜像:
$ sudo apt-get install python3
$ sudo apt-get install python3-setuptools
$ sudo easy_install pip3
$ sudo python3 -m pip install --upgrade git+https://gitee.com/ktfsong/pwntools@dev3
$ sudo pip3 install sortedcontainers==2.0
用法
然后用 python3 演示一个解码和编码的例子:
$ python3
>>> from pwn import *
>>> context(arch='i386', os='linux')
>>> disasm(b'\xe8\xc0\xfe\xff\xff', vma=0x804841b)
' 804841b: e8 c0 fe ff ff call 0x80482e0'
>>> asm('call 0x80482e0', vma=0x804841b)
b'\xe8\xc0\xfe\xff\xff'
vma 表示指令加载的虚拟地址,对于 call 指令的相对地址编/解码是必须的。
这个例子是 hello.c 中,调用 printf 函数的例子,不用 gdb,用这种库就能更好的跟进符号的动态解析过程。用 gdb 的话,它更进一步,直接把 0x80482e0 关联到了 plt 中。
先来看 hello.c:
#include <stdio.h>
int main(void)
{
printf("hello.\n");
return 0;
}
编译完再反汇编:
$ gcc -m32 -o hello hello.c
$ objdump -d hello | grep puts
$ objdump -d hello | grep -A1 "<puts@plt>$"
804841b: e8 c0 fe ff ff call 80482e0 <puts@plt>
8048420: 83 c4 10 add $0x10,%esp
这个 0x80482e0 是刚好指向了 plt 中的 puts@plt 这一项:
$ objdump -d -j .plt hello | grep -A 4 "<puts@plt>"
080482e0 <puts@plt>:
80482e0: ff 25 0c a0 04 08 jmp *0x804a00c
80482e6: 68 00 00 00 00 push $0x0
80482eb: e9 e0 ff ff ff jmp 80482d0 <_init+0x28>
用上述库的好处是,可以更灵活的分析特定的代码和二进制。
也可以对照 I386 的编程手册 去看 call 指令的编码规则,例如,这里的 call 指令在手册的页面是 P275:
IF rel16 or rel32 type of call
THEN (* near relative call *)
IF OperandSize = 16
THEN
Push(IP);
EIP ← (EIP + rel16) AND 0000FFFFH;
ELSE (* OperandSize = 32 *)
Push(EIP);
EIP ← EIP + rel32;
FI;
FI;
在这里刚好是 EIP+ rel32,EIP 是 Call 的下一条指令的地址:
8048420: 83 c4 10 add $0x10,%esp
所以,EIP = 0x8048420,rel32 是 Call 指令中的数据部分,e8 是操作码,数据部分 c0 fe ff ff,而这个是小端的,所以实际数值是:
$ echo -ne '\xc0\xfe\xff\xff' | xxd -e
00000000: fffffec0 ....
两个加上:
$ echo "obase=16;ibase=10;$((0xfffffec0+0x8048420))" | bc -l
1080482E0
最高两个字节是超出 32 位的,抹掉,刚好是 0x80482E0。这样的话,我们参考手册解码出来也是对的。
再回到 pwntools,真地很简单快捷,不需要对照手册一个一个去看了。
还可以指定 endian 和 bits,很方便:
$ python3
>>> from pwn import *
>>> asm('call 0x80482e0', vma=0x804841b, os='linux', arch='i386', endian='little', bits='32')
b'\xe8\xc0\xfe\xff\xff'
因此,这个对于学习处理器 ISA 和指令集编码会有非常特别的意义。
再试 pwntools
其实更为简单的使用方法是直接用 pwntools 的 docker 镜像:
$ docker run -it pwntools/pwntools /bin/bash
pwntools@7edec355c114:~$ python2.7
>>> from pwn import *
当前支持的架构列表:
['aarch64', 'alpha', 'amd64', 'arm', 'avr', 'cris', 'i386', 'ia64', 'm68k', 'mips', 'mips64', 'msp430', 'powerpc', 'powerpc64', 's390', 'sparc', 'sparc64', 'thumb', 'vax']
比较可惜的是 riscv 和 riscv64 暂时还没支持,不过有同学似乎加了,好像也不是很复杂:CTF/2018/HITCON-CTF/Baldis-RE-Basics at master · O…。
因为 qemu 已经支持 riscv 了,所以 spike 可以换回 qemu,而 riscv binutils 部分记得确保已经安装。
三用 pwntools
安装 pwntools 后,还可以使用类似这样的工具:asm, phd,很好用:
$ docker run -it pwntools/pwntoolspwntools@7edec355c114:~$ asm ‘mov eax, 0x1’ | phd
四用 pwntools
默认情况下,对于 X86 架构,pwntools 用的 intel 语法,见 pwnlib/asm.py
:
def _objdump():
path = [which_binutils('objdump')]
if context.arch in ('i386', 'amd64'):
path += ['-Mintel']
return path
def _arch_header():
prefix = ['.section .shellcode,"awx"',
'.global _start',
'.global __start',
'_start:',
'__start:']
headers = {
'i386' : ['.intel_syntax noprefix'],
'amd64' : ['.intel_syntax noprefix'],
'arm' : ['.syntax unified',
'.arch armv7-a',
'.arm'],
'thumb' : ['.syntax unified',
'.arch armv7-a',
'.thumb'],
'mips' : ['.set mips2',
'.set noreorder',
],
}
return '\n'.join(prefix + headers.get(context.arch, [])) + '\n'
这个语法不是 Linux GCC 的 AT&T
默认语法,如果不习惯,可以改造一下,用 locate 工具找到 asm.py,修改上面的两处即可:
- 把
-Mintel
改为-Matt
。 - 把
.intel_syntax nonprefix
去掉
这样就可以直接用 AT&T 语法了:
$ python3
>>> from pwn import *
>>> asm('mov $0, %eax')
b'\xb8\x00\x00\x00\x00'
>>> disasm(b'\xb8\x00\x00\x00\x00')
' 0: b8 00 00 00 00 mov $0x0, %eax'
作为对比,如果不修改的话:
>>> asm('mov eax, 0')
b'\xb8\x00\x00\x00\x00'
也可以这样:
$ asm 'mov $0, %eax' | phd
00000000 b8 00 00 00 00 │····│·│
00000005
上述用法其实也可以用到实际编码中,可以用 objdump -M
来指定反汇编的语法,用 .intel_syntax nonprefix
来指定 gas 汇编时的汇编语言语法。
小结
有了 pwntools,就可以更为方便地学习和理解指令集编解码,也可以用于分析开发中遇到的相关实际问题。
猜你喜欢:
- 我要投稿:发表原创技术文章,收获福利、挚友与行业影响力
- 知识星球:独家 Linux 实战经验与技巧,订阅「Linux知识星球」
- 视频频道:泰晓学院,B 站,发布各类 Linux 视频课
- 开源小店:欢迎光临泰晓科技自营店,购物支持泰晓原创
- 技术交流:Linux 用户技术交流微信群,联系微信号:tinylab
支付宝打赏 ¥9.68元 | 微信打赏 ¥9.68元 | |
请作者喝杯咖啡吧 |