栈溢出①-浅学ret2text

发布于 2022-12-01  116 次阅读


author: bilala

0x00 前言

前前后后花了三天终于完成了这篇文章,从对二进制毫不了解到一点一点去剖析每一步程序都在做什么,期间踩了无数的坑,也有无数的错误理解,不过不同于web的是,我可以从很底层去看到程序的运行流程。每次解决了一个疑问后,自己也总是会马上有新的问题,但是在gdb动调后或者多看几篇文章后,又或者自己突然顿悟后,问题也全都迎刃而解了(就是过程非常非常耗时)。

本文站在一个纯二进制小白的角度,已经尽力的讲清楚每个部分了,当然阅读过程中你也会产生自己的问题,希望你可以自己去找到答案,比如改某个参数后再重新动调看看差别,或者画画图帮助自己理解等。

0x01 相关概念

通用寄存器

寄存器分为三类:通用寄存器,控制寄存器和段寄存器。

通用寄存器分为:数据寄存器指针寄存器索引寄存器

先放一张图,算是很好的诠释了各个通用寄存器的分工(64位寄存器)。

0

上图为64位通用寄存器,以r开头,若是32位的则是e开头,比如rax对应着eaxrbp对应着ebp

以下为对上图的分析:

  • 每个寄存器的用途并不是单一的。
  • %rax 通常用于存储函数调用的返回结果,同时也用于乘法和除法指令中。在imul 指令中,两个64位的乘法最多会产生128位的结果,需要 %rax 与 %rdx 共同存储乘法结果,在div 指令中被除数是128 位的,同样需要%rax 与 %rdx 共同存储被除数。
  • %rsp 是堆栈指针寄存器,通常会指向栈顶位置,堆栈的 pop 和push 操作就是通过改变 %rsp 的值即移动堆栈指针的位置来实现的。
  • %rbp 是栈帧指针,用于标识当前栈帧的起始位置
  • %rdi, %rsi, %rdx, %rcx,%r8, %r9 六个寄存器用于存储函数调用时的6个参数(如果有6个或6个以上参数的话)。
  • 被标识为 “miscellaneous registers” 的寄存器,属于通用性更为广泛的寄存器,编译器或汇编程序可以根据需要存储任何数据。

数据寄存器

数据寄存器用于算术,逻辑和其他操作。使用方式如下(以32位的为例)

  • 作为完整的32位数据寄存器:EAX,EBX,ECX,EDX。
  • 32位寄存器的下半部分可用作4个16位数据寄存器:AX,BX,CX和DX。
  • 上述四个16位寄存器的低半部分和高半部分可用作8个8位数据寄存器:AH,AL,BH,BL,CH,CL,DH和DL。

image-20221130202923374

而64位的也就是在EAX的前面多32bit成为rax,然后rax的下半部分可用作eax,至于最终cpu怎么用这些寄存器完全由程序来决定。 例如定义了一个int数值,一个int占四个字节也就是32位,那么当我们的64位电脑在处理时,就用不到rax,只用一个rax的后半部分eax就行。

而EAX,EBX,ECX,EDX在运算过程中也有特定用途

AX is the primary accumulator ; 它用于输入/输出和大多数算术指令。 例如,在乘法运算中,根据操作数的大小,一个操作数存储在EAX或AX或AL寄存器中。

BX is known as the base register ,因为它可以用于索引寻址。

CX is known as the count register ,因为ECX,CX寄存器在迭代操作中存储循环计数。

DX is known as the data register 。 它也用于输入/输出操作。 它还与AX寄存器以及DX一起用于涉及大值的乘法和除法运算。

指针寄存器

(32位为例)指针寄存器是32位EIP,ESP和EBP寄存器以及相应的16位右部分IP,SP和BP。 指针寄存器分为三类 -

  • Instruction Pointer (IP) - 16位IP寄存器存储下一条要执行的指令的偏移地址。 与CS寄存器相关联的IP(作为CS:IP)给出代码段中当前指令的完整地址。指向下一条要执行命令的地址
  • Stack Pointer (SP) - 16位SP寄存器提供程序堆栈中的偏移值。 与SS寄存器(SS:SP)相关联的SP指的是程序堆栈内的数据或地址的当前位置。也就是指向栈顶
  • Base Pointer (BP) - 16位BP寄存器主要用于引用传递给子程序的参数变量。 SS寄存器中的地址与BP中的偏移量组合以获得参数的位置。 BP也可以与DI和SI组合作为特殊寻址的基址寄存器。也就是指向当前栈帧的栈底,标识当前栈帧的起始位置

image-20221130204519592

索引寄存器

(32位为例)32位索引寄存器,ESI和EDI,以及它们最右边的16位部分。 SI和DI用于索引寻址,有时用于加法和减法。 有两组索引指针 -

  • Source Index (SI) - 用作字符串操作的源索引。
  • Destination Index (DI) - 用作字符串操作的目标索引。

image-20221130204657502

汇编指令

只需知道mov,pop,push,lea,call就行,可以下边碰到不懂了再百度

gdb基本使用

b: 下断点
    - b main: 在main函数处下断点
    - b *0x80486ae: 在地址为0x80486ae的地方下断点
