浅谈缓冲区溢出之栈溢出

By 浅墨

发表于 2012-12-02

有段时间没有用windows了,刚一开机又是系统补丁更新。匆匆瞥了一眼看到了“内核缓冲区溢出漏洞补丁”几个字眼。靠,又是内核补丁。打完这个补丁后MD的内核符号文件又得更新了。于是抱怨了几句,一旁的兄弟问什么是缓冲区溢出。这个…三两句话还真说不清楚。解释这个问题用C语言比较方便,但是单从C代码是看不出来什么的,具体原理要分析机器级代码才能说清楚。既然是浅谈原理,那就从最基本的开始吧。

本文的定位是对此方面一无所知的读者,所以大牛们可以直接飘过…

缓冲区溢出这个名词想必大家并不陌生吧,在微软的系统漏洞补丁里经常可以看到这个词(微软这算是普及计算机知识么? – -)。从C语言来分析的话,最简单的一种溢出就是向数组中写入数据时超出了预定义的大小,比如定义了长度为10的数组,偏偏写入了10+个数据。C标准告诉我们这种做会产生不可预料的结果,而在信息安全领域看来,缓冲区溢出的艺术就是要让这种“不可预料的结果”变成攻击者想达成的结果。比如远程攻击服务器上的程序,使其返回一个具有管理员权限的shell什么的。千万别觉得这是天方夜谭,印象中微软历史上爆出过不少这样的漏洞,前段时间不就有覆盖微软全版本的MS12-020么(新的也有,但是我没关注 – -)。虽然网上广为流传的只是一个远程让服务器死机的shellcode,但是让远程服务器执行任意代码理论上是可行的。关于漏洞利用这块的东西我不怎么擅长,所以就不敢再多说了。

一般来说关于缓冲区溢出漏洞,官方的描述都是诸如“攻击者通过提交一个精心构造的字符串使得缓冲区溢出从而执行任意代码”之类的。这里的重点词是两个,“精心构造”和“字符串”。精心构造可以理解,那“字符串”呢?我们都知道,一段二进制代码是什么东西取决于机器对其的解释,如果把这段代码当作变量,当作整型是一个值,当作浮点型又是一个值,如果把它当成可执行代码的话,又会是另外一种解释。所以这里的字符串实际上就是一段可执行代码的字符串表现形式。接下来我们的重点就是如何“精心构造”这个“字符串”和如何让机器把我们构造的字符串(也就是数据)当作可执行代码来执行。

必须说明的是,真正意义上的shellcode要解决诸如函数地址重定位,汇编级系统调用,以及应对编译器抵抗此类缓冲区溢出攻击的“栈随机化”等技术,这些东西对于我们这篇“科普性质”的文章来说显然过于艰深,加之作者本人也是一个水货,故不会提及。我们只研究最浅显的原理。

我们先来看一段代码:

编译运行后我们看到了什么?

why_it_run函数居然被执行了。可是,我们并没有对该函数进行任何的显式调用啊。代码本身很简单,唯一值得怀疑的地方就在于we_call函数中buff[3] = (int)why_it_run;这一行了。我们定义了一个长度为2的数组,正常的访问范围是应该是buff[0]和buff[1]。但我们却访问了buff[3]这个超出了数组末端4个字节之后的地址,在这里写入了一个函数的地址。(为了便于之后解释变量地址的关系,我们在源代码中加入两句对buf[0]和buf[1]的赋值操作,即buf[0] = 0;与buf[1] = 1;)

为什么这样就能“诱使”系统执行了why_it_run函数呢?这里储存的到底是什么值呢?看来我们的问题越来越多了。一开始我们就说过,单从C代码是看不出任何东西的,所以我们需要研究机器代码的相关实现。那么我们需要基础的汇编指令知识,尤其是关于栈和C语言函数调用时call/ret相关的概念。如果之前没有基础,那么最好先补充一点相关的原理再继续吧,虽然这只是一篇基础文章,会尽量解释一切出现的术语和指令集。但是如果我们再解释这些基本概念的话,就过于偏离主线了。

这里是传送门:百度百科关于堆栈的解释 http://baike.baidu.com/view/93201.htm。

