构建/dev/kmem枚举所有Linux内核模块(包括隐藏的)

送分小仙女□ 提交于 2020-07-28 04:12:22

Linux系统不是可以有lsmod枚举所有内核模块吗?procfs不香吗?干嘛还要费事从/dev/kmem里去枚举?

其实,Linux是后来的事了,在最初的UNIX时代,像ps之类的枚举进程的,都是从/dev/kmem里扫描出来的,这就是 一切皆文件 ,后来的Linux内核很不恰当地拓展了procfs,将乱八七糟的东西都往里面塞,像modules,filesystems,vmallocinfo之类,这些明明不是进程,全部扔进去了,这并不合适,但就是因为这是Linux内核,所以什么都是对的!

当然,Linux也保留了/dev/mem,/dev/kmem这两个极其特殊且好玩的文件:

  • /dev/mem:映射系统所有的物理内存。
  • /dev/kmem:映射系统所有的内核态虚拟内存。

再后来由于/dev/kmem暴露的权限过大,存在安全隐患,所以一般的内核都封堵了这个字符设备,仅仅保留了/dev/mem,并且还是在受限的情况下:

# CONFIG_DEVKMEM is not set
CONFIG_STRICT_DEVMEM=y

本文我们就展示一下如何通过扫描/dev/kmem来枚举所有的内核模块。

什么?不是说CONFIG_DEVKMEM被禁用了吗?好办!重新移植过来便是了,并且,我重新移植的版本还能支持vmalloc空间的映射呢。

代码如下:

// kmem.c
#include <linux/module.h>
#include <linux/mm.h>
#include <linux/kallsyms.h>
#include <linux/cdev.h>
#include <linux/fs.h>

pgprot_t (*_phys_mem_access_prot)(struct file *, unsigned long, unsigned long, pgprot_t);
phys_addr_t (*_slow_virt_to_phys)(void *);
pte_t *(*_lookup_address)(unsigned long, unsigned int *);

static const struct vm_operations_struct mmap_mem_ops = {
	.access = generic_access_phys
};

static int mmap_kmem(struct file *file, struct vm_area_struct *vma)
{
	unsigned long pfn;
	pte_t *pte;
	unsigned int level = 0;
	size_t size;

	// 这个是我添加的,由于包含了vmalloc空间,要防止去未映射page的虚拟地址取值造成crash。
	pte = _lookup_address((u64)vma->vm_pgoff << PAGE_SHIFT, &level);
	if (!pte || !pte_present(*pte))
		return -EIO;

	// 通过通用的方法获得pfn,而不再仅仅考虑线性映射。
	pfn = _slow_virt_to_phys((void *)(vma->vm_pgoff << PAGE_SHIFT)) >> PAGE_SHIFT;

	if (!pfn_valid(pfn))
		return -EIO;

	vma->vm_pgoff = pfn;

	size = vma->vm_end - vma->vm_start;
	vma->vm_page_prot = _phys_mem_access_prot(file, vma->vm_pgoff,
						 size,
						 vma->vm_page_prot);
	vma->vm_ops = &mmap_mem_ops;
	if (remap_pfn_range(vma,
			    vma->vm_start,
			    vma->vm_pgoff,
			    size,
			    vma->vm_page_prot)) {
		return -EAGAIN;
	}

	return 0;
}

static const struct file_operations kmem_fops = {
	.mmap		= mmap_kmem,
};

dev_t dev = 0;
static struct cdev kmem_cdev;
static int __init devkmem_init(void)
{
	_phys_mem_access_prot = (void *)kallsyms_lookup_name("phys_mem_access_prot");
	_slow_virt_to_phys = (void *)kallsyms_lookup_name("slow_virt_to_phys");
	_lookup_address = (void *)kallsyms_lookup_name("lookup_address");

	if((alloc_chrdev_region(&dev, 0, 1, "test_dev")) <0){
		printk("alloc failed\n");
		return -1;
	}
	printk("major=%d minor=%d \n",MAJOR(dev), MINOR(dev));

	cdev_init(&kmem_cdev, &kmem_fops);

	if ((cdev_add(&kmem_cdev, dev, 1)) < 0) {
		printk("add failed\n");
		goto out;
	}
	return 0;

out:
	unregister_chrdev_region(dev,1);
	return -1;
}

