接着上一篇文章《解决Linux内核问题实用技巧之 - Crash工具结合/dev/mem任意修改内存》继续,本文中,我们来领略几种关于/dev/mem的玩法。

/dev/mem里有什么

简单来讲,/dev/mem是系统物理内存的映像文件,这里的 “物理内存” 需要进一步解释。

物理内存是指我们插在内存槽上的内存条吗?当然是,但物理内存不单单指内存条。

物理内存严格来讲应该是指 物理地址空间 ,内存条只是映射到这个地址空间的一部分,其余的还有各种PCI设备,IO端口等。我们可以从/proc/iomem中看到这个映射:

  1. [root@localhost mem]# cat /proc/iomem

  2. 00000000-00000fff : reserved

  3. 00001000-0009fbff : System RAM

  4. 0009fc00-0009ffff : reserved

  5. 000c0000-000c7fff : Video ROM

  6. 000e2000-000ef3ff : Adapter ROM

  7. 000f0000-000fffff : reserved

  8. 000f0000-000fffff : System ROM

  9. 00100000-31ffffff : System RAM

  10. 01000000-01649aba : Kernel code

  11. 01649abb-01a74b7f : Kernel data

  12. 01c13000-01f30fff : Kernel bss

  13. 32000000-33ffffff : RAM buffer

  14. 3fff0000-3fffffff : ACPI Tables

  15. e0000000-e0ffffff : 0000:00:02.0

  16. e0000000-e0ffffff : vesafb

  17. f0000000-f001ffff : 0000:00:03.0

  18. f0000000-f001ffff : e1000

  19. ...

  20. ...

其中,只有RAM才是指内存条。关于物理地址空间的详细情况,请参考E820相关的资料。

明白了物理内存的构成之后,我们来看看/dev/mem里有什么。事实上,它就是一个活着的Linux系统实时映像,所有的进程taskstruct结构体,sock结构体,skbuff结构体,进程数据等等都在里面的某个位置:如果能定位它们在/dev/mem里的位置,我们就能得到系统中这些数据结构的实时值,所谓的调试工具所做的也不过如此。其实我们在调试内核转储文件的时候,vmcore也是一个物理内存映像,和/dev/mem不同的是,它是一具尸体。

无论是活体,还是尸体,均五脏俱全,分析它们的手段是一致。和静态分析vmcore不同的是,/dev/mem是一个动态的内存映像,有时候借助它可以做一些正经的事情。

下面通过几个小例子,介绍和展示/dev/mem的一些玩法。

映射系统保留内存

Linux内核的内存管理子系统非常强大,同时也非常复杂。我们受其恩惠的同时,偶尔也会被其折磨得痛苦不堪。

动辄OOM杀掉关键进程,动辄刷脏页导致CPU飙高...

为了避免任意进程任意使用内存,我们引入资源隔离的机制,比如cgroup,但这样事情会变得更加复杂。

能不能保留一部分内存,不受内核的内存管理控制呢?就好像很多数据库不经文件系统直接访问裸盘一样,内核有没有什么机制让我们不经内存管理系统而直接使用内存呢?

当然有!加上mem启动参数即可实现。这里介绍一种关于保留内存的最简单配置,设置mem启动参数如下:

  1. mem=800M

假设我们的系统总共有1G的内存(指内存条的总容量),那么上述启动参数将会保留 1G-800M 的内存不被系统内存管理系统所管理。因此我的保留内存就是200M:

  1. [root@localhost mem]# cat /proc/cmdline

  2. BOOT_IMAGE=/vmlinuz-3.10.0-327.x86_64 root=/dev/mapper/centos-root ro crashkernel=auto rd.lvm.lv=centos/root rd.lvm.lv=centos/swap rhgb quiet LANG=en_US.UTF-8 vga=793 mem=800M

  3. [root@localhost mem]# cat /proc/iomem |grep RAM

  4. 00001000-0009fbff : System RAM

  5. 00100000-31ffffff : System RAM

  6. 32000000-33ffffff : RAM buffer

  7. [root@localhost mem]#

我们关注一下保留内存的物理地址0x34000000(0x33ffffff+1) 。此时,如果用free命令或者/proc/meminfo,会看到物理内存少了200M,我们保留的200M内存不会记入内核的任何统计。

换句话说, 内核不再管这200M内存,你的程序可以任意涂抹,任意泄漏,任意溢出,任意覆盖它们,也不会对系统产生任何影响。

所谓的系统保留的含义就是 “内核不会为该段内存创建一一映射页表(x86_64位系统可以映射64T的物理内存)”

我们经常使用的crash工具读取内存使用的就是一一映射。

在x86_64平台,每一个非保留的物理内存页面可能会有多个映射,而保留物理内存页面不会有下面第一种映射:

  1. 一一映射到0xffff880000000000开始虚拟地址。【保留页面缺失】

  2. 映射到用户态使用它的进程地址空间。

  3. 临时映射到内核态空间临时touch。

  4. .....

我们试着用crash工具来读取一下保留内存:

  1. crash> rd -p 0x34000000

  2. rd: read error: physical address: 34000000 type: "64-bit PHYSADDR"

  3. crash>

显然,内核并未对保留页面建立一一映射页表项,所以读取是失败的。

我们知道 /dev/mem 文件是整个物理内存映像,所以用户态进程可以使用mmap系统调用来重建用户态地址空间的页表。方法如下:

  1. #include <stdio.h>

  2. #include <unistd.h>

  3. #include <sys/mman.h>

  4. #include <fcntl.h>

  5. int main(int argc, char **argv)

  6. {

  7. int fd;

  8. unsigned long *addr;

  9. fd = open("/dev/mem", O_RDWR);

  10. // 0x34000000 即/dev/mem的偏移,也就是保留内存在物理地址空间的偏移,我的例子就是0x34000000

  11. addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34000000);

  12. // ... 随意使用保留内存

  13. close(fd);

  14. munmap(addr, 4096);

  15. return 1;

  16. }

