65.9K
CodeProject 正在变化。 阅读更多。
Home

Windows 编程中的安全函数指针和回调

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.64/5 (18投票s)

2011年5月4日

CPOL

14分钟阅读

viewsIcon

58781

downloadIcon

3627

本文档解释了函数指针和回调在 Windows 应用程序编程接口 (API) 中的用法。

引言

函数指针是指向函数的指针,类似于指向变量的指针。用户可以在解引用后使用所需的参数调用函数指针。函数指针可以使用 C 或 C++ 语言实现。函数指针对于后期绑定、实现回调和将函数作为参数传递给另一个函数非常有用。本文档解释了函数指针和回调在 Windows 应用程序编程接口 (API) 中的用法。

C++ 支持以下两种类型的函数指针:

  • 静态成员函数指针
  • 非静态成员函数指针

C 语言支持的函数指针类似于 C++ 的静态成员函数。应用程序可以定义函数地址的变量名,并通过该变量名进行调用。

// define a function pointer and initialize to NULL
void (*FunctionPointer)(int, int, int) = NULL;                        // C

C 语言函数指针可以使用可变参数来处理未知数量的参数。

int (*ptr)(...);

C++ 中指向静态成员函数的指针与指向 C 函数的指针相同。

// define a function pointer and initialize to NULL
int (TMyClass::*pt2Member)(float, char, char) = NULL;                // C++
int (TMyClass::*pt2ConstMember)(float, char, char) const = NULL;     // C++

开发人员可以使用 typedef 来声明函数指针。typedef 定义可用于初始化函数指针和通过函数指针调用函数。

// declare typedef for array of integer and integer arguments
typedef void (*FunctionPointer)(int arr[], int);

//Define the function pointer
FunctionPointer fpdisplay = NULL;

// Assign Display function address to function pointer and call the function
fpdisplay = &Display;
	if(fpdisplay)
		(*fpdisplay)(arr,9);

Don Clugston 的 成员函数指针和最快的 C++ 委托 文章更深入地解释了 C++ 成员函数指针。

调用约定

调用约定用于将参数传递给函数以及从内存中清理堆栈。函数指针声明应与实际函数调用约定匹配。Microsoft C/C++ 编译器支持 __cdecl__clrcall__stdcall__fastcall__thiscall 调用约定。__stdcall 调用约定用于调用 Win32 API 函数。__cdecl 是 C 和 C++ 程序的默认调用约定。__cdecl 调用约定生成的代码比 __stdcall 大。它要求每个函数都包含清理代码。

WinDef.h 定义了调用约定宏。CALLBACKWINAPIAPIPRIVATEPASCAL 都使用 __stdcall 调用约定。函数和函数指针应使用相同的调用约定。如果开发者没有明确指定,Microsoft C/C++ 编译器将采用 __decl 调用约定。但是,如果开发者使用任何 Win32 API,则应使用 __stdcall 调用约定。

回调

回调函数是指向被传递给另一个函数的函数指针,第二个函数可以通过该函数指针参数来调用该函数。第二个函数在不知道参数函数的情况下直接调用函数指针。每个回调函数都有自己的原型。因为每个函数的函数名、返回类型和参数都不同。所有 C++ 成员函数都有一个 internal 的 this 指针来访问类的成员。因此,开发者在 C++ 中使用回调函数时无需使用此指针。但是,当我们使用非成员函数和 static 函数时,我们必须显式使用回调函数。这两者看起来相同的原因是 C 没有引用,因此您无法发送引用。

函数指针和回调函数之间的区别

函数指针 回调
1 函数指针是指向函数的指针。 回调函数是作为函数参数传递的函数指针,当事件发生时可以被调用。
2 示例 vtable 是函数指针数组 线程结束时调用的回调函数。
3 函数指针是函数的地址 回调函数是以函数指针作为参数传递的,当发生某些事情时,调用者会进行回调。

示例 1 解释了 C 函数指针和回调函数。BubbleSort 对元素数组进行排序,Display 显示整数数组。BinarySearchLinearSearch 是用于搜索元素的两个回调函数。二分搜索仅适用于已排序的元素,并且比线性搜索花费的时间少(性能更好)。线性搜索适用于已排序或未排序的元素数组。