void __exit devkmem_exit(void)
{
	cdev_del(&kmem_cdev);
	unregister_chrdev_region(dev, 1);
}

module_init(devkmem_init);
module_exit(devkmem_exit);
MODULE_LICENSE("GPL");

OK,我们编译加载之,并且创建字符设备:

insmod ./kmem.ko
mknod /dev/kmem c 248 0

然后,下面的脚本展示了如何枚举所有模块。由于我是一个地址一个地址去映射解析的,并且我的bash水平很low,python,go也不会,所以脚本的效率非常低,运行非常慢,如果一下子映射从0xffffffffa0000000到0xffffffffff000000的所有内存,那就快多了,虽然慢,但是绝对足够详细,看吧:

#!/bin/bash

# mlist.sh

start=''
end=''
base=''

moktype=$(cat /proc/kallsyms|grep module_ktype|awk '{print $1}')
# 用于模式匹配
moktype=$(echo $moktype|tr 'a-z' 'A-Z')

for line in $(cat /proc/vmallocinfo |grep 0xffffffffa|awk '{print $1}')
do
	start=$(echo $line|awk -F '-' '{print $1}'|awk -F '0x' '{print $2}')
	start=$(echo $start|tr 'a-z' 'A-Z')
	end=$(echo $line|awk -F '-' '{print $2}'|awk -F '0x' '{print $2}')
	end=$(echo $end|tr 'a-z' 'A-Z')
	base=$start
	next=$base
	while true; do
		val=$(./a.out $next);
		if [ $? -ne 0 ]; then
			break;
		fi
		if [ $val == $base ]; then
			mod=$(echo "ibase=16;$next-138"|bc)
			mod=$(echo "obase=16;$mod"|bc)
			state=$(./a.out $mod)
			if [ $? -ne 0 ] || [ $state != '0' ]; then
				next=$(echo "ibase=16;$next+8"|bc)
				next=$(echo "obase=16;$next"|bc)
				continue;
			fi
			ktype=$(echo "ibase=16;$mod+78"|bc)
			ktype=$(echo "obase=16;$ktype"|bc)
			type=$(./a.out $ktype)
			if [ $? -ne 0 ] || [ $type != $moktype ]; then
				next=$(echo "ibase=16;$next+8"|bc)
				next=$(echo "obase=16;$next"|bc)
				continue;
			fi

			namea=$(echo "ibase=16;$mod+18"|bc)
			namea=$(echo "obase=16;$namea"|bc)
			name=$(./a.out $namea)
			# 仅仅截断名字的前8个字符
			name=$(echo -n $name|sed 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf)
			name=$(echo $name|rev 2>/dev/null)
			if [ $? -eq 0 ]; then
				echo name-- $name
			fi
		fi
		next=$(echo "ibase=16;$next+8"|bc)
		next=$(echo "obase=16;$next"|bc)
	done
done;

解析的结果如下:

[root@localhost test]# ./mlist.sh
name-- dm_mod
name-- dm_regio
name-- serio_ra
name-- dm_log
name-- dm_mirro
name-- libata
name-- ahci
name-- ata_gene
name-- crct10di
name-- e1000
name-- pata_acp
name-- cdrom
name-- sr_mod
name-- crc_t10d
name-- sd_mod
name-- ablk_hel
name-- libcrc32
name-- ip_table
name-- video
name-- i2c_piix
name-- parport
name-- cryptd
name-- parport_
name-- ata_piix
name-- kmem
name-- i2c_core
...

下面简单解释一下。

