2015-11-03 74 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条装配线),则每个中级程序员都可以对其进行解码并找出几天或几小时所需的内容。