/****************************************************************
 The calling function takes a single callback as a parameter.
*******************************************************************/
void Search(int (*SelectSearch)(int arr[], int, int,int),int arr[],
	int value,int left,int right) {

	int result = SelectSearch(arr,value,left,right);
	if(result == -1)
		printf("Search element not found!\n");
	else
		printf("Element %d found in %d position!\n", value,result);

}

上述函数将函数指针用作参数,并使用函数指针来调用相应的搜索函数。主函数在对未排序的输入元素进行排序后调用 BinarySearch

安全的 CRT

“安全的 string 处理”解释了安全 C 运行时库 string 处理函数的基础知识。安全的 CRT 还提供了一组安全算法函数。C 运行时算法使用回调函数来实现元素比较。

function 安全版本 描述
1 bsearch bsearch_s 二分搜索
2 _lfind _lfind_s 线性搜索
3 _lsearch _lsearch_s 如果未找到则添加到数组的线性搜索
4 qsort qsort_s 快速排序

上述排序和搜索函数的比较函数指针将接收三个参数。这些 C 运行时库函数使用 __cdecl 调用约定。比较函数将接收两个参数。第一个参数是指向搜索键的指针,第二个参数是指向要与键进行比较的数组元素的指针。

安全版本比较函数将接收三个参数。参数 compare 是指向用户提供的例程的指针,该例程比较两个数组元素并返回一个值来指定它们之间的关系。安全版本比较还会接收上下文指针。上下文参数使得执行线程安全排序更加容易。与其使用必须同步以确保线程安全的 static 变量,不如为每次排序传递不同的上下文参数。比较函数可能在快速排序过程中被调用一次或多次。MSDN 提供了上述安全版本 CRT 算法的示例。

Visual Studio 2008 C 运行时库在 Windows 2000、Windows XP、Server 2003 和 Vista 上运行。Visual Studio 2010 C 运行时库支持 Windows XP(带 SP2、SP3)和 Windows Server 2003(带 SP1、SP2)、Vista、Windows Server 2008 和 Windows 7。安全 C 运行时库随 Visual Studio 一起提供。如果开发者需要 C 运行时库,可以从 Microsoft 网站下载 [12,13]。

Windows 中函数指针和回调函数的列表

Windows 应用程序编程接口 (API) 在许多地方使用函数指针和回调函数。(本文档仅讨论用户界面应用程序 Windows API。Windows 也可能在其他 Windows 编程 API 中使用函数指针和回调函数。)每个 Windows 应用程序都使用 WNDCLASS/WNDCLASSEX 结构,通过 RegisterClass/RegisterClassEx Windows API 来注册 Windows 应用程序。它包含窗口类信息。回调函数作为窗口注册过程的一部分进行注册。WNDCLASS/WNDCLASSEX 结构在 lpfnWndProc 参数中注册 Windows 回调函数 WndProcCreateWindow 函数创建一个窗口,该函数指定窗口类(WNDCLASS/EX)、窗口标题、窗口样式和窗口大小。

ATOM MyRegisterClass(HINSTANCE hInstance)
{
	WNDCLASSEX wcex;
	wcex.cbSize = sizeof(WNDCLASSEX);

	wcex.style			= CS_HREDRAW | CS_VREDRAW;
	wcex.lpfnWndProc	= WndProc;
	wcex.cbClsExtra		= 0;
	wcex.cbWndExtra		= 0;
	wcex.hInstance		= hInstance;
	wcex.hIcon		= LoadIcon(hInstance, 
|				MAKEINTRESOURCE(IDI_SIMPLEWIN32));
	wcex.hCursor		= LoadCursor(NULL, IDC_ARROW);
	wcex.hbrBackground	= (HBRUSH)(COLOR_WINDOW+1);
	wcex.lpszMenuName	= MAKEINTRESOURCE(IDC_SIMPLEWIN32);
	wcex.lpszClassName	= szWindowClass;
	wcex.hIconSm		= LoadIcon(wcex.hInstance, 
				MAKEINTRESOURCE(IDI_SMALL));

	return RegisterClassEx(&wcex);
}

