B站pwn教程笔记-9

发布于:2025-05-10 ⋅ 阅读:(27) ⋅ 点赞:(0)

前言:可以去一些开源镜像站下载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

具体了解堆,且听下回分解


网站公告

今日签到

点亮在社区的每一天
去签到