我们可以用gcc输出这段代码的汇编形式,命令是gcc -S overflow.c -o overflow.asm(vc的话可以用cl /Fa overflow.c命令),gcc默认输出的是AT&T风格的汇编代码,如果更习惯Intel格式的汇编的话,可以在命令行加上 -masm=intel参数,这样gcc就会输出Intel风格的汇编了。不过今天我们采用另外一种方法查看生成的机器指令,即使用objdump命令对最终形成的可执行文件进行反汇编来查看其机器代码。操作指令是objdump -d overflow -M intel,这样我们便得到了why_it_run、we_call以及main函数的执行代码,-M intel的意思是让objdump生成intel风格的汇编,objdump默认是AT&T风格的。我们可以看到输出的结果中有很多我们没定义的函数,它们来自C运行时库,它们才是这个可执行文件真正意义上的入口函数和结束函数。

下面是我们定义的三个函数反汇编的截图:

在这里我们需要关心的是main函数和we_call函数的实现,我们先给出程序运行到这里的时候栈的分布情况:

关于这里的栈地址并不是一个不变的地址,也就是说程序每次运行的时候栈起始位置都不一定,这是现代编译器采用的一大类技术“线性地址随机化”中的一个子集,一般翻译为“栈地址随机化”的技术。为的便是在一定程度上抵制缓冲区溢出攻击,攻击者暴力抵制的方法有“空操作雪橇”(nop sled)等方法,暴力去探测返回地址。

额…又扯远了,言归正传,虽然栈地址是随机的,但是并不会影响数据的相对位置。对应着汇编代码,我们来一起分析栈里的数据。

先从main函数里对we_call函数的调用开始吧,调用的语句是call 8048402这一句,objdump贴心的给出了提示,这里正是we_call函数的起始位置。其实call语句执行了两件事,第一,将main函数里调用完we_call函数之后要继续执行的下一条语句的地址0x8048428入栈,接着跳到了we_call函数的地址去执行。其实这里的call指令可以等同为push 0x8048428和jmp 0x8048402两句。我们知道内存里指令是线性排列的,那么当我们去调用函数时,必须先存下我们返回源函数的时候要跳转的地址,否则回哪里去呢?

接下来我们转到we_call函数的代码去看看,代码第一行是在栈里保存main函数之前使用的ebp寄存器的值,因为我们要使用ebp寄存器,同时要在回main之前恢复到原先的ebp的值,所以需要暂存。

各函数对寄存器的使用一般有这样的规则:寄存器分为调用者保存寄存器和被调用者保存寄存器。按照惯例,eax,edx,ecx寄存器是调用者保存,ebx,esi,edi,ebp等寄存器是被调用者负责保存。举个例子,一个函数想使用ebx寄存器那么必须在返回前恢复ebp原先的值,而使用edx寄存器就无需暂存和恢复。因为寄存器就那几个,被调函数要是修改了调用它的函数正在使用的寄存器而没有恢复到以前的话会引起错误。C语言编程我们无需关注这些 ,编译器会为我们打点好这一切,而自己写汇编代码就要注意了。

保存完ebp之后,函数将esp存到了ebp里,此时ebp的值是0xBFF02490。因为每一次的push和pop都会修改esp的值,而我们需要在栈里保存函数的临时变量,所以需要ebp寄存器来保存一个暂时不变的基址便于我们对临时变量进行操作。ebp和esp是一对兄弟寄存器,它们默认的内存段都保存在段寄存器ss里。

再下来是sub esp 0x10这句,其实这等同于四个push语句,程序将栈指针向下移动了16个字节(0x10是16进制),这个减少的值视函数的临时变量尺寸而变。空出来的区域就是保存函数临时变量的地方了。必须强调的是,我们要一直记得栈的增长方向是从高地址到低地址。所以开辟新的栈空间是给esp寄存器减少某个值。而我们在使用临时变量区域的时候,是从下向上使用的。我们继续看,接下来是buf[0] = 0;与buf[1] = 1;两条语句了。我们可以看到,栈里0xBFF02488是buff[0]的地址,我们存入了0,后面的0xBFF0248C是buff[1]的地址,我们存入了1(ebp的值是0xBFF02490)。

我们另起一段来看看最关键的一步,即对buff[3]的越界访问。我们存入了why_it_run函数的地址,也就是0x80483e4。从图中我们看到,此处存放的是调用完we_call函数后返回main函数里执行的指令的地址!换句话说,我们修改了程序的流程,让函数返回到了why_it_run函数里去执行了。原先的情况下we_call函数会继续执行leave指令,它等同于add esp 0x10,pop ebp两条语句,即就是函数刚开始执行的指令的反指令,以保证堆栈平衡。最后ret指令取出main存入的返回地址,再跳转回到main里执行。但是我们违规的修改了main函数原先的安排,转移了执行方向。

