Qualcomm Securitybulletin CVE-2023-43515 漏洞分析

漏洞分析

高通漏洞2024年4月份公告中披露对 CVE-2023-43515 漏洞修复的情况
https://docs.qualcomm.com/product/publicresources/securitybulletin/april-2024-bulletin.html#_cve-2023-43515
根据漏洞披露信息来看,了解到这是一个不检查出入大小,导致缓冲区溢出的漏洞,漏洞存在于tmecom驱动模块中。

首先查看修复的情况,, 可以看到这里对 tdev->pkt.size 的带大小做了限制,这个值要小于TMECOM_RX_HDR_SIZE,才会执行后面的tmecom_decode() 函数。

因此来查看tmecom_decode() 函数的代码,函数最有可能导致缓冲区溢出的是memcpy 函数,那么复制的字节len 大小是 (tdev->pkt.size - TMECOM_RX_HDR_SIZE) 计算的值,前面修复的情况是tdev->pkt.size <= TMECOM_RX_HDR_SIZE,那么就错误,因此如果 tdev->pkt.size 的值小于 TMECOM_RX_HDR_SIZE 这这个值,那 len 就为负数,memcpy 中的len 参数是一个无符号整性,为负数的len向下溢出 会被强制转换为一个很大的正数,进而导致 rbuf 的缓冲区溢出。

1
2
3
4
5
6
7
8
static inline size_t tmecom_decode(struct tmecom *tdev, void *respbuf)
{
unsigned int *msg = tdev->pkt.data + TMECOM_RX_HDR_SIZE;
unsigned int *rbuf = (unsigned int *)respbuf;

memcpy(rbuf, msg, (tdev->pkt.size - TMECOM_RX_HDR_SIZE));
return (tdev->pkt.size - TMECOM_RX_HDR_SIZE);
}

那么这里的 TMECOM_RX_HDR_SIZE 值是多大呢?

1
2
3
4
5
6
7
struct tmecom_msg_hdr {
unsigned int reserved; /* for future use */ // start address 0
unsigned int txnid; /* transaction id */ // start address 4
} __packed;
#define TMECOM_TX_HDR_SIZE sizeof(struct tmecom_msg_hdr)
#define CBOR_NUM_BYTES (sizeof(unsigned int))
#define TMECOM_RX_HDR_SIZE (TMECOM_TX_HDR_SIZE + CBOR_NUM_BYTES)

找到这个宏定义的地方,可以看到 TMECOM_RX_HDR_SIZE = TMECOM_TX_HDR_SIZE + CBOR_NUM_BYTES

  • TMECOM_TX_HDR_SIZE 的大小是 结构体tmecom_msg_hdr的sizeof 的值,也就是 4 字节。
  • CBOR_NUM_BYTES 的值是一个无符号整型,也是 4 字节。
    因而 TMECOM_RX_HDR_SIZE 的值为 8字节。

由于要确定 tdev->pkt.size 值小于TMECOM_RX_HDR_SIZE (8字节)的可能性,所以要找到这个指针是否由外部传入赋值。
在line~118 , 会调用 tmecom_encode 函数来赋值。

1
    tdev->pkt.size = tmecom_encode(tdev, reqbuf, reqsize);  // reqsize + 4

tmecom_encode 函数会将size +TMECOM_TX_HDR_SIZE( 4字节 ) 的值返回给 tdev->pkt.size , 看到这个函数中的memcpy ,可能会觉得这个也有可能会导致overflow,因为size 是外部传入的。

1
2
3
4
5
6
7
8
9
static inline size_t tmecom_encode(struct tmecom *tdev, const void *reqbuf,
        size_t size)
{
    unsigned int *msg = tdev->txbuf + TMECOM_TX_HDR_SIZE; // 1024   +4
    unsigned int *src = (unsigned int *)reqbuf;

    memcpy(msg, src, size);  // size < 1024
    return (size + TMECOM_TX_HDR_SIZE);   // size + 4
}

在这个函数中,是否存在问题取决于指针msg 缓冲区大小,下面的代码为其分配了1024个字节的内存空间。

1
line~260    tdev->txbuf =devm_kzalloc(&pdev->dev, MBOX_MAX_MSG_LEN, GFP_KERNEL);

tmecom_encode 函数size参数由 reqsize 传入,在 tmecom_process_request函数中,已经对reqsize 的大小做了限制,不允许大于1024,因而size 的值永远不会大于1024 +4 。

