软件安全实验——lab6(缓冲区溢出3:返回导向编程技术ROP)

Posted 大灬白

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了软件安全实验——lab6(缓冲区溢出3:返回导向编程技术ROP)相关的知识,希望对你有一定的参考价值。

1、举例详细解释什么是Return-orientd Programming ROP?(至少两个例子:x86 和arm)

  ROP(Return-oriented programming),即“返回导向编程技术”。其核心思想是在整个进程空间内现存的函数中寻找适合指令片断(gadget),并通过精心设计返回堆栈把各个gadget拼接起来,从而达到恶意攻击的目的。构造ROP攻击的难点在于,我们需要在整个进程空间中搜索我们需要的gadgets,这需要花费相当长的时间。但一旦完成了“搜索”和“拼接”,这样的攻击是无法抵挡的,因为它用到的都是内存中合法的的代码,普通的杀毒引擎对ROP攻击是无计可施的。

  ROP(Return-orientedprogramming)是1种全新的攻击方式,它利用代码复用技术。攻击者扫描已有的动态链接库和可履行文件,提取出可以利用的指令片断(gadget),这些指令片断均以ret指令结尾,即用ret指令实现指令片断履行流的衔接。最初ROP攻击实现在x86体系结构下,随后扩大到各种体系结构.与以往攻击技术不同的是,ROP歹意代码不包括任何指令,将自己的歹意代码隐藏在正常代码中。因此,它可以绕过WX的防御技术。

  面向返回编程(ROP)是一种强大的技术,用于对抗常见的攻击预防策略。特别地,ROP对于绕过地址空间布局随机化(ASLR)1和DEP2非常有用。当使用ROP时,攻击者在从函数返回到程序中的其他位置之前,使用他/她对堆栈的控制。除了非常坚固的二进制文件之外,攻击者可以很容易地找到位于固定位置(绕过ASLR)和可执行(绕过DEP)的部分代码。此外,将几个有效负载链接起来以实现(几乎)任意的代码执行相对简单。

  ROP的指导思想就是希望所有的恶意攻击都通过现存函数的指令片断串连而成。这种指令片断通过跳转指令(x86上是RET和JMP, ARM上是pc的相关指令)可以相互连接的逻辑片断,称为gadget。ROP攻击最终的体现就是一串gadgets。ROP的强大之处在于,只要找到一个溢出缺陷,并且堆栈空间足够大,就可以实现任何逻辑,而且ROP攻击一直是各大病毒扫描引擎的难题。

payload : padding + address of gadget 1 + address of gadget 2 + … + address of gadget n

  在这样的构造下,被调用函数返回时会跳转执行 gadget 1,执行完毕时 gadget 1 的 RET 指令会将此时的栈顶数据(也就是 gadget 2 的地址)弹出至 eip,程序继续跳转执行 gadget 2,以此类推。

  我们从ROPEmporium网站找的rop实战题目, ROP Emporium 挑战是用来学习 ROP 技术的一系列挑战,网址:https://ropemporium.com/,一共有8道题,难度从低到高:

我们这以第一题ret2win为例子:https://ropemporium.com/challenge/ret2win.html,它有四个不同指令集架构版本x86、x86_64、ARMv5、MIPS:

它的最终目的是让我们调用ret2win函数输出flag。

(1)ROP在X86指令集上的实例

我们先来看一下x86指令集架构的EIF文件:

运行ret2win32程序:

程序是使用read()函数读取输入56bytes数据去填充在32byte的缓冲区,所以这有一个缓冲区溢出漏洞。
python -c “print ‘A’*100” | ./ret2win32

Gdb调试,
info function
查看程序使用的函数:

我们在上面看到了main函数,
disas mian
查看main函数:

上面关键的就是pwnme函数,开始地址在0x0804858b,返回地址在0x08048590,
disas pwnme
查看pwnme函数:

我们在pwnme函数退出之前下一个断点
b * 0x08048626
就可以看到当时栈中的值的情况:

运行程序r,输入字符串AAAA,程序会停在我们设置的断点0x08048626处:

查看从当前esp位置开始的48个四字节16进制数据:
x/48wx $esp

所以我们只要把返回地址0x08048590改为ret2win函数的地址,就能让程序在返回时调用ret2win函数执行system输出flag:

输入的字符串AAAA的开始地址和pwnme的返回地址距离时44个字节,所以我们构建的payload是
python -c “print ‘A’*44 + ‘\\x2c\\x86\\x04\\x08’” | ./ret2win32

这里我们已经成功调用了ret2win函数,只是我们没有flag.txt文件,我们用root权限创建一个只要root账户可读的flag.txt文件内容是
ROPE{a_placeholder_32byte_flag!)}

再运行payload:
python -c “print ‘A’*44 + ‘\\x2c\\x86\\x04\\x08’” | ./ret2win32

这个只是最简单的第一关ROP,只需要调用现成ret2win32就行了,后面的关卡就会需要多次拼接逻辑片断才能获得flag了。

(2)ROP在ARM上的可行性

由于x86(x64)跟ARM的PCS规范不同,而且指令格式也不一致,但ROP的思想同样可以作用于ARM平台两者的指令对比:

寄存器作用也不尽相同
r15即pc,是程序计数器,相对于x86上的EIP
r14即lr,连接寄存器,相对于x86上没有对应
r13即sp,堆栈寄存器,相对于x86上的ESP
r11(r7)即fp, 栈底寄存器,相对于x86上的EBP
r4 – r10, r12,作为局部变量使用
r0 – r3,保存参数的前三个参数,如果存在四个参数,则放入堆栈,这个是跟x86最大的区别
r0保存函数的返回值, 跟x86上保存在EAX一致
ARM里pc可以直接修改,这在一定程度上为ROP攻击提供便利
android上实施ROP攻击
目前市面上大部android手机都是基于ARM平台的,因此理论上在android上实现ROP攻击是可行的,但也需要注意到,android上的libc是bionic libc,而不是普遍使用glibc,google对其提高了其安全性,把大部分涉及r0的指令都优化掉了,这就大大提高了ROP攻击的难度。

2、举例描述一个远程缓冲区溢出,思考和本地溢出的区别,攻击难点在哪里?

溢出攻击通常可以分为远程溢出攻击和本地溢出攻击,其中尤其以远程溢出攻击的威胁最大。利用远程溢出攻击,黑客可以在没有任何系统账号的情况下获得系统的最高控制权,对其进行未经许可的操作。包括获取用户文件和传输中的数据, 获取超级用户的权限,对系统的非法访问,进行不许可的操作, 如拒绝服务,涂改信息,暴露信息等。
远程缓冲区溢出攻击,是指远程用户通过网络攻击具有溢出漏洞的服务器(或主机)。他们针对不同的系统和不同的漏洞, 发送输入参数为超长字符串的数据包到远程主机相应的应用 端口(如http的 端口等),超长字符串造成的溢出使得服 器找不到返回地址而中止应用程序甚至造成拒绝服务的效果或死机等。如果数据溢出后的返回地址指向精心设计的恶意代码,则有可能取得超级用户权,可以在攻击目标内植入木马程序等许多未经许可的操作。但是,无论是因程序设计考虑不全的普通溢出,还是精心设计的攻击溢出,都需要发送一个超长的字符串作为输入参数的数据包到缓冲区里造成溢出。
远程缓冲区溢出是指溢出行为发生在远程主机上,而非攻击者本主机.远程缓冲区溢出攻击实质就是一种入侵行为,主要是攻击者为了得到一个基本访问权限,如果被溢出的进程是以root运行的话,那么溢出后直接是rootshell权限,攻击者就具有root访问权限。另外,利用远程缓冲区溢出漏洞,也可以发起拒绝服务攻击。
远程缓冲区溢出原理和本地缓冲区溢出原理一一样,不同之处在于远程缓冲区溢出需要利用套接口进行远程连接,本小节通过一一个例子来分析远程缓冲区溢出的工作原理和攻击过程。分为两个部分:一个是有缓冲区溢出漏洞的服务程序,一个是针对该漏洞编写的利用程序。假设服务端程序运行的机器操作系统类型是linux.CPU类型是x86,而利用程序则运行在另外一个类似配置的linux/x86机器上运行,进行远程攻击。

3、描述arm和x64下的缓冲区溢出,比较和x86的差异。

由于每个平台的ABI(应用二进制接口)不同,所以各个平台上调用子函数时对参数、局部变量、返回地址和返回值的处理也是不一样的,目前Android的大部分终端使用的ARM平台,遵循的ABI叫做ATPCS,即ARM-THUMB procedure call standard(ARM-Thumb过程调用标准)的简称。
ABI主要解决了下面三个问题,
1、被调用函数如何获取函数参数,即函数参数的传递规则。
2、函数中的局部变量如何定义和使用。
3、函数调用结束后如果回到被调用位置继续执行,调用函数如何拿到返回值。
参数传递规则:
根据参数个数是否固定可以将子程序参数传递规则分为以下两种:参数个数可变的子程序参数传递规则和参数个数固定的子程序参数传递规则,这里主要以参数个数固定且不包含浮点型描述为主。

注意:

  1. 前四个参数是顺序放入R0~R3,但超过四个的参数是按参数倒序存入栈中,即最后一个参数最先压入栈中。
  2. 被调用的子函数在返回前无需恢复寄存器R0~R3。
  3. 浮点参数将按照下面的规则传递:
    (1) 各个浮点参数按顺序处理;
    (2) 为每个浮点参数分配FPU寄存器。
    分配的方法是,满足该浮点参数需要的且编号最小的一组连续的FPU寄存器。

局部变量:
优先存储在寄存器中:
ARM:依次存储在R4~R11。
Thumb:依次存储在R4~R7。
不能在寄存器中存储的,则在当前栈帧中分配。
PS:大于32bit的变量和指针引用的变量会存储在栈中,eg:数组、对象等等。

函数返回方式:
除了叶节点函数(Non-leaf-Function),返回地址都存储在栈上。注意这并不是由调用指令直接把返回地址存在栈上。ARM平台的调用指令BL和BLX会把下一条指令地址保存在LR寄存器中,接着CPU去执行子函数代码,在子函数的代码中,为了能够继续调用子函数,所以需要把LR寄存器的值保存在栈中,并在函数返回时通过POP或者BX指令恢复到PC寄存器,

返回值:
如果结果为一个32位的整数,可以通过寄存器R0返回。
如果结果为一个64位整数,可以通过寄存器R0和R1返回,依此类推。
如果结果为一个浮点数,可以通过浮点运算的寄存器F0、D0或S0返回。
如果结果为复合型的浮点数(如复数),可以通过寄存器F0~FN或者D0~DN返回。
对于为数更多的结果,需要通过内存来传递。
缓冲区溢出:
由于ARM平台下非叶节点函数的返回值最终还是存储在栈中,故仍可以简单的利用栈溢出漏洞。
x86和x64的区别:
第一个主要区别就是内存地址的大小。这没啥可惊奇的: 不过即便内存地址有64位长用户空间也只能使用前47位要牢记这点因为当你指定一个大于0x00007fffffffffff的地址时会抛出一个异常。那也就意味着0x4141414141414141会抛出异常而0x0000414141414141是安全的。当你在进行模糊测试或编写利用程序的时候我觉得这是个很巧妙的部分。
有三个CPU寄存器与栈有关:
(1)SP(StackPointer,x86指令中为ESP,x64指令中为RSP),即栈顶指针,它随 着数据入栈出栈而变化;
(2)BP(Base Pointer,x86指令中为EBP,x64指令中为RBP),即基地址指针,它 用于标示栈中一个相对稳定的位置,通过BP,可以方便地引用函数参数及局部变量;
(3)IP(InstructionPointer,x86指令中为EIP,x64指令中为RIP),即指令寄存器, 在调用某个子函数(call指令)时,隐含的操作是将当前的IP值(子函数调用返回后下 一条语句的地址)压入栈中。

以上是关于软件安全实验——lab6(缓冲区溢出3:返回导向编程技术ROP)的主要内容,如果未能解决你的问题,请参考以下文章

软件安全实验——lab5(Buffer_Overflow缓冲区溢出攻击)

软件安全实验——lab9(缓冲区溢出:return-to-libc绕过非可执行堆栈)

软件安全实验——pre5(缓冲区溢出漏洞预习)

2018-2019-1 20165228 《信息安全系统设计基础》缓冲区溢出漏洞实验报告

2018-2019-1 20165320 《信息安全系统设计基础》 缓冲区溢出漏洞实验

使用Linux进行缓冲区溢出实验的配置记录