2015-11-03 51 views
6

John Viega在他的書C和C++的安全編程指南中提出了一種混淆函數調用的方法。它可以被讀取here模糊函數調用

#define SET_FN_PTR(func, num)     \ 
    static inline void *get_##func(void) { \ 
     int i, j = num/4;     \ 
     long ptr = (long)func + num;   \ 
     for (i = 0; i < 2; i++) ptr -= j; \ 
     return (void *)(ptr - (j * 2));  \ 
    } 
#define GET_FN_PTR(func) get_##func() 

#include <stdio.h> 

void my_func(void) { 
    printf("my_func() called!\n"); 
} 

SET_FN_PTR(my_func, 0x01301100); /* 0x01301100 is some arbitrary value */ 

int main(int argc, char *argv[ ]) { 
    void (*ptr)(void); 

    ptr = GET_FN_PTR(my_func);  /* get the real address of the function */ 
    (*ptr)();      /* make the function call */ 
return 0; 

} 

gcc fp.c -S -O2的,Ubuntu 15.10 64位,gcc5.2.1編譯它,並檢查了assemby:

... 
my_func: 
.LFB23: 
     .cfi_startproc 
     movl $.LC0, %edi 
     jmp  puts 
     .cfi_endproc 
.LFE23: 
     .size my_func, .-my_func 
     .section  .text.unlikely 
.LCOLDE1: 
     .text 
.LHOTE1: 
     .section  .text.unlikely 
.LCOLDB2: 
     .section  .text.startup,"ax",@progbits 
.LHOTB2: 
     .p2align 4,,15 
     .globl main 
     .type main, @function 
main: 
.LFB25: 
     .cfi_startproc 
     subq $8, %rsp 
     .cfi_def_cfa_offset 16 
     call my_func 
     xorl %eax, %eax 
     addq $8, %rsp 
     .cfi_def_cfa_offset 8 
     ret 
     .cfi_endproc 
... 

我看到my_func,並將被稱爲main。有人可以解釋這種方法如何混淆函數調用嗎?

我看到很多讀者只是來來回踱步。我花時間瞭解問題,以及何時未能在此處發佈。請至少寫一些評論,而不是推動downvote按鈕。

UPDATE:關閉優化我:

... 
my_func: 
... 
get_my_func: 
... 
main: 
... 
    call get_my_func 
    movq %rax, -8(%rbp) 
    movq -8(%rbp), %rax 
    call *%rax 
... 

我覺得現在沒有inlineing。然而我不明白爲什麼它很重要......

我仍在尋找解釋,即使它不適用於今天的智能編譯器,作者使用此代碼的目標是什麼。

+0

親愛的downvoter,我會很感激你的評論 – robert

+7

SO是爲了幫助人們寫出更好的**代碼,而不是更糟。 – Olaf

+3

@Olaf我正試圖保護一個商業軟件。 – robert

回答

4

建議的方法的想法是使用間接函數調用,以便函數地址必須首先計算然後調用。 C預處理器用於爲實際功能定義一個代理函數,並且此代理函數提供確定代理函數提供訪問的實際函數的實際地址所需的計算。

代理設計模式允許你創建一個包裝類爲代理提供給其他 對象的接口:

關於其中有這樣一段話的代理設計模式的詳細信息,請參閱Wikipedia article Proxy pattern。作爲代理的包裝類 可以將附加功能添加到 的感興趣對象中,而無需更改對象的代碼。

我會建議一種實現相同類型的間接調用的替代方法,但它不需要使用C預處理器以這種方式隱藏實現細節,以便使源代碼的讀取變得困難。

C編譯器允許struct包含函數指針作爲成員。是什麼樣的這個漂亮的是,你可以定義與功能的外部可見的結構體變量的指針一個成員尚未定義的結構時,在結構變量的定義規定的功能可以static這意味着他們有文件能見度只有(見What does "static" mean in a C program。 )

所以我可以有兩個文件,一個頭文件func.h和一個實現文件func.c,它們定義了struct類型,外部可見結構變量的聲明,static修飾符使用的函數以及具有函數地址的外部可見結構體變量定義。

這種方法的吸引力在於,源代碼易於閱讀,大多數IDE將處理這種間接更好,因爲C預處理器沒有被用來在編譯時創建源,影響人們的可讀性和通過諸如IDE的軟件工具。

一個例子func.h文件,這將進行#included到使用的功能的C源文件,可能看起來像:

// define a type using a typedef so that we can declare the externally 
// visible struct in this include file and then use the same type when 
// defining the externally visible struct in the implementation file which 
// will also have the definitions for the actual functions which will have 
// file visibility only because we will use the static modifier to restrict 
// the functions' visibility to file scope only. 
typedef struct { 
    int (*p1)(int a); 
    int (*p2)(int a); 
} FuncList; 

// declare the externally visible struct so that anything using it will 
// be able to access it and its members or the addresses of the functions 
// available through this struct. 
extern FuncList myFuncList; 

而func.c文件示例可能看起來像:

#include <stdio.h> 

#include "func.h" 

