CVE-2014-4322

来源:互联网 发布:淘宝开店视频教学视频 编辑:程序博客网 时间:2024/06/05 09:06

声明:由于本人水平有限,上面所有内容中出现的错误请各位博友和大牛批评指正,毕竟我还只是个"脚本小子"......

Android contributions for MSM是一个Android的MSM项目,该项目的主要目的是建立一个包含高通MSM芯片组的Android平台。QSEECOM driver是一个提供了ioctl系统调用接口到用户空间的客户端进行通信的驱动程序。Qualcomm InnovationCenter(QuIC)Android contributions for MSM设备中使用的QSEECOM driver for the Linux kernel 3.x版本中的drivers/misc/qseecom.c文件存在安全漏洞,该漏洞源于程序没有正确验证ioctl调用中的‘offset’、‘length’和‘base’值。攻击者可借助特制的应用程序利用该漏洞获取权限或造成拒绝服务(内存损坏)。

预备知识:

高通的TrustZone平台被12年之后的所有使用高通芯片组设备所采用。然而,他们也同样允许其他的OEM对其作出修改或者添加一些自己的东西进去。

客观的讲,我相信高通的TrustZone platform设计初衷是好的,然而爆出漏洞也是情理之中,任何一套系统或产品都有缺点或者漏洞。同时由于高通芯片的覆盖设备范围之广,

影响之大,研究其漏洞原因就很有必要。

在过去几年中,Android不断有新的版本发布,很多安全机制被应用到Android上,像ASLR,DEX,SElinux等。虽然底层安全架构没有改变,但是现代设备上的防御机制变得比以前强大了很多。因此,想要获得更高权限变得非常困难,有时候不得不使用好几个漏洞一起来实现获得root权限的目的。

前面不断提到TrustZone,对于没有接触过的人一定很迷茫。下面就简单介绍一下。

1.什么是TrustZone?

根据ARM LTd的描述:TrustZone is a system-wide approach to security for a wide array of client and server computing platforms。别整英文的了,直接说我的理解吧。TrustZone是一个系统范围的方法,广泛应用于客户端和服务器的安全平台,包括手机,平板,可穿戴设备和企业系统。

其中在应用程序中启用的方法多种多样,包括支付保护技术,DRM(Digital Rights Management)数字版权管理,BYOD,提供企业解决方案等。简而言之,TrustZone就是一套让目标设备“安全执行的系统”。

2.如何实现TrustZone?

来看一下ARM架构下TrustZone系统的框架和设计原理:


为了执行安全的TrustZone代码,一种特殊的处理器被设计。这种处理器不但可以执行非安全代码,而且可以执行安全代码。

而其他的处理器只受限于去执行上图中“Normal World”中的代码。

TrustZone在Android设备上的目的有多种:

.verifying kernel integrity (验证内核完整性)

.Using the Hardware Credential storage(使用硬件证书存储)

.Secure Element Emulation for Mobile Payments(移动支付的安全要素)

.Implementing and Manging Secure Boot(实现安全启动)

.DRM(Digital Rights Management)(数字版权管理)

.Accessing platform hardware features(访问硬件平台特性)

为了确保整个系统的安全,而不仅仅是应用处理器。当进入“Secure World”时特殊的标示位会被置位,当返回“Normal World”时

特殊标示位复位。

外围的设备可以访问这些位的状态,因此可以推断出我们是否正在运行在“Secure World”。


3.TrustZone的安全模式是怎样工作的?

我们都知道,在"Normal World"时,在用户模式和内核(超级)模式之间有明确的安全边界,也就是我们熟知的内核空间和用户空间的边界。不同模式由CPSR(程序当前状态寄存器)决定区别和管理。如下图:


在Linux内核中,M位[4:0]为b10000代表用户模式,此时可以执行常规的用户代码。

M位[4:0]为b10011来标示内核模式。

然而这只是在确定是在"Normal World"下,区分用户模式还是内核模式,而我们还不能知道自己当前是处于“Normal World”还是“Secure World”。那么就需要有一个单独的寄存器来区别当前是处于“Normal World”还是“Secure World”,这个寄存器的名字叫SCR(Secure Configuration Register),按照字面意思就是安全配置寄存器,如下图:


这个寄存器协处理器CP15的c1寄存器,也就意味这可以通过MRC/MCR操作码来访问这个寄存器。MRC/MCR是协处理器和ARM寄存器之间数据传输指令。对协处理器和相关操作码不清楚的话,请先跳到附录1阅读相关知识。如果清楚的话,那么我们继续。

