ROP及栈溢出
ROP
ROP即返回导向的编程
我认为就是不断的去思考代码的作用和计算位置,调试中一步步确定下的代码,这样的手段用于在一个已经成型的程序上调试并写出利用代码,是一种攻击技术
这一篇是结合经典文档:《蒸米的一步步学ROP》自己的笔记
排布首先是栈溢出的简述,然后就是按照攻击的思路去划分出的知识框架。
文章目录
栈溢出
首先还是先介绍有名的栈溢出漏洞,
首先是程序接受的输入数据,一般是保存到栈中,而程序运行时调控变量参量和函数调用的主要工具就是栈,(eax一直用于返回值传递,在64位中寄存器会参数参与参数传递),
栈中的栈帧结构保证函数的调用,
栈帧:
栈内的一块区域,是函数调用时形成的,作用就是保存和该函数有关的参数变量返回地址等,且一个函数能够控制的栈内空间也只有自己的栈帧, 这样也就实现了函数之间的隔离
也辅助程序的流程控制,
程序运行流程
首先程序运行时是永远在运行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:
格式:
payload = 'a' * xx + p64(gadget_addr) + p64(rdi) + p64(func_addr)
格式:
payload = 'a' * xx + p64(gadget) + p64(func_addr) + p64(rdi)
一个rdi适合一个参数的函数传参,比如喜闻乐见的system('/bin/sh')
三个参数的gadget
这是加载libc使用的函数,在64位下一定会存在在程序中,
这个函数后段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
这一段可以修改掉七个寄存器,其中的六个是我们64位程序中参数传递使用的,但是最后的jmp的值是最开始的rax,所以要先得到一个修改rax的gadget:
关于这个gadget的位置:
在函数.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写入。然后跳转过去执行。
下一篇: 反弹Shell的方式和详解