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,还能用什么呢?

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

技术
©2019-2020 Toolsou All rights reserved,
java实现抢红包功能AndroidStudio开发笔记1--第一个appMybatis错误解决:There is no getter for property named '*' in 'class Java.lang.String单个按键控制多种流水灯状态用Python做自动化测试(pytest框架的精髓)关于keras使用fit_generator中遇到StopIterationMySQL面试必会!Python基础知识整理笔记Redis队列实现java秒杀系统,无脚本,可用于生产崮德好文连载 - 活该你是工程师(自序)