配合CPSR寄存器,处在“Normal World”时不能直接修改SCR寄存器的值,然而能够执行SMC(管理方式调用的简称,用来直接向TrustZone内核发送请求指令的操作码) ,而这个SMC操作码只能来源于内核上下文,不能来自用户空间。

为了能调用TrustZone中的相关功能,就我们Android系统而言,Linux内核必须注册一些可以使用的服务,在需要的时候调用相关的SMC调用。

4.使用高通芯片组的Android设备怎么做的?

在使用高通芯片组的设备中,是通过一个名为“qseecom”-Qualcomm Secure Execution Environment Communication的设备驱动来实现的。


遥遥的路多斜(my eason’s song):

so the road ahead is pretty long,为了从用户模式的android应用程序触发去执行一段TrustZone中的代码我们必须有下面一些的可提升权限的漏洞:

.从没有许可的android 应用程序来提升到有一定的Android 用户权限。

.从有一定的Android用户权限提升到可以在Linux内核中执行代码的权限。

.从有Linux内核中代码执行权限到可在TrustZone中执行代码的权限。


揭开CVE-2014-4322的红盖头:

下面这一部分将为大家详细的分析,如何利用CVE-2014-4322这个Linux内核漏洞来达到内核中任意代码执行的目的。

0x00 漏洞概述:

Android contributions for MSM是一个Android的MSM项目,该项目的主要目的是建立一个包含高通MSM芯片组的Android平台。QSEECOM driver是一个提供了ioctl系统调用接口到用户空间的客户端进行通信的驱动程序。Qualcomm Innovation Center(QuIC)Android contributions for MSM设备中使用的QSEECOM driver for the Linux kernel 3.x版本中的drivers/misc/qseecom.c文件存在安全漏洞,该漏洞源于程序没有正确验证ioctl调用中的‘offset’、‘length’和‘base’值。攻击者可借助特制的应用程序利用该漏洞获取权限或造成拒绝服务(内存损坏)。

0X01 漏洞影响范围:

所有2012年2月之后生产的搭载有高通芯片组(基于”msm”内核)Linux kernel版本为3.x的设备都会受到这个漏洞的影响。

0X02 漏洞利用鸟瞰:

在准备知识的最后我们提到,让一个没有权限的应用程序变得具有在TrustZone中执行代码的权利这件事情是条漫漫长路。并且提出了三步曲,即:

.从没有许可的android 应用程序来提升到有一定的Android 用户权限。

.从有一定的Android用户权限提升到可以在Linux内核中执行代码的权限。

.从有Linux内核中代码执行权限到可在TrustZone中执行代码的权限。

在最近完成了从Linux内核到TrustZone的工作后,我们现在在寻一种能够在Linux内核执行代码的方式。

而在三部曲中我们还需要去获得一些权限,现在可以想到的是“mediaserver”进程。所以我们之后要做的还有从mediaserver来构建exploit chain达到从zero permissions到能在mediaserver中执行代码的权限。


因为我们想要去攻击Linux内核,那么显而易见,我们需要/dev目录下看一看驱动中有哪些驱动是比较低级权限的Android用户可以访问的,结果如图(需要先root手机,root工具+supersu最好):



不幸的是,我们看到这个结果列表很短,而且这些驱动都是比较通用的Android驱动,除了kgsl-3d0之外,其它的在所有设备上都有,并且被很多前人所研究过。

在经过一段时间对每个驱动的简单研究后,我发现,一个更有效的策略是将网撒的更大一点即扩大研究漏洞的数量,然后呢,一旦找到一个漏洞,我们仅仅就只需要另外一个漏洞与之结合使用来达到从Zero permissions 到 TrustZone的目的。

其实你若是从文章开头认真读下来的话,会发现在准备知识中我已经剧透过qseecom"driver就是我们要利用漏洞。

qseecom就是Android代码与TrustZone内核进行交互的负责人,通过一组定义的命令来实现。具体qseecom驱动的实现比较庞大复杂。不过准备知识中,我们已经有了比较详细的介绍,这对后面的理解至关重要。 



我们从上图中看到,和qseecom驱动进行交互需要的权限为system,意味着我们必须以system user-ID来运行,或者是我们是drmpc组的一个成员。其中system权限是比较难获得。  

然而,寻找在系统中寻找属于drmpc组的所有进程发现,下面这些成员属于drmpc组:

·        surfaceflinger(running with "system" user-ID)

·        drmserver(running with "drm" user-ID)

·        mediaserver(running with "media" user-ID)

·        keystore(running with "keystore" user-ID)

