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 64bit,gcc5.2.1编译了它,并检查了汇编:
...
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按钮。
更新:关闭优化:
...
my_func:
...
get_my_func:
...
main:
...
call get_my_func
movq %rax, -8(%rbp)
movq -8(%rbp), %rax
call *%rax
...
我认为现在没有内联。但是我真的不明白为什么这很重要......
我仍然在寻找一个解释作者使用此代码的目标是什么,即使它不适用于今天的智能编译器。
答案 0 :(得分:7)
混淆函数调用的方法的问题依赖于编译器不够聪明以透视混淆。这里的想法是调用者不应该包含对要调用的函数的直接引用,而是从另一个函数中检索指向该函数的指针。
然而,现代编译器会这样做,并且在应用优化时,它们会再次删除混淆。编译器的作用可能是GET_FN_PTR
的简单内联扩展,当内联展开时,如何优化是非常明显的 - 它只是一堆常量,它们组合成一个指针,然后被调用。在编译时,常量表达式很容易计算(并且经常完成)。
在对代码进行模糊处理之前,您应该有充分的理由这样做,并使用适合需要的方法。
答案 1 :(得分:4)
建议方法的想法是使用间接函数调用,以便首先计算函数地址然后再调用。 C预处理器用于提供一种为实际函数定义代理函数的方法,该代理函数提供了确定代理函数提供访问的实际函数的实际地址所需的计算。
有关代理设计模式的详细信息,请参阅Wikipedia article Proxy pattern:
代理设计模式允许您为其他人提供界面 通过创建包装类作为代理来创建对象。包装类, 这是代理,可以为对象添加额外的功能 兴趣而不改变对象的代码。
我建议使用一种替代方法来实现相同类型的间接调用,但是它不需要使用C预处理器来隐藏实现细节,以致于难以阅读源代码。
C编译器允许struct
包含函数指针作为成员。有什么好处是你可以定义一个外部可见的struct变量,函数指针是一个成员但是在定义struct时,struct变量定义中指定的函数可以是static
,这意味着它们只有文件可见性(见What does "static" mean in a C program。)
所以我可以有两个文件,一个头文件func.h和一个实现文件func.c,它定义了struct
类型,外部可见结构变量的声明,与{{1一起使用的函数修饰符,以及带有函数地址的外部可见结构变量定义。
这种方法的吸引力在于源代码易于阅读,并且大多数IDE将更好地处理这种间接,因为C预处理器在编译时不用于创建源,这会影响人们的可读性和IDE等软件工具。
使用函数将#include包含在C源文件中的示例func.h文件可能如下所示:
static
func.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;
使用此外部可见结构的简单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
};
Visual Studio 2005针对上述#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;
}
发出的汇编程序如下所示:显示通过指定地址的计算调用:
main()
正如您所看到的,此函数调用现在通过结构中的偏移量指定的结构进行间接函数调用。
这种方法的好处在于,只要在通过数据区调用函数之前,就可以对包含函数指针的内存区域执行任何操作,就可以将正确的函数地址放在那里。所以你实际上可以有两个函数,一个用正确的地址初始化区域,另一个用于清除区域。因此,在使用函数之前,您可以调用函数来初始化区域,在完成函数后调用函数来清除区域。
; 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();
的调用位于源中的某个位置,而这些位置不会接近实际使用函数的位置。
另一个有趣的选择是加密数据区域,为了使用该程序,用户需要输入正确的密钥才能正确解密数据以获得正确的指针地址。
答案 2 :(得分:0)
&#34;混淆&#34;在C / C ++中主要与编译代码的大小有关。如果它太短(例如500-1000个装配线),每个中级程序员都可以对其进行解码并找到几天或几小时所需的内容。