// the functions that we will be providing through the externally visible struct 
// are here. we mark these static since the only access to these is through 
// the function pointer members of the struct so we do not want them to be 
// visible outside of this file. also this prevents name clashes between these 
// functions and other functions that may be linked into the application. 
// this use of an externally visible struct with function pointer members 
// provides something similar to the use of namespace in C++ in that we 
// can use the externally visible struct as a way to create a kind of 
// namespace by having everything go through the struct and hiding the 
// functions using the static modifier to restrict visibility to the file. 

static int p1Thing(int a) 
{ 
    return printf ("-- p1 %d\n", a); 
} 

static int p2Thing(int a) 
{ 
    return printf ("-- p2 %d\n", a); 
} 

// externally visible struct with function pointers to allow indirect access 
// to the static functions in this file which are not visible outside of 
// this file. we do this definition here so that we have the prototypes 
// of the functions which are defined above to allow the compiler to check 
// calling interface against struct member definition. 
FuncList myFuncList = { 
    p1Thing, 
    p2Thing 
}; 

使用這種外部可見的結構可能看起來像一個簡單的C源文件:

#include "func.h" 

int main(int argc, char * argv[]) 
{ 
    // call function p1Thing() through the struct function pointer p1() 
    myFuncList.p1 (1); 
    // call function p2Thing() through the struct function pointer p2() 
    myFuncList.p2 (2); 
    return 0; 
} 

的作爲通過Visual Studio 2005中對上述main()發出sembler看起來像下面顯示通過指定地址的計算呼叫:

; 10 : myFuncList.p1 (1); 

    00000 6a 01  push 1 
    00002 ff 15 00 00 00 
    00  call DWORD PTR _myFuncList 

; 11 : myFuncList.p2 (2); 

    00008 6a 02  push 2 
    0000a ff 15 04 00 00 
    00  call DWORD PTR _myFuncList+4 
    00010 83 c4 08  add  esp, 8 

; 12 : return 0; 

    00013 33 c0  xor  eax, eax 

正如你可以看到這個函數調用現在是間接功能通過內的偏移量規定的結構要求結構。

這種方法的好處在於,只要在通過數據區調用函數之前,就可以對包含函數指針的內存區域執行任何操作,正確的函數地址已放在那裏。所以你實際上可以有兩個功能,一個是用正確的地址初始化區域,另一個是清理該區域的功能。因此,在使用這些功能之前,您可以調用該功能來初始化該區域,並在完成該功能後調用該功能來清除該區域。

// file scope visible struct containing the actual or real function addresses 
// which can be used to initialize the externally visible copy. 
static FuncList myFuncListReal = { 
    p1Thing, 
    p2Thing 
}; 

// NULL addresses in externally visible struct to cause crash is default. 
// Must use myFuncListInit() to initialize the pointers 
// with the actual or real values. 
FuncList myFuncList = { 
    0, 
    0 
}; 

// externally visible function that will update the externally visible struct 
// with the correct function addresses to access the static functions. 
void myFuncListInit (void) 
{ 
    myFuncList = myFuncListReal; 
} 

// externally visible function to reset the externally visible struct back 
// to NULLs in order to clear the addresses making the functions no longer 
// available to external users of this file. 
void myFuncListClear (void) 
{ 
    memset (&myFuncList, 0, sizeof(myFuncList)); 
} 

所以你可以做這樣的事情修改main()

myFuncListInit(); 
myFuncList.p1 (1); 
myFuncList.p2 (2); 
myFuncListClear(); 

但是你真的想要做的是有調用myFuncListInit()在源某處那會不會是不遠的地方該功能實際上被使用。

另一個有趣的選擇是將數據區域加密,並且爲了使用該程序,用戶需要輸入正確的密鑰來正確解密數據以獲得正確的指針地址。

+0

爲什麼p1和p2是靜態的?刪除static關鍵字不會改變main的彙編程序。 – robert

+1

@ franz1,函數'p1'和'p2'是靜態的,以減少它們對文件範圍的可見範圍。換句話說,函數'p1'和'p2'作爲文件'func.c'之外的函數是不可見的,唯一可以訪問的方法是通過外部可見結構體'myFuncList'中的函數指針。刪除'static'不會影響'main()'的彙編程序,因爲'main()'通過結構'myFuncList'訪問它們,即使它們在'main()'中可見時,一旦你刪除了'static'修飾符。 –

+0

@ franz1我已經將函數的名稱從p1更改爲p1Thing,將p2更改爲p2Thing,以清楚地表明結構成員是指向函數的指針變量。我想知道是否使用相同的文本作爲函數名稱和成員名稱,這是不同的實體,讓你感到困惑。 –

7

這種混淆函數調用方式的問題依賴於編譯器不夠聰明來查看混淆。這裏的想法是,調用者不應該包含要調用的函數的直接引用,而應該從另一個函數中檢索指向該函數的指針。

但是現代編譯器會這樣做,並且在應用優化時會再次刪除混淆。編譯器做的事可能是簡單的內聯擴展GET_FN_PTR,並且在內聯擴展時,如何優化是非常明顯的 - 這只是一些常量,它們被組合成一個隨後被調用的指針。常量表達式在編譯時很容易計算(通常是這樣做的)。

在混淆代碼之前,您應該有充足的理由這樣做,並使用適合需求的方法。

0

C/C++中的「混淆」主要與編譯代碼的大小有關。如果它太短(例如500-1000條裝配線),則每個中級程序員都可以對其進行解碼並找出幾天或幾小時所需的內容。