是不是很简单呢?

此时,在我们实施mmap的进程中便可以访问保留内存了:

  1. crash> vtop 0x7f3751c3a000

  2. VIRTUAL PHYSICAL

  3. 7f3751c3a000 34000000

  4. PML: 6e477f0 => 2dbf7067

  5. PUD: 2dbf76e8 => c524067

  6. PMD: c524470 => 2c313067

  7. PTE: 2c3131d0 => 8000000034000277

  8. PAGE: 34000000

  9. PTE PHYSICAL FLAGS

  10. 8000000034000277 34000000 (PRESENT|RW|USER|PCD|ACCESSED|DIRTY|NX)

  11. VMA START END FLAGS FILE

  12. ffff88000b7e7af8 7f3751c3a000 7f3751c3b000 50444fb /dev/mem

这个例子中,我们展示了/dev/mem如何用来访问保留内存。接下来我们继续用简单的小例子演示/dev/mem的其它玩法。

进程间交换页面

有这么一种需求:

  • 我们不希望进程A和进程B共享任何页面,这意味着它们不能同时操作同一份数据。

  • 偶尔我们希望进程A和进程B交换数据,却又不想用低效的传统进程间通信机制。

是不是觉得两难了呢?其实我们可以让两个进程的页面进行交换来达到目的。为了让页表项交换尽可能简单,我们依然使用保留内存,解除内核内存管理对操作的约束。

下面给出示例程序代码,先看进程A,master.c:

  1. // gcc master.c -o master

  2. #include <stdio.h>

  3. #include <unistd.h>

  4. #include <sys/mman.h>

  5. #include <string.h>

  6. #include <fcntl.h>

  7. int main(int argc, char **argv)

  8. {

  9. int fd;

  10. unsigned long *addr;

  11. fd = open("/dev/mem", O_RDWR);

  12. // 映射保留地址的一个页面P1

  13. addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34000000);

  14. // 写页面P1的内容

  15. *addr = 0x1122334455667788;

  16. printf("address at: %p content is: 0x%lx\n", addr, addr[0]);

  17. // 等待交换

  18. getchar();

  19. printf("address at: %p content is: 0x%lx\n", addr, addr[0]);

  20. close(fd);

  21. munmap(addr, 4096);

  22. return 1;

  23. }

接下来看希望与之进程页面交换的进程B,slave.c:

  1. // gcc slave.c -o slave

  2. #include <stdio.h>

  3. #include <unistd.h>

  4. #include <sys/mman.h>

  5. #include <string.h>

  6. #include <fcntl.h>

  7. int main(int argc, char **argv)

  8. {

  9. int fd;

  10. unsigned long *addr;

  11. fd = open("/dev/mem", O_RDWR);

  12. // 映射保留地址的页面P2

  13. addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0x34004000);

  14. // 写页面P2的内容

  15. *addr = 0x8877665544332211;

  16. printf("address at: %p content is: 0x%lx\n", addr, addr[0]);

  17. // 等待交换

  18. getchar();

  19. printf("address at: %p content is: 0x%lx\n", addr, addr[0]);

  20. close(fd);

  21. munmap(addr, 4096);

  22. return 1;

  23. }

页面交换的原理非常简单,互换两个进程的两个虚拟地址的页表项即可。实现这件事意味着需要编写内核模块,但是由于我们只是演示,所以我们可以用crash工具轻松达到目标。

小帖士:如若希望crash工具可写/dev/mem,参见上一篇文章,用systemtap HOOK住devmemisallowed,使其恒返回1.

操作演示过程如下:这个实例非常适用于设计微内核的进程间通信机制。配合以cache一致性协议,会非常高效。

安全篡改程序的内存

所谓的安全篡改程序的内存指的是用一种可靠的方法改程序内存,而不是通过手工hack页表的方式,简单起见,这次我们借助crash工具来完成。

首先我们看一个程序:

  1. #include <stdio.h>

  2. #include <unistd.h>

  3. #include <sys/mman.h>

  4. #include <string.h>

  5. int main(int argc, char **argv)

  6. {

  7. unsigned char *addr;

  8. // 匿名映射一段内存

  9. addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_ANONYMOUS|MAP_SHARED, -1, 0);

  10. // 为其拷贝数据

  11. strcpy(addr, "浙江温州皮鞋湿");

  12. // 只是演示,所以我直接将地址打印出,真实场景需要自己hack出来

  13. printf("address at: %p content is: %s\n", addr, addr);

  14. getchar();

  15. printf("address at: %p content is: %s\n", addr, addr);

  16. munmap(addr, 4096);

  17. return 1;

  18. }

运行它:

  1. [root@localhost mem]# ./a.out

  2. address at: 0x7fa5990f2000 content is: 浙江温州皮鞋湿

我们想把 “浙江温州皮鞋湿” 这块内存内容改成 “下雨进水不会胖”,如何做呢?

