深度解析Linux经典栈溢出攻击方式 (linux 经典栈溢出)

栈(Stack)是计算机内存中的一种数据结构,用于保存函数调用中的参数和局部变量等信息。但是,栈在实现过程中存在缺陷,即栈溢出(Stack Overflow),也就是在栈空间中写入超过其分配空间的数据。攻击者可以利用这个漏洞实现栈溢出攻击。而Linux系统是这类栈溢出攻击的主要受害者,因此本文将深入探讨Linux经典栈溢出攻击的实现原理和防范方法。

一、栈溢出攻击原理

我们需要了解一些基础概念。

1.1 调用栈

当一个函数被调用时,它的程序参数存储在栈中的一部分内存中,同时在栈的顶部存储了该函数的返回地址,也就是说当该函数执行完毕之后会返回到该地址处继续执行。调用栈是这样的一个数据结构,调用函数时使用,当函数返回时撤销之前的函数调用。

1.2 栈溢出

当向一个缓冲区写入超过其本身容量的数据时,数据就会覆盖那个缓冲区后面的内存地址。如果这个被写入的位置正好是调用栈元素的位置,那么就会覆盖这个调用栈上的栈帧(Stack Frame)中的数据,同时也会覆盖存储在栈中的返回地址。由此,攻击者就可以在函数返回时跳转到自己编写的代码中去执行,从而完全控制程序的执行流程。

一旦攻击者控制了程序的执行流程,他就可以做任何事情,例如启动后门程序、读取机密文件、改变环境变量、执行任意代码等等。由于栈溢出是一种常见的漏洞类型,恶意攻击者利用栈溢出攻击常常能够成功。

1.3 栈溢出攻击的实现方式

基本原理已经解释清楚了,攻击者实现这么一个栈溢出攻击的方法通常有两种:

1.3.1 覆盖函数返回地址

攻击者通过覆盖栈上的返回地址,使得当前函数执行完成时会跳转到攻击者编写的代码处继续执行。这个方法实现起来比较简单,只要知道栈帧的大小,计算出攻击者的代码地址就可以,因为跳转地址就是覆盖掉的返回地址。

1.3.2 利用shellcode

Shellcode可以看作是一段正常的程序,但是它的目标是执行攻击者想要的命令。攻击者可以在栈溢出时把shellcode放入到栈缓冲区中,然后同时覆盖掉返回地址,这样函数执行完毕时就会跳转到这个shellcode执行攻击者编写的程序,这种方式在绕过程序栈非执行区域的保护措施上比较有用。

二、Linux下的栈溢出攻击

Linux系统是一种受到极大威胁的操作系统。因此,对于Linux下的栈溢出攻击,我们需要重点关注以下几点。

2.1 编译器优化引起的问题

Ubuntu发行版下,gcc 5 开始默认启用了栈保护技术,即在栈上加入了一段特定的随机值作为stack canary,用于在函数返回时防止栈被破坏,这样就可以一定程度上防止栈溢出攻击。当栈被修改时,会触发 stack canary 检查,导致程序异常终止。

但是,在编写某些特定类型程序时,利用未经验证的用户输入,或者字符串格式化等可疑操作时,这种技术可能未能及时发现栈溢出攻击,同时这种技术对程序带来的额外负荷也是不能忽略的。

2.2 代码注入

代码注入通常是通过覆盖函数栈帧返回地址的方法实现的。在覆盖函数返回地址时,攻击者可以将指向攻击者编写的恶意代码的地址写入该位置。当缓冲区溢出时,程序就会跳转到恶意代码,并执行该代码。这种攻击方法几乎可以越过任何内存保护,从而成为Linux系统下更受欢迎的攻击类型之一。

2.3 思考:是否需要编写地址

上文提到的栈溢出攻击,常常需要覆盖函数栈帧中的返回地址,其中最重要的是攻击者需要知道攻击代码的地址。另外一种思路是直接使用已经存在的函数的地址(终止器函数)将代码放入堆栈上,从而在下次调用时执行该代码。这种方法不需要知道恶意代码的地址,因此会比较高效。这种方法跟之前的方法一样,同时也是比较受欢迎的栈溢出攻击方式,特别是在堆栈上跳转到快速终止器函数调用的时候。

三、防范栈溢出攻击的方法

3.1 栈保护技术

很多操作系统现在都提供了一种在栈中加入 stack canary 的保护机制。这种机制会在栈中添加一个随机数,随机数的值只有在函数返回时才能计算出来。这个随机数会被保存在一次返回的信息中,因此,如果在返回时该值被修改了,那么程序就会中断执行,从而避免了栈溢出攻击的发生。