我们看到在why_it_run函数里有一个函数调用exit(),我们从这里结束了程序的执行。如果没有结束呢呢?会段错误的。可以想到,原本的栈被我们破坏了,如果此时不退出,程序从why_it_run函数返回后面对的将是一个混乱的错误的栈区。那么造成内存访问段错误是显而易见的。

综上,我们通过越界访问影响了程序原先应该进行的流程,让程序走了另外一条执行的线路。这只是一个很基本的原理说明,距离我们想要实现的依旧相距甚远。这是在程序代码里实现的,那么一个已经编译好的程序如何让它执行我们想要执行的代码呢?先卖个关子,我们在之后的文章里继续说明。不过我们总算迈出了万里长征第一步,接下来的<下>我们会继续深入,继续探索缓冲区溢出的简单实现。

By 浅墨

发表于 2012-12-02

有段时间没有用windows了,刚一开机又是系统补丁更新。匆匆瞥了一眼看到了“内核缓冲区溢出漏洞补丁”几个字眼。靠,又是内核补丁。打完这个补丁后MD的内核符号文件又得更新了。于是抱怨了几句,一旁的兄弟问什么是缓冲区溢出。这个…三两句话还真说不清楚。解释这个问题用C语言比较方便,但是单从C代码是看不出来什么的,具体原理要分析机器级代码才能说清楚。既然是浅谈原理,那就从最基本的开始吧。

本文的定位是对此方面一无所知的读者,所以大牛们可以直接飘过…

缓冲区溢出这个名词想必大家并不陌生吧,在微软的系统漏洞补丁里经常可以看到这个词(微软这算是普及计算机知识么? – -)。从C语言来分析的话,最简单的一种溢出就是向数组中写入数据时超出了预定义的大小,比如定义了长度为10的数组,偏偏写入了10+个数据。C标准告诉我们这种做会产生不可预料的结果,而在信息安全领域看来,缓冲区溢出的艺术就是要让这种“不可预料的结果”变成攻击者想达成的结果。比如远程攻击服务器上的程序,使其返回一个具有管理员权限的shell什么的。千万别觉得这是天方夜谭,印象中微软历史上爆出过不少这样的漏洞,前段时间不就有覆盖微软全版本的MS12-020么(新的也有,但是我没关注 – -)。虽然网上广为流传的只是一个远程让服务器死机的shellcode,但是让远程服务器执行任意代码理论上是可行的。关于漏洞利用这块的东西我不怎么擅长,所以就不敢再多说了。

一般来说关于缓冲区溢出漏洞,官方的描述都是诸如“攻击者通过提交一个精心构造的字符串使得缓冲区溢出从而执行任意代码”之类的。这里的重点词是两个,“精心构造”和“字符串”。精心构造可以理解,那“字符串”呢?我们都知道,一段二进制代码是什么东西取决于机器对其的解释,如果把这段代码当作变量,当作整型是一个值,当作浮点型又是一个值,如果把它当成可执行代码的话,又会是另外一种解释。所以这里的字符串实际上就是一段可执行代码的字符串表现形式。接下来我们的重点就是如何“精心构造”这个“字符串”和如何让机器把我们构造的字符串(也就是数据)当作可执行代码来执行。

必须说明的是,真正意义上的shellcode要解决诸如函数地址重定位,汇编级系统调用,以及应对编译器抵抗此类缓冲区溢出攻击的“栈随机化”等技术,这些东西对于我们这篇“科普性质”的文章来说显然过于艰深,加之作者本人也是一个水货,故不会提及。我们只研究最浅显的原理。

我们先来看一段代码:

编译运行后我们看到了什么?

why_it_run函数居然被执行了。可是,我们并没有对该函数进行任何的显式调用啊。代码本身很简单,唯一值得怀疑的地方就在于we_call函数中buff[3] = (int)why_it_run;这一行了。我们定义了一个长度为2的数组,正常的访问范围是应该是buff[0]和buff[1]。但我们却访问了buff[3]这个超出了数组末端4个字节之后的地址,在这里写入了一个函数的地址。(为了便于之后解释变量地址的关系,我们在源代码中加入两句对buf[0]和buf[1]的赋值操作,即buf[0] = 0;与buf[1] = 1;)

