如何优雅的使用内核符号表

主要针对arm64,现在ida6.8并不支持arm64的反汇编,所以在定位代码过程中会很麻烦,而32位是直接可以f5对照源码看的。

获取内核符号表

先放一个内核符号表的部分样子:

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
ffffffc000081000 do_undefinstr
ffffffc000081000 _stext
ffffffc000081000 __exception_text_start
ffffffc000081348 do_mem_abort
ffffffc0000813e4 do_sp_pc_abort
ffffffc000081428 do_debug_exception
ffffffc0000814d4 gic_handle_irq
ffffffc000081554 gic_handle_irq
ffffffc0000815d4 __exception_text_end
ffffffc0000815d8 match_dev_by_uuid
ffffffc000081618 name_to_dev_t
ffffffc000082000 swp_handler.part.1
ffffffc000082138 swp_handler
ffffffc0000821fc clear_os_lock
ffffffc000082208 create_debug_debugfs_entry
ffffffc000082238 brk_handler.part.2
ffffffc0000822a0 brk_handler
ffffffc00008238c single_step_handler.part.3
ffffffc0000823f8 single_step_handler
ffffffc0000824f8 debug_monitors_arch
ffffffc000082504 enable_debug_monitors
ffffffc000082620 disable_debug_monitors
ffffffc00008270c register_step_hook
ffffffc000082764 unregister_step_hook
ffffffc0000827c4 register_break_hook
...

有root权限

两条命令即可:

1
2
adb shell, echo 0 > /proc/sys/kernel/kptr_restrict sysctl 
adb pull /proc/kallsyms kallsyms.txt

无root权限

1.获取kernel文件,解压rom包找到boot.img文件,然后bootimg提取kernel压缩文件,直接解压获取kernel文件。如果解压失败,用二进制文件打开,查找特征值1F 8B 08,删除之前数据保存并解压。

2.使用kallsymsprint获取,参数为kernel文件路径。

3.懒人的方法,打包一个脚本dump_kallsyms搞定 : )

patch selinux mode

内核提权过程中,完成了addr_limit字段的patch之后,那么用户态就可以任意读写内核态了,但是如果系统的selinux使用的是enforcing模式,后续的提权过程需要pacth selinux mode。

源码中#define selinux_enforcing 1,该模式为enforcing,获得任意读写能力之后将其置0即可改为permissive

查找内核符号表,变量selinux_enforcing并没有导出地址(部分会有),查找源码找到使用该值的地方,例如在enforcing_setup函数中:

1
2
3
4
5
6
7
static int __init enforcing_setup(char *str)
{

unsigned long enforcing;
if (!kstrtoul(str, 0, &enforcing))
selinux_enforcing = enforcing ? 1 : 0;
return 1;
}

在内核符号表中找到函数enforcing_setup的地址为0xffffffc000d75878,反汇编该地址处的代码。
ida无法反汇编arm64,可以通过python的capstone模块进行反汇编,用法可参考

1
2
3
4
5
6
7
8
9
10
11
12
13
14
0xffffffc000d75878:     stp     x29, x30, [sp, #-0x20]!
0xffffffc000d7587c: movz w1, #0
0xffffffc000d75880: mov x29, sp
0xffffffc000d75884: add x2, x29, #0x18
0xffffffc000d75888: bl #0xffffffc000324c88
0xffffffc000d7588c: cbnz w0, #0xffffffc000d758a4
0xffffffc000d75890: ldr x0, [x29, #0x18]
0xffffffc000d75894: cmp x0, xzr
0xffffffc000d75898: adrp x0, #0xffffffc000f2f000
0xffffffc000d7589c: cset w1, ne
0xffffffc000d758a0: str w1, [x0, #0xc0c]
0xffffffc000d758a4: movz w0, #0x1
0xffffffc000d758a8: ldp x29, x30, [sp], #0x20
0xffffffc000d758ac: ret

enforcing_setup = 0xffffffc000f2f000 + 0xc0c

patch ioctl

在linux内核PXN开启且代码段只读开启的前提下,如果获得了任意地址写的能力,怎么提权呢,ret2user是不行了,安装一个syscall调用commit_creds也不行了,sys_call_table也是不能改的(引自网络)。一种思路是通过改写ptmx_fops->unlocked_ioctl指向rop,然后调用/dev/ptmx的ioctl即可跳入rop中执行。

同样ptmx_fops在内核符号表中是没有导出的,如何确定该地址并修改unlocked_ioctl的指针。

查看源码ptmx_fops使用的地方。

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
static int __init pty_init(void)
{

legacy_pty_init();
unix98_pty_init();
return 0;
}

static void __init unix98_pty_init(void)
{

...
/* Now create the /dev/ptmx special device */
tty_default_fops(&ptmx_fops);
ptmx_fops.open = ptmx_open;

cdev_init(&ptmx_cdev, &ptmx_fops);
if (cdev_add(&ptmx_cdev, MKDEV(TTYAUX_MAJOR, 2), 1) ||
register_chrdev_region(MKDEV(TTYAUX_MAJOR, 2), 1, "/dev/ptmx") < 0)
panic("Couldn't register /dev/ptmx driver");
device_create(tty_class, NULL, MKDEV(TTYAUX_MAJOR, 2), NULL, "ptmx");
}

void tty_default_fops(struct file_operations *fops)
{

*fops = tty_fops;
}

在ptmx设备初始化时tty_default_fops会将指针tty_fops赋值给ptmx_fops,调用ptmx_fops的时候会通过tty_fops指针调相应的函数,其tty_fops定义为:

1
2
3
4
5
6
7
8
9
10
11
static const struct file_operations tty_fops = {
.llseek = no_llseek,
.read = tty_read,
.write = tty_write,
.poll = tty_poll,
.unlocked_ioctl = tty_ioctl,
.compat_ioctl = tty_compat_ioctl,
.open = tty_open,
.release = tty_release,
.fasync = tty_fasync,
};

最后会通过fops->unlocked_ioctl调用tty_ioctl.

unix98_pty_init函数为inline类型,编译器编译时候会直接内联到调用函数中,所以从内核符号表中找到pty_init函数地址为0xffffffc000d78e80,然后进行反汇编:

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
0xffffffc000d78e80:     stp     x29, x30, [sp, #-0x70]!
0xffffffc000d78e84: mov x29, sp
0xffffffc000d78e88: stp x19, x20, [sp, #0x10]
0xffffffc000d78e8c: adrp x20, #0xffffffc000df5000
0xffffffc000d78e90: stp x23, x24, [sp, #0x30]
0xffffffc000d78e94: stp x21, x22, [sp, #0x20]
0xffffffc000d78e98: ldr w0, [x20, #0xef0]
0xffffffc000d78e9c: adrp x23, #0xffffffc000df5000
0xffffffc000d78ea0: stp x25, x26, [sp, #0x40]
0xffffffc000d78ea4: stp x27, x28, [sp, #0x50]
0xffffffc000d78ea8: cmp w0, wzr
0xffffffc000d78eac: adrp x25, #0xffffffc000c1e000
0xffffffc000d78eb0: adrp x22, #0xffffffc000a35000
0xffffffc000d78eb4: adrp x24, #0xffffffc000c1e000
0xffffffc000d78eb8: b.le #0xffffffc000d79014
0xffffffc000d78ebc: movz x1, #0
0xffffffc000d78ec0: movz x2, #0x46
0xffffffc000d78ec4: bl #0xffffffc0003413d0
0xffffffc000d78ec8: cmn x0, #1, lsl #12
0xffffffc000d78ecc: mov x19, x0
0xffffffc000d78ed0: b.ls #0xffffffc000d78ee0
0xffffffc000d78ed4: adrp x0, #0xffffffc000c1e000
0xffffffc000d78ed8: add x0, x0, #0x7c0
0xffffffc000d78edc: bl #0xffffffc0009a799c
0xffffffc000d78ee0: ldr w0, [x20, #0xef0]
0xffffffc000d78ee4: movz x1, #0
0xffffffc000d78ee8: movz x2, #0x46
...

pty_init函数比较长,纯看汇编很难定位到tty_default_fops(&ptmx_fops)这个地方,一种方法是找到32位的kernel反汇编对照着看,因为32位可以用ida的F5,但是依然麻烦。

这里注意到tty_default_fops这个函数在内核符号表中是有导出的,地址为0xffffffc0003431b4,现在重新看反汇编,直接查找这个值,定位附近的代码:

1
2
3
4
5
6
7
0xffffffc000d79174:     add     x0, x0, #0x8f8
0xffffffc000d79178: bl #0xffffffc0009a799c
0xffffffc000d7917c: add x19, x20, #0x10
0xffffffc000d79180: add x20, x20, #0xe8
0xffffffc000d79184: mov x0, x19
0xffffffc000d79188: bl #0xffffffc0003431b4
0xffffffc000d7918c: adrp x2, #0xffffffc00034a000

这里第一个参数x0即为ptmx_fops值。
x0 = x19 = x20 + 0x10
往上看x20赋值的地方。

1
2
3
4
5
6
7
0xffffffc000d79010:     bl      #0xffffffc0009a799c
0xffffffc000d79014: adrp x21, #0xffffffc000f36000
0xffffffc000d79018: movz w0, #0x10, lsl #16
0xffffffc000d7901c: movz x1, #0
0xffffffc000d79020: movz x2, #0x5e
0xffffffc000d79024: add x20, x21, #0xd80
0xffffffc000d79028: bl #0xffffffc0003413d0

x20 = x21 + 0xd80 = 0xffffffc000f36000 + 0xd80
得出
ptmx_fops = 0xffffffc000f36000 + 0xd80 + 0x10

计算函数偏移

接上边,在path ioctl过程中,如何找到ptmx对应的tty_ioctl,这个syscall调用流程为:
sys_ioctl -> do_vfs_ioctl -> tty_ioctl
部分源码:

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
int do_vfs_ioctl(struct file *filp, unsigned int fd, unsigned int cmd,
unsigned long arg)

{

...
default:
if (S_ISREG(inode->i_mode))
error = file_ioctl(filp, cmd, arg);
else
error = vfs_ioctl(filp, cmd, arg);
break;
}
return error;
}

static long vfs_ioctl(struct file *filp, unsigned int cmd,
unsigned long arg)

{

int error = -ENOTTY;

if (!filp->f_op || !filp->f_op->unlocked_ioctl)
goto out;

error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
if (error == -ENOIOCTLCMD)
error = -ENOTTY;
out:
return error;
}

最后调用filp->f_op->unlocked_ioctl(filp, cmd, arg)

所以确定tty_ioctl的偏移即可以从相应的反汇编的寄存器传值看出,当然也可以直接从定义的tty_fops结构中算出,但是如果是复杂的结构体呢。

通过内核符号表找到do_vfs_ioctl的地址为0xffffffc0001a4ca4,反汇编代码:

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
40
41
42
43
44
45
46
47
0xffffffc0001a4ca4: stp x29, x30, [sp, #-0x90]!
0xffffffc0001a4ca8: movz w4, #0x5452
0xffffffc0001a4cac: mov x29, sp
0xffffffc0001a4cb0: stp x19, x20, [sp, #0x10]
0xffffffc0001a4cb4: stp x21, x22, [sp, #0x20]
0xffffffc0001a4cb8: str x23, [sp, #0x30]
0xffffffc0001a4cbc: cmp w2, w4
0xffffffc0001a4cc0: mov x19, x0
0xffffffc0001a4cc4: mov x20, x3
0xffffffc0001a4cc8: ldr x21, [x0, #0x20]
0xffffffc0001a4ccc: b.eq #0xffffffc0001a5068
0xffffffc0001a4cd0: b.ls #0xffffffc0001a4e4c
0xffffffc0001a4cd4: movz w0, #0x5877
0xffffffc0001a4cd8: movk w0, #0xc004, lsl #16
0xffffffc0001a4cdc: cmp w2, w0
0xffffffc0001a4ce0: b.eq #0xffffffc0001a4fa0
...
0xffffffc0001a4fa0: movz w0, #0x15
0xffffffc0001a4fa4: ldr x19, [x21, #0x28]
0xffffffc0001a4fa8: bl #0xffffffc0000a7294
0xffffffc0001a4fac: uxtb w0, w0
0xffffffc0001a4fb0: cbz w0, #0xffffffc0001a51a0
0xffffffc0001a4fb4: ldr x0, [x19, #0x30]
0xffffffc0001a4fb8: ldr x0, [x0, #0x40]
0xffffffc0001a4fbc: cbz x0, #0xffffffc0001a5218
0xffffffc0001a4fc0: mov x0, x19
0xffffffc0001a4fc4: bl #0xffffffc000197d18
0xffffffc0001a4fc8: b #0xffffffc0001a4e84
0xffffffc0001a4fcc: ldrh w0, [x21]
0xffffffc0001a4fd0: and w0, w0, #0xf000
0xffffffc0001a4fd4: cmp w0, #8, lsl #12
0xffffffc0001a4fd8: b.eq #0xffffffc0001a5100
0xffffffc0001a4fdc: ldr x0, [x19, #0x28]
0xffffffc0001a4fe0: cbz x0, #0xffffffc0001a5004
0xffffffc0001a4fe4: ldr x3, [x0, #0x40]
0xffffffc0001a4fe8: cbz x3, #0xffffffc0001a5004
0xffffffc0001a4fec: mov w1, w2
0xffffffc0001a4ff0: mov x0, x19
0xffffffc0001a4ff4: mov x2, x20
0xffffffc0001a4ff8: blr x3
0xffffffc0001a4ffc: cmn w0, #0x203
0xffffffc0001a5000: b.ne #0xffffffc0001a4e84
0xffffffc0001a5004: movn w0, #0x18
0xffffffc0001a5008: b #0xffffffc0001a4e84
0xffffffc0001a500c: mov x0, sp
0xffffffc0001a5010: and x1, x0, #0xffffffffffffc000
...

代码比较长,直接看是看不出来的,查看源码中函数调用:

1
2
3
error = filp->f_op->unlocked_ioctl(filp, cmd, arg);
if (error == -ENOIOCTLCMD)
error = -ENOTTY;

返回值会与ENOIOCTLCMD做比较,查找源码:

1
#define ENOIOCTLCMD    515    /* No ioctl command */

返回值在w0或x0中,直接在反汇编中搜特征值515对应的16进制0x203,如果结果过多,可以通过查看前后调用相关寄存器操作等其他特征进一步确定,这里就找到一处,定位到调用代码片段:

1
2
3
4
5
6
7
8
9
0xffffffc0001a4fdc: ldr x0, [x19, #0x28]
0xffffffc0001a4fe0: cbz x0, #0xffffffc0001a5004
0xffffffc0001a4fe4: ldr x3, [x0, #0x40]
0xffffffc0001a4fe8: cbz x3, #0xffffffc0001a5004
0xffffffc0001a4fec: mov w1, w2
0xffffffc0001a4ff0: mov x0, x19
0xffffffc0001a4ff4: mov x2, x20
0xffffffc0001a4ff8: blr x3
0xffffffc0001a4ffc: cmn w0, #0x203

所以x0为f_op指针,其偏移0x40即为tty_ioctl地址。

总结

  1. 内核漏洞利用过程中,不管是path哪些地址,最好的方法就是直接找到该地方调用处,然后根据编译好的kernel文件去定位。
  2. 同样在找结构体偏移值时,通过调用地方查看寄存器传参来确定偏移值,如果通过源码计算不仅麻烦还容易算错。
  3. 多读源码涨姿势。