我们知道,模块的地址映射空间是从0xffffffffa0000000到0xffffffffff000000的,所以我们需要在这个空间里找到它们,只要模块是通过正规途径init_module系统调用加载的,那么它就一定在这个空间里,所以我们只需要扫描这个空间的内存即可,配合关键的两个特征匹配:

  • module结构体module_core的自指特征。
  • module的kobject的ktype特征。

OK,接下来我们来看看如何在这个地址空间里找到被隐藏的模块,也就是摘链的模块:

  • 扫描空隙呗!!

来吧:

#!/bin/bash

start=''
end=''
base=''

moktype=$(cat /proc/kallsyms|grep module_ktype|awk '{print $1}')
moktype=$(echo $moktype|tr 'a-z' 'A-Z')

for line in $(cat /proc/vmallocinfo |grep 0xffffffffa|awk '{print $1}')
do
	start=$(echo $line|awk -F '-' '{print $1}'|awk -F '0x' '{print $2}')
	start=$(echo $start|tr 'a-z' 'A-Z')
	if [ $start == 'FFFFFFFFA0000000' ]; then
		end=$(echo $line|awk -F '-' '{print $2}'|awk -F '0x' '{print $2}')
		end=$(echo $end|tr 'a-z' 'A-Z')
		continue;
	fi
	if [ $start == $end ];then
		end=$(echo $line|awk -F '-' '{print $2}'|awk -F '0x' '{print $2}')
		end=$(echo $end|tr 'a-z' 'A-Z')
		continue;
	fi

	base=$end
	next=$base

	end=$(echo $line|awk -F '-' '{print $2}'|awk -F '0x' '{print $2}')
	end=$(echo $end|tr 'a-z' 'A-Z')

	while true; do
		val=$(./a.out $next);
		if [ $? -ne 0 ]; then
			break;
		fi
		if [ $val == $base ]; then
			mod=$(echo "ibase=16;$next-138"|bc)
			mod=$(echo "obase=16;$mod"|bc)
			state=$(./a.out $mod)
			if [ $? -ne 0 ] || [ $state != '0' ]; then
				next=$(echo "ibase=16;$next+8"|bc)
				next=$(echo "obase=16;$next"|bc)
				continue;
			fi
			ktype=$(echo "ibase=16;$mod+78"|bc)
			ktype=$(echo "obase=16;$ktype"|bc)
			type=$(./a.out $ktype)
			if [ $? -ne 0 ] || [ $type != $moktype ]; then
				next=$(echo "ibase=16;$next+8"|bc)
				next=$(echo "obase=16;$next"|bc)
				continue;
			fi

			namea=$(echo "ibase=16;$mod+18"|bc)
			namea=$(echo "obase=16;$namea"|bc)
			name=$(./a.out $namea)
			name=$(echo -n $name|sed 's/\([0-9A-F]\{2\}\)/\\\\\\x\1/gI' | xargs printf)
			name=$(echo $name|rev 2>/dev/null)
			if [ $? -eq 0 ]; then
				echo name-- $name
			fi
		fi
		next=$(echo "ibase=16;$next+8"|bc)
		next=$(echo "obase=16;$next"|bc)
	done
done;

试试看,只要模块是通过insmod命令加载后动的手脚,很容易就把隐藏模块找出来了,只是时间久一些。

时间久是因为我这个脚本每个地址折腾一番,效率很低,事实上如果每个page一次映射,那就会好很多,但因为我不会编程,所以只能先这样了。

前面说, 只要通过insmod命令,即init_module系统调用加载的模块,都能被找出来。 那如果不通过这种 正规途径 加载模块呢?

其实,你想想看,进入内核的入口,特别是代码进入内核的入口,有多少:

  • init_module
  • ptrace
  • ftrace
  • eBPF

本来就不多,init_module是最常用最容易的,如果不用init_module,还能用什么呢?


浙江温州皮鞋湿,下雨进水不会胖。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!