为什么这样就能“诱使”系统执行了why_it_run函数呢?这里储存的到底是什么值呢?看来我们的问题越来越多了。一开始我们就说过,单从C代码是看不出任何东西的,所以我们需要研究机器代码的相关实现。那么我们需要基础的汇编指令知识,尤其是关于栈和C语言函数调用时call/ret相关的概念。如果之前没有基础,那么最好先补充一点相关的原理再继续吧,虽然这只是一篇基础文章,会尽量解释一切出现的术语和指令集。但是如果我们再解释这些基本概念的话,就过于偏离主线了。

这里是传送门:百度百科关于堆栈的解释 http://baike.baidu.com/view/93201.htm。

我们可以用gcc输出这段代码的汇编形式,命令是gcc -S overflow.c -o overflow.asm(vc的话可以用cl /Fa overflow.c命令),gcc默认输出的是AT&T风格的汇编代码,如果更习惯Intel格式的汇编的话,可以在命令行加上 -masm=intel参数,这样gcc就会输出Intel风格的汇编了。不过今天我们采用另外一种方法查看生成的机器指令,即使用objdump命令对最终形成的可执行文件进行反汇编来查看其机器代码。操作指令是objdump -d overflow -M intel,这样我们便得到了why_it_run、we_call以及main函数的执行代码,-M intel的意思是让objdump生成intel风格的汇编,objdump默认是AT&T风格的。我们可以看到输出的结果中有很多我们没定义的函数,它们来自C运行时库,它们才是这个可执行文件真正意义上的入口函数和结束函数。

下面是我们定义的三个函数反汇编的截图:

在这里我们需要关心的是main函数和we_call函数的实现,我们先给出程序运行到这里的时候栈的分布情况:

关于这里的栈地址并不是一个不变的地址,也就是说程序每次运行的时候栈起始位置都不一定,这是现代编译器采用的一大类技术“线性地址随机化”中的一个子集,一般翻译为“栈地址随机化”的技术。为的便是在一定程度上抵制缓冲区溢出攻击,攻击者暴力抵制的方法有“空操作雪橇”(nop sled)等方法,暴力去探测返回地址。

额…又扯远了,言归正传,虽然栈地址是随机的,但是并不会影响数据的相对位置。对应着汇编代码,我们来一起分析栈里的数据。

先从main函数里对we_call函数的调用开始吧,调用的语句是call 8048402这一句,objdump贴心的给出了提示,这里正是we_call函数的起始位置。其实call语句执行了两件事,第一,将main函数里调用完we_call函数之后要继续执行的下一条语句的地址0x8048428入栈,接着跳到了we_call函数的地址去执行。其实这里的call指令可以等同为push 0x8048428和jmp 0x8048402两句。我们知道内存里指令是线性排列的,那么当我们去调用函数时,必须先存下我们返回源函数的时候要跳转的地址,否则回哪里去呢?

接下来我们转到we_call函数的代码去看看,代码第一行是在栈里保存main函数之前使用的ebp寄存器的值,因为我们要使用ebp寄存器,同时要在回main之前恢复到原先的ebp的值,所以需要暂存。

各函数对寄存器的使用一般有这样的规则:寄存器分为调用者保存寄存器和被调用者保存寄存器。按照惯例,eax,edx,ecx寄存器是调用者保存,ebx,esi,edi,ebp等寄存器是被调用者负责保存。举个例子,一个函数想使用ebx寄存器那么必须在返回前恢复ebp原先的值,而使用edx寄存器就无需暂存和恢复。因为寄存器就那几个,被调函数要是修改了调用它的函数正在使用的寄存器而没有恢复到以前的话会引起错误。C语言编程我们无需关注这些 ,编译器会为我们打点好这一切,而自己写汇编代码就要注意了。

保存完ebp之后,函数将esp存到了ebp里,此时ebp的值是0xBFF02490。因为每一次的push和pop都会修改esp的值,而我们需要在栈里保存函数的临时变量,所以需要ebp寄存器来保存一个暂时不变的基址便于我们对临时变量进行操作。ebp和esp是一对兄弟寄存器,它们默认的内存段都保存在段寄存器ss里。

再下来是sub esp 0x10这句,其实这等同于四个push语句,程序将栈指针向下移动了16个字节(0x10是16进制),这个减少的值视函数的临时变量尺寸而变。空出来的区域就是保存函数临时变量的地方了。必须强调的是,我们要一直记得栈的增长方向是从高地址到低地址。所以开辟新的栈空间是给esp寄存器减少某个值。而我们在使用临时变量区域的时候,是从下向上使用的。我们继续看,接下来是buf[0] = 0;与buf[1] = 1;两条语句了。我们可以看到,栈里0xBFF02488是buff[0]的地址,我们存入了0,后面的0xBFF0248C是buff[1]的地址,我们存入了1(ebp的值是0xBFF02490)。

