国产精品久久久aaaa,日日干夜夜操天天插,亚洲乱熟女香蕉一区二区三区少妇,99精品国产高清一区二区三区,国产成人精品一区二区色戒,久久久国产精品成人免费,亚洲精品毛片久久久久,99久久婷婷国产综合精品电影,国产一区二区三区任你鲁

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

    文章

    7402

    瀏覽量

    129290
  • C語言
    +關注

    關注

    183

    文章

    7644

    瀏覽量

    145581
  • null
    +關注

    關注

    0

    文章

    19

    瀏覽量

    4306

原文標題:Linux C程序真的不能訪問NULL指針嗎?

文章出處:【微信號:LinuxDev,微信公眾號:Linux閱碼場】歡迎添加關注!文章轉載請注明出處。

收藏 人收藏
加入交流群
微信小助手二維碼

掃碼添加小助手

加入工程師交流群

    評論

    相關推薦
    熱點推薦

    傳遞一個無符號的 long 型指針給函數

    C語言允許傳遞指針給函數,只需要簡單地聲明函數參數為指針類型即可。 下面的實例中,我們傳遞一個無符號的 long 型指針給函數,并在函數內改變這個值: 實例 #include #include
    發表于 01-27 11:51

    指針與函數詳解

    ,然后通過指針訪問數組中的內容,那么我們就可以定義一個函數指針,將函數名賦給函數指針,通過這個函數指針調用函數。 #include v
    發表于 01-23 06:02

    C語言重點—指針

    int *p;p =a; //這種方式正確printf("p = %dn",p); 結果:p = 6618636 變量p存放的a的地址 重點: 先了解,指針類型,int
    發表于 01-22 08:23

    函數指針介紹

    就是一個指針函數。其返回值是一個 int 類型的指針,是一個地址指針函數也沒什么特別的,和普通函數對比不過就是其返回了一個指針(即
    發表于 01-21 08:11

    值傳遞、指針傳遞、引用傳遞介紹

    中有開辟了內存空間來存放主調函數放進來實參的值,從而成為一個副本。因為指針傳遞的是外部參數的地址,當調用函數的形參發生改變時,自然外部實參也發生改變。   3、引用傳遞:被調函數的形參雖然也作為
    發表于 01-21 06:48

    飛凌嵌入式ElfBoard-系統信息與資源之設置時間

    timezone 結構的指針,通常用于指定時區信息。這個參數在許多現代操作系統中不再被使用,可以將其設置為 NULL。struct timezone 結構包含兩個字段:tz_minuteswest: 本地時間
    發表于 01-19 09:30

    指針難學的4點原因分析

    不同的位置,有不同的作用,這是剛開始學指針時難學的一個地方。 難點2. 分不清址與值 這里的址就是地址,就是很多教材喜歡說的指針變量是個地址地址
    發表于 01-16 06:12

    C語言指針p、*p、&amp;p、*&amp;p、&amp;*p分別代表什么

    在C語言中,指針是非常重要的概念。指針是一個變量,其值為另一個變量的地址。使用指針可以直接訪問內存中的數據,這使得C語言非常靈活和強大。在學
    發表于 01-07 07:34

    C語言訪問某特定內存位置

    : 這一問題測試你是否知道為了訪問一絕對地址把一個整型數強制轉換(typecast)為一指針合法的。 int *ptr; ptr = (int *)0x67a9; *ptr
    發表于 12-22 15:42

    指針的基礎

    1. int va; 這是一個整型變量,32位CPU的話,占有32個bite 2. int *va; 這是一個整型指針變量,用于存放一個整型變量的地址 3. int **va; 這是一個整型
    發表于 12-15 06:06

    函數指針指針函數的區別

    指針的函數,即本質是一個函數。函數返回類型是某一類型的指針   類型標識符 *函數名(參數表)   int *f(x,y);   首先它是一個函數,只不過這個函數的返回值是一個地址值。函數返回值必須
    發表于 12-12 06:34

    函數指針的概念

    函數指針是指向函數的指針變量。 通常我們說的指針變量是指向一個整型、字符型或數組等變量,而函數指針是指向函數。 函數指針可以像一般函數一樣
    發表于 12-11 08:10

    如何用函數指針調用函數

    給大家舉一個例子: int Func(int x);/*聲明一個函數*/ int (*p) (int x);/*定義一個函數指針*/ p = Func; /*將Func函數的首地址賦給指針變量
    發表于 12-11 06:26

    飛凌嵌入式ElfBoard-文件I/O的深入學習之存儲映射I/O

    )addr:用于指定映射到內存區域的起始地址。通常將其設置為NULL,這表示由系統選擇該映射區的起始地址,這是最常見的設置方式;如果參數ad
    發表于 12-06 16:39

    RT-Thread SPI鏈式傳輸非法訪問?揭秘致命陷阱!

    structrt_spi_message的next指針。由于next未賦值為RT_NULL,鏈式傳輸時觸發非法內存訪問(next指向不可控地址)。修復方案:將next顯式置空后,
    的頭像 發表于 06-24 19:38 ?1655次閱讀
    RT-Thread SPI鏈式傳輸非法<b class='flag-5'>訪問</b>?揭秘致命陷阱!