这并不是drmpc组的全部成员。在Linux内核中,每个进程都有一个名为“dumpable”的flag,这个flag决定了对应的进程是否可以被使用ptrace attach(附加)。一旦一个进程通过执行setuid或者setgid改变了其权限,这个flag就会被内核自动清除,来表明这个进程不能attach。 

因为“surfaceflinger”和“drmserver”进程在运行时修改他们的User ID,这么做的好处就是可以保护自己不被外来的ptrace attach。

然而“mediaserver”和“keystore”进程就不这么做。


通过ptrace来attach一个进程很有意思,因为这允许完全控制进程内存,因此能够在此进程中执行代码。那么这样的结果就是,任何以和“mediaserver”和“keystore”这两个进程有相同User ID运行起来的进程,通过ptrace attach,就有可能访问到qseecom驱动。

讲的好像很拗口,没关系,在这里总结一下:

总结起来就是,为了能成功访问到qseecom驱动,攻击者必须满足下列条件之一:

.获得在“surfaceflinger”,“mediaserver”,“drmserver”,“keystore”进程中执行代码的能力。

.以“system”,“drm”或者“keystore” User-ID运行一个进程。

.以“drmpc” Group-ID运行一个进程。

0X03 步步为营:

因为驱动程序负责处理用户输入,所以他们必须非常小心的对待哪些从不信任的用户提供的数据,并且对这些数据范围做检查--所有的由用户空间传入的参数被内核认为可能是“污染”。这听起来很轻松,可是往往内核开发人员在内核开发过程中会忽略对这些参数的检查,也就成了漏洞。

为了阻止内核开发者再犯这类错误,一些机制被引入到了内核代码中,来帮助编译器检测和防止这样的(越界)尝试。

内核中通过“__user”宏来标记变量是指向用户的虚拟地址空间所指地址的内存中的。

void __user *argp = (void __user *)arg;

这个宏是“noderef”属性的变量。这个属性是用来标记不能直接解引用的指针的。如果试图直接解引用一个被标记为“noderef”的指针,编译器就会报错并拒绝编译代码。

当内核希望从指针所指的位置读取或写入时,必须使用专门的内核函数确保位置所指实际上确实是在用户地址空间中的(而不是在内核空间)。


0x04 认识QSEECOM: 

驱动程序可以通过比较广泛的函数来与之进行通信,这些驱动中可能存在其独特的陷阱和广泛性的错误。

当一个字符设备在内核中注册时,它必须提供一个成员是函数指针的结构体,通过这个结构体,来实现针对不同驱动程序特有的操作,这些操作决定了驱动程序如何和系统进行交互。这映射出一个初步的驱动程序攻击层面,我们先来看看qseecom驱动对应的结构体file_operations中的内容:


在qseecom驱动程序中,值得关注的一个函数就是ioctl函数调用实现。

一般情况下,字符设备可以类似于系统中的其他文件一样来与之交互,比如,一个字符设备可以被打开读,写等(Linux系统一个比较核心和全局的思想就是:一切皆文件。)。然而有一个操作和普通文件不相同,那就是

ioctl函数(Input/Output Control)。

调用ioctl需要传入两个参数:要执行的命令command和提供验证功能的argument

关于ioctl的完整内容请自行阅读源码中相关部分。

说了这么多,下面我们来看一下qseecom_ioctl函数所支持的command吧。乍一看,似乎qseecom_ioctl函数支持相当大范围的commands,比如:

  • Sending command requests to TrustZone(向TrustZone 发送请求命令)
  • Loading QSEE TrustZone applications(加载QSEE TrustZone 应用程序)
  • Provisioning different encryption keys (配置不同类型的加密密钥)
  • Setting memory parameters for the client of the driver(为驱动的客户端设置内存参数)
下面来详细说一下什么是设置内存参数?

为了允许用户向TrustZone发送的大的请求和从TrustZone接受大的响应,qseecom驱动程序为用户提供了ioctl函数允许用户设置它的内存参数。

为了与内核共享大块内存,用户使用“ion”驱动第一次分配一块连续的物理块内存。

我们并不关心“ion”驱动的详细内容,但是这里要明白的是,它是一个Android驱动程序,用于分配连续的物理内存,并通过文件描述符来暴露给用户。用户接收到一个文件描述符后,可以将其映射到任意选择的虚拟地址,然后按照喜欢的方式去使用它。(看到这儿是不是有点小激动呢~~~)

这种机制有利于共享内存,因为任何拥有文件描述符的用户可以将其映射到自己进程虚拟地址空间的任何地址,互相独立。

“ion”驱动还支持从不同的pools进行内存分配,通过不同的flag来实现,如果你对此感兴趣,可以阅读更多关于“ion”的工作原理。