我们另起一段来看看最关键的一步,即对buff[3]的越界访问。我们存入了why_it_run函数的地址,也就是0x80483e4。从图中我们看到,此处存放的是调用完we_call函数后返回main函数里执行的指令的地址!换句话说,我们修改了程序的流程,让函数返回到了why_it_run函数里去执行了。原先的情况下we_call函数会继续执行leave指令,它等同于add esp 0x10,pop ebp两条语句,即就是函数刚开始执行的指令的反指令,以保证堆栈平衡。最后ret指令取出main存入的返回地址,再跳转回到main里执行。但是我们违规的修改了main函数原先的安排,转移了执行方向。

我们看到在why_it_run函数里有一个函数调用exit(),我们从这里结束了程序的执行。如果没有结束呢呢?会段错误的。可以想到,原本的栈被我们破坏了,如果此时不退出,程序从why_it_run函数返回后面对的将是一个混乱的错误的栈区。那么造成内存访问段错误是显而易见的。

综上,我们通过越界访问影响了程序原先应该进行的流程,让程序走了另外一条执行的线路。这只是一个很基本的原理说明,距离我们想要实现的依旧相距甚远。这是在程序代码里实现的,那么一个已经编译好的程序如何让它执行我们想要执行的代码呢?先卖个关子,我们在之后的文章里继续说明。不过我们总算迈出了万里长征第一步,接下来的<下>我们会继续深入,继续探索缓冲区溢出的简单实现。

上回我们简单的介绍了缓冲区溢出的基本原理和机器级代码的解释,对此类问题的分析和研究都必须建立在对程序的机器级表示有一定的了解的基础上。记得有句话是这样说的,“真正了不起的程序员是对自己代码的每一个字节都了如指掌的程序员。”我们也许做不到每一字节,但至少得明晰机器级程序的组成结构和执行流程。

言归正传,我们今天在上回的基础上继续探索缓冲区溢出。之前的例子都是简单的通过越界访问来实现对程序执行流程的变动,而且执行的函数都是编译前写入的,那么如何对一个发行版的可执行程序进行缓冲区溢出呢? 首先,这个程序必须存在缓冲区溢出漏洞(这不是废话么),一般来说C语言中容易引起缓冲区溢出的函数有strcpy,strcat之类的不顾及缓冲区大小的内存操作函数以及scanf,gets之类的IO函数。如果你使用vs2010以及vs2012附带的C编译器cl.exe编译使用了这些函数的C代码,编译器一般会给出一个编号为4996的警告,大致的意思是这类函数如scanf不安全,请使用它们的安全版本scanf_s什么的。其实也就是给这些函数加上一个描述缓冲器大小的参数,以防止缓冲区溢出。

我们就以一个相对简单的函数gets开始研究吧。gets函数的实现想必大家都比较清楚吧,gets不考虑缓冲区大小,将输入缓冲中的内容逐一复制到内存指定位置,遇’\n’结束并且自动将’\n’替换为’\0’。

编译后我们同objdump反汇编,命令是 objdump -d -M intel overflow (overflow是可执行文件名字),同理,我们只要 main函数的实现:

上次有朋友提出来让我解释下汇编指令,所以今天可能会扯一些汇编指令的含义,如果跑题了还请大家不要介意,权当照顾下不熟悉汇编的朋友了。不过得强调下,对底层感兴趣的朋友是必须懂一些汇编的,不是最好懂,而是必须。

如果试着每一次运行打印buff数组的入口,我们会发现每一次都不一样。因为在Linux系统中,栈随机化已经成为了一个标准行为。也就是说每一次运行的时候栈地址会不相同。

我们继续看代码,前两句几乎是固有模式了。保存ebp寄存器到栈里和将esp寄存器的内容复制到ebp去。 接下来是and esp,0xfffffff0,用C语言描述就是esp = esp & 0xfffffff0; 这是执行16字节对齐,如果esp的数值不是16的倍数,这样会使得esp的数值减小一点变为16的倍数。因为栈是从高地址向低地址增长的,所以让栈向下移动一点不会出问题。这是为了执行效率,无需解释。

