欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

ROP及栈溢出

程序员文章站 2022-03-09 22:43:03
...

ROP

ROP即返回导向的编程

我认为就是不断的去思考代码的作用和计算位置,调试中一步步确定下的代码,这样的手段用于在一个已经成型的程序上调试并写出利用代码,是一种攻击技术

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

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

栈溢出

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

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

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

栈帧:

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

ROP及栈溢出

也辅助程序的流程控制,

程序运行流程

首先程序运行时是永远在运行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及栈溢出

格式:

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

ROP及栈溢出

格式:

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

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

三个参数的gadget

这是加载libc使用的函数,在64位下一定会存在在程序中,
ROP及栈溢出

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

格式:

payload = ''
payload += 'a' * xx + p64(gadget1_addr) + p64(0) 
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及栈溢出

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

ROP及栈溢出

关于这个gadget的位置:

ROP及栈溢出

在函数.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 rop