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,还能用什么呢?
浙江温州皮鞋湿,下雨进水不会胖。
来源:oschina
链接:https://my.oschina.net/u/4392473/blog/4281610