再下来是sub esp,0x20,程序会向下减少32字节(必须时刻注意0x开始是表示十六进制数字)。后面两句比较难理解,gcc编译的程序不像vc那样在这里使用push和pop,而是直接运算来操作,据说又是为了效率,不过看起来不是很直白。[]表示存储器,[esp+0x16]表示esp+0x16这个地址指向的存储器内容,用C语言解释就是 (esp+0x16),前面的指令lea是指取[esp+0x16]的地址存入eax里,即eax = &((esp+0x16)),这不就是eax=esp+0x16么,汇编干嘛不写mov eax,esp+0x16呢。因为mov不支持后一个操作数写成寄存器减去一个数字,但是lea支持,所以这样代替(不知道他们当时设计时怎么想的)。下一句用C语言描述就是*esp = eax,就是把eax的值存入esp表示的地址所对应的存储器空间去。

大家别介意我拿wps表格画出的简单草图,就说个意思。顺便给wps for linux打打广告,目前内测版表现很出色。

我们可以看到,esp+0x16的地址存入了esp指向的内存空间。之后调用gets函数,gets函数取得参数0xBFB14406为缓冲区起始位置(也就是我们定义的buff数组起始)开始写入。不过gcc默认是动态链接的,所以看不到gets函数的实现,如果想看的话可以在gcc命令行加入-static要求静态链接运行时库。

