一次OPENSSL指针错误的排查

同事在arm下使用了我们封装好的openssl库,应用程序在调用EC_KEY_new_by_curve_name接口时发生了错误。以下是分析定位过程。

通过gdb很容易就能定位到,

#0  BN_POOL_finish (p=0x26f44 <BN_STACK_finish+52>) at bn_ctx.c:368
#1  0x00026ad8 in BN_CTX_free (ctx=0x174dd0) at bn_ctx.c:252
#2  0x0006c2f0 in ec_group_new_from_data (data=0x12867c <_EC_SM2_PRIME256+8>) at ec_curve.c:2036
#3  0x0006c460 in EC_GROUP_new_by_curve_name (nid=893) at ec_curve.c:2062
#4  0x0002b178 in EC_KEY_new_by_curve_name (nid=893) at ec_key.c:96
……

程序挂在BN_CTX_free上,即对BN_CTX结构析构时刻,错误是free了非法的指针。

BN_CTX是BIGNUM对象的缓冲池,毕竟BIGNUM不像int,float,要用时临时分配内存还是很影响性能的,于是才有了这个东西。他需要结合BN_CTX_start,BN_CTX_end,BN_CTX_get来使用。

首先我们怀疑是不是BN_CTX内部的数据由于未知原因被破坏了。

先启用BN_CTX_DEBUG宏重新编译OPENSSL,看看BN_CTX的使用上出了什么问题。

编译的时候看看BN_CTX的定义

struct bignum_ctx
	{
	/* The bignum bundles */
	/* 这个pool就是一个链表,每个链表节点内部含有一个定长的指针数组,可以存储BN_CTX_POOL_SIZE个BIGNUM对象 */
	BN_POOL pool;
	/* The "stack frames", if you will */
	/* 一个不定长数组,当做栈使用,存放每一次BN_CTX_start时pool内BIGNUM对象的个数,或者说索引值*/
	BN_STACK stack;
	/* The number of bignums currently assigned */
	unsigned int used;
	/* Depth of stack overflow */
	int err_stack;
	/* Block "gets" until an "end" (compatibility behaviour) */
	int too_many;
	};

再查看源码,ctxdbg函数是用来输出DEBUG信息的。这样就好理解DEBUG输出是什么含义了。
意外发现OPENSSL的BUG一枚


static void 
ctxdbg(BN_CTX *ctx)
	{
	unsigned int bnidx = 0, fpidx = 0;
	BN_POOL_ITEM *item = ctx->pool.head;
	BN_STACK *stack = &ctx->stack;
	fprintf(stderr,"(%08x): ", (unsigned int)ctx);
	while(bnidx < ctx->used)
		{
		fprintf(stderr,"%03x ", item->vals[bnidx++ % BN_CTX_POOL_SIZE].dmax);
		if(!(bnidx % BN_CTX_POOL_SIZE))
			item = item->next;
		}
	fprintf(stderr,"\n");
	bnidx = 0;
	fprintf(stderr,"          : ");
	while(fpidx < stack->depth)
		{
		//这里是OPENSSL的小BUG,当循环条件不成立的时候bnidx仍旧会被+1,导致DEBUG日志中栈的位置是错误的。
		//正确的做法应该是把bnidx的自增放到循环体内部。,或者把fprintf后面的bnidx++去掉。
		while(bnidx++ < stack->indexes[fpidx]) 
			fprintf(stderr,"    ");
		fprintf(stderr,"^^^ ");
		bnidx++;
		fpidx++;
		}
	fprintf(stderr,"\n");
	}

再看一下BN_CTX_start,BN_CTX_end,BN_CTX_get的时候都干了什么

/* 把当前pool里BIGNUM的个数压到stack内部的数组里 */
void BN_CTX_start(BN_CTX *ctx)
	{
	CTXDBG_ENTRY("BN_CTX_start", ctx);
	/* If we're already overflowing ... */
	if(ctx->err_stack || ctx->too_many)
		ctx->err_stack++;
	/* (Try to) get a new frame pointer */
	else if(!BN_STACK_push(&ctx->stack, ctx->used))
		{
		BNerr(BN_F_BN_CTX_START,BN_R_TOO_MANY_TEMPORARY_VARIABLES);
		ctx->err_stack++;
		}
	CTXDBG_EXIT(ctx);
	}

/* 把上一次压入到stack内的BIGNUM的个数A弹出,并认为当前pool里有A个BIGNUM已被使用*/
void BN_CTX_end(BN_CTX *ctx)
	{
	CTXDBG_ENTRY("BN_CTX_end", ctx);
	if(ctx->err_stack)
		ctx->err_stack--;
	else
		{
		unsigned int fp = BN_STACK_pop(&ctx->stack);
		/* Does this stack frame have anything to release? */
		if(fp < ctx->used)
			BN_POOL_release(&ctx->pool, ctx->used - fp);
		ctx->used = fp;
		/* Unjam "too_many" in case "get" had failed */
		ctx->too_many = 0;
		}
	CTXDBG_EXIT(ctx);
	}

/* 基于pool当前状态,从pool中返回一个未使用的BIGNUM结构 */
BIGNUM *BN_CTX_get(BN_CTX *ctx)
	{
	BIGNUM *ret;
	CTXDBG_ENTRY("BN_CTX_get", ctx);
	if(ctx->err_stack || ctx->too_many) return NULL;
	if((ret = BN_POOL_get(&ctx->pool)) == NULL)
		{
		/* Setting too_many prevents repeated "get" attempts from
		 * cluttering the error stack. */
		ctx->too_many = 1;
		BNerr(BN_F_BN_CTX_GET,BN_R_TOO_MANY_TEMPORARY_VARIABLES);
		return NULL;
		}
	/* OK, make sure the returned bignum is "zero" */
	BN_zero(ret);
	ctx->used++;
	CTXDBG_RET(ctx, ret);
	return ret;
	}

重新运行测试程序,查看日志输出

Starting BN_CTX_start
(00174dd0): 
          : 
Ending BN_CTX_start
(00174dd0): 
          : ^^^ 
Starting BN_CTX_get
(00174dd0): 
          : ^^^ 
Starting BN_CTX_end
(00174dd0): 001 
          : ^^^ 
Ending BN_CTX_end
(00174dd0): 
          : 
Starting BN_CTX_start
(00174dd0): 
          : 
Ending BN_CTX_start
(00174dd0): 
          : ^^^ 
Starting BN_CTX_get
(00174dd0): 
          : ^^^ 
Starting BN_CTX_start
(00174dd0): 001 
          : ^^^ 
Ending BN_CTX_start
(00174dd0): 001 
          : ^^^ ^^^ 
Starting BN_CTX_get
(00174dd0): 001 
          : ^^^ ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 
          : ^^^ ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 001 
          : ^^^ ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 001 001 
          : ^^^ ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 001 001 001 
          : ^^^ ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 001 001 001 001 
          : ^^^ ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 001 001 001 001 001 
          : ^^^ ^^^ 
Starting BN_CTX_start
(00174dd0): 001 001 002 001 001 001 001 001 
          : ^^^ ^^^ 
Ending BN_CTX_start
(00174dd0): 001 001 002 001 001 001 001 001 
          : ^^^ ^^^                         ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 002 001 001 001 001 001 
          : ^^^ ^^^                         ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 002 001 001 001 001 001 001 
          : ^^^ ^^^                         ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 002 001 001 001 001 001 001 001 
          : ^^^ ^^^                         ^^^ 
Starting BN_CTX_get
(00174dd0): 001 001 002 001 001 001 001 001 001 001 001 
          : ^^^ ^^^                         ^^^ 
Starting BN_CTX_end
(00174dd0): 001 001 002 001 001 001 001 001 001 004 002 001 
          : ^^^ ^^^                         ^^^ 
Ending BN_CTX_end
(00174dd0): 001 001 002 001 001 001 001 001 
          : ^^^ ^^^ 
Starting BN_CTX_end
(00174dd0): 001 001 002 001 001 001 001 001 
          : ^^^ ^^^ 
Ending BN_CTX_end
(00174dd0): 001 
          : ^^^ 
Starting BN_CTX_end
(00174dd0): 001 
          : ^^^ 
Ending BN_CTX_end
(00174dd0): 
          : 

这么看,CTX的内部结构似乎没有什么问题,进一步调试,发现每次都是pool里最后一个BIGNUM在BN_CTX_free时出错。尝试跟踪这个BIGNUM的整个生命周期。
怎么跟踪?从BN_CTX_get断点开始!有了DEBUG信息,我们很容易就能跟踪到下面这个BIGNUM的创建过程。

Starting BN_CTX_get
(00174dd0): 001 001 002 001 001 001 001 001 001 001 001 
          : ^^^ ^^^                         ^^^ 

这个BIGNUM是在一次除法过程中作为临时变量产生的,栈回溯如下:

#0  BN_div (dv=0x0, rm=0x174ff0, num=0x174ff0, divisor=0x174fdc, ctx=0x174dd0) at bn_div.c:230
#1  0x0005fe70 in BN_nnmod (r=0x174ff0, m=0x174ff0, d=0x174fdc, ctx=0x174dd0) at bn_mod.c:132
#2  0x0005f25c in BN_mod_inverse (in=0x174fc8, a=0x175384, n=0xbeffd320, ctx=0x174dd0) at bn_gcd.c:246
#3  0x000610fc in BN_MONT_CTX_set (mont=0x175380, mod=0x174e00, ctx=0x174dd0) at bn_mont.c:479
#4  0x0006cb5c in ec_GFp_mont_group_set_curve (group=0x174e90, p=0x174e00, a=0x174e30, b=0x174e60, 
    ctx=0x174dd0) at ecp_mont.c:224

在bn_div.c中发现这么一个片段

BN_ULONG *resp,*wnump;

//……省略

res=BN_CTX_get(ctx);

//……省略

/* Setup to 'res' */
res->neg= (num->neg^divisor->neg);
if (!bn_wexpand(res,(loop+1))) goto err;
res->top=loop;
resp= &(res->d[loop-1]);

程序运行到这里,loop = 0,这个resp指针本意应该是指向BIGNUM的二进制数据区d,现在却指向了数据区前面的地址去了。可想而知,后面的一系列操作都没有在正确的地址空间进行,我们知道,很多编译器的malloc函数在给用户分配内存空间时,会在返回给用户地址前面保留一段跟内存分配有关的私有数据,一旦这些数据被破坏,free就会出错。

问题总算是定位到了,为什么只有arm下才会出现这个问题?我们还需要进一步分析

查看OpenSSL文档不难知道,BN_div是一个触发运算

BN_div() divides a by d and places the result in dv and the remainder
in rem (dv=a/d, rem=a%d). Either of dv and rem may be NULL, in which
case the respective value is not returned. The result is rounded
towards zero; thus if a is negative, the remainder will be zero or
negative. For division by powers of 2, use BN_rshift(3).

结合文档,根据栈回溯可知本次BN_div调用的参数分别为:

参数说明
dv=0x0商,不需要返回结果
rm=0x174ff0待返回余数
num=0x174ff0被除数
divisor=0x174fdc除数

不难发现待返回余数和被除数是同一个BIGNUM。看一下具体数值

(gdb) p *rm          
$47 = {d = 0x175468, top = 2, dmax = 2, neg = 0, flags = 0}
(gdb) x/8c rm->d
0x175468:       0 '\000'        0 '\000'        0 '\000'        0 '\000'        1 '\001'        0 '\000' 0 '\000' 0 '\000'
(gdb) p *divisor
$48 = {d = 0x1753e8, top = 1, dmax = 1, neg = 0, flags = 0}
(gdb) x/4c divisor->d
0x1753e8:       -1 '\377'       -1 '\377'       -1 '\377'       -1 '\377'
(gdb) 

BN_div函数下断点,程序重新运行一遍

norm_shift=BN_BITS2-((BN_num_bits(divisor))%BN_BITS2);
if (!(BN_lshift(sdiv,divisor,norm_shift))) goto err;
sdiv->neg=0;
norm_shift+=BN_BITS2;
if (!(BN_lshift(snum,num,norm_shift))) goto err;

这里好好的除数怎么变得如此巨大?这里看起来是在做左移运算,BN_BITS2又是什么?找到定义,焕然大悟!

/* This is where the long long data type is 64 bits, but long is 32.
 * For machines where there are 64bit registers, this is the mode to use.
 * IRIX, on R4000 and above should use this mode, along with the relevant
 * assembler code :-).  Do NOT define BN_LLONG.
 */
#ifdef SIXTY_FOUR_BIT
#undef BN_LLONG
#undef BN_ULLONG
#define BN_ULONG	unsigned long long
#define BN_LONG		long long
#define BN_BITS		128
#define BN_BYTES	8
#define BN_BITS2	64
#define BN_BITS4	32
#define BN_MASK2	(0xffffffffffffffffLL)
#define BN_MASK2l	(0xffffffffL)
#define BN_MASK2h	(0xffffffff00000000LL)
#define BN_MASK2h1	(0xffffffff80000000LL)
#define BN_TBIT		(0x8000000000000000LL)
#define BN_DEC_CONV	(10000000000000000000ULL)
#define BN_DEC_FMT1	"%llu"
#define BN_DEC_FMT2	"%019llu"
#define BN_DEC_NUM	19
#define BN_HEX_FMT1	"%llX"
#define BN_HEX_FMT2	"%016llX"
#endif

#ifdef THIRTY_TWO_BIT
#ifdef BN_LLONG
# if defined(_WIN32) && !defined(__GNUC__)
#  define BN_ULLONG	unsigned __int64
#  define BN_MASK	(0xffffffffffffffffI64)
# else
#  define BN_ULLONG	unsigned long long
#  define BN_MASK	(0xffffffffffffffffLL)
# endif
#endif
#define BN_ULONG	unsigned int
#define BN_LONG		int
#define BN_BITS		64
#define BN_BYTES	4
#define BN_BITS2	32
#define BN_BITS4	16
#define BN_MASK2	(0xffffffffL)
#define BN_MASK2l	(0xffff)
#define BN_MASK2h1	(0xffff8000L)
#define BN_MASK2h	(0xffff0000L)
#define BN_TBIT		(0x80000000L)
#define BN_DEC_CONV	(1000000000L)
#define BN_DEC_FMT1	"%u"
#define BN_DEC_FMT2	"%09u"
#define BN_DEC_NUM	9
#define BN_HEX_FMT1	"%X"
#define BN_HEX_FMT2	"%08X"
#endif

原来我们可爱的开发人员在对openssl源码config的时候是用的宿主机操作的,生成的opensslconf.h里定义了SIXTY_FOUR_BIT_LONG这个宏,然而我们arm板子上的系统是32位的……

简单修改了一下opensslconf.h,启用THIRTY_TWO_BIT宏,重新编译,运行——

一切顺利!

不得不感叹,问题的排查过程是如此复杂,然而问题真正的原因却如此简单!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值