Openssl 多线程支持

                                                                                  **公司  安全研究专家    李泉

背景

      在平时开发的过程中发现了这么一个问题。如果以多线程的方式调用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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,080评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,422评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,630评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,554评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,662评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,856评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,014评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,752评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,212评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,541评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,687评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,347评论 4 331
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,973评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,777评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,006评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,406评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,576评论 2 349

推荐阅读更多精彩内容