0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

如何对NULL指针地址建立合法映射,从而合法访问NULL指针

Linux阅码场 来源:Linuxer 2019-11-29 14:26 次阅读
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

本文将介绍如何对NULL指针地址建立合法映射,从而合法访问NULL指针。本文表达的宗旨:

任何虚拟地址,只要有合法的页表映射,就能访问!

提到C语言编程,我想几乎所有人都遭遇过NULL指针。我们的代码中总是在不断的判断指针是否为NULL:

if (p1 != NULL) {

//...

}

if (p2 == NULL) {

exit(-1);

}

如果我们忘记了这种判断,我们会收获到段错误:

[15445.731305] a.out[3511]: segfault at 0 ip 000000000040071c sp 00007ffedbacbdd0 error 4 in a.out[400000+1000]

诚然,我们都讨厌segfault,但segfault并非由于访问NULL指针引起的,相反,我们要感谢NULL指针,它帮助我们的程序排除了大量的segfault。

在现代操作系统中,程序访问的地址都是虚拟地址,硬件MMU结合操作系统创建的页表会在进程私有虚拟地址和全局物理地址之间做映射,当程序访问一个虚拟地址的时候,该映射会将这次访问转换成到物理地址的访问。

所以,segfault的本质是程序访问的虚拟内存地址无法合理映射到物理地址的一种错误通知。

引发segfault的地址成为非法地址。

现在,随意给出两个虚拟地址:

unsigned char *p1 = 0x7f1233443344;

unsigned char *p2 = 0xaa12bb443344;

谁能说出哪个虚拟地址是合法的,哪个是非法的?谁也说不出,只有试着访问它的时候才知道,引发segfault的地址就是非法的,否则就是合法的。这可能会对程序数据造成严重的伤害。

因此有必要人为规定一个非法地址,这样在程序中就可以做判断了,只要不是人为规定的那个非法地址,那就是合法的。至于说谁来严格保证其合法性,除了需要编程规范和编程习惯之外,操作系统也确实不会为该非法地址映射可以访问的物理页面。有法可依只是安全的必要条件,加上违法必究才是充分且必要的。

数字0是最特殊的,判断一个值是否为0在硬件层面上也很高效,把0作为非法地址具有高度的可辨识性,于是几乎所有的编程语言都用0来表示非法地址:

#define NULL 0

这就是NULL指针的本质。

现在让我们忘掉编程层面的原则,重新审视NULL指针。

NULL指针指示地址0,地址0没有什么特殊的,它就是进程地址空间的一个普通地址,只要为其映射一个可以访问的物理地址,它就是可以访问的。下面我们就来试试。

首先我们写个简单的C程序:

// gcc access0.c -o access0

#include

#include

#include

int main(int argc, char **argv)

{

int i, j;

unsigned char *nilp = NULL;

unsigned char *used = NULL;

used = (unsigned char *)calloc(128, 1);

// 写页面,调物理页面到内存。

strcpy(used, "zhejiang wenzhou pixie shi");

// 以下的打印便于将信息传递到内核模块,这只是为了方便,真正

// 正确的做法应该自己去hack这些信息,然后传递到内核模块。

printf("pid=%d addr=%p ", getpid(), used);

// 等待内核模块创建NULL地址的页表,完成后敲回车。

getchar();

// 打印NULL指针的前64个字节

for (i = 0; i < 4; i++) {

for (j = 0; j < 16; j++) {

printf("0x%0.2x ", *nilp);

nilp++;

}

printf(" ");

}

getchar();

free (used);

return 0;

}

可以看到,从for循环开始,我们的程序访问NULL指针地址后的64字节的数据。我们希望把NULL指针映射到calloc的地址处,然后看看是不是打印出了 “zhejiang wenzhou pixie shi”。

这个很简单,写一个内核模块,把NULL开始的一个page和calloc返回的used开始的一个page映射到同一个物理页面即可。

下面该写内核模块了,为了简化操作,这里采用Guru模式的stap脚本来进行编程:

// mapNULL.stp

