**公司 安全研究专家 李泉
背景
在平时开发的过程中发现了这么一个问题。如果以多线程的方式调用Openssl库函数进行安全加密解密的话,发生了内存空间的UAF与double free的异常。
(图:SSL指针被改写成0x21)
RSA_new_method用于创建一个SSL指针而这个指针是公用的,在openssl整个生态中多处采取了调用,这里就是因为其中某成员在其他线程中被改写形成了冲突,大多是SSL_write、SSL_read等共享操作,如果有其他线程访问了已经被Free掉的对象,自然会出现异常。在这里,本人则介绍一下Openssl官方的最新针对多线程操作的解决方案。
Openssl是线程安全的
程序员们经常有一些误解就是Openssl不是线程安全的。究其原因就是开发人员通常使用网络上公开的加密解密算法,并没有详细的阅读Openssl的相关说明文档。Openssl在于算法封装的性能优势,多种安全加密算法均可以利用该库调用。高性能的运算必然面对的是多种共享资源的读写。经过研究SSL结构中的多处成员,必须要运行在原子锁级别上。关键就是SSL_write函数,经常会并行修改其他成员变量,造成内存冲突。所以查找多方资料,了解到Openssl是有针对多线程情况提出的解决方案。
Openssl首先判断自身是否处于线程调用中,获得线程的标识符。然后Openssl要求每个线程捆绑一个互斥体对象(互斥锁)来保持线程安全性。特殊的是,Openssl的全平台支持特性迫使其无法再次外接互斥体创建、释放、激活等方法。(不同操作系统线程管理原理不同,其实也是偷懒)其选择了回调的方式,将线程管理部分转接系统自身进行操作。回调函数分为静态锁与动态锁,下面就分别来介绍这两种解决方案。
Openssl静态锁回调
静态锁要求程序员提供两个回调函数,第一个主要是告诉组件在适当的时机获取或者释放锁。定义如下:
void locking_function(int mode, int n, const char *file, int line);
mode:确定锁执行的操作。存在CRYPTO_LOCK标记时,进行枷锁,否则它应该被释放。
n:获取或者释放的锁的编号。第一个锁从0标识。该值永远不会大于或者等于CRYPTO_num_locks变量的值。
File:请求互斥对象的对象名称。用于辅助调试,通常由_FILE_预处理器宏提供。
Line:请求创建互斥对象的源行号。与file参数一样,它也用于辅助调试,通常由_line_preprocessor宏提供。
下一个回调函数用于获取调用线程的唯一标识符。类似于windows中的GetCurrentThreadId函数。我们容易就明白这是用来获取当前线程信息的,并且保证绝对的唯一。函数需要定义成如下格式。
unsigned long id_function(void);
最后,我们引入Openssl中的两个库函数:CRYPTO_set_id_callback和CRYPTO_set_locking_callback,并且在初始化的时候调用他们,具体用法如下:
案例. Win32 与POSIX内核下实现的Openssl静态锁
int THREAD_setup(void);
int THREAD_cleanup(void);
#if defined(WIN32)
#define MUTEX_TYPE HANDLE
#define MUTEX_SETUP(x) (x) = CreateMutex(NULL, FALSE, NULL)
#define MUTEX_CLEANUP(x) CloseHandle(x)
#define MUTEX_LOCK(x) WaitForSingleObject((x), INFINITE)
#define MUTEX_UNLOCK(x) ReleaseMutex(x)
#define THREAD_ID GetCurrentThreadId()
#elif defined(_POSIX_THREADS)
/* _POSIX_THREADS is normally defined in unistd.h if pthreads are availableon your platform. */
#define MUTEX_TYPE pthread_mutex_t
#define MUTEX_SETUP(x) pthread_mutex_init(&(x), NULL)
#define MUTEX_CLEANUP(x) pthread_mutex_destroy(&(x))
#define MUTEX_LOCK(x) pthread_mutex_lock(&(x))
#define MUTEX_UNLOCK(x) pthread_mutex_unlock(&(x))
#define THREAD_ID pthread_self()
#else
#error You must define mutex operations appropriate for your
platform!
#endif
/* 保存有效的mutex. */
static MUTEX_TYPE *mutex_buf = NULL;
static void locking_function(int mode, int n, const char * file, int
line)
{
if (mode & CRYPTO_LOCK)
MUTEX_LOCK(mutex_buf[n]);
else
MUTEX_UNLOCK(mutex_buf[n]);
}
static unsigned long id_function(void)
{
return ((unsigned long)THREAD_ID);
}
int THREAD_setup(void)
{
int i;
mutex_buf = (MUTEX_TYPE *)malloc(CRYPTO_num_locks() *
sizeof(MUTEX_TYPE));
if (!mutex_buf)
return 0;
for (i = 0; i < CRYPTO_num_locks(); i++)
MUTEX_SETUP(mutex_buf[i]);
CRYPTO_set_id_callback(id_function);
CRYPTO_set_locking_callback(locking_function);
return 1;
}
int THREAD_cleanup(void)
{
int i;
if (!mutex_buf)
return 0;
CRYPTO_set_id_callback(NULL);
CRYPTO_set_locking_callback(NULL);
for (i = 0; i < CRYPTO_num_locks(); i++)
MUTEX_CLEANUP(mutex_buf[i]);
free(mutex_buf);
mutex_buf = NULL;
return 1;
}
使用这些静态加锁函数,我们需要在程序启动线程或调用OpenSSL函数之前至少进行一次的函数调用,并且我们必须调用THREAD_setup,如果不能分配容纳互斥体的内存,该函数通常会返回1或0。一旦THREAD_setup调用并成功返回,我们就可以在多个线程中调用Openssl,在程序线程执行完成之后,或者使用Openssl完成之后,我们应该调用Thread_cleanup来回收用于互斥体中的所有内存。在以上实例中,如果担心存在异常的情况,需要您添加异常处理代码在捕获异常。
Openssl动态锁回调
动态所需要一个数据结构(CRYPTO_dynlock_value)和三个回调函数。该结构用于保存互斥对象所使用的数据,这三个函数分别对应创建,锁定/解锁和销毁的操作。与静态锁定机制相同,我们还必须告诉O penssl关于回调函数的信息,以便在适当的时候调用他们。首先第一步我们需要定义CRYPTO_dynlock_value结构,这个结构很简单,只有一个成员。
struct CRYPTO_dynlock_value
{
MUTEX_TYPE mutex;
};
第一个回到函数用于创建一个新的互斥对象,Openssl使用干净的内存区域来创建他。必须为返回的结构体分配内存,并且对其初始化。回调的定义如下。
struct CRYPTO_dynlock_value *dyn_create_function(const char *file,
int line);
file:请求互斥对象的对象名称。用于辅助调试,通常由_FILE_预处理器宏提供。
Line:请求创建互斥对象的源行号。与file参数一样,它也用于辅助调试,通常由_line_preprocessor宏提供。
下一个回调函数用于获取或释放互斥对象。它的定义如下:
void dyn_lock_function(int mode, struct CRYPTO_dynlock_value *mutex, constchar *file, int line);
mode:确定锁定函数应该执行的操作。设置CRYPTO_LOCK标志时,应获加锁;否则,它应该被释放。
Mutex:互斥体应该处于被获取或释放状态。它永远不会为空。
file:请求互斥对象的对象名称。用于辅助调试,通常由_FILE_预处理器宏提供。
Line:请求创建互斥对象的源行号。与file参数一样,它也用于辅助调试,通常由_line_preprocessor宏提供。
第三个也就是最后一个回调函数用于销毁一个不再需要OpenSSL的互斥体,使用当前平台的释放方式对这段内存进行销毁,并释放分配给CRYPTO_dynlock_value结构的任何内存。它的定义如下:
void dyn_destroy_function(struct CRYPTO_dynlock_value *mutex, const char*file, int line);
Mutex:互斥体应该处于被获取或释放状态。它永远不会为空。
file:请求互斥对象的对象名称。用于辅助调试,通常由_FILE_预处理器宏提供。
Line:请求创建互斥对象的源行号。与file参数一样,它也用于辅助调试,通常由_line_preprocessor宏提供。
案例. 扩展库以支持动态锁定机制
struct CRYPTO_dynlock_value
{
MUTEX_TYPE mutex;
};
static struct CRYPTO_dynlock_value * dyn_create_function(const char *file,
int line)
{
struct CRYPTO_dynlock_value *value;
value = (struct CRYPTO_dynlock_value *)malloc(sizeof(
struct CRYPTO_dynlock_value));
if (!value)
return NULL;
MUTEX_SETUP(value->mutex);
return value;
}
static void dyn_lock_function(int mode, struct CRYPTO_dynlock_value *l,
const char *file, int line)
{
if (mode & CRYPTO_LOCK)
MUTEX_LOCK(l->mutex);
else
MUTEX_UNLOCK(l->mutex);
}
static void dyn_destroy_function(struct CRYPTO_dynlock_value *l,
const char *file, int line)
{
MUTEX_CLEANUP(l->mutex);
free(l);
}
int THREAD_setup(void)
{
int i;
mutex_buf = (MUTEX_TYPE *)malloc(CRYPTO_num_locks() *
sizeof(MUTEX_TYPE));
if (!mutex_buf)
return 0;
for (i = 0; i < CRYPTO_num_locks(); i++)
MUTEX_SETUP(mutex_buf[i]);
CRYPTO_set_id_callback(id_function);
CRYPTO_set_locking_callback(locking_function);
/* The following three CRYPTO_... functions are the OpenSSL functions
for registering the callbacks we implemented above */
CRYPTO_set_dynlock_create_callback(dyn_create_function);
CRYPTO_set_dynlock_lock_callback(dyn_lock_function);
CRYPTO_set_dynlock_destroy_callback(dyn_destroy_function);
return 1;
}
int THREAD_cleanup(void)
{
int i;
if (!mutex_buf)
return 0;
CRYPTO_set_id_callback(NULL);
CRYPTO_set_locking_callback(NULL);
CRYPTO_set_dynlock_create_callback(NULL);
CRYPTO_set_dynlock_lock_callback(NULL);
CRYPTO_set_dynlock_destroy_callback(NULL);
for (i = 0; i < CRYPTO_num_locks(); i++)
MUTEX_CLEANUP(mutex_buf[i]);
free(mutex_buf);
mutex_buf = NULL;
return 1;
}
作者|李泉(liquan165) 某集团安全研究专家
主要研究领域|互联网黑产、汽车安全、物联网安全、终端安全等
关注我们公众号:ExploitLee