Memcheck。这是valgrind应用最广泛的工具,一个重量级的内存检查器,能够发现开发中绝大多数内存错误使用情况,比如:使用未初始化的内存,使用已经释放了的内存,内存访问越界等。这也是本文将重点介绍的部分。
Callgrind。它主要用来检查程序中函数调用过程中出现的问题。
Cachegrind。它主要用来检查程序中缓存使用出现的问题。
Helgrind。它主要用来检查多线程程序中出现的竞争问题。
Massif。它主要用来检查程序中堆栈使用中出现的问题。
Extension。可以利用core提供的功能,自己编写特定的内存调试工具
Valgrind 使用用法: valgrind [options] prog-and-args [options]: 常用选项,适用于所有Valgrind工具
-tool=<name>最常用的选项。运行 valgrind中名为toolname的工具。默认memcheck。
h –help 显示帮助信息。
-version 显示valgrind内核的版本,每个工具都有各自的版本。
q –quiet 安静地运行,只打印错误信息。
v –verbose 更详细的信息, 增加错误数统计。
-trace-children=no|yes 跟踪子线程? [no]
-track-fds=no|yes 跟踪打开的文件描述?[no]
-time-stamp=no|yes 增加时间戳到LOG信息? [no]
-log-fd=<number>输出LOG到描述符文件 [2=stderr]
-log-file=<file>将输出的信息写入到filename.PID的文件里,PID是运行程序的进行ID
-log-file-exactly=<file>输出LOG信息到 file
-log-file-qualifier=<VAR>取得环境变量的值来做为输出信息的文件名。 [none]
-log-socket=ipaddr:port 输出LOG到socket ,ipaddr:port
LOG信息输出-xml=yes 将信息以xml格式输出,只有memcheck可用
-num-callers=<number>show <number>callers in stack traces [12]
-error-limit=no|yes 如果太多错误,则停止显示新错误? [yes]
-error-exitcode=<number>如果发现错误则返回错误代码 [0=disable]
-db-attach=no|yes 当出现错误,valgrind会自动启动调试器gdb。[no]
-db-command=<command>启动调试器的命令行选项[gdb -nw %f %p]
适用于Memcheck工具的相关选项:
-leak-check=no|summary|full 要求对leak给出详细信息? [summary]
-leak-resolution=low|med|high how much bt merging in leak check [low]
-show-reachable=no|yes show reachable blocks in leak check? [no]
Valgrind 使用举例(一)下面是一段有问题的C程序代码test.c
#i nclude <stdlib.h>
void f(void)
{
int* x = malloc(10 * sizeof(int))
x[10] = 0 //问题1: 数组下标越界
} //问题2: 内存没有释放
int main(void)
{
f()
return 0
}
1、 编译程序test.c
gcc -Wall test.c -g -o test
2、 使用Valgrind检查程序BUG
valgrind --tool=memcheck --leak-check=full ./test
使用未初始化内存问题
问题分析:对于位于程序中不同段的变量,其初始值是不同的,全局变量和静态变量初始值为0,而局部变量和动态申请的变量,其初始值为随机值。如果程序使用了为随机值的变量,那么程序的行为就变得不可预期。
下面的程序就是一种常见的,使用了未初始化的变量的情况。数组a是局部变量,其初始值为随机值,而在初始化时并没有给其所有数组成员初始化,如此在接下来使用这个数组时就潜在有内存问题。
结果分析:假设这个文件名为:badloop.c,生成的可执行程序为badloop。用memcheck对其进行测试,输出如下。
输出结果显示,在该程序第11行中,程序的跳转依赖于一个未初始化的变量。准确的发现了上述程序中存在的问题。
内存读写越界问题分析:
这种情况是指:访问了你不应该/没有权限访问的内存地址空间,比如访问数组时越界;对动态内存访问时超出了申请的内存大小范围。下面的程序就是一个典型的数组越界问题。pt是一个局部数组变量,其大小为4,p初始指向pt数组的起始地址,但在对p循环叠加后,p超出了pt数组的范围,如果此时再对p进行写 *** 作,那么后果将不可预期。
结果分析:
假设这个文件名为badacc.cpp,生成的可执行程序为badacc,用memcheck对其进行测试,输出如下。
输出结果显示,在该程序的第15行,进行了非法的写 *** 作;在第16行,进行了非法读 *** 作。准确地发现了上述问题
缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种 *** 作系统、应用软件中广泛存在。利用缓冲区溢出攻击,可以导致程序运行失败、系统当机、重新启动等后果。更为严重的是,可以利用它执行非授权指令,甚至可以取得系统特权,进而进行各种非法 *** 作。缓冲区溢出攻击有多种英文名称:bufferoverflow,bufferoverrun,smashthestack,trashthestack,scribblethestack,manglethestack,memoryleak,overrunscrew;它们指的都是同一种攻击手段。第一个缓冲区溢出攻击--Morris蠕虫,发生在十年前,它曾造成了全世界6000多台网络服务器瘫痪。1.概念
缓冲区溢出是指当计算机向缓冲区内填充数据位数时超过了缓冲区本身的容量溢出的数据覆盖在合法数据上,理想的情况是程序检查数据长度并不允许输入超过缓冲区长度的字符,但是绝大多数程序都会假设数据长度总是与所分配的储存空间想匹配,这就为缓冲区溢出埋下隐患. *** 作系统所使用的缓冲区又被称为"堆栈".在各个 *** 作进程之间,指令会被临时储存在"堆栈"当中,"堆栈"也会出现缓冲区溢出。
2.危害
在当前网络与分布式系统安全中,被广泛利用的50%以上都是缓冲区溢出,其中最著名的例子是1988年利用fingerd漏洞的蠕虫。而缓冲区溢出中,最为危险的是堆栈溢出,因为入侵者可以利用堆栈溢出,在函数返回时改变返回程序的地址,让其跳转到任意地址,带来的危害一种是程序崩溃导致拒绝服务,另外一种就是跳转并且执行一段恶意代码,比如得到shell,然后为所欲为。
3.缓冲区攻击
一.缓冲区溢出的原理
通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏程序的堆栈,使程序转而执行其它指令,以达到攻击的目的。造成缓冲区溢出的原因是程序中没有仔细检查用户输入的参数。例如下面程序:
voidfunction(char*str){
charbuffer[16]
strcpy(buffer,str)
}
上面的strcpy()将直接吧str中的内容copy到buffer中。这样只要str的长度大于16,就会造成buffer的溢出,使程序运行出错。存在象strcpy这样的问题的标准函数还有strcat(),sprintf(),vsprintf(),gets(),scanf()等。
当然,随便往缓冲区中填东西造成它溢出一般只会出现“分段错误”(Segmentationfault),而不能达到攻击的目的。最常见的手段是通过制造缓冲区溢出使程序运行一个用户shell,再通过shell执行其它命令。如果该程序属于root且有suid权限的话,攻击者就获得了一个有root权限的shell,可以对系统进行任意 *** 作了。
缓冲区溢出攻击之所以成为一种常见安全攻击手段其原因在于缓冲区溢出漏洞太普遍了,并且易于实现。而且,缓冲区溢出成为远程攻击的主要手段其原因在于缓冲区溢出漏洞给予了攻击者他所想要的一切:植入并且执行攻击代码。被植入的攻击代码以一定的权限运行有缓冲区溢出漏洞的程序,从而得到被攻击主机的控制权。
在1998年Lincoln实验室用来评估入侵检测的的5种远程攻击中,有2种是缓冲区溢出。而在1998年CERT的13份建议中,有9份是是与缓冲区溢出有关的,在1999年,至少有半数的建议是和缓冲区溢出有关的。在Bugtraq的调查中,有2/3的被调查者认为缓冲区溢出漏洞是一个很严重的安全问题。
缓冲区溢出漏洞和攻击有很多种形式,会在第二节对他们进行描述和分类。相应地防卫手段也随者攻击方法的不同而不同,将在第四节描述,它的内容包括针对每种攻击类型的有效的防卫手段。
二、缓冲区溢出的漏洞和攻击
缓冲区溢出攻击的目的在于扰乱具有某些特权运行的程序的功能,这样可以使得攻击者取得程序的控制权,如果该程序具有足够的权限,那么整个主机就被控制了。一般而言,攻击者攻击root程序,然后执行类似“exec(sh)”的执行代码来获得root权限的shell。为了达到这个目的,攻击者必须达到如下的两个目标:
1.在程序的地址空间里安排适当的代码。
2.通过适当的初始化寄存器和内存,让程序跳转到入侵者安排的地址空间执行。
根据这两个目标来对缓冲区溢出攻击进行分类。在二.1节,将描述攻击代码是如何放入被攻击程序的地址空间的。在二.2节,将介绍攻击者如何使一个程序的缓冲区溢出,并且执行转移到攻击代码(这个就是“溢出”的由来)。在二.3节,将综合前两节所讨论的代码安排和控制程序执行流程的技术。
二.1在程序的地址空间里安排适当的代码的方法
有两种在被攻击程序地址空间里安排攻击代码的方法:
1、植入法:
攻击者向被攻击的程序输入一个字符串,程序会把这个字符串放到缓冲区里。这个字符串包含的资料是可以在这个被攻击的硬件平台上运行的指令序列。在这里,攻击者用被攻击程序的缓冲区来存放攻击代码。缓冲区可以设在任何地方:堆栈(stack,自动变量)、堆(heap,动态分配的内存区)和静态资料区。
2、利用已经存在的代码:
有时,攻击者想要的代码已经在被攻击的程序中了,攻击者所要做的只是对代码传递一些参数。比如,攻击代码要求执行“exec(“/bin/sh”)”,而在libc库中的代码执行“exec(arg)”,其中arg使一个指向一个字符串的指针参数,那么攻击者只要把传入的参数指针改向指向”/bin/sh”。
二.2控制程序转移到攻击代码的方法
所有的这些方法都是在寻求改变程序的执行流程,使之跳转到攻击代码。最基本的就是溢出一个没有边界检查或者其它弱点的缓冲区,这样就扰乱了程序的正常的执行顺序。通过溢出一个缓冲区,攻击者可以用暴力的方法改写相邻的程序空间而直接跳过了系统的检查。
分类的基准是攻击者所寻求的缓冲区溢出的程序空间类型。原则上是可以任意的空间。实际上,许多的缓冲区溢出是用暴力的方法来寻求改变程序指针的。这类程序的不同之处就是程序空间的突破和内存空间的定位不同。主要有以下三种:1、活动纪录(ActivationRecords):
每当一个函数调用发生时,调用者会在堆栈中留下一个活动纪录,它包含了函数结束时返回的地址。攻击者通过溢出堆栈中的自动变量,使返回地址指向攻击代码。通过改变程序的返回地址,当函数调用结束时,程序就跳转到攻击者设定的地址,而不是原先的地址。这类的缓冲区溢出被称为堆栈溢出攻击(StackSmashingAttack),是目前最常用的缓冲区溢出攻击方式。
2、函数指针(FunctionPointers):
函数指针可以用来定位任何地址空间。例如:“void(*foo)()”声明了一个返回值为void的函数指针变量foo。所以攻击者只需在任何空间内的函数指针附近找到一个能够溢出的缓冲区,然后溢出这个缓冲区来改变函数指针。在某一时刻,当程序通过函数指针调用函数时,程序的流程就按攻击者的意图实现了。它的一个攻击范例就是在Linux系统下的superprobe程序。
3、长跳转缓冲区(Longjmpbuffers):
在C语言中包含了一个简单的检验/恢复系统,称为setjmp/longjmp。意思是在检验点设定“setjmp(buffer)”,用“longjmp(buffer)”来恢复检验点。然而,如果攻击者能够进入缓冲区的空间,那么“longjmp(buffer)”实际上是跳转到攻击者的代码。象函数指针一样,longjmp缓冲区能够指向任何地方,所以攻击者所要做的就是找到一个可供溢出的缓冲区。一个典型的例子就是Perl5.003的缓冲区溢出漏洞;攻击者首先进入用来恢复缓冲区溢出的的longjmp缓冲区,然后诱导进入恢复模式,这样就使Perl的解释器跳转到攻击代码上了。
二.3代码植入和流程控制技术的综合分析
最简单和常见的缓冲区溢出攻击类型就是在一个字符串里综合了代码植入和活动纪录技术。攻击者定位一个可供溢出的自动变量,然后向程序传递一个很大的字符串,在引发缓冲区溢出,改变活动纪录的同时植入了代码。这个是由Levy指出的攻击的模板。因为C在习惯上只为用户和参数开辟很小的缓冲区,因此这种漏洞攻击的实例十分常见。
代码植入和缓冲区溢出不一定要在在一次动作内完成。攻击者可以在一个缓冲区内放置代码,这是不能溢出的缓冲区。然后,攻击者通过溢出另外一个缓冲区来转移程序的指针。这种方法一般用来解决可供溢出的缓冲区不够大(不能放下全部的代码)的情况。
如果攻击者试图使用已经常驻的代码而不是从外部植入代码,他们通常必须把代码作为参数调用。举例来说,在libc(几乎所有的C程序都要它来连接)中的部分代码段会执行“exec(something)”,其中somthing就是参数。攻击者然后使用缓冲区溢出改变程序的参数,然后利用另一个缓冲区溢出使程序指针指向libc中的特定的代码段。
三、缓冲区溢出攻击的实验分析
2000年1月,Cerberus安全小组发布了微软的IIS4/5存在的一个缓冲区溢出漏洞。攻击该漏洞,可以使Web服务器崩溃,甚至获取超级权限执行任意的代码。目前,微软的IIS4/5是一种主流的Web服务器程序;因而,该缓冲区溢出漏洞对于网站的安全构成了极大的威胁;它的描述如下:
浏览器向IIS提出一个HTTP请求,在域名(或IP地址)后,加上一个文件名,该文件名以“.htr”做后缀。于是IIS认为客户端正在请求一个“.htr”文件,“.htr”扩展文件被映像成ISAPI(InternetServiceAPI)应用程序,IIS会复位向所有针对“.htr”资源的请求到ISM.DLL程序,ISM.DLL打开这个文件并执行之。
浏览器提交的请求中包含的文件名存储在局部变量缓冲区中,若它很长,超过600个字符时,会导致局部变量缓冲区溢出,覆盖返回地址空间,使IIS崩溃。更进一步,在如图1所示的2K缓冲区中植入一段精心设计的代码,可以使之以系统超级权限运行。
四、缓冲区溢出攻击的防范方法
缓冲区溢出攻击占了远程网络攻击的绝大多数,这种攻击可以使得一个匿名的Internet用户有机会获得一台主机的部分或全部的控制权。如果能有效地消除缓冲区溢出的漏洞,则很大一部分的安全威胁可以得到缓解。
目前有四种基本的方法保护缓冲区免受缓冲区溢出的攻击和影响。在四.1中介绍了通过 *** 作系统使得缓冲区不可执行,从而阻止攻击者植入攻击代码。在四.2中介绍了强制写正确的代码的方法。在四.3中介绍了利用编译器的边界检查来实现缓冲区的保护。这个方法使得缓冲区溢出不可能出现,从而完全消除了缓冲区溢出的威胁,但是相对而言代价比较大。在四.4中介绍一种间接的方法,这个方法在程序指针失效前进行完整性检查。虽然这种方法不能使得所有的缓冲区溢出失效,但它能阻止绝大多数的缓冲区溢出攻击。然后在四.5,分析这种保护方法的兼容性和性能优势。
四.1非执行的缓冲区
通过使被攻击程序的数据段地址空间不可执行,从而使得攻击者不可能执行被植入被攻击程序输入缓冲区的代码,这种技术被称为非执行的缓冲区技术。在早期的Unix系统设计中,只允许程序代码在代码段中执行。但是近来的Unix和MSWindows系统由于要实现更好的性能和功能,往往在数据段中动态地放入可执行的代码,这也是缓冲区溢出的根源。为了保持程序的兼容性,不可能使得所有程序的数据段不可执行。
但是可以设定堆栈数据段不可执行,这样就可以保证程序的兼容性。Linux和Solaris都发布了有关这方面的内核补丁。因为几乎没有任何合法的程序会在堆栈中存放代码,这种做法几乎不产生任何兼容性问题,除了在Linux中的两个特例,这时可执行的代码必须被放入堆栈中:
(1)信号传递:
Linux通过向进程堆栈释放代码然后引发中断来执行在堆栈中的代码来实现向进程发送Unix信号。非执行缓冲区的补丁在发送信号的时候是允许缓冲区可执行的。
(2)GCC的在线重用:
研究发现gcc在堆栈区里放置了可执行的代码作为在线重用之用。然而,关闭这个功能并不产生任何问题,只有部分功能似乎不能使用。
非执行堆栈的保护可以有效地对付把代码植入自动变量的缓冲区溢出攻击,而对于其它形式的攻击则没有效果。通过引用一个驻留的程序的指针,就可以跳过这种保护措施。其它的攻击可以采用把代码植入堆或者静态数据段中来跳过保护。
四.2编写正确的代码
编写正确的代码是一件非常有意义的工作,特别象编写C语言那种风格自由而容易出错的程序,这种风格是由于追求性能而忽视正确性的传统引起的。尽管花了很长的时间使得人们知道了如何编写安全的程序,具有安全漏洞的程序依旧出现。因此人们开发了一些工具和技术来帮助经验不足的程序员编写安全正确的程序。
最简单的方法就是用grep来搜索源代码中容易产生漏洞的库的调用,比如对strcpy和sprintf的调用,这两个函数都没有检查输入参数的长度。事实上,各个版本C的标准库均有这样的问题存在。
此外,人们还开发了一些高级的查错工具,如faultinjection等。这些工具的目的在于通过人为随机地产生一些缓冲区溢出来寻找代码的安全漏洞。还有一些静态分析工具用于侦测缓冲区溢出的存在。
虽然这些工具帮助程序员开发更安全的程序,但是由于C语言的特点,这些工具不可能找出所有的缓冲区溢出漏洞。所以,侦错技术只能用来减少缓冲区溢出的可能,并不能完全地消除它的存在。
出现内存泄漏的主机为集群机器,运行时间约5天,内存使用超90%,其上运行 集群管理软件 和 docker并执行测试脚本反复启停容器。
长时间运行后,集群主机内存占用逐渐增加,出现应用 OOM 现象。
而实际查看时发现主机内存总占用高,但应用实际占用内存低或未见显著异常。
可以看到内存占用 83.6% ,而实际top显示的内存占用最高也才 0.6% 没有占用内存过高的应用。
内存占用除了用户应用占用还有内核占用,遂查看内核内存占用。
使用linux文件系统接口查看
可以看到占用超高的项目为 slab 内核占用:
继续查看内核详细占用,按照缓存大小进行排序
可以看到此处:
kmalloc-2048,kmalloc-4096,kernfs_node_cache,kmalloc-1024,kmalloc-192,kmalloc-512 均占用较高,对比了正常主机,已经严重超过正常值。
如果是内核缓存过高则可以尝试进行内核缓存释放:
但执行上述 *** 作后,内存占用依旧无显著下降,也符合上面看到的 SUnreclaim: 2447108 kB //slab 不可回收内存大小 。这部分内存不能被释放。
kmalloc 为内核进行分配的内存,参考价值较大的为 kernfs_node_cache 占用高,遂搜索该项是作何作用。
很明显,该现象为内核占用严重超标,于是在搜索时加入了 memory leak 关键字,很快发现了该 Issues docker-run --memory slab cache leak on centos7
该 issue 表示 centos7 在反复运行 docker run --rm --memory 1g hello-world 时存在明显的内核内存占用升高,且无法被释放。且现象和当前现象一致。
最终指向内kernel c group内存泄露问题 slab leak causing a crash when using kmem control group
大致原因是在 3.10 内核上如果使用了 kmem limit 参数,会导致cgroup回收时无法释放部分已分配内存。至于更深入的了解,还需要其他时间,先解决目前的问题。
原因大概确定,为了再次确定这个问题,如果能够通过上述手段复则可以确定是该问题。
在一台仅运行docker的机器上执行上述语句,查看 slab 内存占用,可以看见内存占用明显上升。且最终表现和已有环境上的问题一致,总内存占用高,用户态内存占用低,内核内存占用高且无法被释放。
既然是内核问题,且知道了明确复现路径,则可以通过两种方式进行解决:
最终,进过测试后,选择了更换内核版本,将使用 Ubuntu 18.04 作为新的 *** 作系统。
Linux内核使用层次化内存管理的方法,每一层解决不同的问题,从下至上的关键部分如下:
slab是Linux *** 作系统的一种内存分配机制。其工作是针对一些经常分配并释放的对象,如进程描述符等,这些对象的大小一般比较小,如果直接采用伙伴系统来进行分配和释放,不仅会造成大量的内碎片,而且处理速度也太慢。而slab分配器是基于对象进行管理的,相同类型的对象归为一类(如进程描述符就是一类),每当要申请这样一个对象,slab分配器就从一个slab列表中分配一个这样大小的单元出去,而当要释放时,将其重新保存在该列表中,而不是直接返回给伙伴系统,从而避免这些内碎片。slab分配器并不丢弃已分配的对象,而是释放并把它们保存在内存中。当以后又要请求新的对象时,就可以从内存直接获取而不用重复初始化。
Slab导致的占用内存过高,Slab可以对可回收缓存手动释放, *** 作如下:
其中drop_caches的4个值有如下含义:
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)