3.2 编译器选项

编译器有一些选项可以使得栈更安全,比如可以让编译器自动寻找可疑的缓冲区、强制编译器检测函数调用的大小。

3.3 堆栈空间初始化

很多攻击者利用指针未被初始化的漏洞来实现栈溢出攻击,因此,适当的初始化变量非常有必要。在一些高级语言中则更为自动化,例如 Java、Python、C# 等高级语言会将变量自动置为 null 或 0。

3.4 输入验证

正常的输入应该被验证其完整性和大小,并且需要确保不会造成过度的数据输入。同时,输入验证还可以保证程序在运行期间不会出现意外溢出。

3.5 限制系统权限

操作系统不需要 root 权限就可以对程序进行控制和执行,因此操作系统对程序所属的用户、资源的权限等方面的控制也是很重要的。

对于栈溢出攻击方面,Linux系统并没有真正解决这个问题,因此,良好的程序设计、开发和实施安全措施是保护 Linux 系统的更佳方法。希望今天的文章能够为大家了解 Linux 系统栈溢出攻击提供一些参考,同时也帮助大家更好地应对这种致命漏洞。

相关问题拓展阅读:

linux系统中线程同步实现机制有哪些

LinuxThread的线程机制

LinuxThreads是目前Linux平台上使用最为广泛的线程库,由Xavier Leroy () 负责开发完成,并已绑定在GLIBC中发行。它所实现的就是基于核心轻量级进程的”一对一”线程模型,一个线程实体对应一个核心轻量级进程,而线程之间的 管理在核外函数库中实现。

1.线程描述数据结构及实现限制

LinuxThreads定义了一个struct _pthread_descr_struct数据结构来描述线程,并使用全局数组变量 __pthread_handles来描晌胡基述和引用进程所辖线程。在__pthread_handles中的前两项,LinuxThreads定义了两个全 局的系统线程:__pthread_initial_thread和__pthread_manager_thread,并用 __pthread_main_thread表征__pthread_manager_thread的父线程(初始为 __pthread_initial_thread)。

struct _pthread_descr_struct是一个双环链表结构,__pthread_manager_thread所在的链表仅包括它 一个元素,实际上,__pthread_manager_thread是一个特殊线程,LinuxThreads仅使用了其中的errno、p_pid、 p_priority等三个域。而__pthread_main_thread所在的链则将进程中所有用户线程串在了一起。经过一系列 pthread_create()之后形成的__pthread_handles数组将如下图所示:

图2 __pthread_handles数组结构

新创建的线程将首先在__pthread_handles数组中占据一项,然后通过数据结构中的链指针连入以__pthread_main_thread为首指针的链表中。这个链表的使用在介绍线程的创建和释放的时候将提到。

LinuxThreads遵循POSIX1003.1c标准,其中对线程库的实现进行了一些范围限制,比如进程更大线程数,线程私有数据区大小等等。在 LinuxThreads的实现中,基本遵循这些限制,但也进行了一定的改动,改动的趋势是放松或者说扩大这些限制,使编程更加方便。这些限定宏主要集中 在sysdeps/unix/sysv/linux/bits/local_lim.h(不同平台使用的文件位置不同)中,包括如下几个:

每进程的私有数据key数,POSIX定义_POSIX_THREAD_KEYS_MAX为128,LinuxThreads使用 PTHREAD_KEYS_MAX,1024;私有数据释放时允许执行的操作数,LinuxThreads与POSIX一致,定义 PTHREAD_DESTRUCTOR_ITERATIONS为4;每进程的线程数,POSIX定义为64,LinuxThreads增大到1024 (PTHREAD_THREADS_MAX);线程运行栈最小空间大小,POSIX未指定,LinuxThreads使用 PTHREAD_STACK_MIN,16384(字节)。

2.管理线程

“一对一”模型的好处之一是线程的调度由核心完成了,而其他诸如线程取消、线程间的同步等工作,宴谨都是在核外线程库中完成的。在LinuxThreads 中,专门为每一个进程构造了一个管理线程,负责处理线程相关的管理工作。当进程之一次调用pthread_create()创建一个线程的时候就会创建 (__clone())并启动管理线程。

在一个进程空间内,管理线程与其他线程之间通过一对”管理管道(manager_pipe)”来通讯,该管道在创建管理线程之前创建,在成功启动 了管理线程之后,管理管道的读端和写端分别做姿赋给两个全局变量__pthread_manager_reader和 __pthread_manager_request,之后,每个用户线程都通过__pthread_manager_request向管理线程发请求, 但管理线程本身并没有直接使用__pthread_manager_reader,管道的读端(manager_pipe)是作为__clone ()的参数之一传给管理线程的,管理线程的工作主要就是监听管道读端,并对从中取出的请求作出反应。

