2016-07-18 54 views
3

我知道我可以通過引用複製函數,但我想了解下面的代碼中會產生段錯誤。Memcpy與函數指針導致段錯誤

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 

int return0() 
{ 
    return 0; 
} 

int main() 
{ 
    int (*r0c)(void) = malloc(100); 
    memcpy(r0c, return0, 100); 
    printf("Address of r0c is: %x\n", r0c); 
    printf("copied is: %d\n", (*r0c)()); 
    return 0; 
} 

這是我的心理模型,我認爲應該工作。

進程擁有分配給r0c的內存。我們正在複製對應於return0的數據段的數據,並且複製成功。

我認爲解引用函數指針與調用函數指針指向的數據段相同。如果是這種情況,那麼指令指針應該移動到對應於r0c的數據段中,該段將包含函數return0的指令。對應於return0的二進制代碼不包含任何將取決於return0地址的跳轉或函數調用,因此它應該只返回0並恢復ip ... 100個字節對於函數指針來說肯定是足夠的,而且0xc3很好在r0c的範圍內(在字節11處)。

那麼爲什麼分段錯誤?這是對C函數指針語義的誤解嗎?還是有一些安全特性阻止了我不知道的自修改代碼?

+0

'的printf( 「R0C的地址是:%X \ n」 個,R0C);'沒有很好地限定。 – chux

+3

有些東西告訴我,整個事情沒有很好的定義。 – HolyBlackCat

+2

首先,函數(代碼)駐留在標記爲可執行文件的內存段中。分配的數據不那麼明顯,特別是如果您的系統正在使用DEP(數據執行預防)。如果您希望執行數據段中的代碼,則需要弄清楚如何將數據標記爲可執行文件。其次,'memcpy(r0c,return0,100);'可能是從內存末尾複製而來的。第三,包含代碼的內存地址很可能不受訪問。 – GreatAndPowerfulOz

回答

6

malloc用於分配內存的內存頁未標記爲可執行文件。您不能將代碼複製到堆並期望它運行。

如果你想做這樣的事情,你必須深入操作系統,並自己分配頁面。然後你需要將這些標記爲可執行文件。您很可能需要管理員權限才能在內存頁面上設置可執行標誌。

這真的很危險。如果您在發佈的程序中執行此操作,並且存在某種可讓攻擊者使用我們的程序寫入分配的內存頁的錯誤,則攻擊者可以獲得管理員權限並控制計算機。


您的代碼還存在其他問題,例如函數指針可能無法很好地轉換爲所有平臺上的常規指針。這是非常困難的(更不用說非標準)來預測或獲得函數的大小。您還可以在代碼示例中打印出錯誤的指針。 (使用"%p"格式打印void *,需要將指針投射到void *)。

此外,當您聲明像int fun()這樣的函數時,這與聲明不帶參數的函數不同。如果你想聲明一個不帶參數的函數,你應該明確地使用void,如int fun(void)

+0

我只是想提供一個最小的例子......這只是關於理解程序的語義。 –

+0

將頁面設置爲可執行文件不需要大多數操作系統的管理員權限。 – Dani

-1

Malloc返回一個指向已分配內存的指針(在您的案例中爲100個字節)。該內存區域未初始化;假設內存可以由CPU執行,爲了使代碼正常工作,您必須使用函數實現的可執行指令(如果實際上可以保存100個字節)填充這100個字節。但正如已經指出的那樣,你的分配在堆上,而不是在文本(程序)部分,我認爲它不能作爲指令執行。或許,這將實現什麼是你想要的:

int return0() 
{ 
    return 0; 
} 

typedef int (*r0c)(void); 

int main(void) 
{ 
    r0c pf = return0; 
    printf("Address of r0c is: %x\n", pf); 
    printf("copied is: %d\n", pf()); 
    return 0; 
} 
+0

我很欣賞答案,但我解釋說我知道可以通過引用來調用函數;只是我想知道是否/如何使用函數指針來執行實際數據,而不僅僅是使用函數指針來引用調用。 –

2

的標準說:

memcpy功能複製n對象字符由s2到對象的指向指向s1

[C2011,7.24.2.1/2;強調]

在標準的術語,功能不是「對象」。標準沒有定義源指針指向函數的情況下的行爲,因此這樣的調用會產生未定義的行爲。

另外,通過malloc()返回的指針是一個對象的指針。 C不提供將對象指針直接轉換爲函數指針,並且它不提供要作爲函數調用的對象。可以通過中間整數值在對象指針和函數指針之間進行轉換,但這樣做的效果至少是雙重實現定義的。在某些情況下,它是未定義的。

正如在其他情況下,UB可以練得正是你希望的行爲,但它不是安全的依靠這一點。在這種特殊情況下,其他答案提供了很好的理由給而不是希望得到你所希望的行爲。

+0

這是真的,但海灣合作委員會是鬆懈的,事實證明,這不是導致段錯誤。 –

+0

@AndrewSalmon,相反 - segfault * always *是由實現定義或未定義的行爲引起的,我指出了在您的程序中出現這種行爲的確切操作。你可以依靠實現擴展來獲得你想要的行爲,但這樣做本質上是不可移植的。儘管如此,這可能是你能接受的。 –

0

正如一些評論說,你需要使數據的可執行文件。這需要與操作系統通信以更改對數據的保護。在Linux上,這是系統調用int mprotect(void* addr, size_t len, int prot)(請參閱http://man7.org/linux/man-pages/man2/mprotect.2.html)。

這是一個使用VirtualProtect的Windows解決方案。

#include <stdio.h> 
#include <stdlib.h> 
#include <string.h> 
#include <stdint.h> 
#ifdef _WIN32 
#include <Windows.h> 
#endif 

int return0() 
{ 
    return 0; 
} 

int main() 
{ 
    int (*r0c)(void) = malloc(100); 
    memcpy((void*) r0c, (void*) return0, 100); 
    printf("Address of r0c is: %p\n", (void*) r0c); 
#ifdef _WIN32 
    long unsigned int out_protect; 
    if(!VirtualProtect((void*) r0c, 100, PAGE_EXECUTE_READWRITE, &out_protect)){ 
     puts("Failed to mark r0c as executable"); 
     exit(1); 
    } 
#endif 
    printf("copied is: %d\n", (*r0c)()); 
    return 0; 
} 

它的工作原理。