方法很多,这里介绍crash /dev/mem的方法。首先我们找到addr对应的物理页面:

  1. crash> set 1819

  2. PID: 1819

  3. ...

  4. crash> vtop 0x7f360c756000

  5. VIRTUAL PHYSICAL

  6. 7f360c756000 6d3d000

  7. PML: 2ddec7f0 => 150b6067

  8. PUD: 150b66c0 => 1506b067

  9. PMD: 1506b318 => 2c591067

  10. PTE: 2c591ab0 => 8000000006d3d067

  11. PAGE: 6d3d000

  12. PTE PHYSICAL FLAGS

  13. 8000000006d3d067 6d3d000 (PRESENT|RW|USER|ACCESSED|DIRTY|NX)

  14. VMA START END FLAGS FILE

  15. ffff880015070000 7f360c756000 7f360c757000 fb dev/zero

  16. PAGE PHYSICAL MAPPING INDEX CNT FLAGS

  17. ffffea00001b4f40 6d3d000 ffff88002fe24710 0 2 1fffff00080038 uptodate,dirty,lru,swapbacked

我们得到了addr对应的物理地址是 0x6d3d000。

现在让我们写另一个程序,映射/dev/mem,然后修改偏移0x6d3d000处的内存即可:

  1. // gcc hacker.c -o hacker

  2. #include <stdio.h>

  3. #include <unistd.h>

  4. #include <sys/mman.h>

  5. #include <string.h>

  6. #include <fcntl.h>

  7. int main(int argc, char **argv)

  8. {

  9. int fd;

  10. unsigned char *addr;

  11. unsigned long off;

  12. off = strtol(argv[1], NULL, 16);

  13. fd = open("/dev/mem", O_RDWR);

  14. addr = mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_SHARED, fd, off);

  15. strcpy(addr, "下雨进水不会胖");

  16. close(fd);

  17. munmap(addr, 4096);

  18. return 1;

  19. }

直接执行:

  1. [root@localhost mem]# ./hacker 0x6d3d000

然后在a.out的运行终端敲入回车:

  1. [root@localhost mem]# ./a.out

  2. address at: 0x7fa5990f2000 content is: 浙江温州皮鞋湿

  3. address at: 0x7fa5990f2000 content is: 下雨进水不会胖

  4. [root@localhost mem]#

这个例子比较简单,显得无趣,OK,下面这个例子稍微有点意思。

通过写/dev/mem修改任意进程的名字

本例我们将放弃crash工具的使用,仅仅依靠hack /dev/mem来修改一个进程的名字。

这对于一些互联网产品的运营是有意义的。\ 特别是在一些托管机器上,为了防止数据泄漏,一般是不允许使用类似crash & gdb工具debug的,当然了,systemtap接口有限制,所以安全,内核模块一般也会禁止。但是有systemtap和/dev/mem就够了!

我们来做这样一个实验:

  • 修改已经在运行的进程的名字。

看看如何完成。先来看一个很简单的程序:

  1. // gcc pixie.c -o pixie

  2. #include <stdio.h>

  3. int main(int argc, char **argv)

  4. {

  5. getchar();

  6. }

运行它:

  1. [root@localhost mem]# gcc pixie.c -o pixie

  2. [root@localhost mem]# ./pixie

现在我们想办法把进程的名字pixie改成skinshoe。

没有crash工具,没有gdb工具,只有一个可以读写的/dev/mem(假设我们已经HOOK了devmemsiallowed函数)。怎么做到呢?

我们知道,内核所有的数据结构都在/dev/mem中可以找到,因此,我们要找到pixie进程的task_struct结构体的位置,然后更改它的comm字段。问题是/dev/mem是物理地址空间,而操作系统操作的任何内存都基于虚拟地址,如何建立两者之间的关联是关键的。

我们注意到三点事实:

  • x86_64可以直接映射64T的物理内存,足以一一映射当前常见的任意物理内存。

  • Linux内核对所有物理内存建立一一映射。物理地址和虚拟地址之间固定偏移。

  • Linux内核的数据结构是彼此关联的网状结构,因此便可以顺藤摸瓜。

这意味着,只要我们提供一个Linux内核空间数据结构的虚拟地址,我们就能求出它的物理地址,然后顺藤摸瓜就能找到我们的pixie进程的task_struct结构体。

在Linux系统中,很多地方都可以找到内核数据结构的地址:

  • /proc/kallsyms文件。

  • /boot/System.map文件。

  • lsof的结果

最简单的方法,那便是通过在/proc/kallsyms或者System.map里找到init_task的地址,比如在我的环境下:

  1. ffffffff81951440 D init_task

然后在 arch/x86/kernel/vmlinux.lds.S 里找到inittask到物理内存的映射规则,从inittask开始遍历系统的task链表,找到我们的目标pixie进程,改之。

但这种方法无法让人体验在/dev/mem里顺藤摸瓜的快乐的感觉,所以我们最后再来说它,现在我们尝试用一种稍微麻烦的方法来实现修改特定进程名字目标。

我的方法是创建一个tcpdump进程,却不抓任何包,它只是一个提供蛛丝马迹的幌子,我们就从它入手:

  1. [root@localhost ~]# tcpdump -i lo -n

  2. tcpdump: verbose output suppressed, use -v or -vv for full protocol decode

  3. listening on lo, link-type EN10MB (Ethernet), capture size 262144 bytes

之所以创建tcpdump进程,是因为tcpdump会创建一个packet套接字,而该套接字的虚拟地址会被展示在procfs中:

  1. [root@localhost mem]# cat /proc/net/packet

  2. sk RefCnt Type Proto Iface R Rmem User Inode

  3. ffff88002c201000 3 3 0003 1 1 0 0 22050

  4. [root@localhost mem]#

OK,就是这个 0xffff88002c201000 作为我们的突破口。我们从它开始顺藤摸瓜!

我们知道,0xffff88002c201000 是一个struct sock对象的内存地址,我们熟悉sock结构体的详情,知道它的偏移224字节处是一个等待队列waitqueuehead_t对象。