创建管理线程的流程如下所示:

(全局变量pthread_manager_request初值为-1)

图3 创建管理线程的流程

初始化结束后,在__pthread_manager_thread中记录了轻量级进程号以及核外分配和管理的线程id, 2*PTHREAD_THREADS_MAX+1这个数值不会与任何常规用户线程id冲突。管理线程作为pthread_create()的调用者线程的 子线程运行,而pthread_create()所创建的那个用户线程则是由管理线程来调用clone()创建,因此实际上是管理线程的子线程。(此处子 线程的概念应该当作子进程来理解。)

__pthread_manager()就是管理线程的主循环所在,在进行一系列初始化工作后,进入while(1)循环。在循环中,线程以2秒为 timeout查询(__poll())管理管道的读端。在处理请求前,检查其父线程(也就是创建manager的主线程)是否已退出,如果已退出就退出 整个进程。如果有退出的子线程需要清理,则调用pthread_reap_children()清理。

然后才是读取管道中的请求,根据请求类型执行相应操作(switch-case)。具体的请求处理,源码中比较清楚,这里就不赘述了。

3.线程栈

在LinuxThreads中,管理线程的栈和用户线程的栈是分离的,管理线程在进程堆中通过malloc()分配一个THREAD_MANAGER_STACK_SIZE字节的区域作为自己的运行栈。

用户线程的栈分配办法随着体系结构的不同而不同,主要根据两个宏定义来区分,一个是NEED_SEPARATE_REGISTER_STACK,这个属 性仅在IA64平台上使用;另一个是FLOATING_STACK宏,在i386等少数平台上使用,此时用户线程栈由系统决定具置并提供保护。与此同 时,用户还可以通过线程属性结构来指定使用用户自定义的栈。因篇幅所限,这里只能分析i386平台所使用的两种栈组织方式:FLOATING_STACK 方式和用户自定义方式。

在FLOATING_STACK方式下,LinuxThreads利用mmap()从内核空间中分配8MB空间(i386系统缺省的更大栈空间大小,如 果有运行限制(rlimit),则按照运行限制设置),使用mprotect()设置其中之一页为非访问区。该8M空间的功能分配如下图:

图4 栈结构示意

低地址被保护的页面用来监测栈溢出。

对于用户指定的栈,在按照指针对界后,设置线程栈顶,并计算出栈底,不做保护,正确性由用户自己保证。

不论哪种组织方式,线程描述结构总是位于栈顶紧邻堆栈的位置。

4.线程id和进程id

Linux 进程栈和线程栈的区别

进程好比公交车,线程好比公交车上的人。。 一个进程可以包含多个线程,颂差当然也可以只有一个磨消线程,野游皮就是司机。。线程是任务调度单位,因为这更方便。进程 更多的是提供资源,比如进程的地址空间,所有的线程都运行在该 进程的地址空间里

返回博客列表

转 Linux 进程栈和线程栈的区别

地狱的烈火

发布时间: 2023/05/25 01:10

阅读: 1141

收藏: 22

点赞: 0

评论: 0

注:本文所涉及的环境为Linux, 下文讨论的栈跟内核栈,没有任何的关系,关于内核栈,请参考《深入Linux内核架构前悄》中的2.4.1 进程复制

这顷信里有如下几个问题,线程栈的空间是开辟在那里的? 线程栈之间可以互访吗?为什么在使用pthread_attr_setstack函数时,需要设置栈的大小,而进程task_struct的 mm_struct *mm 成员中却并没有却并没有stack_size这个成员项,怎么保存的栈大小呢?

进程栈:

进程用户空间的管理在task_struct 的mm_struct *mm成员中体现, mm中的成员定义了用户空间的布局情况如图一。 用户空间的栈起始于STACK_TOP, 如果设置了PF_RANDOMIZE,则起始点会减少一个小的随机量,每个体系结构都必须定义STACK_TOP, 大多数都设置为TASK_SIZE, 在32位机上该值为0XC。经过随机处理后,进程栈的起始地址将存放在mm->start_stack中,可以通过cat /proc//stat 查看慧乎渣。

如图一,栈从上而下扩展,而用于内存映射的区域起始于mm->mmap_base, mm->mmap_base通过调用mmap_base函数来初始化,为了确保栈不与mmap区域不发生冲突,两者之间设置了一个安全间隙。mmap_base函数源代码如下:

#define MIN_GAP (128*1024*1024)

#define MAX_GAP (TASK_SIZE/6*5)

static inline unsigned long mmap_base(struct mm_struct *mm)