不出所料的话你还会同时看到gcc的友情提示“the `gets’ function is dangerous and should not be used.”静态连接编译出的程序比较大,反汇编会输出几万行,即使是我们这个小程序也得两三万行。

通过简单的计算,我们得出缓冲区的大小是0x20-0x16=0xA也就是10个字节,正好对应我们的buf[10]。但是别忘了,之前有对esp进行过对齐操作,所以有可能会有一些空间使得我们即使超过这个输入也能保证程序不崩溃。我本地测试的结果是21个字节之后才出现副作用(但是不能把这个当作特性用在平时的程序设计中,永远不要对编译器或者机器做出假设)。 看到这里不知道你有什么想法没有。是不是对缓冲区溢出攻击有了一点点的想法?对,我们通过输入数据可以逐渐测试出缓冲区的大小(或者直接反汇编大概计算出大小)。根据我们在上一回中分析的结论,我们可以通过逐渐的输入数据使得栈后面保存的ebp原始值被我们的输入数据覆盖掉,再之后是当前函数要返回的地址,修改掉它,我们岂不是可以让程序跳转到我们想执行的任意位置去了呢?当然实现这一目的还要解决很多问题,但我们已经有了一个大致的思路了不是吗?

如果我在这里结束掉今天的文章恐怕大家会很失望的。所以我决定继续下去,尽管已经萌生困意…

喝杯咖啡我们继续。 我们接下来实现这样一个任务,通过对某处的缓冲区溢出使其返回一个shell,就用最熟悉的/bin/bash吧。 我们知道linux下exec族的函数可以实现替换掉当前进程映像,执行另一个程序。exec函数一共有六个,其中execve为内核级的系统调用,其他(execl,execle,execlp,execv,execvp)都是调用execve的库函数。execve函数大家不陌生吧?

123456
// 函数 execve// 头文件       unistd.h// 原型int execve(const char *filename,         char *const argv[ ],        char *const envp[ ]);

第一个参数filename字符串所代表的文件路径,第二个参数是利用数组指针来传递给执行文件参数,并且需要以空指针(NULL)结束,最后一个参数则为传递给执行文件的新环境变量数组。我们只用第一个参数即可,传入的参数自然就是”/bin/bash”了。 利用代码自然是要用汇编写了。这里涉及到linux的系统调用如何用汇编实现的问题。

我们知道,内核级的系统调用是通过中断实现的,具体到这里是 int 0x80号中断,在此之前系统调用号要保存在eax寄存器中,如果还需要其它的参数,会使用其它的寄存器。 查阅内核头文件我们得知(不介意麻烦的话也可以写出C代码反汇编得出系统调用的编号)execve函数的系统调用号为11(即0xB)。网上的资料告诉我们,EBX寄存器保存第一个参数也就是filename的地址,ecx,edx分别是第二第三个,直接赋0即可。

另外大家还记得上篇里面提到的exit函数吧?这里我们执行完execve函数后/bin/bash在子函数执行,那么当前的执行体被我们破坏了,我们必须退出,否则基本上会是段错误什么的。exit的系统调用很简单了,系统调用号为1号,我们也不传什么参数。 接下来面临的是最麻烦的地方了,我们在自己构造的输入数据里可以带上实现这些功能的代码。但是别忘了我们一直强调的栈地址随机化。每次执行的栈地址都不相同。那又如何知道本次程序执行时的栈地址呢? 我们来看一个对付栈随机化的一个技术,英文名叫nop seld,一般译为“空操作雪橇”。因为缓冲区一般有个几KB,这个技术其实就是用nop指令(0x90)填充缓冲区,nop就是什么也不做的意思,会让当前程序跳过一个CPU指令周期。

我们程序的结果大致是这样: nop N个 + shellcode + buff地址 N个 这样可以增大命中的几率,因为32位系统栈随机化也是有一个范围的,只要跳转到任意一个nop,那么程序最终会“滑行”到攻击代码的位置。nop自然就是填充缓冲区的无意义代码了。我们需要的就是填充缓冲区,最后把要返回的任意一个nop的地址写入程序原先要返回的地址,在leave指令执行后程序就会跳转到nop处向后执行我们构造的程序,那么我们的目的也就达到了。当然还有更好的技术,我们以后在说。

等等,又有问题了。攻击代码中“/bin/bash”这个字符串必须得到起始地址的准确值,而程序每次运行的栈地址都不相同。这又如何得知呢?别急,通过一些手段自然可以实现,我们看以下实现结构: jmp begin fun: pop esi …… begin: call fun “/bin/bash” 结构。

当然不是我发明的,我们只不过是站在别人的肩膀上罢了。

分析下这个程序吧,一开始是jmp到了call这里,按照规则,下一条指令的地址(也就是字符串的地址)被压栈。然后跳转到fun标号出运行,pop指令把栈里存储的字符串地址复制到了esi寄存器里,此时我们获得了我们需要的地址了。我们必须强调的是call/ret指令可以很优雅的实现函数调用,但是,这并不是函数存在的证据,它们只不过是两个指令罢了,只不过适合实现函数而已。call/ret指令是函数存在的既不必要也不充分的条件。

今天时间不早了,我们先实现一个简单的shellcode,其它的留待以后研究。 gcc使用AT&T风格的嵌入式汇编,所以我们不能使用Intel风格的了。 我们给出的代码如下:

要注意不能出现使得机器码为0的语句,因为诸如strcpy之类的函数遇到’\0’结束拷贝,所以诸如movl $0x0, %eax的语句不可以使用了。 因为对AT&T风格的汇编不大熟悉,这段小程序写了有段时间了。我们想要的不过是这段程序的机器码而已,gcc编译后objdump反汇编,得到的机器码如下: xebx11x5bx31xc0xb0x0bx31xc9x31xd2xcdx80x31xc0xb0x01 xcdx80xe8xeaxffxffxffx2fx62x69x6ex2fx62x61x73x68x00 用这段shellcode实现一个简单的试验吧,至于从输入溢出我们下次再谈。 在我的机器上并没有成功实现攻击,新版的gcc实现了stack protector(栈保护者机制),检测到栈异常的话程序会自动结束程序(我的gcc没有貌似这个功能…),另外数据段是不允许执行的,直接编译出的程序运行会段错误的。要在gcc编译程序的命令行中添加-z execstack参数才可以(限制好多…看来现代的操作系统和编译器越来越重视这个问题了)。

测试代码如下:

编译,执行。

看,我们成功了哎…我的bash每次启动会随机打印一首诗。我特意退出了一次shell,大家看到了是子程序的shell了吧?OK,真不容易啊。这篇文章到这里就告一段落了。至于更复杂的应用,大家可以参考网上的文章。本人才疏学浅,此文纯粹抛砖引玉,让大家看网上的文章时能有个平缓的过度。

至此,本系列结束。

来源:

http://www.0xffffff.org/2012/12/02/5-stack-overflow-1/
http://www.0xffffff.org/2012/12/09/6-stack-overflow-2/

[转]浅谈缓冲区溢出之栈溢出相关推荐

  1. 浅谈缓冲区溢出之栈溢出上

    有段时间没有用windows了,刚一开机又是系统补丁更新.匆匆瞥了一眼看到了"内核缓冲区溢出漏洞补丁"几个字眼.靠,又是内核补丁.打完这个补丁后MD的内核符号文件又得更新了.于是抱 ...

  2. 缓冲区溢出(栈溢出)

    栈溢出(缓冲区溢出) 文章目录 栈溢出(缓冲区溢出) 堆栈: Call指令: 栈溢出(缓冲区溢出): 在了解栈溢出之前,我们需要了解几个概念:堆栈是什么以及汇编语言中的call指令的特点. 堆栈: ​ ...

  3. 缓冲区溢出之栈溢出利用(手动编写无 payload 的 Exploit)

    0x01 介绍 Exploit 的英文意思就是利用,它在黑客眼里就是漏洞利用.有漏洞不一定就有Exploit(利用),有Exploit就肯定有漏洞.编写缓冲区溢出的Exploit分为3个方面:漏洞溢出 ...

  4. 缓冲区溢出(栈溢出)实验 之 JMP ESP

    3.缓冲区溢出之JMP ESP 本文属于原创,如有错误请指正.其中引用他人的部分已经标出,如涉及版权问题请联系本人 这里不得不讲一讲JMP ESP的原理了,在实验之前我一直没看懂他是如何试下跳转ESP ...

  5. 浅谈Metaspace内存溢出原因及JVM参数设置

    浅谈Metaspace内存溢出原因及JVM参数设置 1.Metaspace内存溢出(oom) 日志 原因分析 从Java8开始,Java中的内存模型引入了一个称为元空间(Metaspace)的新内存区 ...

  6. 【JVM调优】JVM内存管理调优浅谈

    什么是JVM Java Virtual Machine,Java虚拟机 Java虚拟机有自己完善的硬件架构,如处理器.堆栈等,还具有相应的指令系统. Java虚拟机本质上就是一个程序,当它在命令行上启 ...

  7. 浅谈V8引擎中的垃圾回收机制

    浅谈V8引擎中的垃圾回收机制 这篇文章的所有内容均来自 朴灵的<深入浅出Node.js>及A tour of V8:Garbage Collection,后者还有中文翻译版V8 之旅: 垃 ...

  8. 浅谈Windows XP系统漏洞的封堵

    浅谈Windows XP系统漏洞的封堵 2010年11月18日 9号    浏览:96次 发表评论 阅读评论 微软WindowsXP自出世以来就在中国市场中获得了广泛好评和客户的认同,它出色的兼容性和 ...

  9. 软件漏洞及缓冲区溢出

    软件漏洞及缓冲区溢出 文章作者:davy_yan 本文是我做溢出的一点心得,希望大家提出宝贵的修改意见,也希望对大家有一定的帮助:) 软件漏洞及缓冲区溢出 一.     缓冲区溢出的发展简史 1.   ...

最新文章

  1. 深度强化学习为什么在实际当中用的比较少 ?
  2. 转:ASP.NET程序中常用小技巧
  3. 转载:正则表达式30分钟入门教程
  4. arcgis中python坡度计算_ArcGIS不同坡度植被覆盖率分析步骤
  5. EJB3 学习笔记六
  6. 学生信息系统求助_一个学生信息录入和查询的系统
  7. Hibernate开发和对象状态
  8. c语言赋值运算符 amp amp 怎么读,重载赋值运算符 amp;amp; 对象
  9. Maven CXF wsdl2Java ListXxx生成ArrayOfXxx包装对象 解决方法
  10. 安徽省级办公室高级应用计算机二级,2019年9月安徽省计算机等级二级考试教程:二级MSOffice高级应用上机指导...
  11. mysql 存储过程 长字符串_MySQL存储过程--长字符串扯分
  12. bin文件如何编辑_如何将PS中图片模糊文字(位图)转换为AI的高清矢量图和CDR文件相关编辑?...
  13. Linux下tomcat 8安装与配置
  14. 怎么改自己手机的ip地址
  15. Dom(二十一) 拖拽
  16. 这 6 个开源项目很 Cool
  17. m277打印机 重置_惠普M277n打印机使用说明书(惠普M277n打印机使用指南PDF资料)V1.0 最新版...
  18. 2021-2025年中国健身训练软件行业市场供需与战略研究报告
  19. 非线性最小二乘法之Gauss Newton、L-M、Dog-Leg
  20. cocos2dx版本热更新梳理

热门文章

  1. 微星GE62 NVIDIA960m 双系统ubuntu16.04 配置caffe-ssd
  2. 用java生成永远唯一的id
  3. Ubuntu环境下制作Windows U盘启动工具
  4. 九段刀客:express连接MySQL并实现增、删、改、查
  5. IT痴汉的工作现状30-刀客许三爷(下)
  6. 魔兽世界怀旧服——按键精灵 Java版(自动技能,练级释放者)
  7. ngrok 把内网URL转换成外网URL
  8. 武汉理工大学新思维研究生英语课文翻译和课后习题答案(1~12单元)
  9. 东周科目三考场5号线_深圳东周科目三考场路线及注意事项
  10. 算法工程师5——计算机视觉知识点概览