标签 Unix 下的文章

字符串拷贝密码

在近期的一次工作交接中,在我的代码中发现了很多’安全隐患’,主要是以’字符串拷贝’为主。这种安全漏洞在C编程中是较为常见的,防范起来也较为容易,这里我们就来一起探索一下’字符串拷贝’的’密码’。

在正常情况下,我们在考量目的缓冲区大小时都会以源缓冲区大小作为依据的,一般会适当的比源缓冲区多出一些空间,其中一种’居中’状况:即sizeof(dstbuf) = strlen(srcbuf) + 1。

当sizeof(dstbuf) > strlen(srcbuf) + 1时,使用strcpy, strncpy都不会出现问题(缓冲区溢出问题);
[Ex1.]
int main() {
        /*
         * 测试char *strcpy(char *s1, const char *s2);
         */
        char    dstbuf1[10];
        char    *srcbuf1 = "Hello";

        memset(dstbuf1, 0, sizeof(dstbuf1));
        strcpy(dstbuf1, srcbuf1);

        printf("%s\n", dstbuf1);        /* 输出结果:Hello */

        /*
         * 测试char *strncpy(char *s1, const char *s2, size_t n);
         */
        char    dstbuf2[10];
        char    *srcbuf2 = "Hello";

        memset(dstbuf2, 0, sizeof(dstbuf2));
        strncpy(dstbuf2, srcbuf2, sizeof(dstbuf2)-1);

        printf("%s\n", dstbuf2);        /* 输出结果:Hello */
}

当sizeof(dstbuf) < strlen(srcbuf) + 1时,当然这种情况就是异常情况,是否能很好的处理这样的异常情况恰恰体现了你的程序的健壮性好坏。我们分别讨论一下使用strcpy、strncpy和strlcpy在这种情况下出现的问题:

(1) 使用strcpy
使用strcpy会出现什么问题呢?strcpy会将srcbuf中的所有字符(直到并包括结尾0)拷贝到dstbuf中,即使sizeof(dstbuf)不够大。这样会导致dstbuf缓冲区溢出,看下面例子:

[Ex2.]
int main() {
        /*
         * 测试char *strcpy(char *s1, const char *s2);
         */
        char    dstbuf1[6];
        char    *srcbuf1 = "HelloWorld";

        memset(dstbuf1, 0, sizeof(dstbuf1));
        strcpy(dstbuf1, srcbuf1);

        printf("%s\n", dstbuf1);        /* 缓冲区溢出,输出结果:HelloWorld */
}
strcpy将’HelloWorld’拷贝到了dstbuf中,由于strcpy不检查目的缓冲区大小,所以即使目的缓冲区dstbuf大小不够,strcpy也继续拷贝,直至碰到源缓冲区的结尾0,strcpy同样不会放过源缓冲区的结尾0,该结尾0也被拷贝到目的缓冲区中,这样我们在输出dstbuf时,printf将结尾0之前的字符悉数打印出来。

(2) 使用strncpy
使用strncpy会出现什么问题呢?如果你这样使用(一般都应该这样用)strncpy(dstbuf, srcbuf, sizeof(dstbuf) – 1),则不会出现问题,最后的sizeof(dstbuf)-1就是为了在dstbuf的结尾留出’结尾0′的空间。但是如果你这样用:strncpy(dstbuf, srcbuf, n), n > sizeof(dstbuf) – 1, 则由于目前srcbuf中的数量已经大于dstbuf的长度,一旦n也大于sizeof(dstbuf)-1,那么dstbuf的最终结果就是其没有结尾0,你printf(dstbuf)会得到结尾为乱码的字符串。看下面例子:
[Ex3.]
int main() {
        /*
         * 测试char *strncpy(char *s1, const char *s2, size_t n);
         */
        char    dstbuf2[6];
        char    *srcbuf2 = "HelloWorld";

        memset(dstbuf2, 0, sizeof(dstbuf2));
        strncpy(dstbuf2, srcbuf2, sizeof(dstbuf2));

        printf("%s\n", dstbuf2);        /* dstbuf2的结尾0被覆盖,输出结果:HelloW鯰 */
}

(3) 使用strlcpy
strlcpy会出现什么情况呢?首先strlcpy并不是标准C库函数,不过在大部分Unix/Linux平台下都提供这个接口,它会在适当的时候截断srcbuf并保证dstbuf最后结尾0不被覆盖,保证缓冲区不溢出,也就是说使用strlcpy是安全的,但是并不一定是你期望的结果。

