rop [ 一] 栈溢出和简单ROP思路

ROP

ROP即返回导向的编程

主要是用在栈溢出
在栈堆不可执行开启后,栈溢出不能简单的向栈中写入shellcode利用jmp rsp运行,因此开发出的一种栈溢出利用手段,主要的意思就是修改ret的位置,运行到一些程序内部的代码片段(gadget)的位置,然后通过这些去控制程序的栈空间,来控制:寄存器,参数传递,函数调用,
由于gadget的使用要是最后ret结尾,由此成为面向ret的编程。

这一篇是结合经典文档:《蒸米的一步步学ROP》自己的笔记

排布首先是栈溢出的简述,然后就是按照攻击的思路去划分出的知识框架。

文章目录

  • ROP
  • 栈溢出
    • 栈帧:
    • 程序运行流程
    • 漏洞
  • 利用
    • 修改ret
      • 后门函数
      • ELF中查找函数
    • 参数传递
      • 32位参数传递 ---> 栈帧
      • 64位参数传递 ---> gadgets
        • 一个参数的gadget:
        • 三个参数的gadget
        • 六个参数的gadget
      • 比较常见的函数调用的参数 32/64
    • shellcode
      • 写在栈里 ---> gadget jmp rsp/esp
      • 写在内存 ---> mmap函数调用

栈溢出

首先还是先介绍有名的栈溢出漏洞,

首先是程序接受的输入数据,一般是保存到栈中,而程序运行时调控变量参量和函数调用的主要工具就是栈,(eax一直用于返回值传递,在64位中寄存器会参数参与参数传递),

栈中的栈帧结构保证函数的调用,

栈帧:

栈内的一块区域,是函数调用时形成的,作用就是保存和该函数有关的参数变量返回地址等,且一个函数能够控制的栈内空间也只有自己的栈帧, 这样也就实现了函数之间的隔离

rop [ 一] 栈溢出和简单ROP思路_第1张图片

也辅助程序的流程控制,

程序运行流程

首先程序运行时是永远在运行rip/eip寄存器指向的指令,而我们的程序运行的底层汇编语言永远不会直接改变这个寄存器,而是通过jmp,call,ret,各种条件的jmp,来间接的影响rip/eip寄存器,而影响程序的运行,

jmp和条件的jmp类:

都可以其后直接跟一个地址,直接跳转到该位置运行(其实相当于直接给rip/eip赋值了)

call:

函数调用指令,调用其实相当与jmp,但还需要顾及调用函数需要返回,这里就用到了栈保存这个返回地址,
相当与push rip+xx; jmp xxx,这里就是将原本该call下一个语句的rip保存到栈,然后跳转到目标位置,

ret:
函数执行完毕后的返回指令,从栈中取出call指令保存的返回地址,然后跳转过去执行,

相当与pop jmp,或者就pop rip/eip

漏洞

而将用户输入的信息传入栈中是一个比较危险的事情,

因为我们的返回地址同样保存在栈中,当程序读入的值超过了该函数的栈帧空间的大小,就有可能导致输入的数值覆盖掉保存的返回地址,也就是可以构造一个输入,随意的修改返回地址,修改ret指令执行后rip/eip的值,从而控制程序流程,

利用

栈溢出的利用主要就是修改ret控制程序流程,从而让我们的程序执行我们想让其执行的代码。

这就牵扯到一个问题 q1: 将ret改成啥?

将ret修改调用函数时又会出现另一个问题 q2:修改了以后我们调用函数时需要传入的参数怎么改?

最后假如我们的程序和导入的库中不能够得到我们想要的函数呢,q3:如何写一个shellcode并使其执行。

修改ret

后门函数

一般很简单的pwn会出现这种类型,就是一个比较典型的后门函数,直接计算距离然后就可以修改ret运行到对应的函数直接getshell或者catflag的:

payload = 'a' * xxx + p64(ret_addr)/p32(ret_addr)

ELF中查找函数

我们首先说到pwntools的ELF函数,导入一个elf文件,并允许从其中查找相应的地址。

常用来导入我们的程序和该程序用到的libc.so文件

这个就需要一定的理解了,想要理解需要一些关于动态链接和重定向的知识。

elf = ELF('levelxx')
libc = ELF('libc.so')

从其中查看值和查看函数地址,

func_got = elf.got['function']
func_addr = elf.symbol['function']
str_addr = next(elf.search('string'))

其中这个elf.search('string')将会返回一个类似迭代器的东西,我们加上next()就可以的得到地址了,

但是要注意的是这里得到的地址都是文件中得到的,并不是在运行时的内存里,当程序运行起来的时候,当开启PIE(ASLR)程序运行时的地址会随机化。

但是我们可以使用相对地址,这个在底层的运用极为普遍。

首先我们的libc中的地址两个函数之间的相对地址或者说相对距离是一致的,我们只要知道一个函数的地址,就可以得到其他地址,

这里的公式:

fun2地址等于 泄漏出来的fun1地址 减去libc中fun1和fun2的差值(相对地址)

fun2_addr = leak_fun1_addr - (libc_fun1_addr - libc_fun2_addr)

func_addr = write_addr - (libc.symbols['write'] - libc.symbols['function'])
str_addr = write_addr - (libc.symbols['write'] - next(libc.search('/bin/sh'))

其中的write_addr是write函数泄漏出来的地址,一般如果没有直接给出一个内存地址的话我们首先是通过write去泄漏,这时候也就直接泄漏出来write的地址,然后去计算其他的地址,

另外需要注意,只要是调用的libc中的地址就需要泄漏地址然后计算,如果只是程序自身的并不需要。

参数传递

调用函数时,特别是去调用libc中的函数和泄漏地址使用write时的参数都是需要我们去控制的。

32位参数传递 —> 栈帧

32位程序通过栈来传递参数,

所以我们也可以相应的构造,例:

payload = 'a'*xxx + p32(write_plt) + p32(ret_addr) + p32(1) + p32(got_write) + p32(4)

这个是使用write函数去泄漏出来libc中的write的地址的payload,

这里要注意的是p32(ret_addr),这个位置是写的是函数write调用完以后的再次的返回地址,

32位含参的调用,payload大概的格式如下:

payload = 'a' * xxx + p32(fun1_addr) + p32(fun2_addr) + p32(arg_1) + p32(arg_2)....

64位参数传递 —> gadgets

64位程序的前六个参数传递是靠的寄存器,

rdi, rsi, edx, ecx, r8, r9

我们仅能控制栈中的数据,因此需要pop之类的指令辅助,这里工具ROPgadget, 使用:

ROPgadget --binary levelxx --only 'xxx'
ROPgadget --binary levelxx --only 'xxx' | grep xx

其中的levelxx是我们查找的程序,而‘xxx’是我们想查找的gadget, 还可以使用管道搭配grep细化查找,

一般使用的’pop | ret’ ‘pop|call’

注意参数中如果使用地址,和前面函数调用使用地址规则一致,涉及到libc中的地址必须计算,使用程序自身的不需要计算。

一个参数的gadget:

rop [ 一] 栈溢出和简单ROP思路_第2张图片

格式:

payload = 'a' * xx + p64(gadget_addr) + p64(rdi) + p64(func_addr)

rop [ 一] 栈溢出和简单ROP思路_第3张图片

格式:

payload = 'a' * xx + p64(gadget) + p64(func_addr) + p64(rdi)

一个rdi适合一个参数的函数传参,比如喜闻乐见的system('/bin/sh')

三个参数的gadget

这是加载libc使用的函数,在64位下一定会存在在程序中,
另外注意自己的文件中的r13.r12等位置, 不同的版本等可能会导致中间位置不对,写exp使用设置好的模板或自定义代码片段的话,注意审查一下。
rop [ 一] 栈溢出和简单ROP思路_第4张图片

这个函数后段0x400606以后可以通过栈内数据影响到几个寄存器,为gadget1,然后返回地址设置为0x4005f0, 就可以通过这几个寄存器再修改其他的寄存器,为gadget2,

格式:

payload = ''
payload += 'a' * xx + p64(gadget1_addr) 
payload += p64(rbx) + p64(rbp) + p64(r12-call) + p64(r13-edi) + p64(r14-rsi) + p64(r15-rdx) 
payload += p64(gadget2_addr) + 'a' * 56 + p64(ret_addr)

这个函数可以适合三个参数传递的调用,比如write,read,

六个参数的gadget

rop [ 一] 栈溢出和简单ROP思路_第5张图片

这一段可以修改掉七个寄存器,其中的六个是我们64位程序中参数传递使用的,但是最后的jmp的值是最开始的rax,所以要先得到一个修改rax的gadget:

rop [ 一] 栈溢出和简单ROP思路_第6张图片

关于这个gadget的位置:

rop [ 一] 栈溢出和简单ROP思路_第7张图片

在函数.plt中,后面jmp的 0x600ff8中就保存这个_dl_runtime_resolve的地址,然后我们的gadget地址在这个地址+53的位置,但要注意这个位置是需要计算的。

就可以配合使用修改六个参数,比如mmap

比较常见的函数调用的参数 32/64


wirte(1, addr, length) //标准输出

read(0, addr, length)  //标准输入

system('/bin/sh') //getshell`

mmap(addr, length, 7, 34, 0, 0)//开辟RWX的内存

//参数顺序   rdi, rsi, edx, ecx, r8, r9 

shellcode

写在栈里 —> gadget jmp rsp/esp

一般会找到一个jmp rsp的gadget就可以直接运行,或者写在前面,然后栈迁移sub rsp,xxx; jmp rsp

写在内存 —> mmap函数调用

写内存中有时候可以直接靠一个read写入,然后ret过去运行,

但是有时候需要自己调用i一个mmap开辟一个内存,注意参数三的7,为了保证有读写和运行权限,再一次read写入。然后跳转过去执行。

你可能感兴趣的:(pwn)