首页
论坛
课程
招聘
[工具脚本] [源码分析] [系统相关] [HOOK注入] [程序开发] [逆向分析] [基础理论] [原创]开源一个Linux内核里进程内存管理模块源码
2020-5-5 07:59 7429

[工具脚本] [源码分析] [系统相关] [HOOK注入] [程序开发] [逆向分析] [基础理论] [原创]开源一个Linux内核里进程内存管理模块源码

2020-5-5 07:59
7429

Linux它是一款开源的内核系统。本人也非常喜欢嵌入式Linux系统,特别是它的内核源码,书写的风格,都非常讨我心欢。这个驱动是之前业余的时候写的对于新手来说,还是有学习价值的。


下面将对源码进行简单的讲解。

首先是隐藏内核驱动模块。

1
2
list_del_init(&__this_module.list);
kobject_del(&THIS_MODULE->mkobj.kobj);

list_del_init是将自身驱动模块从驱动列表(lsmod)中抹掉
kobject_del是将自己从/sys/class/xxxxxx中抹掉

接下来是打开进程接口。

在Linux内核里,不区分进程与线程。统一按照线程来看待。那么每个线程都有一个对应的pid_t、pid、task_struct。
他们之间的关系是这样的:
pid_t <–> struct pid

nr为进程pid数值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
#include <linux/pid.h>
 
pid_t pid_vnr(struct pid *pid)
{
    return pid_nr_ns(pid, current->nsproxy->pid_ns);
}
EXPORT_SYMBOL_GPL(pid_vnr);
 
struct pid *find_pid_ns(int nr, struct pid_namespace *ns);
EXPORT_SYMBOL_GPL(find_pid_ns);
 
struct pid *find_vpid(int nr)
{
    return find_pid_ns(nr, current->nsproxy->pid_ns);
}
EXPORT_SYMBOL_GPL(find_vpid);
 
struct pid *find_get_pid(int nr)
{
    struct pid *pid;
 
    rcu_read_lock();
    pid = get_pid(find_vpid(nr));
    rcu_read_unlock();
 
    return pid;
}
EXPORT_SYMBOL_GPL(find_get_pid);
 
void put_pid(struct pid *pid);
EXPORT_SYMBOL_GPL(put_pid);
 
 
struct pid * –> struct task_struct *
 
 
#include <linux/pid.h>
 
struct pid *get_task_pid(sturct task_struct *task, enum pid_type);
EXPORT_SYMBOL_GPL(get_task_pid);
 
struct task_struct *pid_task(struct pid *pid, enum pid_type);
EXPORT_SYMBOL(pid_task);
 
struct task_struct *get_pid_task(struct pid *pid, enum pid_type)
{
    struct task_struct *result;
    rcu_read_lock();
    result = pid_task(pid, type);
    if (result)
        get_task_struct(result);
    rcu_read_unlock();
    return result;
}
EXPORT_SYMBOL(get_pid_task);
 
#include <linux/sched.h>
#define get_task_struct(tsk) do { atomic_inc(&(tsk)->usage); } while (0)
static inline void put_task_struct(struct task_struct *t)
{
    if (atomic_dec_and_test(&t->usage))
        __put_task_struct(t);
}
 
void __put_task_struct(struct task_struct *t);
EXPORT_SYMBOL_GPL(__put_task_struct);

看完以上的逻辑。大家是不是柳暗花明又一村,心里开朗了许多,他们之间是可以相互转换的。通过进程pid_t可以拿到pid,通过pid可以拿到task_struct。又可以反过来通过task_struct拿到进程pid。

然后是关闭进程接口

驱动源码是使用put_pid将进程pid*的使用次数减去1

 

在Linux内核源码/kernel/pid.c下可以看到

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void put_pid(struct pid *pid)
{
    struct pid_namespace *ns;
 
    if (!pid)
        return;
 
    ns = pid->numbers[pid->level].ns;
    if ((atomic_read(&pid->count) == 1) ||
         atomic_dec_and_test(&pid->count)) {
        kmem_cache_free(ns->pid_cachep, pid);
        put_pid_ns(ns);
    }
}
EXPORT_SYMBOL_GPL(put_pid);

读进程内存、写进程内存接口

这里采用读、写物理内存的思路,因为这样简洁明了,不需要理会其他反调试干扰。
首先根据pid*用get_pid_task取出task_struct。再用get_task_mm取出mm_struct结构。因为这个结构包含了进程的内存信息。首先检查内存是否可读if (vma->vm_flags & VM_READ)
如果可读。那么开始计算物理内存地址位置。由于Linux内核默认开启MMU机制,所以只能以页为单位计算物理内存地址。计算物理内存地址的方法有很多,这里提供三种思路:
第一种是使用get_user_pages,

