大家好,又见面了,我是你们的朋友全栈君。
Author:Liedra https://www.cnblogs.com/LieDra/
入门介绍
学习自 https://zhuanlan.zhihu.com/p/25816426 对部分内容进行整理。
正好最近网络安全也讲到了这一部分,就把这一部分放出来,希望对各位读者学习有所帮助。
0x10 背景知识
栈溢出条件:一是程序要有向栈内写入数据的行为;二是程序并不限制写入数据的长度。 栈顶对应的内存地址在压栈时变小,退栈时变大。(调用函数caller,被调用函数callee) 函数状态主要涉及的三个寄存器: esp 存储函数调用栈的栈顶地址,在压栈和退栈时发生变化。 ebp 存储当前函数状态的基地址,在函数运行时不变,可用来索引确定函数参数或局部变量的位置。 eip 用来存储即将执行的程序指令的地址。(cpu依照eip的存储内容读取指令并执行,eip随之指向相邻的下一条指令) 发生函数调用时,栈顶函数状态以及上述寄存器的变化(知乎有图解): 1.首先将被调用函数的参数逆序压栈(没有则无),之后被压入栈内的数据都会作为被调用函数的函数状态来保存。 2.然后将调用函数进行调用之后的下一条指令地址作为返回地址压入栈内(caller的eip信息得以保存)。 3.再将当前的ebp寄存器的值压入栈中(调用函数的基地址),并将ebp寄存器值更新为当前栈顶的地址。(前者保存调用函数的基地址,后者更新为callee的基地址) 4.再之后是将被调用函数的局部变量等数据压入栈内。 在压栈的过程中,esp寄存器的值不断减小。 压入栈的数据包括调用函数,返回地址,调用函数的基地址,局部变量。 函数调用结束时变化如下: 5.首先被调用函数的局部变量从栈内弹出,栈顶指向被调用函数的基地址 6.然后将基地址内存储的调用函数的基地址从栈内弹出,并存到ebp寄存器内。 7.再将返回地址从栈中弹出,并存到eip寄存器内。这样调用函数的eip(指令)信息得以恢复。 8.将被调用函数的返回地址弹出栈外,并存到eip寄存器内。
0x20 技术清单
当函数正在执行内部指令的过程中我们无法拿到程序的控制权,只有在发生函数调用或者结束函数调用时,程序的控制权会在函数状态之间发生跳转。而控制程序执行指令的最关键的寄存器就是eip,所以我们的目标就是让eip载入攻击指令的地址。(尝试覆盖上述第2步) shellcode 修改返回地址,让其指向溢出数据中的一段指令 return2libc 修改返回地址,让其指向内存中已有的某个函数 ROP 修改返回地址,让其指向内存中已有的一段指令 hijack GOT 修改某个被调用函数的地址,让其指向另一个函数
0x30 Shellcode
=》修改返回地址,让其指向溢出数据中的一段指令 在溢出数据内包含一段攻击指令,用攻击指令的起始地址覆盖掉返回地址。攻击指令一般是用来打开shell,从而获得当前进程的控制权,因此这类指令片段也被称为“shellcode”。可以用汇编语言来写再转成对应的机器码,也可以上网搜索直接复制粘贴。
溢出数据的构造
payload:padding1 address of shellcode padding2 shellcode
padding1 处的数据可以随意填充,长度应该刚好覆盖函数的基地址。 address of shellcode 是后面shellcode起始处的地址,用来覆盖返回地址。 padding2 处的数据也可随意填充,长度可以任意。 shellcode 应该为十六进制的机器码格式。 注意padding1处如果利用字符串程序输入溢出数据不要包含”x00″,否则向程序传入溢出数据时会造成截断)
需要解决的两个问题: 1.返回地址之前的填充数据(padding1)应该多长?
可用调试工具(如gdb)查看汇编代码来确定,也可以在运行程序时用不断增加输入长度的方法来试探(如果返回地址被无效地址覆盖,程序会终止并报错)
2.shellcode起始地址应该是多少?
可用调试工具查看返回地址的位置(可用查看ebp的内容然后再加4(32位机)
但是在调试工具中的这个位置和正常运行时并不一致,这种情况下我们只能得到大致但不确切的shellcode起始地址,解决办法是在padding2里填充若干长度的“x90”。这个机器码对应的指令是NOP(no operation) ,告诉CPU什么也不做,然后跳到下一条指令。这样只要返回地址命中这一段中的任意位置,都可以无副作用跳转到shellcode的起始处,所以方法也称为NOP Sled(滑雪橇)。 可通过NOP填充来配合试验起始地址。
OS可将函数调用栈的起始地址设为随机化(内存布局随机化Address Space Layout Randomization ASLR)这样程序每次运行时函数返回地址会随机变化,如果OS关闭了上述的随机化,那么程序每次运行时函数返回地址相同,这样可以通过输入无效的溢出数据来生成core文件,再通过调试工具在core文件中找到返回地址的位置,从而确定shellcode的起始地址。
这种方法生效的一个前提是在函数调用栈上的数据(shellcode)要有可执行的权限,另一个前提是上面提到的关闭内存布局随机化。很多时候OS会关闭函数调用栈的可执行权限,这样shellcode 方法就失效了,不过我们还可以尝试使用内存里已有的指令或函数。这就包括return2libc 和ROP两种方法。
0x40 Return2libc
=》 修改返回地址,让其指向内存中已有的某个函数 在内存中确定某个函数的地址,并用其覆盖掉返回地址。 libc动态链接库中的函数被广泛应用,所以大概率可在内存中找到该动态库。其中也包含一些系统级的函数,通常使用这些系统级函数来获得当前进程的控制权。可能需要参数,所以溢出数据也要包括必要的参数。(下面以执行system(“/bin/sh”)为例)
payload: padding1 address of system() padding2 address of “/bin/sh”
padding1 随意填充,不含”x00″. address of system()是该函数在内存中的地址,用来覆盖返回地址。 padding2 数据长度为4(32位机时)可随意填充 address of “/bin/sh” 是字符串“/bin/sh”在内存中的地址,作为传给system()的参数 要解决的问题:
1返回地址之前的填充数据(padding1)应该多长? 同上shellcode
2.system() 函数地址应该是多少? 需要看程序是如何调用动态链接库中的函数的。首先确定动态链接库在内存中的起始地址,再加上函数在动态库中的相对偏移量,最终得到函数在内存中的绝对地址。 而确定动态库的内存地址,shellcode中提到的ASLR也是要关闭。在关闭ASLR前提下,可以通过调试工具在运行程序过程中直接查看system()的地址,也可以查看动态库在内存的起始地址,再在动态库内查看函数的相对偏移位置,通过计算得到函数绝对地址。
小结:上面两种都是通过覆盖返回地址来执行输入的指令片段(shellcode)或者动态库中的函数(return2libc)这两种方法都需要操作系统关闭内存布局随机化,而且shellcode还需要有可执行权限。
后两种方法
0x50 相关知识
0x51 寄存器
32位x86架构下的寄存器可以被简单分为通用寄存器和特殊寄存器两类,通用寄存器在大部分汇编指令下是可以任意使用的(虽然有些指令规定了某些寄存器的特定用途),而特殊寄存器只能被特定的汇编指令使用,不能用来任意存储数据。 32位x86架构下的通用寄存器包括一般寄存器(eax、ebx、ecx、edx),索引寄存器(esi、edi),以及堆栈指针寄存器(esp、ebp)。 一般寄存器用来存储运行时数据,是指令最常用到的寄存器,除了存放一般性的数据,每个一般寄存器都有自己较为固定的独特用途。 eax 被称为累加寄存器(Accumulator),用以进行算数运算和返回函数结果等。 ebx 被称为基址寄存器(Base),在内存寻址时(比如数组运算)用以存放基地址。 ecx 被称为记数寄存器(Counter),用以在循环过程中记数。 edx 被称为数据寄存器(Data),常配合 eax 一起存放运算结果等数据。 索引寄存器通常用于字符串操作中,esi 指向要处理的数据地址(Source Index),edi 指向存放处理结果的数据地址(Destination Index)。 堆栈指针寄存器(esp、ebp)用于保存函数在调用栈中的状态,上篇已有详细的介绍。 32位x86架构下的特殊寄存器包括段地址寄存器(ss、cs、ds、es、fs、gs),标志位寄存器(EFLAGS),以及指令指针寄存器(eip)。 现代操作系统内存通常是以分段的形式存放不同类型的信息的。函数调用栈就是分段的一个部分(Stack Segment)。 内存分段还包括堆(Heap Segment)、数据段(Data Segment),BSS段,以及代码段(Code Segment)。
代码段存储可执行代码和只读常量(如常量字符串),属性可读可执行,但通常不可写。数据段存储已经初始化且初值不为0的全局变量和静态局部变量,BSS段存储未初始化或初值为0的全局变量和静态局部变量,这两段数据都有可写的属性。堆用于存放程序运行中动态分配的内存,例如C语言中的 malloc() 和 free() 函数就是在堆上分配和释放内存。 段地址寄存器就是用来存储内存分段地址的,其中寄存器 ss 存储函数调用栈(Stack Segment)的地址,寄存器 cs 存储代码段(Code Segment)的地址,寄存器 ds 存储数据段(DataSegment)的地址,es、fs、gs 是附加的存储数据段地址的寄存器。 标志位寄存器(EFLAGS)32位中的大部分被用于标志数据或程序的状态,例如 OF(OverflowFlag)对应数值溢出、IF(Interrupt Flag)对应中断、ZF(Zero Flag)对应运算结果为0、CF(Carry Flag)对应运算产生进位等等。 指令指针寄存器(eip)存储下一条运行指令的地址,上篇已有详细的介绍。
0x52 汇编指令
32位x86架构下的汇编语言有 Intel 和 AT&T 两种格式,本文所用汇编指令都是 Intel 格式。两者最主要的差别如下。 Intel 格式,寄存器名称和数值前无符号:
“指令名称 目标操作数 DST,源操作数 SRC”
AT&T 格式,寄存器名称前加“%”,数值前加“$”:
“指令名称 源操作数 SRC,目标操作数 DST”
一些最常用的汇编指令如下:
MOV:数据传输指令,将 SRC 传至 DST,格式为
MOV DST, SRC;
PUSH:压入堆栈指令,将 SRC 压入栈内,格式为
PUSH SRC;
POP:弹出堆栈指令,将栈顶的数据弹出并存至 DST,格式为
POP DST;
LEA:取地址指令,将 MEM 的地址存至 REG ,格式为
LEA REG, MEM;
ADD/SUB:加/减法指令,将运算结果存至 DST,格式为
ADD/SUB DST, SRC;
AND/OR/XOR:按位与/或/异或,将运算结果存至 DST ,格式为
AND/OR/XOR DST,SRC;
CALL:调用指令,将当前的 eip 压入栈顶,并将 PTR 存入 eip,格式为
CALL PTR;
RET:返回指令,操作为将栈顶数据弹出至 eip,格式为
RET;
0x60 ROP ( Return Oriented Programming )
=》修改返回地址,让其指向内存中已有的一段指令 在内存中确定某段指令的地址,并用其覆盖返回地址。有时目标函数在内存内无法找到,有时目标操作并没有特定的函数可以完美适配。这时就需要在内存中寻找多个指令片段,拼凑出一系列操作来达成目的。假如要执行某段指令(我们将其称为“gadget”,意为小工具),溢出数据应该以下面的方式构造(padding 长度和内容的确定方式参见上篇)
payload : padding address of gadget
当连续执行若干个指令时,需要每个 gadget 执行完毕可以将控制权交给下一个 gadget。所以 gadget的最后一步应该是RET指令,这样程序的控制权(eip)才能得到切换。执行多个gadget时,溢出数据应该以下面的方式构造:
payload : padding address of gadget 1 address of gadget 2 …… address of gadget n
要解决的问题:
1.栈溢出后要实现什么效果?
ROP 常见的拼凑效果是实现一次系统调用,Linux系统下对应的汇编指令是 int 0x80。执行这条指令时,被调用函数的编号应存入 eax,调用参数应按顺序存入 ebx,ecx,edx,esi,edi 中。例如,编号125对应
mprotect (void *addr, size_t len, int prot)
可以用该函数将栈的属性改为可执行,这样就可以使用 shellcode 了。假如我们想利用系统调用执行这个函数,eax、ebx、ecx、edx 应该分别为“125”、内存栈的分段地址(可以通过调试工具确定)、“0x10000”(需要修改的空间长度,也许需要更长)、“7”(RWX 权限)
2.如何寻找对应的指令片段? 有若干开源工具可以实现搜索以 ret 结尾的指令片段,著名的包括 ROPgadget、rp 、ropeme等。 (如何详细搜索,工具如何使用?)
3.如何传入系统调用的参数? 以上面提到的mprotect函数为例,我们需要将参数传输至寄存器,所以可以用 pop 指令将栈顶数据弹入寄存器。如果在内存中能找到直接可用的数据,也可以用 mov 指令来进行传输,不过写入数据再 pop 要比先搜索再 mov 简单。如果要用 pop 指令来传输调用参数,就需要在溢出数据内包含这些参数,所以上面的溢出数据格式需要一点修改。对于单个 gadget,pop 所传输的数据应该在 gadget 地址之后。 在调用 mprotect() 为栈开启可执行权限之后,我们希望执行一段 shellcode,所以要将 shellcode 也加入溢出数据,并将 shellcode 的开始地址加到 int 0x80 的 gadget之后。但确定 shellcode 在内存的确切地址是很困难的事,我们可以使用 push esp 这个 gadget(如果可以找到的话)。 对于所有包含 pop 指令的 gadget,在其地址之后都要添加 pop 的传输数据,同时在所有 gadget 最后包含一段 shellcode,最终溢出数据结构应该变为如下格式。
payload : padding address of gadget 1 param for gadget 1 address of gadget 2 param for gadget 2 …… address of gadget n shellcode
上述是非常理想的情况,实际情况可能会比较复杂,需要“曲线救国”等,而且要小心后面的gadget不要破坏之前已经实现的部分。
0x70 Hijack GOT
=》修改某个被调用函数的地址,让其指向另一个函数 在内存中修改某个函数的地址,使其指向另一个函数。假设修改 printf() 函数的地址使其指向 system(),这样修改之后程序内对 printf() 的调用就执行 system() 函数。要实现这个过程,我们就要弄清楚发生函数调用时程序是如何“找到”被调用函数的。 程序对外部函数的调用需要在生成可执行文件时将外部函数链接到程序中,链接的方式分为静态链接和动态链接。静态链接得到的可执行文件包含外部函数的全部代码,动态链接得到的可执行文件并不包含外部函数的代码,而是在运行时将动态链接库(若干外部函数的集合)加载到内存的某个位置,再在发生调用时去链接库定位所需的函数。
可程序是如何在链接库内定位到所需的函数呢?这个过程用到了两张表--GOT 和 PLT。GOT 全称是全局偏移量表(Global Offset Table),用来存储外部函数在内存的确切地址。GOT 存储在数据段(Data Segment)内,可以在程序运行中被修改。PLT 全称是程序链接表(Procedure Linkage Table),用来存储外部函数的入口点(entry),换言之程序总会到 PLT 这里寻找外部函数的地址。PLT 存储在代码段(Code Segment)内,在运行之前就已经确定并且不会被修改,所以 PLT 并不会知道程序运行时动态链接库被加载的确切位置。那么 PLT 表内存储的入口点是什么呢?就是 GOT 表中对应条目的地址。 GOT 表的初始值都指向 PLT 表对应条目中的某个片段,这个片段的作用是调用一个函数地址解析函数。当程序需要调用某个外部函数时,首先到PLT表内寻找对应的入口点,跳转到 GOT 表中。如果这是第一次调用这个函数,程序会通过GOT表再次跳转回PLT表,运行地址解析程序来确定函数的确切地址,并用其覆盖掉GOT表的初始值,之后再执行函数调用。当再次调用这个函数时,程序仍然首先通过PLT表跳转到GOT表,此时GOT表已经存有获取函数的内存地址,所以会直接跳转到函数所在地址执行函数。整个过程如下面两张图所示。
上图是第一次,下图为之后调用。
我们的目标可以分解为如下几部分:确定函数 A 在 GOT 表中的条目位置,确定函数 B 在内存中的地址,将函数 B 的地址写入函数 A 在 GOT 表中的条目。
1.确定函数 A 在 GOT 表中的条目位置。
程序调用函数时是通过 PLT 表跳转到 GOT 表的对应条目,所以可以在函数调用的汇编指令中找到 PLT 表中该函数的入口点位置,从而定位到该函数在 GOT 中的条目。
例如
call 0x08048430 <printf@plt>
就说明 printf 在 PLT 表中的入口点是在 0x08048430,所以 0x08048430 处存储的就是 GOT 表中 printf 的条目地址。
2.如何确定函数 B 在内存中的地址? 如果系统开启了内存布局随机化,程序每次运行动态链接库的加载位置都是随机的,就很难通过调试工具直接确定函数的地址。假如函数 B 在栈溢出之前已经被调用过,我们当然可以通过前一个问题的答案来获得地址。但我们心仪的攻击函数往往并不满足被调用过的要求,也就是 GOT 表中并没有其真实的内存地址。幸运的是,函数在动态链接库内的相对位置是固定的,在动态库打包生成时就已经确定。所以假如我们知道了函数 A 的运行时地址(读取 GOT 表内容),也知道函数 A 和函数 B 在动态链接库内的相对位置,就可以推算出函数 B 的运行时地址。
3.如何实现 GOT 表中数据的修改? 很难找到合适的函数来完成这一任务,不过我们还有强大的 ROP。
实践才能真正理解如何操作。纸上得来终觉浅,得知此事须躬行。
发布者:全栈程序员栈长,转载请注明出处:https://javaforall.cn/154961.html原文链接:https://javaforall.cn