二进制漏洞工具的准备
pwngdb与GDB
查看信息
1 | info functions #查看所有function |
程序运行参数
1 | set args 指定运行时参数 (set args "xxx") |
调试
调试过程主要包含四个步骤:
| Step 步骤 | Description 描述 |
|---|---|
Break |
在各个关键位置设置断点 |
Examine |
运行程序,并检查程序在这些阶段的运行状态。 |
Step |
通过程序来观察它如何响应每个指令以及用户输入的情况。 |
Modify |
在特定的断点处修改特定寄存器或地址中的值,以研究这将如何影响程序的执行过程。 |
设置断点
调试的第一步是设置 breakpoints ,使其在达到特定位置或满足特定条件时停止执行。这样我们可以检查程序在那一刻的状态以及寄存器中存储的值。此外, Breakpoints 还允许我们在该时刻暂停程序的执行,从而可以逐条执行每条指令,观察它们如何改变程序及其中的数值。1
2
3break 设置断点,简写为b(b *0x401000)
info break 查看设置好的断点,简写为i b
delete 删除断点,简写为d(d 1删除第一个断点)
我们可以在特定的地址或某个特定函数处设置断点。要设置断点,可以使用 break 或 b 命令,并附带想要暂停执行的地址或函数的名称。
运行程序
运行
1 | run 运行程序 简写为r |
检查
要手动检查任何地址或寄存器,我们可以使用 x 命令,其格式为 x/FMT ADDRESS ,例如 help x 表示我们要检查的地址或寄存器。 ADDRESS 就是我们要检查的地址或寄存器,而 FMT 则表示检查的格式。 FMT 这个检查格式可以包含三个部分:
| Argument 论点/论据 | Description 描述 | Example 示例 |
|---|---|---|
Count |
我们希望重复检查的次数 | 2, 3, 10 |
Format |
我们希望结果以何种格式呈现出来? | x(hex), s(string), i(instruction) |
Size |
我们想要检查的内存大小 | b(byte), h(halfword), w(word), g(giant, 8 bytes) |
查看运行数据
1 | print 打印有符号的变量、字符串、表达式等的值,可简写为p |
pwntools
shellcode转换
每个 x86 指令和每个寄存器都有其对应的 binary 机器代码(通常表示为 hex ),这些机器代码就是直接传递给处理器以指示其执行何种指令的二进制代码(通过指令周期实现)。
此外,各种常见的指令和寄存器的组合也有各自的机器代码。例如, push rax 指令对应的机器代码是 50 ,而 push rbx 指令对应的机器代码则是 53 ,以此类推。当我们使用 nasm 来组装代码时,就能将我们的汇编指令转化为相应的机器代码,这样处理器就能理解这些指令了。
现在,我们可以使用 pwn asm 将任何汇编代码组装成机器代码,具体步骤如下:1
pwn asm 'push rax' -c 'amd64'
提取shellcode
现在我们已经了解了如何将每条汇编指令转换为机器代码(反之亦然),那么接下来我们就来看看如何从任何二进制文件中提取外壳代码吧。
一个二进制文件的 shell 代码仅包含其可执行的 .text 部分,因为 shell 代码本就是用于直接执行的。要提取 .text 部分的 pwntools 数据,我们可以使用 ELF 库来加载 elf 二进制文件,这样就能对其执行各种操作了。
1 | >>> from pwn import * |
我们可以对其执行各种 pwntools 功能。我们需要从可执行文件 .text 部分中提取机器代码,可以使用 section() 函数来实现这一操作,具体步骤如下:1
>>> file.section(".text").hex()
我们看到,我们非常轻松地提取出了该二进制程序的 shellcode。现在,我们可以将这一过程转化为一个 Python 脚本,这样我们就可以快速地使用它来提取任何二进制程序的 shellcode 了:
1 | import sys |
加载shellcode
现在我们已经有了 shell 代码,接下来尝试运行它,这样就能测试我们之前准备的任何 shell 代码了。不过,上面提取的 shell 代码不符合 Shellcoding Requirements 的要求,我们将在下一节讨论这个问题,所以这段代码无法运行。为了演示如何运行 shell 代码,我们将使用以下符合 Shellcoding Requirements 要求的 shell 代码:
要使用 pwntools 执行我们的外壳代码,我们可以调用 run_shellcode 函数,并将我们的外壳代码传递给该函数,具体步骤如下:1
2
3
4
5from pwn import *
context(os="linux", arch="amd64", log_level="error")
+ex
sc = bytes.fromhex(file)
run_shellcode(unhex(sc).interactive()
我们在 shell 代码使用了 unhex() 这个指令,将其转换回二进制形式。
也可以保存为文件:1
ELF.from_bytes(unhex('4831db66bb79215348bb422041636164656d5348bb48656c6c6f204854534889e64831c0b0014831ff40b7014831d2b2120f054831c0043c4030ff0f05')).save('helloworld')
pwn脚本的编写
全局与环境配置
在脚本开头设置目标架构和调试级别,能省去后续生成 shellcode 或 ROP 链时的很多麻烦。
1 | from pwn import * |
建立交互对象
无论是本地调试还是远程打靶机,都会返回一个统一的 IO 管道对象(通常命名为 io, p 或 sh),后续的所有收发操作都基于此对象。
1 | # 本地进程交互 (常用于调试) |
数据接收
接收目标程序的输出,用于同步执行流或泄漏(Leak)内存地址。注意:在 Python 3 中,强烈建议使用字节串 b'...' 进行匹配。
1 | io.recv(numb=2048) # 基础接收:最多接收 numb 个字节的数据。 |
数据发送
将 Payload 发送给目标程序。发送时同样需要注意是否需要附带换行符。
1 | io.send(payload) # 基础发送:直接发送 payload,不附加任何额外字符。常用于程序使用 read() 等按字节读取的情况。 |
ELF 文件解析
避免在脚本中硬编码地址(Hardcoding),利用 ELF 模块动态获取函数、符号的地址,提高脚本在不同环境下的兼容性。
1 | elf = ELF('./pwn_file') |
交互与辅助
取得 Shell 后,或者在开发过程中需要切入动态调试。
1 | io.interactive() |
附:标准 Exploit 脚本模板
1 | from pwn import * |
IDA使用
快捷键
1 | r: 转换成字符/字符串 |
objdump
objdump 是 GNU Binutils 工具集中的一个重要命令行工具,用于显示目标文件(object files)和可执行文件的各种信息。它是 Linux 系统下进行二进制分析、逆向工程和调试的利器。
objdump 的主要功能包括:
- 反汇编二进制文件
- 查看文件头部信息
- 显示节区(section)内容
- 查看符号表
- 显示重定位信息
- 分析文件结构
常用选项详解
反汇编相关选项
1 | -d, --disassemble # 反汇编包含代码的节区 |
节区信息
1 | -h, --section-headers # 显示节区头部信息 |
符号表选项
1 | -t, --syms # 显示符号表 |
文件头部信息
1 | -f, --file-headers # 显示文件头部信息 |
其他
1 | -l, --line-numbers # 显示行号信息(需要调试信息) |