{

unsigned long gap = current->signal->rlim.rlim_cur; // rlim_cur 默认为,及8M, 可以使用 getrlimit(RLIMIT_STACK, &limit) 查看

unsigned long random_factor = 0;

if (current->flags & PF_RANDOMIZE)

random_factor = get_random_int() % (1024*1024);

if (gap MAX_GAP) // 栈的更大空间为TASK_SIZE/6*5, 及2.5G

gap = MAX_GAP;

return PAGE_ALIGN(TASK_SIZE – gap – random_factor); // 通过保留random_factor空间大小的间隙来防止栈溢出

}

图 一 IA-32计算机上虚拟地址空间的布局

线程栈:

线程包含了表示进程内执行环境必需的信息,其中包括进程中标示线程的线程ID,一组寄存器值,栈,调度优先级和策略, 信号屏蔽字,errno变量以及线程私有数据。进程的所有信息对该进程的所有线程都是共享的,包括可执行的程序文本,程序的全局内存和堆内存,栈以及文件描述符,所以线程的mm_struct *mm指针变量和所属进程的mm指针变量相同。

在创建线程的时候,可以通过pthread_attr_t来初始化线程的属性,包括线程的栈布局信息,如栈起始地址stackaddr, 栈大小stacksize。 具体需要通过方法

int pthread_attr_setstack(pthread_attr_t *attr, void *stackaddr, size_t stacksize);

// 注:stackaddr 指向为该线程开辟的空间,该空间可以使用malloc或者mmap来开辟,而不能来自进程的栈区。开辟的stackaddr所指向的动态空间需要自己负责释放。

当然也可将线程栈的空间管理交给系统,如果想改变系统默认的栈大小8MB,可以通过

int pthread_attr_setstacksize(pthread_attr_t *attr, size_t stacksize);

// 注:stacksize最小值为16384,单位为字节

由上面的API接口,可以得到,线程栈的stacksize是保存在pthread_attr_t中的,可以通过人为的指定,也可以通过在创建线程的时候读取系统的配置文件来初始化stacksize,当初始化完栈的起始地址,和大小后,便可以通过

int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);

来初始化线程栈末尾之后用以避免栈溢出的缓冲区的大小,如果应用程序溢出到此缓冲区中,这个错误可能会导致 SIGSEGV 信号被发送给该线程, 从而造成段错误,缓冲区默认设置为PAGESIZE个字节。因为线程的mm->start_stack和所属进程相同,所以线程栈的起始地址并没有存放在task_struct中,应该只是使用attr中的stackaddr,来初始化task_struct->thread-> sp(sp指向struct pt_regs对象,该结构体用于保存用户进程或者线程的寄存器现场)。

请问 怎样分别查看windows系统与Linux系统的栈空间大小?

linux和windows下同样的文件或文件夹的大小有什么区别1.window下文件夹不算大小,linux下文件夹要算大小2.两个系统下的文件系统可能不一样,不同的文件系统,blocksize可能不一样。blocksize不一样,文件占用的磁盘空间可能就不一样。不同操作系统下查看blocksize的命令:AIX:lsfs -q /u01 Windows:fsutil fsinfo ntfsinfo c:linux:tune2fs -l /dev/sda1 3.window和弊衡linux下,文本文件租颂做的换行符不同,windows下是/n/r,linux下是/n。当使用FTP传输文本文件时,默认会进行换行符的转换,造成传输前后文件大小不一致。4.要确认看到的文件大小是指文件本身的樱数大小,还是文件占用的磁盘空间的大小,两者概念不同。

linux 下:

终端输入命令:ulimit -a

这一行就是,大概8M:stack size(kbytes, -s) 8192

windows下:

好像没正衡有命令可以查看,但是可以通过递归调用函数,使其首清源溢出来查看栈者态大小:

#include

void fun()

{

int a;

printf(“%p\n”,&a);

fun();

}

int main(void)

{

fun();

return 0;

}

首先,在fun()函数中的fun();处,打个断点,得到a的

然后,去掉断点,运行,直到栈溢出,致使程序崩溃,得到最后一个a的

两个地址相减,即为栈的大小:约为 :1M

linux 经典栈溢出的介绍就聊到这里吧,感谢你花时间阅读本站内容,更多关于linux 经典栈溢出,深度解析Linux经典栈溢出攻击方式,linux系统中线程同步实现机制有哪些,Linux 进程栈和线程栈的区别,请问 怎样分别查看windows系统与Linux系统的栈空间大小?的信息别忘了在本站进行查找喔。


数据运维技术 » 深度解析Linux经典栈溢出攻击方式 (linux 经典栈溢出)