WindProc 是在 WNDCLASS 结构中使用的函数。WndProc 用于处理所有窗口消息并处理所有消息。如果它在 WndProc 中找不到任何消息,它会调用默认窗口过程。DefWindowProc 函数调用默认窗口过程,为应用程序未处理的任何窗口消息提供默认处理。

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

GetClassLong 函数用于检索窗口类的 WNDCLASS 信息。SetClassLong 函数用于替换指定类(该类属于指定窗口)的额外类内存或 WNDCLASSEX 结构中指定偏移量的 32 位(long)值。S/GetClassLong 函数 nIndex GCL_WNDPROC 替换与类关联的窗口过程的地址。

以下 Windows API 函数使用回调和函数指针。下表仅讨论函数和函数目的。

DialogBox DialogProc 应用程序定义的用于处理发送到模态或非模态对话框的消息的回调函数。
消息和消息队列 SendAsyncProc 应用程序定义的用于 SendMessageCallback 函数的回调函数。
SendMessageCallback 将指定的消息发送到窗口。
定时器 TimerProc 应用程序定义的用于处理 WM_TIMER 消息的回调函数。
窗口类 WNDCLASS/EX lpfnWndProc 指向用于处理所有窗口消息的窗口过程。
窗口过程 WindowProc 应用程序定义的用于处理发送到窗口的消息的函数。WindowProc 是应用程序定义函数名的占位符。
CallWindowProc CallWindowProc 函数将消息信息传递给指定的窗口过程。
窗口属性 EnumProps 通过将窗口属性列表中的所有条目逐个传递给指定的回调函数来枚举它们。
EnumPropsEx 通过将窗口属性列表中的所有条目逐个传递给指定的回调函数来枚举它们。
PropEnumProc 应用程序定义的用于 EnumProps 函数的回调函数。
PropEnumProcEx 应用程序定义的用于 EnumPropsEx 函数的回调函数。
Windows EnumChildProc 应用程序定义的用于 EnumChildWindows 函数的回调函数。它接收子窗口句柄,是应用程序定义函数名的占位符。
EnumChildWindows 通过将每个子窗口的句柄依次传递给应用程序定义的回调函数,来枚举属于指定父窗口的子窗口。
EnumThreadWindows 通过将每个窗口的句柄依次传递给应用程序定义的回调函数,来枚举与线程关联的所有非子窗口。
EnumThreadWndProc 应用程序定义的用于 EnumThreadWindows 函数的回调函数。它接收与线程关联的窗口句柄。
EnumWindows 通过将每个顶层窗口的句柄依次传递给应用程序定义的回调函数,来枚举屏幕上所有顶层窗口。
EnumWindowsProc 应用程序定义的用于 EnumWindowsEnumDesktopWindows 函数的回调函数。它接收顶层窗口句柄。

动态链接库

动态链接库是 Windows 可执行文件,其中包含一组函数。DLL 只在内存中加载一次,进程可以引用 DLL 中可用的函数来执行。如果没有进程引用 DLL,它将从内存中卸载。DLL 可以隐式或显式加载。显式 DLL 使用函数指针在内存中加载函数。隐式链接使用库(*.lib)文件,并在可执行文件加载到内存时加载 DLL。显式链接直接使用 DLL,并在需要时使用 LoadLibrary (Ex) 系列函数加载 DLL。

与隐式链接相比,显式链接具有以下优点:

  • 当应用程序在运行时不知道需要加载的 DLL 名称时,显式链接非常有用。
  • 隐式链接需要 *.lib、*.h 和 *.dll 文件来加载 DLL。但是,显式链接只需要 DLL 文件。
  • 当隐式链接的 DLL 在启动时未能加载时,应用程序将失败。但是,显式链接使用 LoadLibrary 加载 DLL,并且可以进行处理并继续。
  • 如果应用程序包含许多 DLL,隐式链接将在加载应用程序时花费大量时间。但是,隐式链接会在需要时加载 DLL。

进程调用 LoadLibrary/LoadLibraryEx 来在内存中加载 DLL。MFC 应用程序使用 AfxLoadLibrary 来加载显式链接的 DLL。如果 MFC 应用程序使用 AfxLoadLibrary 函数加载 DLL,则应调用 AfxFreeLibrary 来卸载扩展 DLL。如果函数能够成功加载 DLL,它将返回该函数的 HANDLE。如果 DLL 已经在内存中加载,它将增加引用计数并返回 HANDLE。如果找不到入口点,它将简单地返回 NULL。一旦获得了 DLL 的 HANDLE,用户就可以调用 GetProcAddress 函数来检索导出函数的地址并运行该函数或变量。它需要从 LoadLibrary 返回的 DLL 模块句柄以及函数名或函数导出序号。

函数指针没有任何编译时检查。函数指针的 typedef 有助于检查导出函数原型中的类型安全。如果 DLL 使用模块定义(*.def)文件构建,GetProcAddress 可以使用序号。如果 DLL 包含许多导出的 DLL 函数,这会稍微快一些。

typedef int (__cdecl *FUNCTIONPROCADD)(int,int);

// do
FUNCTIONPROCADD ProcAdd;
BOOL bResult, bSuccess = FALSE;

// Get a handle to the DLL module.
HINSTANCE hinstLib = LoadLibrary(TEXT("winadd.dll"));

// If the handle is valid, try to get the function address.
if (hinstLib != NULL)
{
    ProcAdd = (FUNCTIONPROCADD) GetProcAddress(hinstLib, "add");

    // If the function address is valid, call the function.
    if (NULL != ProcAdd)
    {
        bSuccess = TRUE;
        (ProcAdd) (3,6);
    }

    // Free the DLL module.
    bResult = FreeLibrary(hinstLib);
}

// If unable to call the DLL function, use an alternative.
if (!bSuccess)
    printf("Message printed from executable\n");	

安全指针编码

我们已经讨论了将函数指针用作参数的函数指针和回调函数。我们生活在一个不安全的世界。缓冲区溢出漏洞为攻击者提供了攻击代码并改变正常流程的选项。攻击者甚至可能作为我们应用程序函数流程的一部分来运行其他应用程序。当程序调用函数时,它会创建一个新的堆栈帧,其中包含函数参数、返回地址、帧指针、异常处理程序帧和局部变量。良好的编码实践建议使用局部变量或按引用传递,而不是全局变量。如果需要,仅使用不可变变量或指针。函数指针在全局声明以访问函数。如果函数指针是全局声明的,则函数指针将一直存在直到应用程序终止。

Microsoft 安全生命周期 (5.0) 建议对长期存在的指针进行编码。不安全指针会导致缓冲区溢出攻击。全局作用域的函数指针和全局指针是长期存在的指针。长期存在的指针是不安全指针。开发人员或设计人员必须识别长期存在的指针。Microsoft Windows 提供 EncodePointer()DecodePointer() 函数,使用一种秘密技术对给定进程的指针进行加密和解密。如果攻击者试图覆盖指针,编码后的指针将不允许攻击。EncodePointer()/DecodePointer() 函数从 Windows XP SP2 及更高版本(客户端操作系统)和 Windows Server 2003 SP1(服务器操作系统)开始可用。EncodePointer()/DecodePointer() 函数是 Windows 函数,Microsoft 将这些函数用于 CRT 内部,并可能使用其他 API 来编码全局指针。Windows 使用这些函数来编码未处理的异常过滤器地址、长期存在的函数指针、长期运行的堆分配内存、内核执行间接调用等。

编码函数通过 XOR 操作与随机数进行运算。EncodePointer/DecodePointer 函数使用应用程序的进程信息块,而 EncodeSystemPointer/DecodeSystemPointer 使用每次系统重启的值。Michael Howard 的博客解释了 XOR 编码操作的算法。Michael Howard 的 [4, 5] 博客更详细地解释了用于 XOR 运算的算法。

假设 search 是一个函数,而 FunctionPointer 是为函数指针定义的 typedef。它在内存中存在很长时间,并在应用程序终止时才回收内存。search 函数可能需要很长时间才能执行,而函数指针被声明为全局的。攻击者可能通过缓冲区溢出攻击来攻击此函数指针。攻击者可以更改返回地址并添加用于执行某些 shell 代码或其他应用程序。EncodePointer 接收 PVOID Windows 数据类型,并返回相同的 VoidPVOID)数据类型指针。一旦指针被编码,攻击者就无法调用或更改全局函数指针地址。开发者可以按以下语法编码和解码函数指针:

// search function
void Search() {
  // do something
}

typedef void (*FunctionPointer )(void);

// do something

FunctionPointer fp1 = (FunctionPointer)EncodePointer(&Search);
  // Do something
  // Do something
  FunctionPointer fp2 = (FunctionPointer)DecodePointer(fp2);

  if (fp2)
     (*fp2)();

EncodePointer 返回编码后的指针值。攻击者无法攻击编码后的指针。但是,开发者可以使用 DecodePointer 函数和一个编码后的指针来解码指针。Michael Howard [4, 5] 不建议使用所有全局函数指针,因为它可能会影响应用程序性能。

编码指针用于 crt\src\tidtable.c CRT 文件。该文件使用操作系统机制编码指针以防止劫持。EncodePointer/DecodePointer 仅支持 x86 机器。tidtable.c 提供了 _encode_pointer/_decode_pointer 函数来编码和解码 CRT 函数。但是,这些函数是未文档化的,并且不由 CRT DLL 导出。以下 _encode_pointer/_decode_pointerinternal.h 中定义,用于 CRT 的内部辅助函数。

/* internal helper functions for encoding and decoding pointers */
void __cdecl _init_pointers();
_CRTIMP void * __cdecl _encode_pointer(void *);
_CRTIMP void * __cdecl _encoded_null();
_CRTIMP void * __cdecl _decode_pointer(void *);

示例

  • NT 堆内部指针
  • 矢量化异常处理程序指针
  • 长期存在的函数指针

结论

函数指针是 C 和 C++ 编程中的一个重要概念。Windows 应用程序编程接口 (Windows API) 大量使用函数指针和回调函数。因此,Windows 程序员应该了解函数指针和回调函数,无论是否在编程中使用它们。Microsoft 安全开发生命周期 (SDL) 也推荐并提供了 EncodePointer/EncodeSystemPointerDecodePointer/DecodeSystemPointer 函数来进行安全指针操作。指针可能是函数指针或变量指针。不安全指针会导致缓冲区溢出攻击。C/C++ 在初始化或在函数之间移动数据时不支持数组边界检查。缓冲区溢出攻击可以覆盖函数返回地址。当函数返回时,它不是跳回调用者,而是跳到攻击者代码。堆栈溢出可用于更改程序流程或获取操作系统环境的权限。堆溢出涉及应用程序分配的堆内存分配函数。

摘要

  1. delete 对象赋 NULL。这可以避免“悬空”指针。
  2. 对长期存在的 static 或全局指针进行 EncodeDecodeencode 指针对每个进程使用不同的值。因此,攻击者无法预测用于缓冲区溢出攻击的 encode 逻辑。
    1. Windows 消息和运行时动态链接库大量使用函数指针。因此,请使用 EncodeDecode 指针以实现安全的指针访问。
    2. 加载动态链接库 (DLL) 必须安全地加载 DLL 并执行函数。
  3. 如果应用程序使用 STL,请检查安全函数是否可用。
  4. 遵循 Microsoft 安全开发生命周期 (SDL) 5.0 指南

关注点

  1. Microsoft 安全开发生命周期
  2. MSDN Visual C++ 2008
  3. MSDN 杂志
  4. 防范指针诡计(某种程度上!)
  5. 防范指针诡计(重述!)
  6. 击败编译器级别的缓冲区溢出保护
  7. Microsoft 安全开发生命周期 (SDL) – 流程指南
  8. 函数指针教程
  9. MSDN
  10. Windows ISV 软件安全防御
  11. 成员函数指针和最快的 C++ 委托
  12. Microsoft Visual C++ 2008 可再发行组件包 (x86)
  13. Microsoft Visual C++ 2010 可再发行组件包 (x86)
  14. 如何获取 Visual C++ 6.0 运行时组件

历史

  • 2011年5月1日:初版
© . All rights reserved.