第二种是直接使用pagemap文件,

第三种是纯手写算法(将pgd、pud、pmd和pte拼接在一起)

我个人推荐使用第三种方法,也是我正在使用的方法,速度极快而且不触碰任何的进程文件,被调试进程无任何感知。

不推荐使用第一种get_user_pages方法,因为此方法会触发反调试机制,导致调试失败。

知道了物理内存地址后,读、写物理内存地址,其实Linux内核里面也有演示,即drivers/char/mem.c。写的非常详细。最后还要注意MMU机制的离散内存,即buffer不连续问题,通俗的说就是不要一下子读太多,读到另一页去了,要分开页来读

获取进程内存块列表

这个接口就很简单了,通过task_struct取出mm_struct,接下来在mm_struct中遍历取出vma。详情可以参考代码fs\proc\task_mmu.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct mm_struct {
    struct vm_area_struct * mmap;        /* list of VMAs */
    struct rb_root mm_rb;
    struct vm_area_struct * mmap_cache;    /* last find_vma result */
#ifdef CONFIG_MMU
    unsigned long (*get_unmapped_area) (struct file *filp,
                unsigned long addr, unsigned long len,
                unsigned long pgoff, unsigned long flags);
    void (*unmap_area) (struct mm_struct *mm, unsigned long addr);
#endif
    unsigned long mmap_base;        /* base of mmap area */
    unsigned long mmap_legacy_base;         /* base of mmap area in bottom-up allocations */
    unsigned long task_size;        /* size of task vm space */
    unsigned long cached_hole_size;     /* if non-zero, the largest hole below free_area_cache */
    unsigned long free_area_cache;        /* first hole of size cached_hole_size or larger */
    unsigned long highest_vm_end;        /* highest vma end address */
    pgd_t * pgd;
    atomic_t mm_users;            /* How many users with user space? */
    atomic_t mm_count;            /* How many references to "struct mm_struct" (users count as 1) */
    int map_count;                /* number of VMAs */
 
    spinlock_t page_table_lock;        /* Protects page tables and some counters */
    struct rw_semaphore mmap_sem;
 
    struct list_head mmlist;        /* List of maybe swapped mm's.    These are globally strung
                         * together off init_mm.mmlist, and are protected
                         * by mmlist_lock
                         */
 
 
    unsigned long hiwater_rss;    /* High-watermark of RSS usage */
    unsigned long hiwater_vm;    /* High-water virtual memory usage */
 
    unsigned long total_vm;        /* Total pages mapped */
    unsigned long locked_vm;    /* Pages that have PG_mlocked set */
    unsigned long pinned_vm;    /* Refcount permanently increased */
    unsigned long shared_vm;    /* Shared pages (files) */
    unsigned long exec_vm;        /* VM_EXEC & ~VM_WRITE */
    unsigned long stack_vm;        /* VM_GROWSUP/DOWN */
    unsigned long def_flags;
    unsigned long nr_ptes;        /* Page table pages */
    unsigned long start_code, end_code, start_data, end_data;
    unsigned long start_brk, brk, start_stack;
    unsigned long arg_start, arg_end, env_start, env_end;
 
    unsigned long saved_auxv[AT_VECTOR_SIZE]; /* for /proc/PID/auxv */

获取进程命令行

mm_struct结构体里面有个arg_start变量,储存的地址值即是进程命令行。
但这里有个要注意的地方,经过我多台设备测试发现,并不是每个Linux内核系统的arg_start变量偏移值是一样的,这样子就会非常危险,一旦读错就会死机,而且原因还不好查找。

这里我使用了一些玄学的技巧,驱动可以自适应地在不同的Linux内核中自行修正arg_start变量的偏移值,从而读取到正确的值,不会死机。

获取进程PID列表

驱动里写入了两种方法,第一种是遍历/proc/pid目录,第二种是遍历task_struct结构体。这里有个要注意的地方,经过我多台设备测试发现,并不是每个Linux内核系统的task_struct结构体里的tasks变量偏移值是一样的,但具体玄学修正方法我还没时间进行编写,待有空再补充。

获取进程物理内存大小

读取task_struct结构体里的mm_struct,再读取rss_stat就会有进程的物理内存占用大小,这个来源与/proc/pid/status里的源码编写。这里同样需要玄学技巧修正变量的偏移值,具体方法我已编写在内。

获取进程权限、设置进程权限为ROOT

在取得task_struct进程结构后,观察头文件可以发现里面有两个变量值,一个是real_cred,另一个是cred,其实很简单,将两个cred里面的uid、gid、euid、egid、fsuid、fsgid修改成0即可

1
2
const struct cred __rcu *real_cred;
const struct cred __rcu *cred;

real_cred指向主体和真实客体证书,cred指向有效客体证书。通常情况下,cred和real_cred指向相同的证书,但是cred可以被临时改变
同样需要注意,每个Linux内核的cred结构变量偏移值并不是一样的,读错会死机,同理,我也使用了玄学的技巧,驱动能智能修正Linux内核变量的偏移值,能准确的识别出每个Linux内核版本里real_cred与cred的正确偏移位置

开源地址(含使用demo)

Github跳转

总结:

首先,编译此源码需要一定的技巧,再者,手机厂商本身已设置多重障碍用来阻止第三方驱动的加载,如果您需要加载此驱动,则需要将内核中的一些限制给去除。(其实这些验证都可以用IDA暴力Patch之~)。

提醒:

本源码不针对任何程序,仅供交流、学习、调试Linux内核程序的用途,禁止用于任何非法用途,调试器是一把双刃剑,希望大家能营造一个良好的Linux内核Rootkit技术的交流环境。

后续:

后面即将开源:
“不需要源码,强制暴力修改手机Linux内核、去除加载内核驱动的所有验证”
“不需要源码,强制加载启动ko驱动文件、跨Linux内核版本、跨设备启动ko驱动模块文件”
“不需要源码,Linux内核进程万能调试器-可过所有的反调试机制-含硬件断点:Linux天下调,让天下没有难调试的Linux进程!”
“不需要源码,突破Linux内核Elf结构限制、将ollvm混淆加入到ko驱动文件中,增加驱动逆向难度”


[看雪官方培训] Unicorn Trace还原Ollvm算法!《安卓高级研修班》2021年6月班火热招生!!

最后于 2021-4-3 01:42 被abcz316编辑 ,原因: 补充图片
收藏
点赞7
打赏
分享
最新回复 (35)
雪    币: 7001
活跃值: 活跃值 (5667)
能力值: ( LV12,RANK:210 )
在线值:
发帖
回帖
粉丝
pureGavin 活跃值 2 2020-5-5 12:54
2
0
mark,楼主辛苦了
雪    币: 714
活跃值: 活跃值 (537)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
Oday小斯 活跃值 2020-5-5 13:00
3
0
感谢分享
雪    币: 1169
活跃值: 活跃值 (476)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
lionnnn 活跃值 2020-5-5 13:40
4
0
嵌入式开发板点亮LED,需要用到这么高级的技术?
雪    币: 244
活跃值: 活跃值 (416)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
abcz316 活跃值 2020-5-5 16:47
5
0
lionnnn 嵌入式开发板点亮LED,需要用到这么高级的技术?

我觉得只是Linux的基础教程,因为各方面都是浅层操作,还没到Rootkit操作

最后于 2020-10-7 12:35 被abcz316编辑 ,原因:
雪    币: 256
活跃值: 活跃值 (178)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
ZwCopyAll 活跃值 2020-5-5 18:09
6
0
学习渗透 吧windows linux学完 还需要学unicx mac吗
雪    币: 244
活跃值: 活跃值 (416)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
abcz316 活跃值 2020-5-5 19:28
7
0
ZwCopyAll 学习渗透 吧windows linux学完 还需要学unicx mac吗
我不玩渗透
雪    币: 137
活跃值: 活跃值 (362)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
niuzuoquan 活跃值 2020-5-6 21:15
8
0
mark
雪    币:
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
wx_皮皮_598 活跃值 2020-5-11 14:01
9
0
mark,楼主辛苦
雪    币: 756
活跃值: 活跃值 (61)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
mb_pwtdwvrv 活跃值 2021-3-10 23:43
10
0
我的天,太强了,这技术应该已经算是天花板了,期待后续文章
我还是个大学生,学的物联网专业,硬件和软件的相关课程。
看了您这篇内核驱动模块过反调试真的是太精彩了
我直接膜拜拜拜拜拜拜....
雪    币: 36
活跃值: 活跃值 (171)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
..._579212 活跃值 2021-4-19 22:44
11
0
楼主辛苦了。没有源码,在已经Root的定制平板能获取到它新增的驱动文件嘛?
雪    币: 217
活跃值: 活跃值 (352)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
滚动不息的球 活跃值 2021-4-20 00:01
12
0
为什么会含硬件断点,arm有硬件断点吗
雪    币: 1403
活跃值: 活跃值 (296)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
灬哈密瓜 活跃值 2021-4-21 09:52
13
0
mark就是学会
雪    币: 244
活跃值: 活跃值 (416)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
abcz316 活跃值 2021-4-21 10:13
14
0
..._579212 楼主辛苦了。没有源码,在已经Root的定制平板能获取到它新增的驱动文件嘛?[em_13]
能,我现在都不需要源码了~~
雪    币: 11231
活跃值: 活跃值 (371)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
diasstudy 活跃值 2021-4-21 10:45
15
0
厉害,准备深入研究下。
雪    币: 2251
活跃值: 活跃值 (320)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
VNRKDOEA 活跃值 2021-4-21 13:15
16
0
收藏=会了
雪    币: 2
活跃值: 活跃值 (259)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
asdf呵呵 活跃值 2021-4-28 02:40
17
0

楼主,你好!

我参考你的代码,写了个测试ko。把模块导入手机后,使用安装卸载模块命令都能成功,相关日志也证明模块能被成功安装和卸载。又写了一个app做测试,手机是自己编译的android10,按论坛里的贴子内容,改了反反调试的代码,装了magisk。

 app启动后先获取了root权限。大概逻辑是这样的

Runtime.getRuntime().exec("su");
Runtime.getRuntime().exec("id"); // 这里得到的 uid 是0。
Runtime.getRuntime().exec("setenforce 0");

然后,在app中安装ko模块。

Runtime.getRuntime().exec("insmod /* module name */");
Runtime.getRuntime().exec("insmod /* module name */");

lsmod和dmesg,都发现安装模块成功。但是到打开 “/dev/device_name"就总是提示没有权限。参考你的代码:

open(/* module_name_full_path */, O_RDWR);

open总是返回-1,错误提示 "Permission denied"。查看 "/dev/moduld_name" 发现模块权限为:

crw-------  1 root      root

修改java部分代码,在安装模块前,先执行:

chmod 666 /dev/moduld_name

再次open /dev/module_name,成功后,几秒内手机就自动重启了。

之后,在ndk中执行了一次 id 命令,发现在nkd中,id命令执行后uid是普通用户,即使先执行一次su命令,仍然如此。在java层代码中,执行su后,用 id 命令检查uid,是root用户。搜了一下,估计就是这个原因,导致在ndk代码中api open总是失败。

楼主知道有什么办法解决这个重启的问题吗?

最后于 2021-4-28 02:42 被asdf呵呵编辑 ,原因:
雪    币: 16
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_ipugnluu 活跃值 2021-4-30 03:02
18
0
刚接触,有没有完整的编译教程,有偿
雪    币: 159
活跃值: 活跃值 (651)
能力值: ( LV4,RANK:50 )
在线值:
发帖
回帖
粉丝
fengyunabc 活跃值 1 2021-4-30 09:40
19
0
感谢分享!
雪    币: 244
活跃值: 活跃值 (416)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
abcz316 活跃值 2021-5-1 10:36
20
0
asdf呵呵 楼主,你好!我参考你的代码,写了个测试ko。把模块导入手机后,使用安装卸载模块命令都能成功,相关日志也证明模块能被成功安装和卸载。又写了一个app做测试,手机是自己编译的android10,按论坛里的 ...
1.关闭selinux
雪    币: 16
能力值: ( LV1,RANK:0 )
在线值:
发帖
回帖
粉丝
mb_ipugnluu 活跃值 2021-5-1 13:50
21
0
楼主,如何过版本验证
雪    币: 2
活跃值: 活跃值 (259)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
asdf呵呵 活跃值 2021-5-2 22:43
22
0
abcz316 1.关闭selinux
关闭selinux没用,按你的代码写个elf,以根用户权限执行,可以正常执行。我写了个app,在 so 里面执行“连接驱动”,提示没有权限。执行代码 open("su", "w"); open("chmod 666 /dev/FILE_NAME\n set enforce 0"); 都没有效果
雪    币: 244
活跃值: 活跃值 (416)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
abcz316 活跃值 2021-5-4 12:30
23
0
asdf呵呵 关闭selinux没用,按你的代码写个elf,以根用户权限执行,可以正常执行。我写了个app,在 so 里面执行“连接驱动”,提示没有权限。执行代码 open("su", &quo ...
不可能,除非你的手机不是市面上的普通手机。我的手机全部是市面的普通手机,比如小米8 9 10 11
雪    币: 244
活跃值: 活跃值 (416)
能力值: ( LV3,RANK:20 )
在线值:
发帖
回帖
粉丝
abcz316 活跃值 2021-5-4 12:33
24
0
mb_ipugnluu 楼主,如何过版本验证
最简单暴力的方法,找个0day怼一下
雪    币: 2
活跃值: 活跃值 (259)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
asdf呵呵 活跃值 6天前
25
0
abcz316 不可能,除非你的手机不是市面上的普通手机。我的手机全部是市面的普通手机,比如小米8 9 10 11
手机是pixel,系统是自己编译的android10。
游客
登录 | 注册 方可回帖
返回