r: 运行程序
c: 直接下一步到下个断点
s: 类似step into,单步进入
n: 类似step over,单步跳过
x: 查看内存数据,有三个参数,第一个参数代表查看多少个单元,第二个参数代表一个单元多少字节,第三个参数代表显示数据的格式
    - ①,就自定义个数字,想看多少看多少
    - ②,b(一个字节),h(俩字节),w(四字节),g(八字节)
    - ③,x 按十六进制格式显示变量。d 按十进制格式显示变量。(还有很多种,可百度)
    - 示例:x /10gx 0x123456: 从0x123456开始每个单元八个字节,十六进制显示10个单元的数据

0x02 函数调用及栈帧原理

写的比较详细,所以另开了一篇,移步--> http://blog.bilala.top/?p=393

0x03 栈溢出原理

在刚刚的0x02中,我们的子函数add并没有去开辟新的栈空间,假设我们的子函数和main一样会开辟一段新的栈空间,那么此时的图应该是这样的

image-20221201103941601

而新开辟的栈空间是用来存储变量的值的,比如我们在add函数中需要定义一个s变量,然后通过gets()函数获取变量值。

gets()函数是可以一直读取你输入的值的,那我们就可以先输入多个a(或其他字符)来占满新开辟的栈空间

image-20221201125116967

这里写4个a只是意思意思,实际需要多少个a还得看这个栈空间多大

刚说到gets()函数可以一直读取输入,那我们在填满栈空间后,可以继续输入值来覆盖掉rbp所指向的地址处的值,当然我们覆盖这个值没什么意义,我们要覆盖的是再往上一个位置的“返回地址”处,这样在子函数结束时,程序就会根据这个返回地址跳转到我们想让他跳转的地方,实现了对程序运行流的控制。例如程序中有一个调用/bin/sh的函数,那我们就可以修改这个返回地址为这个危险函数的地址。

image-20221201125716166

这里或许会有一个新的疑问,就是e2e0处的值被改成了a,正常情况下,值应该为e300,然后在子函数退出时,rbp指向e300也就是回到父函数的栈底,改成a后,rbp回到的是地址值为a的地方,不过内存中肯定是没有地址值为a的地方,所以此时rbp将会成为一个空指针,也就是不指向任何地方,可能说的有点抽象,待会例题分析时可以再聊聊这个问题。

0x04 ret2text例题分析

此处以CTFwiki中的ret2text附件作为例题讲解

https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/basic-rop/#ret2text

题目为32位的ELF文件,先拖进ida中查看main函数的逻辑

image-20221201141130585

有一个gets()函数,双击char s[100]处查看s变量的栈

image-20221201141345571

此时对应的图为

image-20221201142151981

变量sesp+1C处开始往上覆盖栈的值,根据上边栈溢出原理,此时我们不能满足于单单覆盖给的栈空间,我们还要往上侵入,覆盖到ret为止,那我们就要计算s的初始位置到ret位置之间隔着多少。

这时就需要用到gdb了,先在ida中找到gets函数调用时的地址

image-20221201142639260

找到指令地址为0x80486ae,然后在gdb中下断点

image-20221201142849148

此时EBP指向0xffffd478ESP指向0xffffd3f0,所以s变量的地址在ESP+1C处开始即0xffffd40c

image-20221201143603869

image-20221201143635426

填满栈空间需要ebp-(esp+1C)个字节,也就是0x6C个

image-20221201144527699

因为要继续往上溢出,所以我们再填4个字节覆盖ebp所指的地方,所以需要0x6C+4个字节的脏数据,接下来就可以写入我们需要的ret地址,即可完成栈溢出覆盖。

在此题中,危险函数地址为0x804863a

image-20221201144953614

exp.py:

from pwn import *
context.log_level = 'debug'

sh = process('./ret2text')
target = 0x804863a
pause()
sh.sendline(b'a' * (0x6c+4) + p32(target))
sh.interactive()

这里加了个pause是为了配合gdb调试,运行后返回一个pid,在gdb中attach pid即可

接下来,在受攻击后的程序中查看栈是否被我们成功覆盖,是否和我们预想的一样。

在attach后,exp.py那里就可以按回车结束进程了

在gdb这边,在gets函数的下一个指令处下断点

image-20221201145444163

image-20221201145542265

这里当然看不到内存地址中的值,我们用x命令查看刚刚说的变量s处的值x /50wx 0xffffd40c

image-20221201150543159

箭头所指处就是ret返回地址的值。这里s的值从d45c开始,刚刚没用脚本调试时是d40c,脚本原因,不影响分析,此时图为

image-20221201150949250

这个图就很清晰了,在main函数结束后,就会跳转到ret地址值处继续执行命令

image-20221201151354113

在按下n命令后查看

image-20221201151528051

箭头所指的两个地方,EBP由于找不到0x61616161地址,所以成为空指针,下面那里可以看到程序中已经没有main函数了,只有危险函数了,因为main函数退出才能执行到ret跳转

0x05 参考资料

https://iowiki.com/assembly_programming/assembly_quick_guide.html

https://zhuanlan.zhihu.com/p/27339191