在qseecom驱动中有三个参数共同来配置用户的内存参数:


  • virt_sb_base - The virtual address at which the user decided to map the ION allocated chunk
  • sb_len - The length of the shared buffer used
  • ifd_data_fd - The "ion" file descriptor corresponding to the allocated chunk
qseecom驱动程序会验证从“virt_sb_base”到“virt_sb_base+sb_len”这一整块内存范围对用户来说是否有访问权限(和内核内存重叠)。


然后,执行完所需验证之后,驱动程序将ION buffer映射到内核空间虚拟地址,并将所有的内存参数存储在一个内部数据结构中,之后就可以从这个内部数据结构中检索当前用户要执行的额外的IOCTL调用。


有四种不同的参数存储在这儿:

  • The kernel-space virtual address at which the ION buffer is mapped
  • The actual physical address of the ION buffer
  • The user-space virtual address at which the ION buffer is mapped
  • The length of the shared buffer
如果死记硬背的话好像很难记清楚,那我们就理解记忆吧。从理解虚拟地址空间当前状态开始:


QSEECOM_IOCTL_SEND_MODFD_CMD_REQ

在对所支持commands参数的总体浏览后发现,QSEECOM_IOCTL_SEND_MODFD_CMD_REQ看起来是一个我们首选的利用候选命令。这个命令的使用是为了来请求驱动程序发送一个使用用户提供的缓冲区buffers的command到TrustZone。前面我们说过,任何与内核交互的用户数据可能是不稳定的,或者说是危险的,那么用户提供的内存地址当然就不用说了。

下面来详细说一下什么是设置内存参数?

为了允许用户向TrustZone发送的大的请求和从TrustZone接受大的响应,qseecom驱动程序为用户提供了ioctl函数允许用户设置它的内存参数。

为了与内核共享大块内存,用户使用“ion”驱动第一次分配一块连续的物理块内存。

我们并不关心“ion”驱动的详细内容,但是这里要明白的是,它是一个Android驱动程序,用于分配连续的物理内存,并通过文件描述符来暴露给用户。用户接收到一个文件描述符后,可以将其映射到任意选择的虚拟地址,然后按照喜欢的方式去使用它。(看到这儿是不是有点小激动呢~~~)

这种机制有利于共享内存,因为任何拥有文件描述符的用户可以将其映射到自己进程虚拟地址空间的任何地址,互相独立。

“ion”驱动还支持从不同的pools进行内存分配,通过不同的flag来实现,如果你对此感兴趣,可以阅读更多关于“ion”的工作原理。

在qseecom驱动中有三个参数共同来配置用户的内存参数:


  • virt_sb_base - The virtual address at which the user decided to map the ION allocated chunk
  • sb_len - The length of the shared buffer used
  • ifd_data_fd - The "ion" file descriptor corresponding to the allocated chunk
qseecom驱动程序会验证从“virt_sb_base”到“virt_sb_base+sb_len”这一整块内存范围对用户来说是否有访问权限(和内核内存重叠)。


然后,执行完所需验证之后,驱动程序将ION buffer映射到内核空间虚拟地址,并将所有的内存参数存储在一个内部数据结构中,之后就可以从这个内部数据结构中检索当前用户要执行的额外的IOCTL调用。


有四种不同的参数存储在这儿:

  • The kernel-space virtual address at which the ION buffer is mapped
  • The actual physical address of the ION buffer
  • The user-space virtual address at which the ION buffer is mapped
  • The length of the shared buffer
如果死记硬背的话好像很难记清楚,那我们就理解记忆吧。从理解虚拟地址空间当前状态开始:


QSEECOM_IOCTL_SEND_MODFD_CMD_REQ

在对所支持commands参数的总体浏览后发现,QSEECOM_IOCTL_SEND_MODFD_CMD_REQ看起来是一个我们首选的利用候选命令。这个命令的使用是为了来请求驱动程序发送一个使用用户提供的缓冲区buffers的command到TrustZone。前面我们说过,任何与内核交互的用户数据可能是不稳定的,或者说是危险的,那么用户提供的内存地址当然也是危险的。

经过内部代码的处理,实际负责处理这个特殊IOCTL命令的函数是:“qseecom_send_modfd_command”. 

这个函数首先将用户提供的IOCTL argument安全拷贝到本地结构体qseecom_send_modfd_cmd_req中:


其成员cmd_req_buf和cmd_req_lend定义了要发送command所请求的缓冲区,类似的,resp_buf和resp_len定义了相应后写回的结果缓冲区。

不知道你有没有在上面的结构体中发现可疑的地方?

