author: bilala
0x00 前言
前前后后花了三天终于完成了这篇文章,从对二进制毫不了解到一点一点去剖析每一步程序都在做什么,期间踩了无数的坑,也有无数的错误理解,不过不同于web的是,我可以从很底层去看到程序的运行流程。每次解决了一个疑问后,自己也总是会马上有新的问题,但是在gdb动调后或者多看几篇文章后,又或者自己突然顿悟后,问题也全都迎刃而解了(就是过程非常非常耗时)。
本文站在一个纯二进制小白的角度,已经尽力的讲清楚每个部分了,当然阅读过程中你也会产生自己的问题,希望你可以自己去找到答案,比如改某个参数后再重新动调看看差别,或者画画图帮助自己理解等。
0x01 相关概念
通用寄存器
寄存器分为三类:通用寄存器,控制寄存器和段寄存器。
通用寄存器分为:数据寄存器、指针寄存器和索引寄存器。
先放一张图,算是很好的诠释了各个通用寄存器的分工(64位寄存器)。
上图为64位通用寄存器,以r
开头,若是32位的则是e
开头,比如rax
对应着eax
、rbp
对应着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。
而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组合作为特殊寻址的基址寄存器。也就是指向当前栈帧的栈底,标识当前栈帧的起始位置
索引寄存器
(32位为例)32位索引寄存器,ESI和EDI,以及它们最右边的16位部分。 SI和DI用于索引寻址,有时用于加法和减法。 有两组索引指针 -
- Source Index (SI) - 用作字符串操作的源索引。
- Destination Index (DI) - 用作字符串操作的目标索引。
汇编指令
只需知道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一样会开辟一段新的栈空间,那么此时的图应该是这样的
而新开辟的栈空间是用来存储变量的值的,比如我们在add函数中需要定义一个s变量,然后通过gets()
函数获取变量值。
gets()函数是可以一直读取你输入的值的,那我们就可以先输入多个a
(或其他字符)来占满新开辟的栈空间
这里写4个a只是意思意思,实际需要多少个a还得看这个栈空间多大
刚说到gets()函数可以一直读取输入,那我们在填满栈空间后,可以继续输入值来覆盖掉rbp所指向的地址处的值,当然我们覆盖这个值没什么意义,我们要覆盖的是再往上一个位置的“返回地址”处,这样在子函数结束时,程序就会根据这个返回地址跳转到我们想让他跳转的地方,实现了对程序运行流的控制。例如程序中有一个调用/bin/sh
的函数,那我们就可以修改这个返回地址为这个危险函数的地址。
这里或许会有一个新的疑问,就是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函数的逻辑
有一个gets()函数,双击char s[100]
处查看s变量的栈
此时对应的图为
变量s
从esp+1C
处开始往上覆盖栈的值,根据上边栈溢出原理,此时我们不能满足于单单覆盖给的栈空间,我们还要往上侵入,覆盖到ret为止,那我们就要计算s的初始位置到ret位置之间隔着多少。
这时就需要用到gdb了,先在ida中找到gets函数调用时的地址
找到指令地址为0x80486ae
,然后在gdb中下断点
此时EBP
指向0xffffd478
,ESP
指向0xffffd3f0
,所以s变量的地址在ESP+1C
处开始即0xffffd40c
图
填满栈空间需要ebp-(esp+1C)
个字节,也就是0x6C个
因为要继续往上溢出,所以我们再填4个字节覆盖ebp所指的地方,所以需要0x6C+4
个字节的脏数据,接下来就可以写入我们需要的ret地址,即可完成栈溢出覆盖。
在此题中,危险函数地址为0x804863a
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函数的下一个指令处下断点
这里当然看不到内存地址中的值,我们用x
命令查看刚刚说的变量s处的值x /50wx 0xffffd40c
箭头所指处就是ret返回地址的值。这里s的值从d45c
开始,刚刚没用脚本调试时是d40c
,脚本原因,不影响分析,此时图为
这个图就很清晰了,在main函数结束后,就会跳转到ret地址值处继续执行命令
在按下n命令后查看
箭头所指的两个地方,EBP由于找不到0x61616161地址,所以成为空指针,下面那里可以看到程序中已经没有main函数了,只有危险函数了,因为main函数退出才能执行到ret跳转
0x05 参考资料
https://iowiki.com/assembly_programming/assembly_quick_guide.html
Comments | NOTHING