1 引言

提权漏洞(Privilege Escalation)脱离了纯粹的 CTF 竞赛范围,在真实的红队评估与 APT 攻击的后渗透阶段(Post-Exploitation)是至关重要的核心环节。它标志着攻击者从初始访问(Initial Access)向获取系统全面控制权(Total Control)的转变。

2 提权的基本概念与分类

提权是指攻击者利用操作系统、应用程序、服务或系统配置中的安全缺陷,将自身拥有的受限低权限(如普通的 Web 服务进程用户 www-data、访客用户)提升至更高级别权限(如 Linux 的 root 权限或 Windows 的 NT AUTHORITY\SYSTEM / Administrator 权限)的攻击过程。

  • 垂直提权 (Vertical Privilege Escalation): 核心在于权限层级的跨越。例如从普通用户 leviathan 越权成为超级管理员 root。攻击者通常利用内核漏洞(Kernel Exploits)、服务配置不当(如 SUID 错误配置、弱文件权限)或劫持计划任务(Cron Jobs)来实现。

  • 水平提权 (Horizontal Privilege Escalation): 核心在于信任边界的横向移动。通常涉及越权访问控制缺陷(IDOR),攻击者在保持当前权限等级不变的情况下,获取同级别普通用户 B 的权限(通常是为了获取 B 目录下的敏感文件、窃取凭证或特定执行权限)。

在二进制漏洞利用中的应用: 在 Pwn 等二进制攻防场景中,提权往往与内存破坏漏洞(如栈溢出、堆溢出、UAF)紧密结合。攻击者通过劫持程序的执行流(控制 EIP/RIP 寄存器),结合 ROP (Return-Oriented Programming) 等技术绕过现代操作系统的内存缓解机制(如 NX/DEP, ASLR, PIE),最终执行恶意的 Payload 以提升当前进程的执行权限。

3 Shellcode 技术原理与实现

Shellcode 是一段用于利用软件漏洞而执行的机器码,通常作为漏洞利用的 Payload 注入到目标进程的内存空间中。其最经典的目的是为攻击者提供一个与操作系统交互的命令行外壳(Shell)。

3.1 Linux Shellcode

在 Linux 环境中,当我们发现具备高权限(例如设置了 SUID 位且属主为 root)的二进制程序存在缓冲区溢出漏洞时,可通过执行自定义的 Shellcode 来获取 root 权限的 Shell。

  • 系统调用 (System Calls): Linux Shellcode 的核心在于直接触发内核级的系统调用(例如调用 execve 函数启动 /bin/sh)。
  • 架构差异:
    • x86 架构: 通过设置特定的寄存器(如将系统调用号 0x0b 存入 eax),并通过执行 int 0x80 软中断指令从用户态切换至内核态。
    • x64 架构: 使用 syscall 指令触发调用,且系统调用号和传参寄存器(如 rdi, rsi, rdx)与 32 位系统完全不同。
  • 字符约束: 为了防止 Shellcode 在程序的字符串处理函数(如 strcpy, strlen)中被异常截断,编写汇编代码时必须严格避免出现空字符 (\x00)。通常需要采用异或操作(XOR)或使用低位寄存器(如使用 al 代替 eax)来进行规避。

3.1.1 X86架构下

在 32 位 Linux 环境中,系统调用的触发依赖于软中断指令 int 0x80。传参机制严格遵循特定的寄存器约定:

  • EAX:存储系统调用号(execve 的调用号为 11,即 0x0b)。
  • EBX:存储第一个参数(指向可执行文件路径字符串的指针,如 "/bin/sh")。
  • ECX:存储第二个参数(指向参数数组 argv 的指针,通常设为 NULL)。
  • EDX:存储第三个参数(指向环境变量数组 envp 的指针,通常设为 NULL)。 核心技术难点(消除 Null 字符): 在构造 Payload 时,如果直接使用 mov eax, 0x0b 或直接在内存中定义带结束符的字符串 "/bin/sh\x00",会在生成的机器码中引入大量的 \x00 字节。这会导致 strcpy 等存在溢出漏洞的字符串处理函数提前截断 Payload。因此,需要通过寄存器自异或(XOR)和栈操作(Stack Push)来动态构造参数。

标准汇编实现结构:

; 清空寄存器,准备 NULL 字节,同时避免机器码中出现 \x00
xor eax, eax    
push eax            ; 将 NULL (0x00000000) 压入栈,作为字符串的结束符

; 将 "/bin//sh" 压入栈。使用双斜杠是为了满足 4 字节对齐,不影响路径解析
push 0x68732f2f     ; 压入 "n/sh" (小端序: "//sh")
push 0x6e69622f     ; 压入 "/bi"  (小端序: "/bin")

mov ebx, esp        ; EBX 指向栈顶,即字符串 "/bin//sh\x00" 的首地址

; 配置 ECX 和 EDX 为 NULL
xor ecx, ecx        ; ECX = 0 (argv = NULL)
xor edx, edx        ; EDX = 0 (envp = NULL)

; 配置系统调用号
mov al, 0x0b        ; 使用 8 位寄存器 AL 赋值 11,避免 mov eax, 0x0b 产生的空字节

; 触发系统调用
int 0x80            

3.1.2 X64架构下

在 64 位 Linux 环境中,系统调用的机制发生了重大变化。中断指令被更高效的 syscall 指令取代,系统调用号和传参寄存器也进行了重构:

  • RAX:存储系统调用号(execve 的调用号变更为 59,即 0x3b)。
  • RDI:存储第一个参数(文件路径指针)。
  • RSI:存储第二个参数(argv 指针)。
  • RDX:存储第三个参数(envp 指针)。

架构差异带来的改变: 64 位架构的寄存器宽度增加到 8 字节(64 bits)。在构造路径字符串时,可以直接将完整的 8 字节字符串放入寄存器然后压栈,极大地简化了指令序列。同时,依然需要利用 xor 等位运算技术来保证机器码的纯净(无 \x00)。

标准汇编实现结构:

; 清空 RSI 和 RDX,配置第二个和第三个参数为 NULL
xor rsi, rsi        ; RSI = 0
xor rdx, rdx        ; RDX = 0

push rsi            ; 将 NULL 压入栈,作为字符串的截断符

; 构造 8 字节对齐的路径字符串 "//bin/sh" 
mov rbx, 0x68732f6e69622f2f 
push rbx            ; 将 "//bin/sh" 压入栈中

mov rdi, rsp        ; RDI 指向栈顶,即 "/bin/sh\x00" 的地址

; 配置系统调用号
xor rax, rax        ; 清空 RAX 
mov al, 0x3b        ; RAX = 59 (execve)

; 触发系统调用
syscall            

3.2 Windows Shellcode

相比于 Linux 的直接系统调用,Windows 平台的 Shellcode 编写复杂度显著提升。这主要归因于 Windows API 的封装机制以及 ASLR(地址空间布局随机化)的严格限制。

  • 动态地址解析: Windows 系统的内核系统调用号(Syscall ID)随操作系统版本的更迭变动频繁,且库函数基址随机化。因此,Shellcode 无法硬编码 API 的内存地址,必须在内存中动态定位关键系统函数。
  • PEB 解析技术 (核心步骤):
    1. 利用段寄存器 FS(32位)或 GS(64位)动态定位进程环境块 (PEB, Process Environment Block)。
    2. 遍历 PEB 中的 PEB_LDR_DATA 结构链表,获取当前进程已加载的模块信息。
    3. 通过链表遍历,精准定位到核心系统库 kernel32.dll 的基址。
  • 导出表遍历与哈希匹配: 获取到 kernel32.dll 的基址后,进一步解析其 PE 格式的导出表(Export Directory)。为了缩小 Shellcode 体积并规避硬编码字符串,通常采用哈希算法匹配函数名,从而动态获取 GetProcAddress 和 LoadLibraryA 的绝对内存地址。
  • 执行逻辑: 利用上述两个基础 API 函数,加载后续攻击所需的动态链接库(如 user32.dll),并调用诸如 WinExec 或 CreateProcessA 的 API 来弹出一个 cmd.exe 终端或建立反向连接(Reverse Shell)。