这一点需要你对Linux内核结构体非常熟悉,如果不熟悉,就找到对应源码取手算一下偏移。【 或者借用一下crash工具的struct X.y -o计算偏移也可

而waitqueueheadt对象内部又被一个waitqueuet对象链入,该waitqueueheadt对象被tcpdump始发的pollwqueues结构体所管理,最终它的pollingtask字段指向tcpdump本身,而我们需要的正是tcpdump的task_struct对象本身,因为整个系统的所有task均被链接在一个list中。

整体的关联图示如下:有了这个结构,我们就可以写代码了。

由于x86_64可以直接一一映射64T的内存,而我只有区区1G的内存,可以保证的是,虚拟地址减去一一映射的基地址(在我的系统它就是 0xffff880000000000)就是物理地址了。

假设packet套接字的地址是0xffff88002c201000,我们就能确认其物理地址在0x2c201000处,编写代码,映射/dev/mem,从0x2c201000开始找起:

  1. #include <stdio.h>

  2. #include <unistd.h>

  3. #include <sys/mman.h>

  4. #include <string.h>

  5. #include <fcntl.h>

  6. int main(int argc, char **argv)

  7. {

  8. int fd;

  9. unsigned long off;

  10. unsigned long *pltmp;

  11. unsigned char *addr, *base;

  12. fd = open("/dev/mem", O_RDWR);

  13. off = strtol(argv[1], NULL, 16);

  14. addr = mmap(NULL, 0xffffffff, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

  15. base = addr;

  16. addr += off; // 定位sock

  17. addr += 224;

  18. pltmp = addr;

  19. addr = *pltmp;

  20. addr -= 0xffff880000000000;

  21. addr += (unsigned long)base;

  22. addr += 8;

  23. pltmp = addr;

  24. addr = *pltmp;

  25. addr -= 0xffff880000000000;

  26. addr += (unsigned long)base;

  27. addr -= 24;

  28. addr += 8;

  29. pltmp = addr;

  30. addr = *pltmp;

  31. addr -= 0xffff880000000000;

  32. addr += (unsigned long)base;

  33. addr += 24;

  34. pltmp = addr;

  35. // 找到了tcpdump的task结构体。

  36. addr = *pltmp;

  37. addr -= 0xffff880000000000;

  38. addr += (unsigned long)base;

  39. addr += 1288;

  40. addr += 8;

  41. pltmp = addr;

  42. // 这里开始应该是一个循环,遍历整个tasks链表,然而本例中我们可以保证pixie进程在tcpdump之前,所以就简化了逻辑,直接找它前面的task即可。

  43. addr = *pltmp;

  44. addr -= 0xffff880000000000;

  45. addr += (unsigned long)base;

  46. addr += 8;

  47. pltmp = addr;

  48. addr = *pltmp;

  49. addr -= 0xffff880000000000;

  50. addr += (unsigned long)base;

  51. addr -= 1288;

  52. pltmp = addr;

  53. addr += 1872;

  54. pltmp = addr;

  55. printf("program name is: %s\n", addr);

  56. strcpy(addr, "skinshoe");

  57. close(fd);

  58. munmap(addr, 0xffffffff);

  59. return 1;

  60. }

我们用 0xffff88002c201000 的物理地址作为偏移执行程序:

  1. [root@localhost mem]# ./modname 2c201000

  2. program name is: pixie

然后,你会发现pixie这个程序的名字被改了:

  1. [root@localhost mem]# cat /proc/3442/comm

  2. skinshoe

可见,pixie变成skinshoe了。

修改进程名字只是一个例子,既然我们都已经拿到task_struct结构体了,我们就可以学着crash工具的样子去做点类似debug的事情了。下面我们继续。

解析/dev/mem遍历系统所有进程

提到遍历进程,一般能想到的有两个思路:

  1. 遍历procfs文件系统的进程pid目录,解析目录的内容。

  2. 编写模块,调用 for_each_process 宏遍历进程。

还有第一种方法。

当我们知道/dev/mem是整个系统内存映像时,我们就知道整个系统的所有数据结构都在里面可以被找到,当然也包括进程链表。我们现在的任务显然就是在 /dev/mem 里找到它。

在上一小节里,我们已经可以通过一个 tcpdump 制造的packet 套接字找到了我们任意的名叫pixie的进程,并将其名字改成 skinshoe。我们回顾一下通过packet套接字寻找pixie进程的过程:

  1. 通过sock结构体找到固定偏移处的waitqueueheadt字段skwq。

  2. 通过waitqueueheadt找到waitqueuet字段listhead。

  3. 通过listhead对象找到waitqueue_t字段。

  4. 通过waitqueuet对象找到poll_wqueues字段private。

  5. 通过pollwqueues对象找到taskt字段polling_task。

我们可以从polling_task的固定偏移1288字节处定位到一个list,遍历该list就是遍历整个系统进程链表!

我们既然能找到特定的名字为pixie的进程,自然也就能遍历整个链表。

本小节内容我们接着上一个小节继续写,只不过是把 “定位特定进程” 改成了 “遍历整个链表” 。而后者更加简单。

整个过程中,我们要做的只是确定以下两件事情:

  1. 内存一一映射的起始地址是多少? 在我的 3.10.0-327.el7.x86_64 实验系统中该值是 0xffff880000000000. c//arch/x86/include/asm/page_64_types.h:32: #define __PAGE_OFFSET _AC(0xffff880000000000, UL)

  2. 系统初启时init_task映射到哪个虚拟地址? 在我的 3.10.0-327.el7.x86_64 实验系统中该值是 0xffffffff81951440 bash ffffffff81951440 D init_task

我们看下x86_64内存映射整体的模样:

基础知识就说到这里,现在仅仅靠手撸一个/dev/mem文件,到底如何能遍历整个系统的进程?talk is cheap, show you the code:

  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <unistd.h>

  4. #include <sys/mman.h>

  5. #include <string.h>

  6. #include <fcntl.h>

  7. #define DIRECT_MAPPING_START 0xffff880000000000

  8. // from ./kernel/vmlinux.lds.S

  9. // .data : AT(ADDR(.data) - LOAD_OFFSET)

  10. // #define LOAD_OFFSET __START_KERNEL_map

  11. // #define __START_KERNEL_map 0xffffffff80000000

  12. #define START_MAPPING_START 0xffffffff80000000

  13. int main(int argc, char **argv)

  14. {

  15. int i = 0;

  16. unsigned int pid0 = 0xffffffff, *pitmp;

  17. int fd;

  18. unsigned long off, map_off = DIRECT_MAPPING_START;

  19. unsigned long *pltmp, *pltmp2;

  20. unsigned char *addr, *taddr, *base, *pctmp;

  21. fd = open("/dev/mem", O_RDWR);

  22. off = strtoll(argv[1], NULL, 16) - DIRECT_MAPPING_START & 0x0000ffffffffffff;

  23. addr = mmap(NULL, 0xffffffff, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

  24. base = addr;

  25. addr += off; // 定位sock

  26. #define SK_WQ_OFFSET 224 // sock.sk_wq

  27. addr += SK_WQ_OFFSET;

  28. pltmp = (unsigned long *)addr;

  29. addr = (unsigned char *)*pltmp;

  30. addr -= DIRECT_MAPPING_START;

  31. addr += (unsigned long)base;

  32. #define LIST_PREV_OFFSET 8 // list_head.prev

  33. addr += LIST_PREV_OFFSET;

  34. pltmp = (unsigned long *)addr;

  35. addr = (unsigned char *)*pltmp;

  36. addr -= DIRECT_MAPPING_START;

  37. addr += (unsigned long)base;

  38. #define WAITQ_TASK_OFFSET 24 // __wait_queue.task_list

  39. addr -= WAITQ_TASK_OFFSET;

  40. #define PRIVATE_OFFSET 8 // __wait_queue.private

  41. addr += PRIVATE_OFFSET;

  42. pltmp = (unsigned long *)addr;

  43. addr = (unsigned char *)*pltmp;

  44. addr -= DIRECT_MAPPING_START;

  45. addr += (unsigned long)base;

  46. #define POLLING_TASK_OFFSET 24 // poll_wqueues.polling_task

  47. addr += POLLING_TASK_OFFSET;

  48. pltmp = (unsigned long *)addr;

  49. addr = (unsigned char *)*pltmp;

  50. addr -= DIRECT_MAPPING_START;

  51. addr += (unsigned long)base;

  52. #define PID_OFFSET 1404 // task_struct.pid

  53. #define COMM_OFFSET 1872 // task_struct.comm

  54. pitmp = (unsigned int *)(addr + PID_OFFSET);

  55. pid0 = *pitmp;

  56. printf("pid0 is:%d\n", *pitmp);

  57. #define TASKS_OFFSET 1288 // task_struct.tasks

  58. addr += TASKS_OFFSET;

  59. addr += LIST_PREV_OFFSET;

  60. pltmp = (unsigned long *)addr;

  61. while (1) {

  62. addr = (unsigned char *)*pltmp; // list

  63. addr -= map_off;

  64. addr += (unsigned long)base;

  65. addr += LIST_PREV_OFFSET ;// prev

  66. pltmp = (unsigned long *)addr;

  67. taddr = (unsigned char *)*pltmp; // list_entry

  68. if (*pitmp == 1) {

  69. taddr -= START_MAPPING_START;

  70. } else {

  71. taddr -= DIRECT_MAPPING_START;

  72. }

  73. taddr += (unsigned long)base;

  74. taddr -= TASKS_OFFSET;

  75. pitmp = (unsigned int *)(taddr + PID_OFFSET);

  76. if (*pitmp == pid0) {

  77. break;

  78. }

  79. if (*pitmp == 0) {

  80. map_off = START_MAPPING_START;

  81. } else {

  82. map_off = DIRECT_MAPPING_START;

  83. }

  84. printf("%d\t", *pitmp);

  85. pctmp = (taddr + COMM_OFFSET);

  86. printf("[%s] \n", pctmp);

  87. }

  88. close(fd);

  89. munmap(addr, 0xffffffff);

  90. return 1;

  91. }

以上代码不太难理解,唯一要注意的就是 init_task 的定位。

inittask,也就是Linux系统0号进程,它非常特殊,它并不是fork产生的,它伴随着着系统的初启,也就是说,上电的那一刻,x86进入保护模式的那一刻,就处在0号进程的上下文,然而此时,内存映射规则还没有建立。inittask该在何处?

inittask映射在 *“手写的一个位置”* ,即 *arch/x86/kernel/vmlinux.lds.S*文件里规定的位置。当你通过某种手段找到 inittask 的虚拟地址的时候,减去 LOAD_OFFSET 就是其物理地址:

  1. #define LOAD_OFFSET __START_KERNEL_map

  2. #define __START_KERNEL_map 0xffffffff80000000

  3. ...

  4. /* Data */

  5. .data : AT(ADDR(.data) - LOAD_OFFSET) {

  6. /* Start of data section */

  7. _sdata = .;

  8. /* init_task */

  9. INIT_TASK_DATA(THREAD_SIZE)

所以我们单独定义了:

  1. #define START_MAPPING_START 0xffffffff80000000

这就是定位init_task的依据。

言归正传,将上面的C代码编译,演示一下遍历进程。如下:

  1. [root@localhost mem]# cat /proc/net/packet

  2. sk RefCnt Type Proto Iface R Rmem User Inode

  3. ffff88002dbb8000 3 3 0003 2 1 0 0 42249

  4. [root@localhost mem]# ./listtasks 0x88002dbb8000

  5. pid0 is:6020

  6. 5784 [kworker/1:1]

  7. 5762 [ssh]

  8. 5760 [kworker/3:2H]

  9. 5754 [ssh]

  10. ... // 篇幅所限,省略

  11. 1 [systemd]

  12. 0 [swapper/0]

  13. 6159 [listtasks]

  14. ... // 篇幅所限,省略

  15. [root@localhost mem]#

OK,成功遍历了所有进程!美中不足的是遍历过程未加锁,可能会有同步问题,但无论如何最严重的也只是listtasks进程SEGV。

现在,让我们把上述的代码移植到 3.10.0-862.11.6.el7.x86_64 内核的系统,按照原样执行,出现SEGV。

事实上,每一个运行着的内核的地址链接参数均可能不一致,这也是Linux内核版本间ABI不兼容的原因和结果。不过,要想让 3.10.0-327.el7.x8664 的代码跑在 3.10.0-862.11.6.el7.x8664 系统版本上也不难,按照以下的定义修改宏即可:

  1. #define DIRECT_MAPPING_START 0xffff8b9d40000000

  2. // from ./kernel/vmlinux.lds.S

  3. // .data : AT(ADDR(.data) - LOAD_OFFSET)

  4. // #define LOAD_OFFSET __START_KERNEL_map

  5. // #define __START_KERNEL_map 0xffffffff80000000

  6. #define START_MAPPING_START 0xffffffffa6a00000

  7. #define SK_WQ_OFFSET 224 // sock.sk_wq

  8. #define LIST_PREV_OFFSET 8 // list_head.prev

  9. #define WAITQ_TASK_OFFSET 24 // __wait_queue.task_list

  10. #define PRIVATE_OFFSET 8 // __wait_queue.private

  11. #define POLLING_TASK_OFFSET 24 // poll_wqueues.polling_task

  12. #define PID_OFFSET 1188 // task_struct.pid

  13. #define COMM_OFFSET 1656 // task_struct.comm

  14. #define TASKS_OFFSET 1072 // task_struct.tasks

下面是3.10.0-862.11.6.el7.x86_64内核上的演示:

  1. [root@localhost ~]# cat /proc/net/packet

  2. sk RefCnt Type Proto Iface R Rmem User Inode

  3. ffff8b9d7bf89000 3 3 0003 3 1 0 0 16883

  4. [root@localhost ~]# ./a.out 8b9d7bf89000

  5. pid0 is:959

  6. 719 [NetworkManager]

  7. 716 [firewalld]

  8. 708 [login]

  9. 703 [crond]

  10. 698 [systemd-logind]

  11. 696 [polkitd]

  12. ...

  13. ...

  14. 3 [ksoftirqd/0]

  15. 2 [kthreadd]

  16. 1 [systemd]

  17. 0 [swapper/0]

  18. 2685 [a.out]

  19. 2171 [pickup]

  20. 2166 [stapio]

  21. 1416 [qmgr]

  22. 1414 [master]

  23. 1136 [xinetd]

  24. 1131 [tuned]

  25. 1129 [rsyslogd]

  26. 1126 [sshd]

  27. [root@localhost ~]#

在我们已经掌握了通过特定内存地址的蛛丝马迹顺藤摸瓜在/dev/mem里找到特定结构体这种技巧之后,回过头来再来看相对简单的通过init_task遍历所有进程的方法就很很容易理解了。

下面是通过init_task作为引子遍历所有进程的代码:

  1. #include <stdio.h>

  2. #include <stdlib.h>

  3. #include <unistd.h>

  4. #include <sys/mman.h>

  5. #include <string.h>

  6. #include <fcntl.h>

  7. #define DIRECT_MAPPING_START 0xffff880000000000

  8. #define DIRECT_MAPPING_END 0xffffc7ffffffffff

  9. // from ./kernel/vmlinux.lds.S

  10. // .data : AT(ADDR(.data) - LOAD_OFFSET)

  11. // #define LOAD_OFFSET __START_KERNEL_map

  12. // #define __START_KERNEL_map 0xffffffff80000000

  13. #define START_MAPPING_START 0xffffffff80000000

  14. int main(int argc, char **argv)

  15. {

  16. int i = 0 ;

  17. unsigned int pid0 = 0xffffffff, *pitmp;

  18. int fd;

  19. unsigned long off, map_off = DIRECT_MAPPING_START;

  20. unsigned long *pltmp, *pltmp2;

  21. unsigned char *addr, *taddr, *base, *pctmp;

  22. fd = open("/dev/mem", O_RDWR);

  23. // 参数为我们在/proc/kallsyms里找到的init_task的地址。

  24. off = strtoll(argv[1], NULL, 16) - START_MAPPING_START & 0x0000ffffffffffff;

  25. addr = mmap(NULL, 0xffffffff, PROT_READ|PROT_WRITE, MAP_SHARED, fd, 0);

  26. base = addr;

  27. addr += off;

  28. #define TASKS_OFFSET 1288

  29. addr += TASKS_OFFSET;

  30. pltmp = (unsigned long *)addr;

  31. while (1) {

  32. taddr = (unsigned char *)*pltmp; // list_entry

  33. taddr -= map_off;

  34. taddr += (unsigned long)base;

  35. taddr -= TASKS_OFFSET;

  36. #define PID_OFFSET 1404 // task_struct.pid

  37. #define COMM_OFFSET 1872 // task_struct.comm

  38. pitmp = (unsigned int *)(taddr + PID_OFFSET);

  39. printf("%d\t", *pitmp);

  40. pctmp = (taddr + COMM_OFFSET);

  41. printf("[%s] \n", pctmp);

  42. addr = (unsigned char *)*pltmp; // list

  43. addr -= map_off;

  44. addr += (unsigned long)base;

  45. pltmp = (unsigned long *)addr;

  46. if (*pltmp > DIRECT_MAPPING_END) {

  47. break;

  48. }

  49. }

  50. close(fd);

  51. munmap(addr, 0xffffffff);

  52. return 1;

  53. }

看起来代码短了不少。演示如下:

  1. [root@10 mem]# ./a.out ffff81951440

  2. 1 [systemd]

  3. 2 [kthreadd]

  4. 3 [ksoftirqd/0]

  5. 7 [migration/0]

  6. 8 [rcu_bh]

  7. 9 [rcuob/0]

  8. 10 [rcuob/1]

  9. 11 [rcuob/2]

  10. 12 [rcuob/3]

  11. 13 [rcu_sched]

  12. 14 [rcuos/0]

  13. 15 [rcuos/1]

  14. 16 [rcuos/2]

  15. ...

通过写/dev/mem手刃进程

无条件杀掉一个进程的方式不外乎两种:

  1. SIGKILL杀掉它。

  2. 自身出了严重的问题而自毁。

要么从外部着手,要么由内部腐烂,但一个进程的灭亡均需要理由。然而,一个好好的进程整体被掏空意味着什么?可以做到吗?这就好比一个善良且健康的人,突然间遭遇了严重的意外事故那般不幸。

通过写/dev/mem可以轻而易举掏空一个进程,当进程再次准备执行时,发现自己什么都没有了。

我们可以定位到进程在/dev/mem的位置,进而摘除进程的vma,清空stack...有点残忍,我便不再举例细说。

通过写/dev/mem修改函数指令

作为最后一个例子,我想是时候前后呼应一下了,《解决Linux内核问题实用技巧之 - Crash工具结合/dev/mem任意修改内存》中的第一个例子是可以自由快乐玩转后面例子的前提,即修改devmemisallowed函数的指令,使其恒返回1,现在,我们通过写/dev/mem的方式把它还原回去,从而结束本文。

我们从/proc/kallsyms中查到devmemisallowed的地址:

  1. ffffffff8105e630 T devmem_is_allowed

这是它原来的样子:ja语句被我们改成了两个nop,现在我们要把两个nop还原成ja:值得注意的是,devmemisallowed函数只会约束/dev/mem的open和mmap调用,一旦mmap成功,访问/dev/mem就像正常的访存操作,不再受到文件读写的限制,所以才可以安全地写/dev/mem,而不必像hook它时那样必须原子写才能成功。

OK,从修改devmemisallowed开始,到恢复devmemisallowed结束,我们玩转了圆满。

为了轻松下车,我们最后再安排一个例子。

合法访问NULL地址

到底NULL地址能不能访问呢?到底是谁不让访问NULL地址呢?

先说结论:

  • NULL地址完全可以访问,只要有页表项映射它到一个物理页面就行。

Linux系统有一个参数控制能不能mmap NULL地址:

  1. [root@10 ~]# cat /proc/sys/vm/mmap_min_addr

  2. 4096

  3. [root@10 ~]# echo 0 >/proc/sys/vm/mmap_min_addr

  4. [root@10 ~]# cat /proc/sys/vm/mmap_min_addr

  5. 0

我们做一个实验,看个究竟,先看代码:

  1. // gcc access0.c -o access0

  2. #include <stdio.h>

  3. #include <stdlib.h>

  4. #include <sys/mman.h>

  5. int main(int argc, char **argv)

  6. {

  7. int i;

  8. unsigned char *niladdr = NULL;

  9. unsigned char str[] = "Zhejiang Wenzhou pixie shi,xiayu jinshui buhui pang!";

  10. mmap(0, 4096, PROT_READ|PROT_WRITE, MAP_FIXED|MAP_ANONYMOUS|MAP_SHARED, -1, 0);

  11. perror("a");

  12. for (i = 0 ; i < sizeof(str); i++) {

  13. niladdr[i] = str[i];

  14. }

  15. printf("using assignment at NULL: %s\n", niladdr);

  16. for (i = 0 ; i < sizeof(str); i++) {

  17. printf ("%c", niladdr[i]);

  18. }

  19. printf ("\n");

  20. getchar();

  21. munmap(0, 4096);

  22. return 0;

  23. }

运行它:

  1. [root@10 mem]# ./access0

  2. a: Success

  3. using assignment at NULL: (null)

  4. Zhejiang Wenzhou pixie shi,xiayu jinshui buhui pang!

此时我们crash工具看看NULL的映射:看起来并没有不同。

之所以NULL地址不让访问,是为了更好地区分什么是合法地址,所以人为制造了一个特殊地址NULL,MMU层面上,NULL并无不同。

结语

好了,关于crash工具和/dev/mem的话题要结束,结合前面一篇文章,建议亲自做这些实验,方可获得更深刻地认知。

更加重要的是,每个人在亲自动手做这些实验时,会碰到各种各样新的问题,分析以及最终hack掉这些问题,正是快乐感觉的由来,分享这种快乐本身也是一件快乐的事情,这也是我写这两篇文章的动力。

临渊羡鱼,不如退而结网。


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

(完)

      Linux阅码场原创精华文章汇总

Linux阅码场精选在线视频课程汇总

更多精彩,尽在"Linux阅码场",扫描下方二维码关注

你的随手转发或点个在看是对我们最大的支持!

解决Linux内核问题实用技巧之-dev/mem的新玩法相关推荐

  1. Linux /dev/mem的新玩法

    来自<解决Linux内核问题实用技巧之-dev/mem的新玩法> 接着上一篇文章<解决Linux内核问题实用技巧之 - Crash工具结合/dev/mem任意修改内存>继续,本 ...

  2. 弃 Windows 而拥抱 Linux 之后,这本书教了新玩法

    微软弃 Windows 而拥抱 Linux 之后,国内首本SQL Server On Linux的图书出版,这本书教了很多新玩法. SQL Server作为微软公司著名的数据库管理系统,多年以来一直稳 ...

  3. 硬盘底座linux,微客智品 篇五十二:机械硬盘如何安放?用奥睿科单盘位移动硬盘底座助力新玩法...

    微客智品 篇五十二:机械硬盘如何安放?用奥睿科单盘位移动硬盘底座助力新玩法 2020-01-07 15:36:58 1点赞 10收藏 30评论 随着新一代M.2硬盘的流行与普及,3.5寸机械硬盘愈发变 ...

  4. 淘宝足迹新玩法,如何通过足迹增加商品曝光量,打标后足迹不出解决方法,淘宝详情页的下拉出现足迹怎么实现的

    以上两张图就是淘宝最近新更新的一个对于足迹的一个新玩法(释放查看更多精彩),具体的玩法是在浏览某个详情页时,详情页往下滑,就会跳转出自己足迹的中的商品,这个商品的推荐位的个数是不固定的,左右滑动即可查 ...

  5. linux内核锁死怎么解决_解决Linux内核中的2038年问题

    linux内核锁死怎么解决 由于时间在Linux中的表示方式,带符号的32位数字无法支持20:38(UTC)3:14:07之后的时间. 2038年 (Y2038或Y2K38)问题是关于时间数据类型表示 ...

  6. 解决linux内核更新后VM无法正常运行问题

    本人也是无意之中猜坑,本来讲道理的说,每次linux内核的更新都会都会要求系统中的VMware 对 vmmon 和 vmnet 俩模块重新编译.但这一次,讲道理说应该和往常一样,我像个工具人一样重新编 ...

  7. 抖音直播带货人气提升于实战技巧,附带直播带货玩法套路丨国仁网络

    2020年,是直播带货高速发展之年,人人都能直播带货. 在这样一个繁荣景象下,大家都想迎上这个风口,无数明星.名人纷纷加入"混战". 今天这篇文章纯干货,价值巨大.也是在抖音直播带 ...

  8. 抖音如何定位?抖音技巧必知的13种玩法

    作为营销人,出于职业习惯,对新鲜事物总有一种探索欲.以下结合自己所见所闻以及查找到的资料做了一个关于抖音技巧玩法总结,详细见下方思维导图. 1.技能类 也是在抖音技巧细水长流,一直有流量的一各套路.不 ...

  9. 微信昵称上标电话号码,实用的新玩法

    大家好,我又发现了个新玩意,不知道看完文章后,有多少朋友来撩呢?今天,给大家推荐一款微信昵称的最新玩法,可以在微信昵称上方标注手机号码,有利于从事销售.互联网等行业的小伙伴拓展客户时进行强展示.独特的 ...

  10. linux内核升级写入不了,解决linux内核升级后不能重启系统的故障

    1.闲来无事,想升级下centos的内核.升级方法就不多讲了. 2.升级完后,重启系统,发现系统无法启动,具体原因是系统无法挂载文件系统. 3.在网上找了下资料,原因出在了initrd是旧版本mkin ...

最新文章

  1. 将request中的所有参数存放到自定义的map中
  2. Ubuntu下pip3的安装、升级、卸载
  3. 通过Wireshark抓包分析谈谈DNS域名解析的那些事儿
  4. Qt 从C ++定义QML类型(二)
  5. 201771010126 王燕《面向对象程序设计(Java)》第十六周学习总结
  6. 基于UDP协议的套接字+socketserver模块
  7. iOS开发UI篇—直接使用UITableView Controller
  8. linux脚本大全,shell大全
  9. Java框架篇---spring aop两种配置方式
  10. ASP.NET使用Session的七点认识
  11. 移位运算符 实现 二进制数的 高低位翻转(完整逻辑代码)
  12. 51单片机~红外通信工作原理
  13. 中国流行歌手普遍缺乏科学的高音。
  14. 大二单片机笔记,串口通信代码【郭天祥】【700字】【勿笑】【原创】
  15. 招商银行信用卡中心笔测
  16. flex简介——css
  17. 索(shen)引(keng)大全
  18. 五款剪辑软件,那个更好用?
  19. PyCharm中安装库失败 ERROR: Could not find a version that satisfies the requirement (from version None)
  20. 左手系和右手系转换最最最简便方法

热门文章

  1. 金蝶专业版服务器操作系统,金蝶kis专业版 服务器 设置
  2. 堆密度测定的意义_测定颗粒真密度的意义是什么
  3. java利用poi为excel添加图片水印
  4. 小学生听力测试软件,亲测十款小学英语APP,为了孩子请收藏
  5. iOS利用HealthKit获取健康里的步数和睡眠时间
  6. Win32编程之基于MATLAB与VC交互的幻方阵(魔方阵)输出
  7. 无人机倾斜摄影三维建模过程及方案
  8. BeatSaber节奏光剑双手柄MR教程
  9. MC9S12G128模块化分层化软件架构之八_QAC静态代码分析
  10. 信息系统集成监理费收取标准_信息工程监理取费参考标准.doc