泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:http://tinylab.org
请稍侯

实例讲解支持多种架构指令集编解码的 pwntools 工具

Wu Zhangjin 创作于 2020/01/09

By Falcon of TinyLab.org Dec 09, 2019

背景简介

之前在群里发起了一个讨论:

不用 gcc 如何实现代码的编解码,类似 MIPS Linux 内核中用到的 UASM。

大家的回复总结如下:

而来自龙芯的张老师提到了一个 Python 的库,这个库是:

安装的过程中,发现它依赖 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,修改上面的两处即可:

  1. -Mintel 改为 -Matt
  2. .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,就可以更为方便地学习和理解指令集编解码,也可以用于分析开发中遇到的相关实际问题。



Read Latest: