首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >记一次在Mac系统下因为栈上变量溢出导致的内存泄露问题

记一次在Mac系统下因为栈上变量溢出导致的内存泄露问题

原创
作者头像
bowenerchen
发布2022-11-23 20:07:00
1.7K3
发布2022-11-23 20:07:00
举报
文章被收录于专栏:梵高先生梵高先生梵高先生

栈上变量溢出导致的内存泄漏问题

背景

在Mac上测试TSM SDK C语言版本的SM2Encrypt接口时,遇到一个内存无法释放的问题:

这个截图里面的意思就是说,我的程序尝试去动态释放一块堆上的内存时报错了,因为这块内存没有被动态分配出来。

Mac机器信息为:

os name: macOS,
os release: 12.4,
os version: 21F79,
os platform: x86_64,
processor name: Intel(R) Core(TM) i7-9750H CPU @ 2.60GHz

Apple clang version 12.0.0 (clang-1200.0.32.29)
Target: x86_64-apple-darwin21.5.0
Thread model: posix
InstalledDir: /Library/Developer/CommandLineTools/usr/bin

场景还原

SM2Encrypt接口的定义是这样的:

int SM2Encrypt(struct__anonymous *ctx, 
               const unsigned char *in, size_t inlen, 
               const char *strPubKey, size_t pubkeyLen, 
               unsigned char *out, size_t *outlen)

ctx
函数入参 - 上下文

in
函数入参 - 待加密消息

inlen
函数入参 - 消息长度(字节单位)

strPubKey
函数入参 - 公钥

pubkeyLen
函数入参 - 公钥长度

out
函数出参 - 密文 - 应当为out分配的内存长度遵循以下规则:密文长度 = 明文长度 + 96 + ASN1编码增量,其中ASN1编码增量长度不定,为简单起见,可直接分配 密文长度 = 明文长度 + 200,此长度可保证安全

outlen
函数入参和出参 - 这是一个UNIX C风格的函数参数用法,入参请将*outlen置为out指针所指向内存的分配大小,函数返回后*outlen将被置为输出密文的实际长度

我的核心测试代码大概是长这样的(为了方便分析做了一些小修改,主要是打印输出上的修改,不影响逻辑):

void sm2_test() {

    // 分配内存
    size_t test_plain_len = 16;
    unsigned char *test_plain = malloc(test_plain_len * sizeof(unsigned char)); // 动态分配内存
    printf("malloc test_plain success, size:%lu bytes, test_plain pointer value:%x\n",
           test_plain_len * sizeof(unsigned char), test_plain);

    /* *****
     * SM2密文主要由C1、C2、C3三部分构成,
     * 其中C1是随机数计算出的椭圆曲线、C2是密文数据、C3是SM3杂凑值,
     * C1固定为32字节,C2的长度与明文相同,C3的长度固定为64字节,
     * 如果涉及到 ASN.1 编码,则整个密文长度将会膨胀,
     * 由于 ASN.1 编码带来的膨胀长度不固定,但是长度值绝对小于 104 字节(200 - C1长度 - C3长度)。
     * 因此密文长度的计算,可以按照 明文长度 + 200 个字节来估算。
     * *****/
    int cipher_len = test_plain_len + 32 + 64 + 104; // 注意!!!这里使用int类型,而非size_t类型
    unsigned char *cipher = malloc(cipher_len * sizeof(unsigned char));
    printf("malloc cipher success, size:%lu bytes, cipher pointer value:%x\n",
           cipher_len * sizeof(unsigned char), cipher);

    // 必须初始化sm2_ctx_t,否则会报错-10012
    sm2_ctx_t global_sm2_ctx;
    int sm2_ctx_ret = SM2InitCtx(&global_sm2_ctx);
    printf("init sm2 ctx without pub key, ret=%d\n", sm2_ctx_ret);

    printf("sizeof(int)=%lu, sizeof(size_t)=%lu, sizeof(int*)=%lu, sizeof(size_t*)=%lu\n",
           sizeof(int), sizeof(size_t), sizeof(int *), sizeof(size_t *));

    /* *****
     * SM2Encrypt 默认使用 C1C3C2_ASN1 模式进行加密
     * *****/
    int encrypt_ret = SM2Encrypt(&global_sm2_ctx,
                                 (unsigned char *) (test_plain), (size_t) (test_plain_len * sizeof(unsigned char)),
                                 SM2_TEST_DEMO_PUB_KEY, SM2_PUBKEY_LEN,
                                 (unsigned char *) (cipher), (int *) (&cipher_len));
    printf("sm2 encrypt, ret code:%d, cipher len:%d\n", encrypt_ret, cipher_len);

    sm2_ctx_ret = SM2FreeCtx(&global_sm2_ctx);
    printf("free sm2 ctx, ret=%d\n", sm2_ctx_ret);

    printf("cipher pointer value:%x\n", cipher);
    free(cipher);
    printf("free cipher success, size:%lu bytes\n", cipher_len * sizeof(unsigned char));

    printf("test_plain pointer value:%x\n", test_plain);
    free(test_plain);
    printf("free test_plain success, size:%lu bytes\n", test_plain_len * sizeof(unsigned char));

}

如背景中所描述的那样,在Mac Intel x86_64环境上运行时,得到了报错:

sm2_test_Darwin_x86_64(49620,0x115fec600) malloc: *** error for object 0x600000000000: pointer being freed was not allocated
sm2_test_Darwin_x86_64(49620,0x115fec600) malloc: *** set a breakpoint in malloc_error_break to debug

而这段代码在Linux上运行时,却可以正确运行???

这里强调下,在Linux系统上,也是intel x86_64的cpu:

os name: Linux,
os release: 3.10.107-1-tlinux2_kvm_guest-0055,
os version: #1 SMP Sat Oct 9 14:12:34 CST 2021,
os platform: x86_64,
processor name: Unknown P6 family

gcc (GCC) 4.8.5
Copyright (C) 2015 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

对Mac上运行的结果进行分析

毫无疑问,直观的来看,test_plain是通过malloc进行分配的,在没有重复free的情况下,free(test_plain)应该是没有问题的;

但是现在free(test_plain)时,却报错pointer being freed was not allocated,在充分检查了代码中没有主动对test_plain做修改后,那么只剩下一种可能,那就是test_plain在运行过程中被修改了

再次检查了代码,发现,test_plain除了被打印之外,只在调用SM2Encrypt时,作为入参被传进去:

难道是在调用了SM2Encrypt之后,test_plain就被改了???

我们先加两句打印语句,看看是否值真的变化了:

运行结果:

test_plain指针果然变化了!!!

基于Mac环境下的深层次原因分析

通过对现象的分析,大致可以确定,就是在执行了SM2Encrypt之后,test_plain指针被改变了,那么为什么test_plain会被改变呢?

通过仔细对比SM2Encrypt的接口定义与实际传参的类型,可以发现,在传入cipher_len这个参数时,类型上有点点区别:

传入的参数cipher_len定义的是int型,而接口定义的类型是size_t*,通过打印这两种类型,可以发现,在mac上,int与size_t所占用的字节数是不同的:

    printf("sizeof(int)=%lu, sizeof(size_t)=%lu, sizeof(int*)=%lu, sizeof(size_t*)=%lu\n",
           sizeof(int), sizeof(size_t), sizeof(int *), sizeof(size_t *));

		sizeof(int)=4, sizeof(size_t)=8, sizeof(int*)=8, sizeof(size_t*)=8

int型变量占用的字节是4个字节,而size_t型变量占用的字节是8个字节。

而在SM2Encrypt接口中,对于cipher_len是按照size_t的变量进行赋值的,也就是说默认会有8个字节长度的值给到cipher_len,而cipher_len本身定义为int型,只有4个字节,因此必然会多出4个字节。

由于我们是在Mac Intel x86_64的硬件架构上进行编译和运型,x86_64是小端系统,也就是说,变量值0x01020304的排列顺序是:

04 03 02 01

假设SM2Encrpt中,对cipher_len赋值的值为1024(十六进制表示为0x000000000400),

那么其在小端系统的内存中排列为:

00 40 00 00 00 00 00 00

cipher_len只有4个字节,因此只能接收前4个字节:00 00 40 00, 那么多出来的4个字节将会溢出,写入到别的内存中。

再回到这段代码的内存分布,我们会发现,核心测试代码中,以cipher_len被定义为分界点,按照顺序定义了以下变量:

size_t test_plain_len; -- 8字节
unsigned char *test_plain; -- 指针,8字节
int cipher_len; -- 4字节
unsigned char *cipher; -- 指针,8字节

C程序的内存空间分布如图所示:

由于test_plain在栈上与cipher_len相邻,而test_plain在某次运行时,其指向的地址刚好为:0x00000000013e0070,内存排列为:

70 00 3e 01 00 00 00 00 

最终导致其在栈上的分布应该如图所示:

因此,当最后去free(test_plain)时,相当于free的是一个只想地址为0的内存块,进而就会导致文中一开始描述的报错信息。

为了验证这个猜想,我们可以尝试把cipher_len类型改为size_t试试看:

通过运行结果可以看到,test_plain addr为0xb48d3768,而cipher_len addr为0xb48d3760,刚好相差8个字节,因此对cipher_len进行填充时,不会覆盖test_plain。

问题的再进一步抽象与简化

上述基于tsm库的分析,其实可以再次对逻辑进行简化,不依赖外部第三方库进行这种现象的复现。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void overflow_int(size_t *y) {
    *y = 0x1122334455667788;
}

int main() {
    // 定义一个指针,指针本身占用8字节,只向一个8字节的内存空间
    unsigned char *test_plain = malloc(8 * sizeof(unsigned char));
    printf("test_plain addr:%x, test_plain value:%x\n", &test_plain, test_plain);

    // 定义一个int型变量,向这个变量的地址空间拷贝2 * sizeof(x) 个字节
    // 然后观察test_plain的值是否被覆盖
    int x = 0; // 本身4个字节
    printf("x addr:%x\n", &x);
    overflow_int(&x);

    printf("test_plain addr:%x, test_plain value:%x\n", &test_plain, test_plain);

    free(test_plain);

}

在Mac上验证:

在Linux上验证:

如之前分析的那样,test_plain 的值,在经过overflow_int赋值之后,变成了0x11223344。

最后一个疑问:为什么一开始在Linux上运行不会报错?

在文章的最开始,我们提到过,同样的代码,在Mac上运行会报错,但是在Linux上不会报错:

借由前面分析的经验,我们同样适用打印地址的方式,来进行排查,只不过,这次打印地址,我们需要打印完整地址,也就是说,在代码中,将%x替换为%p,代码类似于:

之所以这里需要以%p的形式来打印指针的值,主要是希望获取到完整地址值,避免%x只取低地址位造成的地址截断,话不多说,跑代码看效果:

Mac下的效果:<img src="栈上变量溢出导致的内存泄漏问题.assets/image-20220601222737843.png" alt="image-20220601222737843" style="zoom:50%;" />

Linux下的效果:

通过对指针值的完整打印,我们可以发现:

在Mac下,test_plain指向的地址的值,其高位始终都是0x6000开头,虽然由于cipher_len溢出,造成了4个字节被覆写为0x00,但是高地址位仍然是0x6000,这样最终去free时实际访问的是0x600000000000,而这个地址可能指向其他有效的内存空间,因此会触发OS的异常终止。

而在Linux下,我们会发现,test_plain指向的地址的值,其高位始终都是0x0000,只有低位是有效位,同样由于cipher_len溢出,造成了4个字节被覆写为0x00,最终导致free时,其实是对指向0地址的内存空间做free,而这个动作对于free函数来数,是被允许的,虽然这样最终还是会造成内存泄漏,但是并不会触发OS的异常终止。

至于为什么Linux下指针值只有低位地址,而Mac下却有高位地址呢?这个应该与OS内存管理的设计有关,也与OS是否开启地址随机化有关系,这块后面有时间再慢慢研究吧!

关于TSM

腾讯国密算法(TencentSM)基于《中华人民共和国密码行业标准》研发,算法性能十分优异,在业界处于领先水平。单个方法的执行效率较目前主流国密算法均有提升,特别在加解密方面的性能提升明显。

咨询TSM欢迎联系腾讯安全云鼎实验室。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 场景还原
  • 对Mac上运行的结果进行分析
  • 基于Mac环境下的深层次原因分析
  • 问题的再进一步抽象与简化
  • 最后一个疑问:为什么一开始在Linux上运行不会报错?
  • 关于TSM
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档