[Ex4.]
int main() {
        /*
         * 测试size_t strlcpy(char *dst, const char *src, size_t dstsize);
         */
        char    dstbuf3[6];
        char    *srcbuf3 = "HelloWorld";

        memset(dstbuf3, 0, sizeof(dstbuf3));
        strncpy(dstbuf3, srcbuf3, sizeof(dstbuf3));
 
       /*
        * strlcpy截断srcbuf, 将srcbuf的前sizeof(dstbuf3)-1个字符拷贝到dstbuf3中,
        * 并在dstbuf3的结尾处添加结尾0,输出结果:Hello
        */
        printf("%s\n", dstbuf3);       
}

通过上面的几个例子的讲解,相信你已经能够找到’字符串拷贝’的’密码’了,拥有这一密码你的程序将会变得更加健壮。^_^

理解’位域’

这也是在ChinaUnix上看了几篇关于C语言'位域(Bit Fields)'的帖子之后,才想写下这篇文章的。其实在平时的工作中很少使用到'位域',我是搞服务器端程序设计的,大容量的内存可以让我毫不犹豫的任意'挥霍'^_^。想必搞嵌入式编程的朋友们对位域的使用应该不陌生吧。这里我也仅仅是凭着对C语言钻研的兴趣来学习一下'位域'的相关知识的,可能有些说法没有实践,缺乏说服力。

具体也不是很清楚当年C语言的创造者为什么要加入位域这一语法支持,那是太遥远的事情了,我们不需要再回顾了,既然大师们为我们创造了它,我们使用便是了。

毋庸置疑,位域的引入给用户的最大的好处莫过于可以有效的利用'昂贵'的内存和操作bit的能力了。而且这种操作bit位的能力很是方便,利用结构体域名即可对这些bit进行操作。例如:

struct foo {
 int a : 1;
 int b : 2;
 short c : 1;
};

struct foo aFoo;
aFoo.a = 1;
aFoo.b = 3;
aFoo.c = 0;

通过结构体实例.域名即可修改某些bit得值,这些都是编译器的'甜头'。当然我们也可以自己通过一些'掩码'和移位操作来修改这些bit,当然如果不是十分需要,我们是不需要这么做的。

位域还提供一种叫'匿名'位域的语法,它常用来'填缺补漏',由于是'匿名',所以你不能像上面那样去访问它。如:
struct foo1 {
 int a : 1;
 int   : 2;
 short c : 1;
};
在foo1的成员a和c之间有一个2 bits的匿名位域。

在foo结构体的定义中,成员a虽然类型为int,但是它仅仅占据着4个字节中的一个bit的空间;类似b占据2个bit空间,但是b到底是占据第一个int的2个bit空间呢还是第二个int的2个bit空间呢?这里实际上也涉及到如何对齐带有'位域'的结构体这样一个问题。我们来分析一下。

我们再来看看下面两个结构体定义:
struct foo2 {
        char    a : 2;
        char    b : 3;
        char    c : 1;
};

struct foo3 {
        char    a : 2;
        char    b : 3;
        char    c : 7;
};
我们来打印一下这两个结构体的大小,我们得到的结果是:
sizeof(struct foo2) = 1
sizeof(struct foo3) = 2
显然都不是我们期望的,如果按照正常的内存对齐规则,这两个结构体大小均应该为3才对,那么问题出在哪了呢?首先通过这种现象我们可以肯定的是:带有'位域'的结构体并不是按照每个域对齐的,而是将一些位域成员'捆绑'在一起做对齐的。以foo2为例,这个结构体中所有的成员都是char型的,而且三个位域占用的总空间为6 bit 8 bit(1 byte),这里位域是不能跨越两个成员基本类型空间的,这时编译器将a和b两个成员'捆绑'按照char做对齐,而c单独拿出来以char类型做对齐,这样实际上在b和c之间出现了空隙,但这也是最节省空间的方法了。我们再看一种结构体定义:

struct foo4 {
        char    a : 2;
        char    b : 3;
        int c : 1;
};

在foo4中虽然三个位域所占用空间之和为6 bit < 8 bit(1 byte),但是由于char和int的对齐系数是不同的,是不能捆绑在一起,那是不是a、b捆绑在一起按照char对齐,c单独按照int对齐呢?我们打印一下sizeof(struct foo4)发现结果为4,也就是说编译器把a、b、c一起捆绑起来并以int做对齐了。

通过上面的例子我们发现很难总结出很规律性的东西,但是带有'位域'的结构体的对齐有条原则可以遵循,那就是:"尽量减少结构体的占用空间"。当然显式的使用内存对齐的机会也并不多。^_^

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats