ROP
栈介绍
在程序运行中用于保存函数调用信息和局部变量,程序的栈是从进程的虚拟地址空间的高地址向低地址增长的。
x86和x64传参
- x86:函数参数在函数返回地址的上方。
- x64:前六个参数一次保存在rdi、rsi、rdx、rcx、r8、r9寄存器中,如果还有更多的参数的话才会保存在栈上。
x64中内存地址不能大于0x0007FFFFFFFFFFF,6字节长度,否则会抛出异常。
栈溢出原理
栈溢出指的是程序向栈中某个变量写入的字节超过了这个变量本身所申请的字节数,因而导致与其相邻的栈中的变量的值被改变。
栈溢出的基本前提:
- 程序必须向栈上写入数据。
- 写入的数据大小没有被良好地控制。
利用步骤
首先寻找危险函数,常见的危险函数如下:
- 输入:
- get,直接读取一行,忽略'\x00'
- scanf
- vscanf
- 输出:
- sprintf
- 字符串:
- strcpy,字符串复制,遇到'\x00'停止
- strcat,字符串拼接,遇到'\x00'停止
- bcopy
其次,计算我们所要操作的地址与我们所要覆盖的地址的距离。
ida中的几种索引:
- 相对于栈基地址的索引,可以直接通过查看ebp相对偏移获得。
- 相对应栈指针的索引,一般需要进行调试,之后还是会转换到第一种类型。
- 直接地址索引,就相当于直接给定了地址。
一般来说,有如下覆盖需求:
- 覆盖函数返回地址,这时候就直接看EBP即可。
- 覆盖栈上某个变量的内容,这时候就需要更加精细的计算了。
- 覆盖bss段某个变量的内容。
- 根据现实执行情况,覆盖特定的变量或地址的内容。
之所以我们想要覆盖某个地址,是因为我们想通过覆盖地址的方法来直接或者间接地控制程序执行流程。
基本ROP
随着NX保护的开启,直接往栈上写shellcode并控制执行的方式已经不可行了,于是提出了新的技术————ROP。其主要思想是在栈缓冲区溢出的基础上,利用程序中已有的小片段(gadgets)来改变某些寄存器或者变量的值,从而控制程序的执行流程。
之所以称之为ROP,是因为核心在于利用了指令集中的ret指令,改变了指令流的执行顺序。ROP攻击一般得满足如下条件:
- 程序存在溢出,并且可以控制返回地址。
- 可以找到满足条件的gadgets以及相应gadgets的地址。
如果gadgets每次的地址都是不固定的,那我们就需要想办法动态获取对应的地址。
ret2text
ret2text即控制程序执行程序本身已有的代码(.text)。其实,这种攻击犯法是一种笼统的描述。我们控制执行程序已有的代码的时候也可以控制程序执行好几段不相邻的程序已有的代码(也就是gadgets),这就是我们所要说的ROP。
ret2shellcode
ret2shellcode即控制程序执行shellcode代码。一般来说,shellcode需要我们自己编写。在栈溢出的基础上,要想执行shellcode,需要对应的binary在运行时,shellcode所在的区域具有可执行权限。
ret2syscall
ret2syscalll,即控制程序执行系统调用,获取shell。
ret2libc
ret2libc及控制程序执行libc中的函数,通常是返回至某个函数的plt处或者函数的具体位置(即函数对应的got表项的内容)。一般情况下,我们会选择执行system("/bin/sh"),故而此时我们需要知道system函数的地址。
注意两点:
- 可以通过覆盖返回地址为pop来抬高栈,不需要再次调用漏洞函数。
- 在利用是通常通过泄露got表地址来泄露libc,由于libc的延迟绑定机制,我们需要泄露已经执行过的函数的地址。
中级ROP
中级ROP主要是使用了一些比较巧妙的Gadgets
ret2csu
在64位程序中,函数的前6个参数是通过寄存器传递的,但是大多数时候,我们很难找到每个寄存器对应的gadgets。这时候,我们可以利用x64下的_libc_csu_init中的gadgets。这个函数是用来对libc进行初始化操作的,而一般程序都会调用libc函数,所以这个函数一定会存在。
改进
并不是所有的程序漏洞都可以让我们输入足够长字节。当允许输入字节数较少时,有以下两种方法:
(1) 提前控制RBX与RBP
这两个寄存器的值主要是为了满足cmp条件,并进行跳转。如果我们可以提前控制这两个数值,那么我们就可以减少16字节。
(2) 多次利用
gadgets是分为两部分的,那么我们其实可以进行两次调用来达到目的,以便于减少一次gadgets所需要的字节数。但这里的多次利用需要更严格的条件:
- 漏洞可以触发
- 在两次触发之间,程序尚未修改r12-r15寄存器,这是因为要两次调用。
当然,有时候我们也会遇到一次性可以读入大量的字节,但是不允许漏洞再次利用的情况,这时候就需要我们一次性将所有的字节布置好,之后慢慢利用。
gadget
由于PC本身只是将程序的执行地址处的数据传递给CPU,而CPU则知识对传递来的数据进行解码,只要解码成功,就会进行执行。所以我们可以将源程序中一些地址进行偏移从而来获取我们所想要的指令,只要可以确保程序不崩溃。
ret2reg
- 查看溢出函数返回时哪个寄存器的值指向溢出缓冲区空间。
- 然后通过call reg或者jmp reg指令,将EIP设置为该指令地址。
- reg所指向的空间上注入Shellcode(需要确保该空间是可以执行的,但通常都是在栈上)
BROP
BROP是没有对应应用程序的源代码或者二进制文件下,对程序进行攻击,劫持程序流。
攻击条件
- 源程序必须存在栈溢出漏洞,以便于攻击者可以控制程序流程。
- 服务器端的进程在崩溃之后会重新启动,并且重新启动的进程地址与先前的地址一样(这也就是说即使程序有ASLR保护,但是其只是在程序最初启动的时候有效果)。
攻击原理
大部分应用都会开启ASLR、NX、Canary保护。分别看看BROP如何绕过这些保护,以及如何进行攻击。
基本思路
- 判断栈溢出长度,通过暴力枚举。
- Stack Reading,获取栈上的数据来泄露canaries,以及ebp和返回地址。
- Blind ROP,找到足够多的gadgets来控制输出函数的参数,并且对其进行调用,比如常见的write函数以及puts函数。
- Build the exploit,利用输出函数来dump出程序以便于来找到更多的gadgets,从而可以写出最后的exploit。
栈溢出长度
直接从1暴力枚举即可,直到发现程序崩溃。
Stack Reading
栈布局
buffer|canary|saved fame pointer|saved returned address
buffer的长度我们可以通过暴力枚举获取。
其次,关于canary以及后面的变量,我们可以采取爆破的方法。
攻击条件2表明了程序本身并不会应为crash有变化,所以每次的canary等值都是一样的。所以我们可以按字节爆破。每个字节最多有256种可能,所以在32位的情况下,我们最多需要爆破1024次,64位最多爆破2048次。(其实canary的最低字节一般都是00,所以爆破次数更小)
Blind ROP
最朴素的执行write函数的方法就是构造系统调用。
pop rdi; ret # socket
pop rsi; ret # buffer
pop rdx; ret # length
pop rax; ret # write syscall number
syscall
但通常来说,想要找到一个syscall的地址基本可能,我们可以通过转换为找write的方式来获取。
通过前文我们知道,我们可以在libc_csu_init中通过偏移源码找到
pop rsi pop rdi
pop r15 ret
ret
这样我们就能获取write函数调用的前两个参数。
我们可以通过plt表来获取write的地址。
write还有个参数length,一般来说程序中的rdx经常性会不是0,但是为了更好地控制程序输出,我们仍然尽量可以控制这个值。但是,在程序
pop rdx; ret
这样的指令几乎没有。那么,我们该如何控制rdx的值呢?字符串比较函数strcmp在执行的时候,rdx会被设置为将要被比较的字符串的长度,所以我们可以找到strcmp函数,从而来控制rdx。
那么接下来问题就简单了,分为两个步骤
一、寻找gadgets
由于尚未知道程序具体长什么样,所以我们只能通过简单的控制程序的返回地址为自己设置的值,从而来猜测相应的gadgets。当我们控制程序的返回地址时,一般有一下几种情况:
- 程序直接崩溃
- 程序运行一段时间后崩溃
- 程序一直运行而并不崩溃
为了寻找合理的gadgets,我们可以分为一下两步:
(1)寻找stop gadgets
stop gadgets一般指的是这样一段代码:当程序执行这段代码时,程序会进入无限循环,这样使得攻击者能够一直保持连接状态。(其实stop gadget也并不一定得是上面的样子,其根本目的在于告诉攻击者,所测试的返回地址是一个gadgets)
之所以要寻找stop gadgets,是因为当我们猜到某个gadgets后,如果我们仅仅是将其布置在栈上,由于执行完这个gadget之后,程序还会跳到栈上的下一个地址。如果该地址是非法地址,那么程序就会crash。这样的话,在攻击者看来程序只是单纯的crash了。因此,攻击者就会认为在这个过程中并没有执行到任何的useful gadget,从而放弃它。
但是如果我们布置了stop gadget,那么对于我们所要尝试的每一个地址,如果它是一个gadget的话,那么程序就不会崩溃。接下来,就是去想办法识别这些gadget。
(2)识别gadgets
那么,我们该如何识别这些gadgets呢?我们可以通过栈布局以及程序的行为来识别。为了更容易介绍,这里定义栈上的三种地址:
- probe,探针,也就是我们想要探测的代码地址。一般来说,都是64位程序,可以直接从0x400000尝试,如果不成功,有可能程序开启了PIE保护,再不济,就可能是程序是32位了。
- stop,不会使得程序崩溃的stop gadget的地址。
- trap,可以导致程序崩溃的地址。
我们可以通过在栈上摆放不同顺序的Stop与Trap从而来识别出正在执行的指令。因为执行Stop意味着程序不会崩溃,执行Trap意味着程序会立即崩溃。
但是即使是这样,我们仍然难以识别出正在执行的gadget到底是在对哪个寄存器进行操作。但如果遇到能通过
probe, trap, trap, trap, trap, trap, trap, stop, traps
这样的gadgets,那么有很大可能这个gadgets就是brop gadgets,此外,这个gadgets通过错位还可以生成pop rsp
等这样的gadgets,可以使得成功内需崩溃也可以作为识别这个gadgets的标志。
此外根据我们之前学的ret2libc_csu_init可以指导该地址减去0x1a就会得到其上一个gadgets。可以供我们调用其他函数。
需要注意的是probe可能是一个stop gadget,我们得去检查一下,怎么检查呢?我们只需要让后面的所有内容变为trap地址即可。因为如果是stop gadget的话,程序会正常执行,否则就会崩溃。
二、寻找plt
程序的plt表具有比较规整的结构,每个plt表项都是16字节。而且,在每个表项的6字节偏移处,是该表项对应的函数的解析路径,即程序最初执行该函数的时候,会执行该路径对函数的got地址进行解析。
此外,对于大多数plt调用来说,一般都不容易崩溃,即使时使用了比较奇怪的参数。所以说,如果我们发现了一系列长度为16的没有使得程序崩溃的代码段,那么我们有一定理由相信我们遇到了plt表。除此之外,我们还可以通过前后偏移6字节,来判断我们是处于plt表项中间还是开头。
(1)控制RDX
当我们找到plt表之后,我们如何确认strcmp的位置呢?需要提前说的是,并不是所有程序都会调用strcmp函数,哪我们就得利用其他方式来控制rdx的值了。这里给出程序中使用strcmp函数的情况。
之前,我们已经找到了brop的gadgets,所以我们可以控制函数的前两个参数了。与此同时,我们定义一下两种地址:
- readable,可读的地址。
- bad,非法地址,不可访问,比如说0x0。
那么我们如果控制传递的参数为这两种地址的组合,会出现以下四种情况:
- strcmp(bad,bad)
- strcmp(bad,readable)
- strcmp(readable,bad)
- strcmp(readable,readable)
只有最后一种格式,程序才会正常执行。
那么我们该如何具体地去做呢?有一种比较直接的方法就是从头到尾一次扫描每个plt表项,但是这个却比较麻烦。我们可以选择如下的一种方法:
- 利用plt表项的慢路径
- 并且利用下一个表项的慢路径的地址来覆盖返回地址。
这样,我们就不用来回控制相应的变量了。
当然,我们也可能碰巧找到strncmp或者strcasecmp函数,它们具有和strcmp一样的效果。
(2)寻找输出函数write@plt
当我们可以控制write函数的三个参数的时候,我们就可以再次遍历所有的plt表,根据write函数将会输出内容来找到对应的函数。需要注意的是,这里有个比较麻烦的地方在于我们需要找到文件描述符的值。一般情况下,我们有两种方法找到这个值:
- 使用rop chain,同时使得每个rop对应的文件描述符不一样。
- 同时打开多个连接,并且我们使用相对较高的数值来试一试。
需要注意的是:
- linux默认情况下,一个进程最多只能打开1024个文件描述符。
- posix标准么此申请的文件描述符数值总是当前最小可用数值。
(3)当然也可以puts@plt
寻找puts函数,我们自然需要控制rdi参数,在上面,我们已经找到了brop gadget。那么,我们根据brop gadget偏移9可以得到相应的gadgets(由ret2libc_csu_init中后续可得)。同时在程序还没有开启PIE保护的情况下,0x400000处为ELF文件的头部,其内容为\x7fELF。所以我们可以根据这个来进行判断。一般来说,其payload如下:
payload = 'A'*length +p64(pop_rdi_ret)+p64(0x400000)+p64(addr)+p64(stop_gadget)
到了这里,攻击者已经可以控制输出函数了,那么攻击者就可以输出.text段更多的内容以便来找到更多合适gadgets。同时,攻击者还可以找到一些其它函数,如dup2或者execve函数。一般来说,攻击者此时会去做如下事情:
- 将socket输出重定向到到输入输出。
- 寻找"/bin/sh"的地址。一般来说,最好是找到一块可写的内存,利用write函数将这个字符串写到相应的地址。
- 执行execve获取shell,获取execve不一定在plt表中,此时攻击者就需要想办法执行系统调用了。
高级ROP
高级ROP其实和一般的ROP基本一样,其主要的区别在于它利用了一些更加低层的原理。
ret2_dl_runtime_resolve
原理
linux中是利用_dl_runtime_resolve(link_map_obj,reloc_index)来对动态链接的函数进行重定位的。如果我们可以控制相应的参数以及其对应地址的内容就可以控制解析的函数。具体利用方式如下:
-
控制程序执行dl_resolve函数
- 给定Link_map以及index两个的参数。
- 当然我们可以直接定plt0对应的汇编代码,这时,我们就需要一个index就足够了。
-
控制index的大小,以便于指向自己所控制的区域,从而伪造一个指定的重定位表项。
-
伪造重定位表项,使得重定位表项所指的符号也在自己可以控制的范围内。
-
伪造符号内容,使得符号对应的名称也在自己可以控制的范围内。
此外,这个攻击成功的很必要条件:
- dl_resolve函数不会检查对应的符号是否越界,它只会根据我们所定的数据来执行。
- dl_resolve函数最后的解析根本上依赖于所给定的字符串。
注意:
-
符号版本信息
- 最好使得 ndx = VERSYM[(reloc->r_info) >> 8] 的值为 0,以便于防止找不到的情况。
-
重定位表项
- r_offset 必须是可写的,因为当解析完函数后,必须把相应函数的地址填入到对应的地址。
具体例子运用可参考ret2_dl_runtime_resolve绕过NX和ASLR的限制
SROP
signal机制
signal机制是类unix系统中进程之间相互传递信息的一种方法。一般,我们称其为软中断信号,或者软中断。比如说,进程之间可以通过系统调用kill来发送软中断信号。一般来说,信号机制常见步骤如下图所示:
- 内核向某个进程发送signal机制,该进程会暂时被挂起,进入内核态。
- 内核会为该进程保存相应的上下文,主要是将所有寄存器压入栈中,以及压入signal信息,以及指向sigreturn的系统调用地址。此时栈的结构如下:
ucontext
singinfo
sigreturn
我们称ucontext以及siginfo这一段为Singal Frame。需要注意的是,这一部分是在用户进程的地址空间的。之后会跳转到注册过的signal handler中处理相应的signal。因此,当signal handler执行完之后,就会执行sigreturn代码。
对于Signal Frame来说,会因为架构的不同而有所区别,这里分别给出x86以及x64的sigcontext。
x86
struct sigcontext
{
unsigned short gs, __gsh;
unsigned short fs, __fsh;
unsigned short es, __esh;
unsigned short ds, __dsh;
unsigned long edi;
unsigned long esi;
unsigned long ebp;
unsigned long esp;
unsigned long ebx;
unsigned long edx;
unsigned long ecx;
unsigned long eax;
unsigned long trapno;
unsigned long err;
unsigned long eip;
unsigned short cs, __csh;
unsigned long eflags;
unsigned long esp_at_signal;
unsigned short ss, __ssh;
struct _fpstate * fpstate;
unsigned long oldmask;
unsigned long cr2;
};
x64
struct _fpstate
{
/* FPU environment matching the 64-bit FXSAVE layout. */
__uint16_t cwd;
__uint16_t swd;
__uint16_t ftw;
__uint16_t fop;
__uint64_t rip;
__uint64_t rdp;
__uint32_t mxcsr;
__uint32_t mxcr_mask;
struct _fpxreg _st[8];
struct _xmmreg _xmm[16];
__uint32_t padding[24];
};
struct sigcontext
{
__uint64_t r8;
__uint64_t r9;
__uint64_t r10;
__uint64_t r11;
__uint64_t r12;
__uint64_t r13;
__uint64_t r14;
__uint64_t r15;
__uint64_t rdi;
__uint64_t rsi;
__uint64_t rbp;
__uint64_t rbx;
__uint64_t rdx;
__uint64_t rax;
__uint64_t rcx;
__uint64_t rsp;
__uint64_t rip;
__uint64_t eflags;
unsigned short cs;
unsigned short gs;
unsigned short fs;
unsigned short __pad0;
__uint64_t err;
__uint64_t trapno;
__uint64_t oldmask;
__uint64_t cr2;
__extension__ union
{
struct _fpstate * fpstate;
__uint64_t __fpstate_word;
};
__uint64_t __reserved1 [8];
};
- signal handler返回后,内核执行sigreturn系统调用,为该进程恢复之前保存的上下文,其中包括将所有压入的寄存器,重新pop回对应的寄存器,最后恢复进程的执行。其中,32位sigreturn的调用号为77,64位的系统调用号为15。
攻击原理
内核在signal信号处理的过程中,主要做的工作就是为进程保存上下文,并且恢复上下文。这个主要的变动都在Signal Frame中。但是需要注意的是:
- Signal Frame被保存在用户的地址空间中,所以用户是可以读写的。
- 由于内核与信号处理程序无关,它并不会去记录这个signal对应的Signal Frame,所以当执行sigreturn系统调用时,此时的Signal Frame并不一定是之前内核为用户进程保存的Signal Frame。
举两个简单的例子:
获取shell
首先,我们假设攻击者可以控制用户进程的栈,那么它就可以伪造一个Signal Frame,如下所示:
当系统执行完sigreturn系统调用之后,会执行一系列的pop指令以便于恢复相应寄存器的值,当执行到rip时,就会将程序执行流指向syscall地址,根据相应寄存器的值,此时,便会得到一个shell。
system call chains
上面的例子只是单独的获得一个shell。有时候,我们可能会希望执行一系列的函数。我们只需要做两处修改即可:
- 控制栈指针
- 把原来rip指向的syscall gadget换成syscall;ret gadget。
如下图所示,这样当每次syscall返回的时候,栈指针都会指向下一个Signal Frame。因此就可以执行一系列的sigreturn函数调用。
攻击条件
我们在构造ROP攻击的时候,需要满足下面的条件:
-
可以通过栈溢出来控制栈的内容
-
需要知道相应的地址
- "/bin/sh"
- Signal Frame
- syscall
- sigreturn
-
需要有足够大的空间来塞下整个sigal frame
值得一说的是,对于sigreturn系统调用来说,在64位系统中,sigreturn系统调用对应的系统调用号为15,只需要RAX=15,并且执行syscall即可实现抵用syscall调用。而RAX寄存器的值又可以通过控制某个函数的返回值来间接控制,比如说read函数的返回值为读取的字节数。
ret2VDSO
VDSO(Virtual Dynamically-linked Shared Object),虚拟动态链接共享对象,所以它应该是虚拟的,与虚拟内存一致,在计算机中本身并不存在。具体来说,它是内核态的调用映射到用户地址空间的库。那么它为什么会存在呢?这是因为有些系统调用经常被用户使用,这就会出现大量的用户态和内核态切换的开销。通过vdso,我们可以大量减少这样的开销,同时也可以使得我们的路径更好。这里的路径更好指的是,我们不需要使用传统的int 0x80来进行系统调用,不同的处理起实现了不同的快速系统调用指令:
- intel是实现了sysenter,sysexit.
- amd实现了syscall,sysret
当不同的处理器架构实现了不同的指令时,自然就会出现兼容性问题,所以linux实现了vsycall接口,在底层会根据具体的结构来进行具体操作。而vsycall就实现在vdso中。
这里,我们顺便来看一下vdso,在linux(kernel 2.6 or upper)中执行ldd /bin/sh,会发现有一个名字叫linux-vdso.so.1的动态文件,而系统中却找不到它,它就是VDSO。
ki@ki-virtual-machine:/mnt/hgfs/gx16$ ldd /bin/sh
linux-vdso.so.1 => (0x00007ffea8bf3000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f2448d63000)
/lib64/ld-linux-x86-64.so.2 (0x00007f2449355000)
除了快速系统调用,glibc也提供了VDSO的支持,open(),read(),write(),gettimeofday()都可以直接使用VDSO中的实现。使得这些调用速度更快。内核新特性在不影响glibc的情况下也可以更快的部署。
这里我们以intel的处理器为例,进行简单说明。
其中sysenter的参数传递方式与int 0x80一致,但我们可能需要自己布置好function prolog(32位为例)
push ebp
mov ebp,esp
此外,如果我们没有提供functtion prolog的话,我们还需要一个可以进行栈迁移的gadgets,以便于可以改变栈的位置。
ROP Tricks
stack pivoting
stack pivoting,正如它所描述的,该技巧就是劫持栈指针指向攻击者所能控制的内存处,然后再在相应的位置进行ROP。一般来说,我们可能在一下情况需要使用stack pivoting:
- 可以控制的栈溢出的字节数较少,难以构造较长的TOP链。
- 开启了PIE保护,栈地址未知,我们可以将栈劫持到已知的区域。
- 其他漏洞难以利用,我们需要进行转换,比如说将栈劫持到堆空间,从而在堆上写rop以及进行对漏洞利用。
此外,利用stack pivoting有以下几个要求
- 可以控制程序流。
- 可以控制sp指针。一般来说,控制栈指针会使用ROP,常见的控制栈指针的gadgets一般是
pop rsp/esp
当然,还会有一些其他的姿势,比如说libc_csu_init中的gadgets,我们通过偏移就可以得到控制rsp指针。
上面是正常的,下面是偏移的。
gef➤ x/7i 0x000000000040061a
0x40061a <__libc_csu_init+90>: pop rbx
0x40061b <__libc_csu_init+91>: pop rbp
0x40061c <__libc_csu_init+92>: pop r12
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
gef➤ x/7i 0x000000000040061d
0x40061d <__libc_csu_init+93>: pop rsp
0x40061e <__libc_csu_init+94>: pop r13
0x400620 <__libc_csu_init+96>: pop r14
0x400622 <__libc_csu_init+98>: pop r15
0x400624 <__libc_csu_init+100>: ret
此外,还有更高级的fake frame。存在可以控制内容的内存,一般有如下:
- bss段。由于进程按页分配内存,分配给bss段的内存大小至少一个页(4K,0x1000大小)。然而一般bss段的内容用不了这么多的空间,并且bss段分配的内存页拥有读写权限。
- heap。但是这个需要我们能够泄露堆地址。
frame faking
正如这个技巧名字所说的那样,这个技巧就是构造一个虚假的栈帧来控制程序额的执行流。
概括地讲,我们在之前讲的栈溢出不外乎两种方式
- 控制程序EIP
- 控制程序EBP
其最终都是控制程序的执行流。在frame faking中,我们所利用的技巧便是同时控制EBP与EIP,这样我们在控制程序执行流的同时,也改变了程序栈帧的位置。一般来说其payload如下:
buffer padding|fake ebp|leave ret addr|
这个原理介绍的文章很多,就不再做解释。
在fake frame中,有一个需求就是,我们必须得有一块可以写的内存,并且我们还知道这块内存的地址,这点与stack pivoting相似。
Stack smash
在程序加了canary保护之后,如果我们读取的buffer覆盖了对应的值时,程序就会报错,而一般来说我们并不会关心报错信息。而stack smash技巧则就是利用这点打印这一信息的程序来得到我们想要的内容。这是因为在程序启动canary保护后,如果发现canary被修改的话,程序就会执行__stack_chk_fail函数来打印argv[0]指针所指向的字符串,正常情况下,这个指针指向了程序名。其代码如下:
void __attribute__ ((noreturn)) __stack_chk_fail (void)
{
__fortify_fail ("stack smashing detected");
}
void __attribute__ ((noreturn)) internal_function __fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
while (1)
__libc_message (2, "*** %s ***: %s terminated\n",
msg, __libc_argv[0] ?: "<unknown>");
}
所以说如果我们利用栈溢出覆盖argv[0]为我们想要输出的字符串的地址,那么在__fortify_fail函数中就会输出我们的想要的信息。
栈上的partial overwrite
partial overwrite这种技巧在很多地方都适用,这里先以栈上的partial overwrite为例介绍。
我们知道,在开启了随机化(ASLR,PIE)后,无论高位的地址如何变化,低12位的页内偏移始终是固定的,也就说如果我们能更改低位偏移,就可以在一定程度上控制程序的执行流,绕过PIE保护
内容来源
CTF Wiki Stack Introduction
CTF Wiki Stack Overflow Principle
CTF Wiki Basic ROP
CTF Wiki Intermediate ROP
CTF Wiki Advanced ROP
CTF Wiki ROP Tricks