首先,在这个结构体中的两个指针没有标记为"tainted"(not marked as __user),这一这驱动程序可能错误的访问它们。

随之会发生什么?那是一系列复杂的验证,来验证给定的arguments是不是实际有效。看起来好像Quacomm这块儿似乎是成功的,那么真的是这样吗?

好,我们来具体看一看到底都做了哪些验证:

  • First, the function makes sure that the request and response buffers are not NULL.(请求和响应缓冲区不为空)
  • Next, the function makes sure that both the request and response buffers are within the range of the shared buffer discussed earlier.(请求和响应缓冲区的范围在之前申请的共享缓冲区范围内)
  • Then, the function makes sure that the request buffer's length is larger than zero, and that both the request and the response size do not exceed the shared buffer's length.(确保请求缓冲区长度比0大,请求和响应缓冲区的长度不能比共享缓冲区长度长)
  • Lastly, for each file descriptor passed, the function validates that the command buffer offset does not exceed the length of the command buffer.(最后,对于每个文件描述符传递,函数验证该命令缓冲器偏移不超过命令缓冲区的长度。)
在评价这面验证“墙”之前,我们来先看看它的另一面。

在完成了上面这些验证之后,函数就开始将请求和响应缓冲区从用户虚拟地址转换到内核虚拟地址:


看一看真正转换实现的函数:


这仅仅相当于只是将给定的虚拟地址偏移加上用户空间虚拟起始地址值作为共享缓冲区,并将给定的虚拟地址偏移添加到内核空间虚拟地址作为共享缓冲区。这是因为正如前面所提到的,内核映射ION buffer到内核空间的虚拟空间地址是和用户虚拟空间映射的缓冲区地址无关的。所以,在内核可以与任何共享缓冲区内的指针交互之前,必须首先将地址转换为它自己的虚拟空间地址。

接下来就非常有趣了,驱动程序传递请求和响应缓冲区,缓冲区现在应该是在内核空间中,"__qseecom_update_cmd_buf"这个这个函数实际上向请求缓冲区转换成内核虚拟地址空间后的地址写入数据。

稍后,我们将扩展原来的写入的数据,这个时候你要相信,如果我们能够绕过以上验证,并且能保证能控制请求缓冲区的最终内核空间地址,我们就能达到内核可写的原始目的,听起来很诱人吧...

步骤一:“Bring down this wall”

首先我们先在虚拟地址空间中映射出请求和响应缓冲区位置。


现在,正如我们知道的,当设置内存参数时,virt_sb_base起始和virt_sb_base+sb_len这段空间必须整个都在用户地址空间内,通过下面的代码进行验证:


同时,上面的验证也会确保cmd_req_buf和resp_buf指针在用户虚拟地址空间的share buffer内。

现在,我们思考一下如果我们映射一个比较大(大到内核空间容纳不下)的share buffer(共享缓冲区)将会发生什么?

一种安全的假设可能是,当我们试图设置这样的缓冲区内存参数,请求将会失败,内核不会映射该缓冲区到内核虚拟地址空间去”。

“幸运的是”,虽然IOCTL与该内存参数的设置仅仅使用用户提供的缓冲区长度,以验证该共享缓冲区的用户空间范围是由用户可访问的(见上面的访问检查)。然而,它的缓冲区实际上映射到自己的地址空间时,它仅仅简单的通过使用ION文件描述符来完成映射,未验证该缓冲区的实际长度是否等于用户提供的长度。


这意味着我们可以申请一个小的ION buffer,并将它传递给qseecom,并且声称这个buffer是一个比较大的buffer。只要整个区域位于用户空间内,并且对用户来说可写,驱动就会很乐意介绍这些参数并为我们存储它,但是,这样到底是否可行?毕竟,我们不能真正在用户空间中分配这么大的内存,因为就没有这么多的物理内存可供映射,也就意味着直到数据被真正写入,这块内存并没有分配,因此,我们可以在上面提到的验证期间时间内自由映射任何大小的区域,然后一旦驱动确认这块区域确实是可写的后取消映射。

现在,我们假设我们映射一个虚假的起始虚拟地址为0x1000000,大小为0x80000000的共享内存。

回想一下,如果command和响应缓冲区被认为是有效的,它们就会被转换为相应的内核空间虚拟地址,然后转换后的缓冲区指定的偏移中被写入command。总结起来就是,现在我们实际写入的内核目标地址为:


其中(-user_virt_sb_base+cmd_req_buf)其实就是cmd_req_buf在sb中的偏移。sb_virt是sb在内核中映射的起始地址,那么上面这个式子的结果就是实际写入内核的目标地址.

因为内核相信共享内存sb是huge的,这意味着cmd_req_buf可能指向在这个sb范围内的任意地址。在我们的假设中,这个范围就是[0x10000000,0x90000000]。也就意味着cmd_buf_offset最大可以是0x80000000,也就是我们的fake sb大小。(这里应该就能明白问题所在,内核至于1G地址空间啊,很明显会导致溢出啊,也就是说目标写入地址很可能不是内核空间中的地址)。

上面的图中我们用红色字体标记出,内核中映射的sb的其实地址对我们来说是不知道的。因为,它是在运行时进行映射的,也没有以别的方式来暴露给用户。但是,这并不意味着我们不能自己来确定这个地址。

试想,如果我们将cmd_buf_offset设置为0,那么内核目标写入地址就是:

sb_virt - 0x10000000 + cmd_req_buf + 0x0

我们知道,sb_virt的地址是在内核的heap中的,所以它肯定是大于0XC0000000的,这就意味着当cmd_req_buf的值大于0xFFFFFFFF-0xB0000000的时候,会产生溢出,结果产生了用户空间内的一个地址。

这正是我们期望的,我们可以在用户空间的低地址范围内分配一个sterile "dropzone"(字面意思是空的空投场),并向其地址内填充单一的 我们知情的值。

然后,在我们使用上面所详细讲解的参数来触发驱动的原始的写操作,我们就能在dropzone中找到我们写的内容,并依此判断sb_virt的地址。




现在我们知道了sb_virt的地址,我们可以的根据希望控制的目的地址来自由的设置参数了:


现在,所有的参数已知,并且cmd_req_buf加上cmd_buf_offset的值可以超过0xFFFFFFFF,也就意味着我们可以修改sb_virt之后的任意地址,设置的参数为:

  • user_virt_sb_base = 0x10000000
  • cmd_req_buf + cmd_buf_offset = (0xFFFFFFFF + 1) + 0x10000000 + wanted_offset
这意味着目标写入地址为:

    sb_virt+wanted_offset

现在,我们已经达到了内核任意写,下面要做的就是利用它。虽然现在我们可以在内核中任意写,但是我们仍然不能控制写的数据。实时上,可以通过浏览漏洞相关代码"_qseecom_update_cmd_buf",你会发现实际上写入了一个与ION buffer相关的物理地址到目标地址。

在前面我们获得sb_virt的操作中,我们想sterile dropzone中写入了一个DWORD的数据,这以为值这个物理地址值我们也是可以知道的。并且,所有与Quelcomm devices 中“System RAM”相关的物理地址事实上都是“low”地址,都比内核地址要低(<0xC0000000)。

好了,明白了这些,剩下的就是来在内核中覆写一个函数的地址。因为DWORD 的写入数据是在用户虚拟地址空间中存放的,于是我们就可以简单的在对应的地址上分配一个可执行代码stub,然后从这个代码块重定向到任何其他想要执行的代码:


在pppolac_proto_ps结构中我们能找到需要的函数指针,这个结构体在内核中用来注册和用PPP_OLAC协议创建的socket进行通信的函数指针,这个结构体十分合适,原因如下:

  • The PPP_OLAC protocol isn't widely used, so there's no immediate need to restore the overwritten function pointer
  • There are no special permissions needed in order to open a PPP_OLAC socket, other than the ability to create sockets
  • The structure itself is static (and therefore stored in the BSS), and is not marked as "const", and is therefore writeable

最后总结起来就是:

  • Open the QSEECOM driver 
  • Map a ION buffer
  • Register faulty memory parameters which include a fake huge memory buffer
  • Prepare a sterile dropzone in low user-space addresses
  • Trigger the write primitive into a low user-space address
  • Inspect the dropzone in order to deduce the address of "sb_virt" and the contents written in the write primitive
  • Allocate a small function stub at the address which is written by the write primitive
  • Trigger the write primitive in order to overwrite a function pointer within "pppolac_proto_ops"
  • Open a PPP_OLAC socket and trigger a call to the overwritten function pointer
  • Execute code within the kernel :)



附录1:

ARM协处理器相关,百度文库:

http://wenku.baidu.com/link?url=Nb6v_lLzpITWvsxjfGDnbAQOxOokidBgwh5NOyQ6Ig-oAtisYy88g5yn60QPFsmHH3I_k4TFCT-ePhA1OiGtQNfeIWfNlvrUhIXHXMPnJS_

附录2:

ION简介:

 ION是Google的下一代内存管理器,用来支持不同的内存分配机制,如CARVOUT(PMEM),物理连续内存(kmalloc), 虚拟地址连续但物理不连续内存(vmalloc), IOMMU等。


ION 框架[1]

ION 定义了四种不同的heap,实现不同的内存分配策略。

  • ION_HEAP_TYPE_SYSTEM : 通过vmalloc分配内存
  • ION_HEAP_TYPE_SYSTEM_CONTIG: 通过kmalloc分配内存
  • ION_HEAP_TYPE_CARVEOUT: 在保留内存块中(reserve memory)分配内存
  • ION_HEAP_TYPE_CUSTOM: 由客户自己定义

 

下图是两个client共享内存的示意图。图中有2个heap(每种heap都有自己的内存分配策略),每个heap中分配了若干个buffer。client的handle管理到对应的buffer。两个client是通过文件描述符fd来实现内存共享的。


ION APIs

用户空间 API

定义了6种 ioctl 接口,可以与用户应用程序交互。

  • ION_IOC_ALLOC: 分配内存
  • ION_IOC_FREE: 释放内存
  • ION_IOC_MAP: 获取文件描述符进行mmap  (? 在code中未使用这个定义)
  • ION_IOC_SHARE: 创建文件描述符来实现共享内存
  • ION_IOC_IMPORT: 获取文件描述符
  • ION_IOC_CUSTOM: 调用用户自定义的ioctl

ION_IOC_SHARE 及ION_IOC_IMPORT是基于DMABUF实现的,所以当共享进程获取文件描述符后,可以直接调用mmap来操作共享内存。mmap实现由DMABUF子系统调用ION子系统中mmap回调函数完成。

内核空间 API

内核驱动也可以注册为一个ION的客户端(client),可以选择使用哪种类型的heap来申请内存。

  • ion_client_create: 分配一个客户端。
  • ion_client_destroy: 释放一个客户端及绑定在它上面的所有ion handle.

ion handle: 这里每个ion handle映射到一个buffer中,每个buffer关联一个heap。也就是说一个客户端可以操作多块buffer。

Buffer 申请及释放函数:

  • ion_alloc: 申请ion内存,返回ion handle
  • ion_free: 释放ion handle

ION 通过handle来管理buffer,驱动需要可以访问到buffer的地址。ION通过下面的函数来达到这个目的

  • ion_phys: 返回buffer的物理地址(address)及大小(size)
  • ion_map_kernel: 给指定的buffer创建内核内存映射
  • ion_unmap_kernel: 销毁指定buffer的内核内存映射
  • ion_map_dma: 为指定buffer创建dma 映射,返回sglist(scatter/gather list)
  • ion_unmap_dma: 销毁指定buffer的dma映射

ION是通过handle而非buffer地址来实现驱动间共享内存,用户空间共享内存也是利用同样原理。

  • ion_share: given a handle, obtain a buffer to pass to other clients
  • ion_import: given an buffer in another client, import it
  • ion_import_fd: given an fd obtained via ION_IOC_SHARE ioctl, import it

Heap API

Heap 接口定义 [drivers/gpu/ion/ion_priv.h]

这些接口不是暴露给驱动或者用户应用程序的。

/** * struct ion_heap_ops - ops to operate on a given heap * @allocate:           allocate memory * @free:               free memory * @phys                get physical address of a buffer (only define on physically contiguous heaps) * @map_dma             map the memory for dma to a scatterlist * @unmap_dma           unmap the memory for dma * @map_kernel          map memory to the kernel * @unmap_kernel        unmap memory to the kernel * @map_user            map memory to userspace */struct ion_heap_ops {        int (*allocate) (struct ion_heap *heap, struct ion_buffer *buffer, unsigned long len,unsigned long align, unsigned long flags);        void (*free) (struct ion_buffer *buffer);        int (*phys) (struct ion_heap *heap, struct ion_buffer *buffer, ion_phys_addr_t *addr, size_t *len);        struct scatterlist *(*map_dma) (struct ion_heap *heap, struct ion_buffer *buffer);        void (*unmap_dma) (struct ion_heap *heap, struct ion_buffer *buffer);        void * (*map_kernel) (struct ion_heap *heap, struct ion_buffer *buffer);        void (*unmap_kernel) (struct ion_heap *heap, struct ion_buffer *buffer);        int (*map_user) (struct ion_heap *mapper, struct ion_buffer *buffer, struct vm_area_struct *vma);};

ION debug

ION 在/sys/kernel/debug/ion/ 提供一个debugfs 接口。

每个heap都有自己的debugfs目录,client内存使用状况显示在/sys/kernel/debug/ion/<<heap name>>