1
2
3
4
5
6
7
8
9
10
11
    if (!reqbuf || !reqsize || (reqsize > MBOX_MAX_MSG_LEN)) {  // MBOX_MAX_MSG_LEN is 1024
        dev_err(tdev->dev, "invalid reqbuf or reqsize\n");
        return -EINVAL;
    }
    if (!respbuf || !respsize || (*respsize > MBOX_MAX_MSG_LEN)) {
        dev_err(tdev->dev, "invalid respbuf or respsize\n");
        return -EINVAL;
    }
    mutex_lock(&tdev->lock);
    tdev->rx_done = false;
    tdev->pkt.size = tmecom_encode(tdev, reqbuf, reqsize);  // reqsize + 4

接着分析reqsize 的值,这个值的传参是tmecom_debugfs_write 传入的len的值,在这里也限制的len的大小不能超过1024。这个函数会从用户空间赋值 len大小的 数据到内核 dpkt 内存空间里。然后交由tmecom_process_request 处理。并且触发这个函数的前提是编译选项中 CONFIG_DEBUG_FS 是开启的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#if IS_ENABLED(CONFIG_DEBUG_FS)
static ssize_t tmecom_debugfs_write(struct file *file,
        const char __user *userstr, size_t len, loff_t *pos)
{
    int ret = 0;
    size_t rxlen = 0;
    struct tme_ext_err_info *err_info = (struct tme_ext_err_info *)dpkt;

    if (!len || (len > MBOX_MAX_MSG_LEN)) {   // MBOX is 1024
        pr_err("invalid message length\n");
        return -EINVAL;
    }

    memset(dpkt, 0, sizeof(*dpkt));
    ret = copy_from_user(dpkt, userstr, len);  // dpkt is 1025
    if (ret) {
        pr_err("%s copy from user failed, ret=%d\n", __func__, ret);
        return len;

    }
    tmecom_process_request(dpkt, len, dpkt, &rxlen);
    ...nop..

调用这个驱动,直接调用该驱动的write函数就可以触发漏洞。

1
2
3
4
5
static const struct file_operations tmecom_debugfs_ops = {
    .open = simple_open,
    .write = tmecom_debugfs_write,
};
#endif /* CONFIG_DEBUG_FS */

这个漏洞的原理还是比较清晰,但漏洞的触发前提是需要在编译内核的时候选定 CONFIG_DEBUG_FS 为开启。

这里要解释一下 simple_open 函数,simple_open,用于在 Linux 内核中实现文件系统。它的作用是在打开文件时将文件对象的私有数据指针指向索引节点(inode)的私有数据。

  • file->private_data 是一个指针,它指向文件对象的私有数据。在 Linux 内核中,当打开一个文件时,可以使用 file->private_data 来存储和访问与该文件相关的特定信息。通常情况下,这个指针会指向文件系统中的一些结构体或数据,以便在文件操作中可以方便地获取和修改相关信息。
  • 前面的 tmecom_debugfs_write 函数,其实并没有用到 file->private_data 指针,我猜测并没有对驱动文件写入的操作,而是对全局变量dpkt写入数据, 因此在调用完simple_open 函数后,可以直接使用tmecom_debugfs_write
    1
    2
    3
    4
    5
    6
    7
    int simple_open(struct inode *inode, struct file *file) 
    {
    if (inode->i_private)
    file->private_data = inode->i_private;
    return 0;
    }
    EXPORT_SYMBOL(simple_open);

完整代码

以下是该temcom.c 的完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
// SPDX-License-Identifier: GPL-2.0-only
/*
* Copyright (c) 2022 Qualcomm Innovation Center, Inc. All rights reserved.
*/

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/mailbox_client.h>
#include <linux/seq_file.h>
#include <linux/debugfs.h>
#include <linux/platform_device.h>
#include <linux/mailbox/qmp.h>
#include <linux/uaccess.h>
#include <linux/mailbox_controller.h>

#include "tmecom.h"

struct tmecom {
struct device *dev;
struct mbox_client cl;
struct mbox_chan *chan;
struct mutex lock;
struct qmp_pkt pkt;
wait_queue_head_t waitq;
void *txbuf;
bool rx_done;
};

#if IS_ENABLED(CONFIG_DEBUG_FS)
#include <linux/tme_hwkm_master_defs.h>
#include <linux/tme_hwkm_master.h>

char dpkt[MBOX_MAX_MSG_LEN + 1]; // MBOX_MAX_MSG_LEN is 1024
struct dentry *debugfs_file;
#endif /* CONFIG_DEBUG_FS */

static struct tmecom *tmedev;

/**
* tmecom_msg_hdr - Request/Response message header between HLOS and TME.
*
* This header is proceeding any request specific parameters.
* The transaction id is used to match request with response.
*
* Note: glink/QMP layer provides the rx/tx data size, so user payload size
* is calculated by reducing the header size.
*/
struct tmecom_msg_hdr {
unsigned int reserved; /* for future use */
unsigned int txnid; /* transaction id */
} __packed;
#define TMECOM_TX_HDR_SIZE sizeof(struct tmecom_msg_hdr)
#define CBOR_NUM_BYTES (sizeof(unsigned int))
#define TMECOM_RX_HDR_SIZE (TMECOM_TX_HDR_SIZE + CBOR_NUM_BYTES)

/*
* CBOR encode emulation
* Prepend tmecom_msg_hdr space
* CBOR tag is prepended in request
*/
static inline size_t tmecom_encode(struct tmecom *tdev, const void *reqbuf,
size_t size)
{
unsigned int *msg = tdev->txbuf + TMECOM_TX_HDR_SIZE;
unsigned int *src = (unsigned int *)reqbuf;

memcpy(msg, src, size);
return (size + TMECOM_TX_HDR_SIZE);
}

/*
* CBOR decode emulation
* Strip tmecom_msg_hdr & CBOR tag
*/
static inline size_t tmecom_decode(struct tmecom *tdev, void *respbuf)
{
unsigned int *msg = tdev->pkt.data + TMECOM_RX_HDR_SIZE;
unsigned int *rbuf = (unsigned int *)respbuf;

memcpy(rbuf, msg, (tdev->pkt.size - TMECOM_RX_HDR_SIZE));
return (tdev->pkt.size - TMECOM_RX_HDR_SIZE);
}

static bool tmecom_check_rx_done(struct tmecom *tdev)
{
return tdev->rx_done;
}

int tmecom_process_request(const void *reqbuf, size_t reqsize, void *respbuf,
size_t *respsize)
{
struct tmecom *tdev = tmedev;
long time_left = 0;
int ret = 0;

/*
* Check to handle if probe is not successful or not completed yet
*/
if (!tdev) {
pr_err("%s: tmecom dev is NULL\n", __func__);
return -ENODEV;
}

if (!reqbuf || !reqsize || (reqsize > MBOX_MAX_MSG_LEN)) {
dev_err(tdev->dev, "invalid reqbuf or reqsize\n");
return -EINVAL;
}

if (!respbuf || !respsize || (*respsize > MBOX_MAX_MSG_LEN)) {
dev_err(tdev->dev, "invalid respbuf or respsize\n");
return -EINVAL;
}

mutex_lock(&tdev->lock);

tdev->rx_done = false;
tdev->pkt.size = tmecom_encode(tdev, reqbuf, reqsize);
/*
* Controller expects a 4 byte aligned buffer
*/
tdev->pkt.size = (tdev->pkt.size + 0x3) & ~0x3;
tdev->pkt.data = tdev->txbuf;

pr_debug("tmecom encoded request size = %u\n", tdev->pkt.size);
print_hex_dump_bytes("tmecom sending bytes : ",
DUMP_PREFIX_ADDRESS, tdev->pkt.data, tdev->pkt.size);

if (mbox_send_message(tdev->chan, &tdev->pkt) < 0) {
dev_err(tdev->dev, "failed to send qmp message\n");
ret = -EAGAIN;
goto err_exit;
}

time_left = wait_event_interruptible_timeout(tdev->waitq,
tmecom_check_rx_done(tdev), tdev->cl.tx_tout);

if (!time_left) {
dev_err(tdev->dev, "request timed out\n");
ret = -ETIMEDOUT;
goto err_exit;
}

dev_info(tdev->dev, "response received\n");

pr_debug("tmecom received size = %u\n", tdev->pkt.size);
print_hex_dump_bytes("tmecom received bytes : ",
DUMP_PREFIX_ADDRESS, tdev->pkt.data, tdev->pkt.size);

if (tdev->pkt.size <= TMECOM_RX_HDR_SIZE) {
dev_err(tdev->dev, "invalid pkt.size received\n");
ret = -EPROTO;
goto err_exit;
}

*respsize = tmecom_decode(tdev, respbuf);

tdev->rx_done = false;
ret = 0;

err_exit:
mutex_unlock(&tdev->lock);
return ret;
}
EXPORT_SYMBOL(tmecom_process_request);

#if IS_ENABLED(CONFIG_DEBUG_FS)
static ssize_t tmecom_debugfs_write(struct file *file,
const char __user *userstr, size_t len, loff_t *pos)
{
int ret = 0;
size_t rxlen = 0;
struct tme_ext_err_info *err_info = (struct tme_ext_err_info *)dpkt;


if (!len || (len > MBOX_MAX_MSG_LEN)) {
pr_err("invalid message length\n");
return -EINVAL;
}

memset(dpkt, 0, sizeof(*dpkt));
ret = copy_from_user(dpkt, userstr, len);
if (ret) {
pr_err("%s copy from user failed, ret=%d\n", __func__, ret);
return len;
}

tmecom_process_request(dpkt, len, dpkt, &rxlen);

print_hex_dump_bytes("tmecom decoded bytes : ",
DUMP_PREFIX_ADDRESS, dpkt, rxlen);

pr_debug("calling TME_HWKM_CMD_BROADCAST_TP_KEY api\n");
ret = tme_hwkm_master_broadcast_transportkey(err_info);

if (ret == 0)
pr_debug("%s successful\n", __func__);

return len;
}

static const struct file_operations tmecom_debugfs_ops = {
.open = simple_open,
.write = tmecom_debugfs_write,
};
#endif /* CONFIG_DEBUG_FS */

static void tmecom_receive_message(struct mbox_client *client, void *message)
{
struct tmecom *tdev = dev_get_drvdata(client->dev);
struct qmp_pkt *pkt = NULL;

if (!message) {
dev_err(tdev->dev, "spurious message received\n");
goto tmecom_receive_end;
}

if (tdev->rx_done) {
dev_err(tdev->dev, "tmecom response pending\n");
goto tmecom_receive_end;
}
pkt = (struct qmp_pkt *)message;
tdev->pkt.size = pkt->size;
tdev->pkt.data = pkt->data;
tdev->rx_done = true;
tmecom_receive_end:
wake_up_interruptible(&tdev->waitq);
}

static int tmecom_probe(struct platform_device *pdev)
{
struct tmecom *tdev;
const char *label;
char name[32];

tdev = devm_kzalloc(&pdev->dev, sizeof(*tdev), GFP_KERNEL);
if (!tdev)
return -ENOMEM;

tdev->cl.dev = &pdev->dev;
tdev->cl.tx_block = true;
tdev->cl.tx_tout = 500;
tdev->cl.knows_txdone = false;
tdev->cl.rx_callback = tmecom_receive_message;

label = of_get_property(pdev->dev.of_node, "mbox-names", NULL);
if (!label)
return -EINVAL;
snprintf(name, 32, "%s_send_message", label);

tdev->chan = mbox_request_channel(&tdev->cl, 0);
if (IS_ERR(tdev->chan)) {
dev_err(&pdev->dev, "failed to get mbox channel\n");
return PTR_ERR(tdev->chan);
}

mutex_init(&tdev->lock);

if (tdev->chan) {
tdev->txbuf =
devm_kzalloc(&pdev->dev, MBOX_MAX_MSG_LEN, GFP_KERNEL);
if (!tdev->txbuf) {
dev_err(&pdev->dev, "message buffer alloc faile\n");
return -ENOMEM;
}
}

init_waitqueue_head(&tdev->waitq);

#if IS_ENABLED(CONFIG_DEBUG_FS)
debugfs_file = debugfs_create_file(name, 0220, NULL, tdev,
&tmecom_debugfs_ops);
if (!debugfs_file)
goto err;
#endif /* CONFIG_DEBUG_FS */

tdev->rx_done = false;
tdev->dev = &pdev->dev;
dev_set_drvdata(&pdev->dev, tdev);

tmedev = tdev;

dev_info(&pdev->dev, "tmecom probe success\n");
return 0;
err:
mbox_free_channel(tdev->chan);
return -ENOMEM;
}

static int tmecom_remove(struct platform_device *pdev)
{
struct tmecom *tdev = platform_get_drvdata(pdev);

#if IS_ENABLED(CONFIG_DEBUG_FS)
debugfs_remove(debugfs_file);
#endif /* CONFIG_DEBUG_FS */

if (tdev->chan)
mbox_free_channel(tdev->chan);

dev_info(&pdev->dev, "tmecom remove success\n");
return 0;
}

static const struct of_device_id tmecom_match_tbl[] = {
{.compatible = "qcom,tmecom-qmp-client"},
{},
};

static struct platform_driver tmecom_driver = {
.probe = tmecom_probe,
.remove = tmecom_remove,
.driver = {
.name = "tmecom-qmp-client",
.suppress_bind_attrs = true,
.of_match_table = tmecom_match_tbl,
},
};
module_platform_driver(tmecom_driver);

MODULE_DESCRIPTION("MSM TMECom QTI mailbox protocol client");
MODULE_LICENSE("GPL v2");


Qualcomm Securitybulletin CVE-2023-43515 漏洞分析
https://tig3rhu.github.io/2024/04/27/74__高通CVE-2023-43515分析/
Author
Tig3rHu
Posted on
April 27, 2024
Licensed under