pwn:从一窍不通到入门的门前
基础pwn例题,栈溢出,格式化字符串。
栈溢出例题
hello_pwn
来源:攻防世界
首先先checksec:
再看伪代码:
再看偏移量:
这里基本就明白了:当dowrd_60106C=1853186401时,执行下面的sub函数,sub函数就直接得出flag了,因此只要再前一步的read函数那里创造栈溢出,再人为满足条件判断即可。
exp:
from pwn import * |
level3
来源:攻防世界
checksec:
32位程序,开启堆栈不可执行,这是一道构造ROP的经典例题。
源码看出漏洞还是栈溢出,但是此时找不到现成的system函数和binsh字符串。
因此要找到system函数,知识点(搬运自wiki)
- system 函数属于 libc,而 libc.so 动态链接库中的函数之间相对偏移是固定的。
- 即使程序有 ASLR 保护,也只是针对于地址中间位进行随机,最低的 12 位并不会发生改变。
- 查找Libc的网址:https://libc.blukat.me/和https://libc.rip/
简单来说:实际的(真实的)地址是需要计算出来的,具体计算方式是:实际地址=libc偏移量+libc基地址在已知libc的情况下,我们需要做的就是找到libc的基址,而想要找到基址,我们需要知道一个函数的实际地址,本题中关键函数内存在write,read函数,因此想法是使用write函数泄露出write函数的真实地址,而plt表,got表,实际加载地址可以参考下图(搬运自csdn(滑稽)):
这样就可以得到基本的思路了: 先构造栈溢出使程序跳到write函数的plt表地址上,此时再次调用main函数或者vulnerable函数再次执行,此时就可以利用write函数打印出write的got表地址,此时需要我们人为的输入write函数的参数。这样就可以得到write函数的真实地址了。栈上的流程大概是这样的(仍然是搬运的(滑天下之大稽):
这样输出之后我们就得到了write函数的真实地址了,再通过libc来求得system和binsh的真实地址,最后第二次调动程序,再次利用漏洞,就可以得到shell。
exp:
from pwn import * |
格式化字符串
基本原理以及利用方法在wiki上有解释。
<原理介绍 - CTF Wiki (ctf-wiki.org)>
主要意思是利用printf一类函数漏洞,当函数没有严格规定格式化字符串时,攻击者可以控制printf函数来输出其他内容,由于格式化字符串输出的是栈上内容,因此此漏洞可以找到栈上其他位置的信息,比如地址,数据。总结一下就是:超出的每一个格式化字符串都能对下一格栈空间造成影响。
%d:打印成数字,只能泄露栈本身内容
%x:打印成16进制,只能泄露栈本身内容
%s:识别栈上内容为指针,打印出该指针指向的位置,这里有可能造成任意地址的泄露(只要能控制栈并完成布局)
%c:输出字符
%n:把前面已经打印的长度写入某个内存地址(实现任意地址写入)
%n:写入4个字节,写入1—>0000 0001
%hn:写入两个字节,写入1—>0001
%hhn:写入单个字节,写入1—>01
堆学习
这里主要是学习了ptmalloc2 – glibc的堆实现和堆利用。首先堆是由低地址向高地址增长,可读可写的数据结构
堆的相关数据结构
/* |
可以看出在一个chunk中有这样几个数据
- prev_size: 如果该 chunk 的物理相邻的前一地址 chunk(两个指针的地址差值为前一 chunk 大小)是空闲的话,那该字段记录的是前一个 chunk 的大小 (包括 chunk 头)。否则,该字段可以用来存储物理相邻的前一个 chunk 的数据。这里的前一 chunk 指的是较低地址的 chunk 。也就是说,上一个chunk空闲,表示大小,调用,则表示值。
- size:该chunk的大小,大小必须是 2 SIZE_SZ 的整数倍。如果申请的内存大小不是 2 SIZE_SZ 的整数倍,会被转换满足大小的最小的 2 SIZE_SZ 的倍数。32 位系统中,SIZE_SZ 是 4;64 位系统中,SIZE_SZ 是 8。 也就是说64位size是16的整数倍,32位size是8的整数倍,这里就说明无论是32还是64位,size的低三位都一定是0,这样的话在chunk中这三位可以表示其他信息,分别从高到低表示为A,M,P。A表示该chunk是否属于主线程,1表示不属于,0表示属于;M表示记录当前chunk是否由mmap分配;P表示记录前一个 chunk 块是否被分配。一般来说,堆中第一个被分配的内存块的 size 字段的 P 位都会被设置为 1,以便于防止访问前面的非法内存。当一个 chunk 的 size 的 P 位为 0 时,我们能通过 prev_size 字段来获取上一个 chunk 的大小以及地址。这也方便进行空闲 chunk 之间的合并。
- fd和bk:fd表示指向下一个空闲的chunk,bk表示指向上一个空闲的chunk,这里要注意的是上下表示的是空闲的顺序而不是地址上的相邻。通过这里两个指针可以让空闲的chunk块加入到chunk块列表统一管理
- fd_nextsize,bk_nextsize:只用chunk空闲才使用,但用于较大的chunk,fd_nextsize指向前一个与当前chunk大小不同的第一个空闲块,不含bin指针;bk_nextsize指向后一个与当前chunk大小不同的第一个空闲块,不含bin指针。
前两个字段称为chunk header,后面的部分称为user data。每次malloc申请得到的内存指针,是指向user data的起始处。
当一个 chunk 处于使用状态时,它的下一个 chunk 的 prev_size 域无效,所以下一个 chunk 的该部分也可以被当前 chunk 使用。这就是 chunk 中的空间复用。
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
被释放的chunk被记录在链表中,具体结构如下:
chunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |
bin是什么
首先,用户释放的chunk不会马上归还给系统,ptmalloc会统一管理heap和mmap映射区域中空闲的chunk,用户再次请求分配内存时,ptmalloc会试图在空闲的chunk中挑选一块合适的给用户,可避免频繁地系统调用。ptmalloc会将空闲chunk的大小以及使用状态将chunk分为4类,fast bins, small bins, large bins, unsorted bin。相似大小的chunk会用双向链表链接起来,也就是说每类bin的内部会有多个互不相关的链表来保存不同大小的chunk。