$cat /sys/kernel/debug/ion/ion-heap-1           client              pid             size        test_ion             2890            16384

每个由pid标识的client也有一个debugfs目录/sys/kernel/debug/ion/<<pid>>

$cat /sys/kernel/debug/ion/2890        heap_name:    size_in_bytes      ion-heap-1:    40960 11


附录3:

kmalloc vmalloc 和malloc的区别详解:

简单的说:

  1. kmalloc和vmalloc是分配的是内核的内存,malloc分配的是用户的内存
  2. kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续,malloc不保证任何东西(这点是自己猜测的,不一定正确)
  3. kmalloc能分配的大小有限,vmalloc和malloc能分配的大小相对较大
  4. 内存只有在要被DMA访问的时候才需要物理上连续
  5. vmalloc比kmalloc要慢

详细的解释:

      对于提供了MMU(存储管理器,辅助操作系统进行内存管理,提供虚实地址转换等硬件支持)的处理器而言,Linux提供了复杂的存储管理系统,使得进程所能访问的内存达到4GB。

      进程的4GB内存空间被人为的分为两个部分--用户空间与内核空间。用户空间地址分布从0到3GB(PAGE_OFFSET,在0x86中它等于0xC0000000),3GB到4GB为内核空间。

      内核空间中,从3G到vmalloc_start这段地址是物理内存映射区域(该区域中包含了内核镜像、物理页框表mem_map等等),比如我们使用 的 VMware虚拟系统内存是160M,那么3G~3G+160M这片内存就应该映射物理内存。在物理内存映射区之后,就是vmalloc区域。对于 160M的系统而言,vmalloc_start位置应在3G+160M附近(在物理内存映射区与vmalloc_start期间还存在一个8M的gap 来防止跃界),vmalloc_end的位置接近4G(最后位置系统会保留一片128k大小的区域用于专用页面映射)

      kmalloc和get_free_page申请的内存位于物理内存映射区域,而且在物理上也是连续的,它们与真实的物理地址只有一个固定的偏移,因此存在较简单的转换关系,virt_to_phys()可以实现内核虚拟地址转化为物理地址:
   #define __pa(x) ((unsigned long)(x)-PAGE_OFFSET)
   extern inline unsigned long virt_to_phys(volatile void * address)
   {
        return __pa(address);
   }
上面转换过程是将虚拟地址减去3G(PAGE_OFFSET=0XC000000)。

与之对应的函数为phys_to_virt(),将内核物理地址转化为虚拟地址:
   #define __va(x) ((void *)((unsigned long)(x)+PAGE_OFFSET))
   extern inline void * phys_to_virt(unsigned long address)
   {
        return __va(address);
   }
virt_to_phys()和phys_to_virt()都定义在include/asm-i386/io.h中。

而vmalloc申请的内存则位于vmalloc_start~vmalloc_end之间,与物理地址没有简单的转换关系,虽然在逻辑上它们也是连续的,但是在物理上它们不要求连续。

我们用下面的程序来演示kmalloc、get_free_page和vmalloc的区别:
#include <linux/module.h>
#include <linux/slab.h>
#include <linux/vmalloc.h>
MODULE_LICENSE("GPL");
unsigned char *pagemem;
unsigned char *kmallocmem;
unsigned char *vmallocmem;

int __init mem_module_init(void)
{
//最好每次内存申请都检查申请是否成功
//下面这段仅仅作为演示的代码没有检查
pagemem = (unsigned char*)get_free_page(0);
printk("<1>pagemem addr=%x", pagemem);

kmallocmem = (unsigned char*)kmalloc(100, 0);
printk("<1>kmallocmem addr=%x", kmallocmem);

vmallocmem = (unsigned char*)vmalloc(1000000);
printk("<1>vmallocmem addr=%x", vmallocmem);

return 0;
}

void __exit mem_module_exit(void)
{
free_page(pagemem);
kfree(kmallocmem);
vfree(vmallocmem);
}

module_init(mem_module_init);
module_exit(mem_module_exit);

我们的系统上有160MB的内存空间,运行一次上述程序,发现pagemem的地址在0xc7997000(约3G+121M)、kmallocmem 地址在0xc9bc1380(约3G+155M)、vmallocmem的地址在0xcabeb000(约3G+171M)处,符合前文所述的内存布局。


参考资料:

http://bits-please.blogspot.sg/2015/08/android-linux-kernel-privilege.html

http://blog.csdn.net/kris_fei/article/details/8588661 &http://blog.csdn.net/kris_fei/article/details/8618587

http://blog.csdn.net/macrossdzh/article/details/5958368


0 0
原创粉丝点击