B站pwn教程笔记-9
前言:可以去一些开源镜像站下载libc老的乌班图镜像,因为堆题的libc可能比较老,没有新的一些保护措施和机制。
格式化字符串漏洞
归根结底,可以读写任意地址内存。
泄露栈数据/任意地址数据
主要问题就是printf不知道自己有没有参数,只会根据格式化字符个数在栈上往上读取,就算不是自己的参数也读取。
如果%s过多,没有足够参数情况下,程序就会崩溃
%x用来输出对应数据的16进制(不加0x的16进制)%p是把栈数据当作指针来输出,和%x相比前面有0x,所以一般也常用%p泄露。如下图,2者差不多。%p太多了,有可能给之前某个函数的canary也泄露了。
如果printf("%p","hello"),实际上入栈的是hello对应的地址。C语言传递字符串就是传递它的地址的,函数参数传递就是如此,不可能直接把数据压栈的。
这也是C语言用指针不安全的一点,比如字符串截断漏洞。将对应地址的字符串的\x00删除或篡改,puts函数这样的输出函数在输出对应地址内容的时候,就会不遇到\x00不停止,一直输出后面的内容。相应的,配合strlen和read(xxx,str,str_len)还可以篡改字符串数据。
因此不难理解,%s则是会读取这段地址,尝试解析为字符串。其实这样也有一个好处,比如栈上存放的got表自己的地址,%s读取后,就是got表内存放的数据的地址了,达到泄露got表的目的。
而%n$d,表示这是第n个参数,并且按照有符号整数打印出来。
那么,格式化字符串自己存放在什么地方呢?如果程序没有刻意搞bss之类存放,那么应该就是在栈上存放,在参数那个栈的地方存放其地址,见下图
因此,我们可以在栈上利用格式化字符串这一特性,在栈上写入敏感数据的地址,而后利用格式化字符串读取,见下图
由于给里面的地址超长了(x86一个栈是4字节),因此%6$s会在下一个栈的区域存放。
这里需要注意,0x00402004会被打印出,具体解释请看下图。
篡改栈数据/任意地址数据
需要用%n,他也是和%s一样解析,但不同的是它是向地址写入,写入前面格式化字符打印成功的个数。但是如果我们要用这个写入很大的数据,必须在前方打印足够长的字节吗?
可以用格式化字符串控制,比如%20d,意思是宽度是20的一个d,这样就算打印出来了一个变量,他也会认为前方已经打印出20个字符了。同时%n也支持%4$n这样的语法。
由于%n不是直接修改栈上的值,想要修改栈的值还得是通过%p泄露EBP之类的值,再利用%n来操作。
这里要注意,%n默认是4字节整数写入。%hn是2字节,%hhn是一个字节(h代表half)
例题讲解
fmtstr1
有canary,估计无法栈溢出。看源码估计也是格式化字符串。因为第10行的printf函数的格式化参数我们是完全可控的。
老师的思路是先调试一下看看。我们得看看x的地址(因为是%n)对于printf来说是第几个参数才行,ida看不出来这,必须得动调。
易错点:printf的第一个参数是格式化字符串,格式化字符串的参数是%n,%s之类。也就是printf的第二个参数才是格式化字符串的第一个参数。以此类推,主要是格式化字符串也是一个参数,他的地址同样入栈,千万不要搞混淆。
同时,格式化字符串中的%n$的n指的是相对于格式化字符串的参数的意思,也就是参数是第N个printf的参数减去1。
更加具体的介绍,下面是程序执行流停在了call printf。这里可以看出esp指向的AAAA就是压栈的格式化字符串地址,这个参数实际上是储存在了eax那一行的。从上往下数刚好是第12个(针对于printf而言)而格式化字符串里面肯定是11$.
exp可以证明这一点:
goodluck
这是个64位程序。
程序竟然让我们自己输入flag,他进行比对。这肯定是不行的。format完全是我们控制,所以这也是格式化字符串漏洞的题目。我估计要想办法泄露fp的地址把。实际上v10保存着flag字符串,应该是泄露v10(刚好他也在栈上)
ms的用法:
在 C 语言里,
scanf
函数的%ms
格式说明符是一个拓展功能,主要用于动态分配内存来存储输入的字符串。下面为你详细介绍它的功能和用法:主要功能
- 动态内存分配:
%ms
会依据输入字符串的实际长度,自动分配足够的内存空间,这样就无需提前指定缓冲区的大小。- 自动字符串终止:和
%s
一样,%ms
会在读取到空白字符(例如空格、制表符、换行符)时停止,并会自动在字符串末尾添加\0
作为结束符。- 返回分配的指针:成功读取输入后,
%ms
会把分配的内存地址存储到对应的指针变量中。关键要点
- 需要指针参数:调用
scanf
时,传给%ms
的参数必须是一个char**
类型的指针,也就是一个指向字符指针的指针。- 内存管理:使用完分配的内存后,要记得用
free()
函数释放内存,防止出现内存泄漏。- 输入截断:
%ms
会一直读取,直到遇到空白字符或者文件结束符(EOF),这一点和%s
是相同的。
这就有意思了,因为泄露任意地址数据肯定要保证格式化字符串在栈上呀 。但是无关紧要,我们只需要泄露栈上的v10。
注意64位printf的参数。首先第一个参数格式化字符串在rdi寄存器,现在我们知道前6个参数肯定在寄存器,第六个才入栈,明显printf也会按照这样的顺序来读取。
可以直接正常执行程序输入%7$p,看看程序泄露出来的第七个参数的地址。当然这样一眼不一定能看出来,可以在输入89多对比看看。因为一般具有栈地址随机化,但是栈上某些部分的值却是特殊的,可以多打印几个明显看出来。
一句话就是由于传参方式不一样,不能直接通过在栈上数参数来找偏移了,必须试出来按正常执行的第七个参数究竟在什么地方。
其实这道题经过我的观察也可以直接从x64入栈方式来找偏移,第七个参数按理来说就是靠近addr的那个栈保存的。根据此数出来的偏移也刚好符合题意,实战中二者结合使用吧。
堆引入
想要解决第三题,必须有堆的基础,接下来就认识一下堆。实际的漏洞栈多于堆,但是CTF中堆的题目数量是大于栈的。
shared这块就是mmap段。
一般linux就是glibc。GNU协会搞linux的软件环境,所以这linux很多地方都有g的身影。
堆管理器是用户态代码,所以必须通过系统调用和内存这方面硬件建立联系。
在Linux进程的虚拟内存布局中,数据段包含已初始化和未初始化的全局及静态变量,其大小在程序加载时就已确定,运行过程中无法改变。而堆位于数据段之后,是动态内存分配的区域,`brk`系统调用并非直接扩展数据段,而是通过调整堆的结束地址(`break`指针)来实现堆空间的动态扩展与收缩。当程序调用`malloc`请求内存,若现有堆空间不足,`malloc`就可能通过`brk`系统调用增加`break`指针的值,使堆向高地址方向延伸;当释放大量内存时,`break`指针可能减小,但内核通常不会立即将内存归还系统,而是保留以供后续分配 。--豆包
mmap有意思了,map就是映射,也就是在物理内存开辟一块空间,映射到相应虚拟内存。
主线程两种方法都可以(申请内存空间相对大就用mmap,否则是brk),子线程只能用mmap。释放内存则是free
具体了解堆,且听下回分解