太坑了,C标准库缓冲区溢出的问题,该搞清楚了

小麦大叔 2021-12-13 21:30


大家好,我是小麦,今天给大家分享一篇文章。在开发的过程中,如果遇到C标准库缓冲区溢出的问题,那么内心肯定是奔溃的。

下面我们来看看有哪些办法来避免这种情况吧。

C中大多数缓冲区溢出问题可以直接追溯到标准 C 库。最有害的罪魁祸首是不进行自变量检查的、有问题的字符串操作strcpystrcatsprintf 和 gets

大部分程序员仍然会使用这些函数,因为从来没有人教开发人员避免使用它们。某些人从各处获得某个提示,但即使是优秀的开发人员也会被这弄糟,下面就来分析一下。

第一位公共的“敌人”是 gets()。建议不要使用 gets()。该函数从标准输入读入用户输入的一行文本,它在遇到 EOF字符或换行字符之前,不会停止读入文本。也就是:gets() 根本不执行边界检查。因此,使用 gets()总是有可能使任何缓冲区溢出。作为一个替代方法,可以使用 fgets()。它可以做与 gets()所做的同样的事情,但它接受用来限制读入字符数目的大小参数,因此,提供了一种防止缓冲区溢出的方法。例如,不要使用以下代码:

void main()
{
char buf[1024];
gets(buf);
}

而使用以下代码:

#define BUFSIZE 1024
void main()
{
char buf[BUFSIZE];
fgets(buf, BUFSIZE, stdin);
}


1C 编程中的主要陷阱

C语言中一些标准函数很有可能使你陷入困境。但不是所有函数使用都不好。通常,利用这些函数之一需要任意输入传递给该函数。这个列表包括:

  • strcpy()
  • strcat()
  • sprintf()
  • scanf()
  • sscanf()
  • fscanf()
  • vfscanf()
  • vsprintf
  • vscanf()
  • vsscanf()
  • streadd()
  • strecpy()
  • strtrns()

我们推荐使用,如果有可能,应尽量避免使用这些函数。这些函数在大多数情况下,都有合理的替代方法。我们将仔细检查它们中的每一个,所以可以看到什么构成了它们的误用,以及如何避免它。

strcpy()函数将源字符串复制到缓冲区。没有指定要复制字符的具体数目。复制字符的数目直接取决于源字符串中的数目。如果源字符串碰巧来自用户输入,且没有专门限制其大小,则有可能会陷入大的麻烦中!

如果知道目的地缓冲区的大小,则可以添加明确的检查:

if(strlen(src) >= dst_size)
{
/* Do something appropriate, such as throw an error. */
}
else
{
strcpy(dst, src);
}

完成同样目的的更容易方式是使用 strncpy() 库例程:

strncpy(dst, src, dst_size-1);
dst[dst_size-1] = '\0'; /* Always do this to be safe! */

如果 src 比 dst 大,则该函数不会抛出一个错误;当达到最大尺寸时,它只是停止复制字符。注意上面调用 strncpy()中的 -1。如果 src 比 dst 长,则那给我们留有空间,将一个空字符放在 dst 数组的末尾。

当然,可能使用strcpy()不会带来任何潜在的安全性问题,正如在以下示例中所见:

strcpy(buf, "Hello!");

即使这个操作造成 buf 的溢出,但它只是对几个字符这样而已。由于我们静态地知道那些字符是什么,并且很明显,由于没有危害,所以这里无须担心 ― 当然,除非可以用其它方式覆盖字符串Hello所在的静态存储器。

确保strcpy()不会溢出的另一种方式是,在需要它时就分配空间,确保通过在源字符串上调用 strlen() 来分配足够的空间。例如:

dst = (char *)malloc(strlen(src));
strcpy(dst, src);

strcat()函数非常类似于strcpy(),除了它可以将一个字符串合并到缓冲区末尾。它也有一个类似的、更安全的替代方法strncat()。如果可能,使用strncat()而不要使用strcat()

函数sprintf()vsprintf()是用来格式化文本和将其存入缓冲区的通用函数。它们可以用直接的方式模仿strcpy()行为。换句话说,使用sprintf()vsprintf()与使用strcpy()一样,都很容易对程序造成缓冲区溢出。例如,考虑以下代码:

void main(int argc, char **argv)
{
char usage[1024];
sprintf(usage, "USAGE: %s -f flag [arg1]\n", argv[0]);
}

我们经常会看到类似上面的代码。它看起来没有什么危害。它创建一个知道如何调用该程序字符串。那样,可以更改二进制的名称,该程序的输出将自动反映这个更改。虽然如此, 该代码有严重的问题。文件系统倾向于将任何文件的名称限制于特定数目的字符。那么,您应该认为如果您的缓冲区足够大,可以处理可能的最长名称,您的程序会安全,对吗?只要将1024改为对我们的操作系统适合的任何数目,就好了吗?但是,不是这样的。通过编写我们自己的小程序来推翻上面所说的,可能容易地推翻这个限制:

void main()
{
execl("/path/to/above/program",
<<insert really long string here>>,
NULL);
}

函数execl()启动第一个参数中命名的程序。第二个参数作为argv[0]传递给被调用的程序。我们可以使那个字符串要多长有多长!

那么如何解决sprintf()带来得问题呢?遗憾的是,没有完全可移植的方法。某些体系结构提供了snprintf()方法,即允许程序员指定将多少字符从每个源复制到缓冲区中。例如,如果我们的系统上有snprintf,则可以修正一个示例成为:

void main(int argc, char **argv)
{
char usage[1024];
char format_string = "USAGE: %s -f flag [arg1]\n";
snprintf(usage, format_string, argv[0],1024-strlen(format_string) + 1);
}

注意,在第四个变量之前,snprintf()sprintf()是一样的。第四个变量指定了从第三个变量中应被复制到缓冲区的字符最大数目。注意,1024 是错误的数目!我们必须确保要复制到缓冲区使用的字符串总长不超过缓冲区的大小。所以,必须考虑一个空字符,加上所有格式字符串中的这些字符,再减去格式说明符 %s。该数字结果为1000,但上面的代码是更具有可维护性,因为如果格式字符串偶然发生变化,它不会出错。

sprintf()的许多(但不是全部)版本带有使用这两个函数的更安全的方法。可以指定格式字符串本身每个自变量的精度。例如,另一种修正上面有问题的sprintf()的方法是:

void main(int argc, char **argv)
{
char usage[1024];
sprintf(usage, "USAGE: %.1000s -f flag [arg1]\n", argv[0]);
}

注意,百分号后与 s 前的 .1000。该语法表明,从相关变量(本例中是 argv[0])复制的字符不超过 1000 个。

如果任一解决方案在您的程序必须运行的系统上行不通,则最佳的解决方案是将snprintf()的工作版本与您的代码放置在一个包中。可以找到以sh归档格式的、自由使用的版本;请参阅 参考资料。

继续,scanf系列的函数也设计得很差。在这种情况下,目的地缓冲区会发生溢出。考虑以下代码:

void main(int argc, char **argv)
{
char buf[256];
sscanf(argv[0], "%s", &buf);
}

如果输入的字大于 buf 的大小,则有溢出的情况。幸运的是,有一种简便的方法可以解决这个问题。考虑以下代码,它没有安全性方面的薄弱环节:

void main(int argc, char **argv)
{
char buf[256];
sscanf(argv[0], "%255s", &buf);
}

百分号和 s 之间的 255 指定了实际存储在变量 buf 中来自 argv[0] 的字符不会超过 255 个。其余匹配的字符将不会被复制。

接下来,我们讨论streadd()strecpy()。由于,不是每台机器开始就有这些调用,那些有这些函数的程序员,在使用它们时,应该小心。这些函数可以将那些含有不可读字符的字符串转换成可打印的表示。例如,考虑以下程序:

#include <libgen.h>