%{

#include

#include

#include

pte_t * get_pte(struct task_struct *task, unsigned long address)

{

pgd_t* pgd;

pud_t* pud;

pmd_t* pmd;

pte_t* pte;

struct mm_struct *mm = task->mm;

static int nil = 0;

static pmd_t gpmd = {0};

static pte_t gpte = {0};

pgd = pgd_offset(mm, address);

if(pgd_none(*pgd) || pgd_bad(*pgd)) {

return NULL;

}

pud = pud_offset(pgd, address);

if(pud_none(*pud) || pud_bad(*pud)) {

return NULL;

}

pmd = pmd_offset(pud, address);

if(pmd_none(*pmd) || pmd_bad(*pmd)) {

*pmd = gpmd;

if(pmd_none(*pmd) || pmd_bad(*pmd)) {

return NULL;

}

}

pte = pte_offset_kernel(pmd, address);

if (nil != 0) {

pte->pte &= 0xfffffffffffff000;

*pte = gpte;

}

if(pte_none(*pte)) {

return NULL;

}

if (nil == 0) {

gpmd = *pmd;

gpte = *pte;

nil = 1;

}

return pte;

}

%}

function mapNULL:long(pid:long, addr:long)

%{

struct task_struct *task;

pte_t* pte;

void (*fun)(void);

fun = (void (*))0xffffffff81066090;

fun();

task = pid_task(find_pid_n***_ARG_pid, &init_pid_ns), PIDTYPE_PID);

if(!(pte = get_pte(task, STAP_ARG_addr))) {

STAP_RETVALUE = -1;

return;

}

fun();

if(get_pte(task, 0) == NULL) {

STAP_RETVALUE = -1;

return;

}

fun();

STAP_RETVALUE = 0;

%}

probe begin

{

mapNULL($1, $2);

exit();

}

下面演示一下效果,先看直接执行access0,不加载内核模块的效果:

[root@localhost mod]# ./access0

pid=4172 addr=0x1c78010

段错误

[root@localhost mod]#

很显然,访问了“非法地址NULL”之后,收获一个segfault。下面,我们结合内核模块再次来运行access0:

[root@localhost mod]# ./access0

pid=4174 addr=0xf38010

另起一个终端,按照打印的pid和addr加载模块:

[root@localhost mod]# stap -g mapNULL.stp 4174 0xf38010

[root@localhost mod]#

access0的终端敲入回车:

[root@localhost mod]# ./access0

pid=4174 addr=0xf38010

0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x91 0x00 0x00 0x00 0x00 0x00 0x00 0x00

0x7a 0x68 0x65 0x6a 0x69 0x61 0x6e 0x67 0x20 0x77 0x65 0x6e 0x7a 0x68 0x6f 0x75

0x20 0x70 0x69 0x78 0x69 0x65 0x20 0x73 0x68 0x69 0x00 0x00 0x00 0x00 0x00 0x00

0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

[root@localhost mod]#

可以看到,第二行开始的就是“zhejiang Wenzhou pixie shi ”了:

0x7a 0x68 0x65 0x6a 0x69 0x61 0x6e 0x67 0x20 0x77 0x65 0x6e 0x7a 0x68 0x6f 0x75

0x20 0x70 0x69 0x78 0x69 0x65 0x20 0x73 0x68 0x69 ...

那么第一行是什么呢?很显然,used内存是calloc返回的,这种内存是被malloc内存管理结构锁管理的,第一行的16字节就是这种管理机构,如果我们破坏掉它,那么在最后的free处就会出错。我们可以试一试:

// 打印NULL指针的前64个字节

for (i = 0; i < 4; i++) {

for (j = 0; j < 16; j++) {

printf("0x%0.2x ", *nilp);

if (i == 0) 将第一行16字节数据设置成0ff。

*nilp = 0xff;

nilp++;

}

printf(" ");

}

效果就是:

[root@localhost mod]# ./access0

pid=4184 addr=0x90a010

0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x91 0x00 0x00 0x00 0x00 0x00 0x00 0x00

0x7a 0x68 0x65 0x6a 0x69 0x61 0x6e 0x67 0x20 0x77 0x65 0x6e 0x7a 0x68 0x6f 0x75

0x20 0x70 0x69 0x78 0x69 0x65 0x20 0x73 0x68 0x69 0x00 0x00 0x00 0x00 0x00 0x00

0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00

*** Error in `./access0': munmap_chunk(): invalid pointer: 0x000000000090a010 ***

======= Backtrace: =========

/lib64/libc.so.6(+0x7f5d4)[0x7f06b56705d4]

./access0[0x400789]

/lib64/libc.so.6(__libc_start_main+0xf5)[0x7f06b56133d5]

./access0[0x4005c9]

======= Memory map: ========

00400000-00401000 r-xp 00000000 fd:00 38533721

通过重写NULL指针地址的映射页表,我们成功访问了NULL指针,并且读出了数据。

由于MMU的映射粒度是页面,即4096字节(x86_64平台,也可以是别的值,比如2M),所以严格来讲,“非法地址”并非只有NULL,而是从0到4096的一个页面。

很多系统正是通过将NULL地址开始的一个page映射到一个不可读写不可访问的物理page来达到捕捉非法地址的效果的。

现在,我们把部分task_struct结构体的内存映射到NULL开始的第一个虚拟地址空间页面,通过修改task结构体的comm来修改自己的名字,达到自省的目的。

修改自己名字的方法很多,prct就可以,但是本文通过映射task结构体的方式进行。

先看用户态C代码:

#include

#include

#include

int main(int argc, char **argv)

{

int i;

unsigned char *nilp = NULL;

// 为模块提供信息。

printf("pid=%d addr=%p ", getpid(), used);

getchar();

// 在一个页面范围查找task的comm字段

for (i = 0; i < 4096; i++) {

// +2是为了跳过“./”,此处没有进行复杂的字符串解析

if (!memcmp(nilp, argv[0]+2, strlen(argv[0])-2)) {

printf("OK ");

// 更改comm字段为皮鞋湿

memcpy(nilp, "pixieshi", 8);

break;

}

nilp++;

}

printf(" ");

getchar();

free (used);

}

下面是对应的内核模块:

// mapCOMM.c

// make -C /lib/modules/`uname -r`/build SUBDIRS=`pwd` modules

#include

#include

#include

#define DIRECT_MAP_START 0xffff880000000000

#define PAGE_TABLE_E 0x8000000000000000

static int pid = 16790;

module_param(pid, int, 0644);

static unsigned long addr = 0;

module_param(addr, long, 0644);

static int nil = 0;

static pmd_t gpmd = {0};

static pte_t gpte = {0};

static unsigned long tskp;

void (*fun)(void);

static pte_t* get_pte(struct task_struct *task, unsigned long address)

{

pgd_t* pgd;

pud_t* pud;

pmd_t* pmd;

pte_t* pte;

struct mm_struct *mm = task->mm;

pgd = pgd_offset(mm, address);

if(pgd_none(*pgd) || pgd_bad(*pgd)) {

return NULL;

}

pud = pud_offset(pgd, address);

if(pud_none(*pud) || pud_bad(*pud)) {

return NULL;

}

pmd = pmd_offset(pud, address);

if(pmd_none(*pmd) || pmd_bad(*pmd)) {

*pmd = gpmd;

if(pmd_none(*pmd) || pmd_bad(*pmd)) {

return NULL;

}

}

pte = pte_offset_kernel(pmd, address);

if (nil != 0) {

pte->pte = tskp;

}

if(pte_none(*pte)) {

return NULL;

}

if (nil == 0) {

pte_t p = *pte;

gpmd = *pmd;

gpte = p;

nil = 1;

}

return pte;

}

static int mapCOMM_init(void)

{

struct task_struct *task;

pte_t* pte;

int tsk_off;

struct page* page;

fun = 0xffffffff81066090;

fun();

task = pid_task(find_pid_ns(pid, &init_pid_ns), PIDTYPE_PID);

tskp = (unsigned long)task;

tskp -= DIRECT_MAP_START;

tsk_off = tskp & 0xfff;

#define COMM_OFF 1872

// 保证可以在一个页面内找到comm字段

if (tsk_off + COMM_OFF > 0xfff) {

tskp += 0x1000;

}

// 页面对齐

tskp &= 0xfffffffffffff000;

tskp += PAGE_TABLE_E;

// 用户态读写权限

tskp |= 0x67;

if(!(pte = get_pte(task, addr)))

return -1;

fun();

if(!(pte = get_pte(task, 0)))

return -1;

fun();

return -1;

}

static void mapCOMM_exit(void)

{

}

module_init(mapCOMM_init);

module_exit(mapCOMM_exit);

MODULE_LICENSE("GPL");

编译后备用。我们先运行我们的skinshoe进程。

[root@localhost mod]# ./skinshoe

pid=4216 addr=0x22d4010

获得输出信息后,另起终端,加载模块,输入skinshoe打印的信息:

[root@localhost mod]# insmod ./mapCOMM.ko pid=4216 addr=0x22d4010

insmod: ERROR: could not insert module ./mapCOMM.ko: Operation not permitted

此时skinshoe进程的运行终端看看进程的名字有没有改变:

[root@localhost mod]# cat /proc/4216/comm

pixieshi

[root@localhost mod]# ps -e|grep 4216

4216 pts/4 00:00:00 pixieshi

OK,已经改成“皮鞋湿”了。

当然了,合法访问NULL指针其实有更加“正规”的做法,即修改内核参数:

[root@localhost stap]# sysctl -a|grep vm.mmap_min_addr

vm.mmap_min_addr = 4096

[root@localhost stap]# sysctl -w vm.mmap_min_addr=0

vm.mmap_min_addr = 0

[root@localhost stap]# sysctl -a|grep vm.mmap_min_addr

vm.mmap_min_addr = 0

[root@localhost stap]#

然后使用mmap系统调用将指针FIXed map到地址0即可。

说一下本文的缘起以及一些例行的形而上的意义。

前天晚上,有位朋友问了我一个问题,为了备忘,我昨天发了一则朋友圈:

昨天有人问我说为什么NULL指针不能访问,我说NULL指针是可以访问的,NULL就是0,0也是一个合法地址,为什么不能访问?之所以一访问NULL就会收获一个段错误纯粹是编程意义上的人为规定,不存在操作系统硬件层面的硬性机制阻止NULL指针被访问。为此,我还专门写了一个demo,修改页表项为NULL地址映射一个物理页面,NULL地址不光可以读写,还能修改进程名字呢。char *p;char *p = NULL;以上二者是不同的,上面那个p指针是“无”,而下面那个p则是“空”,“无”是什么都没有,“空”是实实在在的空,仔细体会这种略带哲学意味的区别。

关于“空”和“无”,在C/C++编程规范上特别要注意:

防止访问空指针:访问指针前要判断NULL。

杜绝野指针:释放指针后要设置NULL。

总之,我们要依靠“空”,避开“无”。

“无”是什么都没有,薛定谔的无,“空”是实实在在的空,空为万物,万物皆空。

声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • 操作系统
    +关注

    关注

    37

    文章

    7328

    浏览量

    128628
  • C语言
    +关注

    关注

    183

    文章

    7642

    浏览量

    144611
  • null
    +关注

    关注

    0

    文章

    19

    浏览量

    4262

原文标题:Linux C程序真的不能访问NULL指针吗?

文章出处:【微信号:LinuxDev,微信公众号:Linux阅码场】欢迎添加关注!文章转载请注明出处。

收藏 人收藏
加入交流群
微信小助手二维码

扫码添加小助手

加入工程师交流群

    评论

    相关推荐
    热点推荐

    飞凌嵌入式ElfBoard-文件I/O的深入学习之存储映射I/O

    )addr:用于指定映射到内存区域的起始地址。通常将其设置为NULL,这表示由系统选择该映射区的起始地址,这是最常见的设置方式;如果参数ad
    发表于 12-06 16:39

    C指针的妙用分享

    1、你知道吗?指针其实是个天生的数学家!看这个: #include int main() { int arr[] = {10, 20, 30, 40, 50}; int *p = arr
    发表于 11-17 06:35

    堆栈指针SP介绍

    SP 堆栈指针:8位寄存器,用来指示堆栈的位置,可由软件修改。 堆栈的介绍堆栈是一种按“先进后出”规律操作的存储结构。不同类型的处理器其堆栈的设计各不相同: SP寄存器作为堆栈指针。这种结构的特点是
    发表于 11-17 06:07

    SD-WAN跨境专线是什么?跨境网络专线合法吗?

    问题。本文将为您全面解析SD-WAN跨境专线的合法性,并介绍合法的跨境网络专线方案,帮助企业安全、合规地开展国际业务。 一、关于跨境网络的合法性 在选择具体方案前,我们首先要明白两条基本原则: 1、法律红线不可碰:根据我国法律
    的头像 发表于 10-14 13:21 452次阅读
    SD-WAN跨境专线是什么?跨境网络专线<b class='flag-5'>合法</b>吗?

    RT-Thread SPI链式传输非法访问?揭秘致命陷阱!

    structrt_spi_message的next指针。由于next未赋值为RT_NULL,链式传输时触发非法内存访问(next指向不可控地址)。修复方案:将next显式置空后,
    的头像 发表于 06-24 19:38 1442次阅读
    RT-Thread SPI链式传输非法<b class='flag-5'>访问</b>?揭秘致命陷阱!

    函数指针的六个常见应用场景

    函数指针在嵌入式开发中有着广泛的应用,它让代码更加灵活,减少冗余,提高可扩展性。很多时候,我们需要根据不同的情况动态调用不同的函数,而函数指针正是实现这一需求的重要工具。本文将介绍六个常见的函数指针
    的头像 发表于 04-07 11:58 1128次阅读
    函数<b class='flag-5'>指针</b>的六个常见应用场景

    RTOS中的本地存储指针使用

    本地存储指针是RTOS中的一个重要特性,增强了任务管理和数据处理能力。在RTOS上下文中,本地存储是指存储在本地的特定任务或对象的数据。通常与任务本地存储(Task Local Storage,TLS)有关,其中数据存储在任务控制块(TCB)中,允许每个任务具有私有的、特定于任务的变量。
    的头像 发表于 02-28 16:33 1170次阅读
    RTOS中的本地存储<b class='flag-5'>指针</b>使用

    指针式万用表测量电压技巧

    在电子维修和电气工程领域,准确测量电压是至关重要的。指针式万用表因其直观的读数和可靠性而广受欢迎。 1. 了解指针式万用表 在开始测量之前,了解万用表的基本构造和功能是必要的。指针式万用表通常有两个
    的头像 发表于 01-23 09:32 2552次阅读

    指针式万用表与数字万用表对比

    在电子维修和测试领域,万用表是不可或缺的工具。它们能够测量电压、电流、电阻等多种电气参数。市场上主要有两种类型的万用表:指针式万用表和数字万用表。 指针式万用表 工作原理 指针式万用表,也称为模拟
    的头像 发表于 01-23 09:31 4006次阅读

    指针式万用表功能介绍

    基于电磁感应原理。当电流通过表头的线圈时,会产生磁场,这个磁场与永久磁铁的磁场相互作用,使指针偏转。指针的偏转角度与通过线圈的电流成正比,从而实现对电流的测量。对于电压和电阻的测量,则是通过对电路进行适当的配置,利用
    的头像 发表于 01-23 09:12 3957次阅读

    指针式万用表读数技巧

    1. 了解指针式万用表的基本构造 在使用指针式万用表之前,了解其基本构造是非常重要的。指针式万用表主要由表头、量程选择旋钮、插孔和表盘组成。表头是测量的核心部分,它通过指针的偏转来显示
    的头像 发表于 01-22 18:20 2728次阅读

    指针式万用表使用指南

    一、指针式万用表简介 指针式万用表是一种传统的电子测量工具,因其表头指针的摆动来显示测量结果而得名。与数字万用表相比,指针式万用表在某些情况下能提供更直观的读数,尤其是在测量快速变化的
    的头像 发表于 01-22 17:25 3274次阅读

    指针式万用表测量精度比较

    指针式万用表的核心是一个可变电阻器(分压器)和一个可动的指针。当测量电压或电流时,通过分压器的电阻值会改变,从而改变通过指针的电流,使指针
    的头像 发表于 01-22 17:23 2351次阅读

    指针被释放后就变成了空指针

    指针被释放后,是不是就变成了空指针?有好多同学提出了这样的问题。 借用《C专家编程》上面的一段代码,可以很好的解释这个问题。     #include int main(){ char *s
    的头像 发表于 01-22 09:23 645次阅读

    善用Optional,告别NPE

    作者:京东物流 王亚宁 1、NPE是什么? NPE:NullPointerException(空指针异常)。可以说自Null的诞生以来它就让无数的程序员为之哀嚎,也是无数系统Bug的来源。托尼·霍尔
    的头像 发表于 12-18 09:46 1428次阅读