题目
观察分析
开始的开始我们先确认一下这份文件的安全性措施有哪些:
1 | └─$ checksec regularity |
注意到这个程序并没有NX(No-eXecute)措施,这会成为我们后续攻击的最核心的前提条件!
用IDA打开文件:
这个read()比较可疑,但是在伪代码页面没有找到漏洞,所以我们转去查看汇编。
在IDA一开始的这个默认页面双击read:
可以发现程序分配了0x100位的Buffer,但是却读取了0x110位数据。也就是说我们可以利用这个Buffer Overflow的漏洞。
但是由于在Exports页面没有找到任何可以利用的函数:
所以这道题我们需要另辟蹊径。
我们回来重新仔细看一下read()函数的汇编代码:
1 | ; signed __int64 read() |
注意到rsi
是当前buffer的起始地址。而同时在阅读main()函数的汇编时可以发现这行命令:
1 | jmp rsi |
所以我们可以写入一段自己手写的/bin/sh命令的汇编代码,并将返回地址修改成rsi,这样程序就会自动运行我们手写的/bin/sh命令。(不过要注意,这个攻击能够奏效最核心的前提是栈是可执行的,即没有NX(No-eXecute)。)
Exploit
这道题在Windows系统里做和在Linux里不太一样,所以我会分别介绍他们所需的操作。
Linux
首先讲在Linux系统下的解题过程,因为可以用便携的命令
1 | next(elf.search(asm('jmp rsi'))) |
这个命令的作用是:
asm('jmp rsi')
:将汇编指令jmp rsi
转换为机器码(字节码);elf.search(...)
:在目标 ELF 文件中搜索包含这段机器码的地址(也就是查找 gadget);next(...)
:获取搜索到的第一个结果,即jmp rsi
的实际地址;
而
1 | asm(shellcraft.sh()) |
这个命令是 PwnTools 库中提供的一个自动生成 shellcode 的便捷方法,具体作用如下:
shellcraft.sh()
:生成一段用于执行/bin/sh
的汇编代码(默认适用于当前脚本所处平台(这里是Linux)的 64 位execve("/bin/sh", NULL, NULL)
系统调用);asm(...)
:将上面的汇编代码转换为真实的机器码(也就是可以直接执行的 shellcode 字节串);
这个命令是在Windows里也可以使用的,只不过需要点额外的声明。
Exploit代码:
1 | from pwn import * |
解释一下flat()
这个函数:
它把一个结构化的数据(如字节串、整数、地址、dict 布局)转换成连续的 bytes
类型,供你发送或写入程序。用于自动构造二进制 payload。
这段代码:
1 | payload = flat({ |
传入的是一个 字典(dict)结构,表示希望构造一个内存布局:
- 从 offset 0 开始:放入 shellcode
- 从 offset 256 开始:放入跳转地址
JMP_RSI
(会被自动转成小端格式)
而flat()
会自动:
- 计算出 offset 之间的 padding(自动补零或 NOP)
- 把整数
JMP_RSI
自动转换为 64 位小端地址(等价于p64(JMP_RSI)
) - 最终拼接出一段完整的、可发送的 payload
而且它会自动把JMP_RSI的值转成小端序(Liitle-Endian),不需要我们额外使用p64()
函数。
Windows
因为在Windows里我们无法使用
1 | next(elf.search(asm('jmp rsi'))) |
这个命令,所以需要手动查找jmp rsi
的地址。
这里介绍3种方法:
1. 在主页面就可以直接查看。用鼠标点击
1 | jmp rsi |
这行命令,然后会在页面下方看到地址:
2. 使用快捷键Alt+t搜索
1 | rsi |
或者
1 | jmp rsi |
(注意是5个空格,多了少了都搜不出来。)
3. 点击左上角的菜单View,然后依次选择Open subviews里的Disassembly:
再往下翻手动查找到:
确定地址了之后剩下的就和之前的一样了:
1 | from pwn import * |