void main(int argc, char **argv)
{
char buf[20];
streadd(buf, "\t\n", "");
printf(%s\n", buf);
}

该程序打印:

\t\n

而不是打印所有空白。如果程序员没有预料到需要多大的输出缓冲区来处理输入缓冲区(不发生缓冲区溢出),则streadd() 和 strecpy()函数可能有问题。如果输入缓冲区包含单一字符 ― 假设是 ASCII 001(control-A)―则它将打印成四个字符\001。这是字符串增长的最坏情况。如果没有分配足够的空间,以至于输出缓冲区的大小总是输入缓冲区大小的四倍,则可能发生缓冲区溢出。

另一个较少使用的函数是strtrns(),因为许多机器上没有该函数。函数strtrns()取三个字符串和结果字符串应该放在其内的一个缓冲区,作为其自变量。第一个字符串必须复制到该缓冲区。一个字符被从第一个字符串中复制到缓冲区,除非那个字符出现在第二个字符串中。如果出现的话,那么会替换掉第三个字符串中同一索引中的字符。这听上去有点令人迷惑。让我们看一下,将所有小写字符转换成大写字符的示例:

#include <libgen.h>
void main(int argc, char **argv)
{
char lower[] = "abcdefghijklmnopqrstuvwxyz";
char upper[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
char *buf;
if(argc < 2) {
printf("USAGE: %s arg\n", argv[0]);
exit(0);
}
buf = (char *)malloc(strlen(argv[1]));
strtrns(argv[1], lower, upper, buf);
printf("%s\n", buf);
}

以上代码实际上不包含缓冲区溢出。但如果我们使用了固定大小的静态缓冲区,而不是用malloc()分配足够空间来复制argv[1],则可能会引起缓冲区溢出情况。

2避免内部缓冲区溢出

realpath()函数接受可能包含相对路径的字符串,并将它转换成指同一文件的字符串,但是通过绝对路径。在做这件事时,它展开了所有符号链接。

该函数取两个自变量,第一个作为要规范化的字符串,第二个作为将存储结果的缓冲区。当然,需要确保结果缓冲区足够大,以处理任何大小的路径。分配的MAXPATHLEN缓冲区应该足够大。然而,使用realpath()有另一个问题。如果传递给它的、要规范化的路径大小大于MAXPATHLEN,则realpath()实现内部的静态缓冲区会溢出!虽然实际上没有访问溢出的缓冲区,但无论如何它会伤害您的。结果是,应该明确不使用realpath(),除非确保检查您试图规范化的路径长度不超过MAXPATHLEN

其它广泛可用的调用也有类似的问题。经常使用的syslog()调用也有类似的问题,直到不久前,才注意到这个问题并修正了它。大多数机器上已经纠正了这个问题,但您不应该依赖正确的行为。最好总是假定代码正运行在可能最不友好的环境中,只是万一在哪天它真的这样。getopt()系列调用的各种实现,以及getpass()函数,都可能产生内部静态缓冲区溢出问题。如果您不得不使用这些函数,最佳解决方案是设置传递给这些函数的输入长度的阈值。

自己模拟gets()的安全性问题以及所有问题是非常容易的。例如,下面这段代码:

char buf[1024];
int i = 0;
char ch;
while((ch = getchar()) != '\n')
{
if(ch == -1) break;
buf[i++] = ch;
}

哎呀!可以用来读入字符的任何函数都存在这个问题,包括getchar()fgetc()getc() 和 read()

缓冲区溢出问题的准则是:总是确保做边界检查。

C 和 C++ 不能够自动地做边界检查,这实在不好,但确实有很好的原因,来解释不这样做的理由。边界检查的代价是效率。一般来讲,C 在大多数情况下注重效率。然而,获得效率的代价是,C 程序员必须十分警觉,并且有极强的安全意识,才能防止他们的程序出现问题,而且即使这些,使代码不出问题也不容易。

在现在,变量检查不会严重影响程序的效率。大多数应用程序不会注意到这点差异。所以,应该总是进行边界检查。在将数据复制到您自己的缓冲区之前,检查数据长度。同样,检查以确保不要将过大的数据传递给另一个库,因为您也不能相信其他人的代码!(回忆一下前面所讨论的内部缓冲区溢出。)

3其它危险是什么?

遗憾的是,即使是系统调用的“安全”版本 ― 譬如,相对于strcpy()strncpy()也不完全安全。也有可能把事情搞糟。即使安全的调用有时会留下未终止的字符串,或者会发生微妙的相差一位错误。当然,如果您偶然使用比源缓冲区小的结果缓冲区,则您可能发现自己处于非常困难的境地。

与我们目前所讨论的相比,往往很难犯这些错误,但您应该仍然意识到它们。当使用这类调用时,要仔细考虑。如果不仔细留意缓冲区大小,包括bcopy()fgets()memcpy()snprintf()strccpy()strcadd()strncpy() 和 vsnprintf(),许多函数会行为失常。

另一个要避免的系统调用是 getenv()。使用getenv() 的最大问题是您从来不能假定特殊环境变量是任何特定长度的。我们将在后续的专栏文章中讨论环境变量带来的种种问题。

到目前为止,我们已经给出了一大堆常见 C 函数,这些函数容易引起缓冲区溢出问题。当然,还有许多函数有相同的问题。特别是,注意第三方 COTS 软件。不要设想关于其他人软件行为的任何事情。还要意识到我们没有仔细检查每个平台上的每个常见库(我们不想做那一工作),并且还可能存在其它有问题的调用。

即使我们检查了每个常见库的各个地方,如果我们试图声称已经列出了将在任何时候遇到的所有问题,则您应该持非常非常怀疑的态度。我们只是想给您起一个头。其余全靠您了。

4静态和动态测试工具

我们将在以后的专栏文章中更加详细地介绍一些脆弱性检测的工具,但现在值得一提的是两种已被证明能有效帮助找到和去除缓冲区溢出问题的扫描工具。这两个主要类别的分析工具是静态工具(考虑代码但永不运行)和动态工具(执行代码以确定行为)。

可以使用一些静态工具来查找潜在的缓冲区溢出问题。很糟糕的是,没有一个工具对一般公众是可用的!许多工具做得一点也不比自动化 grep 命令多,可以运行它以找到源代码中每个有问题函数的实例。由于存在更好的技术,这仍然是高效的方式将几万行或几十万行的大程序缩减到只有数百个“潜在的问题”。(在以后的专栏文章中,将演示一个基于这种方法的、草草了事的扫描工具,并告诉您有关如何构建它的想法。)

较好的静态工具利用以某些方式表示的数据流信息来断定哪个变量会影响到其它哪个变量。用这种方法,可以丢弃来自基于 grep 的分析的某些“假肯定”。David Wagner 在他的工作中已经实现了这样的方法(在“Learning the basics of buffer overflows”中描述;请参阅 参考资料),在 Reliable Software Technologies 的研究人员也已实现。当前,数据流相关方法的问题是它当前引入了假否定(即,它没有标志可能是真正问题的某些调用)。

第二类方法涉及动态分析的使用。动态工具通常把注意力放在代码运行时的情况,查找潜在的问题。一种已在实验室使用的方法是故障注入。这个想法是以这样一种方式来检测程序:对它进行实验,运行“假设”游戏,看它会发生什么。有一种故障注入工具 ― FIST(请参阅 参考资料)已被用来查找可能的缓冲区溢出脆弱性。

最终,动态和静态方法的某些组合将会给您的投资带来回报。但在确定最佳组合方面,仍然有许多工作要做。

5最后

下面表格总结了一些编程构造,我们建议你小心使用或避免使用它们。如果要想代码健壮,最好有一定容错处理最好,比如之前给大家分享过的Assert断言机制》。
函数严重性解决方案
gets最危险使用 fgets(buf, size, stdin)。这几乎总是一个大问题!
strcpy很危险改为使用 strncpy。
strcat很危险改为使用 strncat。
sprintf很危险改为使用 snprintf,或者使用精度说明符。
scanf很危险使用精度说明符,或自己进行解析。
sscanf很危险使用精度说明符,或自己进行解析。
fscanf很危险使用精度说明符,或自己进行解析。
vfscanf很危险使用精度说明符,或自己进行解析。
vsprintf很危险改为使用 vsnprintf,或者使用精度说明符。
vscanf很危险使用精度说明符,或自己进行解析。
vsscanf很危险使用精度说明符,或自己进行解析。
streadd很危险确保分配的目的地参数大小是源参数大小的四倍。
strecpy很危险确保分配的目的地参数大小是源参数大小的四倍。
strtrns危险手工检查来查看目的地大小是否至少与源字符串相等。
realpath很危险(或稍小,取决于实现)分配缓冲区大小为 MAXPATHLEN。同样,手工检查参数以确保输入参数不超过 MAXPATHLEN。
syslog很危险(或稍小,取决于实现)在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getopt很危险(或稍小,取决于实现)在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getopt_long很危险(或稍小,取决于实现)在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getpass很危险(或稍小,取决于实现)在将字符串输入传递给该函数之前,将所有字符串输入截成合理的大小。
getchar中等危险如果在循环中使用该函数,确保检查缓冲区边界。
fgetc中等危险如果在循环中使用该函数,确保检查缓冲区边界。
read中等危险如果在循环中使用该函数,确保检查缓冲区边界。
bcopy中等危险如果在循环中使用该函数,确保检查缓冲区边界。
fgets低危险确保缓冲区大小与它所说的一样大。
memcpy低危险确保缓冲区大小与它所说的一样大。
snprintf低危险确保缓冲区大小与它所说的一样大。
strccpy低危险确保缓冲区大小与它所说的一样大。
strcadd低危险确保缓冲区大小与它所说的一样大。
strncpy低危险确保缓冲区大小与它所说的一样大。
getchar低危险确保缓冲区大小与它所说的一样大。
vsnprintf低危险确保缓冲区大小与它所说的一样大。

转自:嵌入式专栏
免责声明:本文素材来源网络,版权归原作者所有。如涉及作品版权问题,请与我联系删除。

欢迎关注我的公众号回复“加群”按规则加入技术交流群


—— The End —


往期推荐



物联网居然有这么多通信协议...

10大动图:秒懂各种常用通信协议原理

CAN总线简易入门教程

嵌入式学习真的这么烧钱吗?

一键调整PCB丝印,超级好用


长按识别二维码关注我


你点的每个好看,我都认真当成了喜欢
小麦大叔 一位热衷技术的攻城狮,懂点技术,会讲故事,交个朋友?
评论
  • 彼得·德鲁克被誉为“现代管理学之父”,他的管理思想影响了无数企业和管理者。然而,关于他的书籍分类,一种流行的说法令人感到困惑:德鲁克一生写了39本书,其中15本是关于管理的,而其中“专门写工商企业或为企业管理者写的”只有两本——《为成果而管理》和《创新与企业家精神》。这样的表述广为流传,但深入探讨后却发现并不完全准确。让我们一起重新审视这一说法,解析其中的矛盾与根源,进而重新认识德鲁克的管理思想及其著作的真正价值。从《创新与企业家精神》看德鲁克的视角《创新与企业家精神》通常被认为是一本专为企业管
    优思学院 2025-01-06 12:03 113浏览
  • 每日可见的315MHz和433MHz遥控模块,你能分清楚吗?众所周知,一套遥控设备主要由发射部分和接收部分组成,发射器可以将控制者的控制按键经过编码,调制到射频信号上面,然后经天线发射出无线信号。而接收器是将天线接收到的无线信号进行解码,从而得到与控制按键相对应的信号,然后再去控制相应的设备工作。当前,常见的遥控设备主要分为红外遥控与无线电遥控两大类,其主要区别为所采用的载波频率及其应用场景不一致。红外遥控设备所采用的射频信号频率一般为38kHz,通常应用在电视、投影仪等设备中;而无线电遥控设备
    华普微HOPERF 2025-01-06 15:29 125浏览
  • 根据Global Info Research项目团队最新调研,预计2030年全球封闭式电机产值达到1425百万美元,2024-2030年期间年复合增长率CAGR为3.4%。 封闭式电机是一种电动机,其外壳设计为密闭结构,通常用于要求较高的防护等级的应用场合。封闭式电机可以有效防止外部灰尘、水分和其他污染物进入内部,从而保护电机的内部组件,延长其使用寿命。 环洋市场咨询机构出版的调研分析报告【全球封闭式电机行业总体规模、主要厂商及IPO上市调研报告,2025-2031】研究全球封闭式电机总体规
    GIRtina 2025-01-06 11:10 103浏览
  • 根据环洋市场咨询(Global Info Research)项目团队最新调研,预计2030年全球无人机锂电池产值达到2457百万美元,2024-2030年期间年复合增长率CAGR为9.6%。 无人机锂电池是无人机动力系统中存储并释放能量的部分。无人机使用的动力电池,大多数是锂聚合物电池,相较其他电池,锂聚合物电池具有较高的能量密度,较长寿命,同时也具有良好的放电特性和安全性。 全球无人机锂电池核心厂商有宁德新能源科技、欣旺达、鹏辉能源、深圳格瑞普和EaglePicher等,前五大厂商占有全球
    GIRtina 2025-01-07 11:02 63浏览
  • 这篇内容主要讨论三个基本问题,硅电容是什么,为什么要使用硅电容,如何正确使用硅电容?1.  硅电容是什么首先我们需要了解电容是什么?物理学上电容的概念指的是给定电位差下自由电荷的储藏量,记为C,单位是F,指的是容纳电荷的能力,C=εS/d=ε0εrS/4πkd(真空)=Q/U。百度百科上电容器的概念指的是两个相互靠近的导体,中间夹一层不导电的绝缘介质。通过观察电容本身的定义公式中可以看到,在各个变量中比较能够改变的就是εr,S和d,也就是介质的介电常数,金属板有效相对面积以及距离。当前
    知白 2025-01-06 12:04 167浏览
  • 本文介绍Linux系统更换开机logo方法教程,通用RK3566、RK3568、RK3588、RK3576等开发板,触觉智能RK3562开发板演示,搭载4核A53处理器,主频高达2.0GHz;内置独立1Tops算力NPU,可应用于物联网网关、平板电脑、智能家居、教育电子、工业显示与控制等行业。制作图片开机logo图片制作注意事项(1)图片必须为bmp格式;(2)图片大小不能大于4MB;(3)BMP位深最大是32,建议设置为8;(4)图片名称为logo.bmp和logo_kernel.bmp;开机
    Industio_触觉智能 2025-01-06 10:43 87浏览
  • 村田是目前全球量产硅电容的领先企业,其在2016年收购了法国IPDiA头部硅电容器公司,并于2023年6月宣布投资约100亿日元将硅电容产能提升两倍。以下内容主要来自村田官网信息整理,村田高密度硅电容器采用半导体MOS工艺开发,并使用3D结构来大幅增加电极表面,因此在给定的占位面积内增加了静电容量。村田的硅技术以嵌入非结晶基板的单片结构为基础(单层MIM和多层MIM—MIM是指金属 / 绝缘体/ 金属) 村田硅电容采用先进3D拓扑结构在100um内,使开发的有效静电容量面积相当于80个
    知白 2025-01-07 15:02 66浏览
  • By Toradex 秦海1). 简介嵌入式平台设备基于Yocto Linux 在开发后期量产前期,为了安全以及提高启动速度等考虑,希望将 ARM 处理器平台的 Debug Console 输出关闭,本文就基于 NXP i.MX8MP ARM 处理器平台来演示相关流程。 本文所示例的平台来自于 Toradex Verdin i.MX8MP 嵌入式平台。  2. 准备a). Verdin i.MX8MP ARM核心版配合Dahlia载板并
    hai.qin_651820742 2025-01-07 14:52 40浏览
  • 在智能家居领域中,Wi-Fi、蓝牙、Zigbee、Thread与Z-Wave等无线通信协议是构建短距物联局域网的关键手段,它们常在实际应用中交叉运用,以满足智能家居生态系统多样化的功能需求。然而,这些协议之间并未遵循统一的互通标准,缺乏直接的互操作性,在进行组网时需要引入额外的网关作为“翻译桥梁”,极大地增加了系统的复杂性。 同时,Apple HomeKit、SamSung SmartThings、Amazon Alexa、Google Home等主流智能家居平台为了提升市占率与消费者
    华普微HOPERF 2025-01-06 17:23 141浏览
  • 大模型的赋能是指利用大型机器学习模型(如深度学习模型)来增强或改进各种应用和服务。这种技术在许多领域都显示出了巨大的潜力,包括但不限于以下几个方面: 1. 企业服务:大模型可以用于构建智能客服系统、知识库问答系统等,提升企业的服务质量和运营效率。 2. 教育服务:在教育领域,大模型被应用于个性化学习、智能辅导、作业批改等,帮助教师减轻工作负担,提高教学质量。 3. 工业智能化:大模型有助于解决工业领域的复杂性和不确定性问题,尽管在认知能力方面尚未完全具备专家级的复杂决策能力。 4. 消费
    丙丁先生 2025-01-07 09:25 80浏览
我要评论
3
点击右上角,分享到朋友圈 我知道啦
请使用浏览器分享功能 我知道啦