level 1
由于堆的内容我之前没学过,我打算写的详细些。
use-after-free漏洞
这个漏洞的利用方式和fastbin/tcache相关,但是在目前,我们不用做的非常复杂。
首先看这个题目,它提供了四种操作(malloc/free/puts/read_flag)
其中 puts函数会打印ptr中的内容,malloc用于申请空间,read_flag会申请一个新的空间,然后把flag放进去。
所以我说在这一题我们不必要考虑那么复杂。
对于use-after-free漏洞。我们可以就简单理解成,如果我们申请了一个空间,然后把他free掉,然后再申请一个大小相同的空间,这时候新的空间会利用旧的我们free掉的空间的相同位置。
但是事实上,这背后的实现会比这个稍微复杂。
综上,我们的思路就是:
- 先malloc一个218字节的空间,我们获得了一个地址A。
- free(A)。
- read_flag 这时候flag就会存放在A中。
- puts(A)
这时候flags就会被成功打印。
可以看到题目给的信息确实符合上述描述
level 2
读取flag malloc的空间变成了随机的,但其实也差不多。因为每次malloc大小仍然是一样的
因此我们只需要读取一次之后,再重复level1的操作。
第二关我们失去了给出的malloc大小,但无所谓,我们malloc两次即可获得大小,但要注意,每个chunk的结构中都会有额外的16字节数据,一个用来存储上一个chunk用的空间,另一个用来存储该chunk用的空间
分别叫做prev_size和size 他们会用掉16bytes,因此还要在两个地址差的基础上减去16bytes
level 3
flag存放在程序第二次malloc的空间中。这时候就需要了解一个tcache的机制。
tcache是一堆单链表,根据空间大小分类,同时这些链表用一个存储着每个单链表表头的链表连接起来。
每次我们free掉一个空间,例如空间A,
这个空间中存储数据的区域就会被改成用来存储tcache数据,但是A的地址并不会改变。
然后A会被插入到对应单链表的起点,其中的next指针就会指向上一次free的相同大小的空间的地址。如果还没有上一个free的内容,就会存储NULL
同时,我们如果再次申请了相同大小的空间,程序就会利用tcache中的内容,假设这时候链表的起点就是A,A会被重新利用来存储数据,
同时它会被链表删除,这时候会有个奇怪的事情发生,如果我们只是重新malloc一个相同大小的区域而不填入数据,
key会被重置,但是next并没被重置。
后面的漏洞会利用到这个性质,目前我们并不需要这个性质。
总体上来看,这玩意很像一个栈,相同大小的块被塞到同一个链表中,后free的会被先利用。
综上,整理思路:
- A=malloc(215) |
- free(A)
- B=malloc(215)
- free(B)
此时tcache:
tcache |
---|
B |
A |
- read_flag
按照规则 flag就会被填入A中
- puts(A)
在本地调试时,如果你看过最后一个视频,会知道tcache里的数据已经是被混淆过了的。
这不妨碍我们的漏洞利用,但很显然,如果我们要做后面的题,就不能再本机进行调试,或者我们用旧版的libc
这个安全机制也是最新引进的 通过在本地和在pwncollege测试,你就会发现他们之间的差距。
level 4
这一题涉及到了前面说的tcache的知识
我原来的想法:
单链表要做到删除元素,对于tcache,这并不复杂,把head变成head->next即可。
于是我打算free之后覆盖next为该块的地址。这样tcache就会再连接这个块。
后来我发现了问题,是这个链表的长度还决定于count参数(位于索引所有单链表的那个链表里)
也就是说,长度其实是看count的,而不是看链表结尾的Null的。
因此我换了个策略。
把key给覆盖成一个随机值,然后free free。
这样的话两次malloc都会在同一个地方。
因此我们就能读取到flag了。
level 5
这一题我们只能读取和free最新申请的内存区域。
具有一个打印flag功能需要前八个字节不为0。
每次申请flag空间,前16个字节会被清空。
根据以上信息,我们可以整理出一个大致的思路。
- 由于检查的是前八位非零,因此在tcache中,我们所能控制的那个chunk的next必须有值
- 申请flag又会重置,但是我们可以再次free重置回来。
- A=malloc()
- B=malloc()
- free B
- free A
- read_flag
- free A
- puts_flag
level 6
这题会在0x42962c位置存储一个八位的key,我们需要泄露它,然后在send_flag里面输入它获得flag。
我们可以对于16个块进行任意的读写
因此可以组织如下思路。
- A=malloc(16)
- B=malloc(16)
- free B
- free A
此时 tcache 会是A->next=B
利用scanf,我们可以覆盖A->next为p64(0x42962c)。此时tcache A->next = key - A=malloc(16)
- B=malloc(16),此时的B指向KEY
- print(B) 泄露出KEY
from pwn import * |
level 7
前面提到, tcache里面的东西被重新取出的时候, key位置会被放空值。
由于程序比较用的是memcpy,而不是strncmp ,这就需要\x00也要一样,因此输入的时候加上八个\x00即可
from pwn import * |
level 8
这一题要求从42c80A开始检测16个字符。
由于scanf遇到\x0a停止。我们显然不能输入42c80a这种特殊的地址。但是这其实不妨碍我们从0x42c800开始利用
完全可以malloc出一个32字节的空间,覆盖到0x42c820。(因为我们有随意写入的权限,可以直接把flag全覆盖掉)
from pwn import * |
level 9
对于这一题,虽然说我们没法获得malloc之后的指针,不能对他进行scanf和puts,但是并不是不允许malloc,也就是说,实际上它是被malloc了的。
既然如此,我们就有利用空间了。
之前提到过,每次free掉的chunk被重新malloc之后,他们的key部分会被重置成0
利用这个就可以控制key周围全都变成0
但是要注意,每次只能控制传送掉一个chunk过去。原因是,tcache中的链的内容会受到指针的控制,一旦控制了一个,后面就都是连着的。
这样子重复三次,。从后往前,就可以把key部分全部变成0
这里几个有趣的现象。
- malloc其实并不是强制要求16字节对齐的,至少用漏洞这样控制并没有产生报错。
事实上,涉及到了xmm寄存器,才会导致程序崩溃,如果只是malloc而不进行任何操作,并不会导致崩溃 - 从tcache中重新malloc的chunk并不会设置元数据,它只是沿用free时的数据。简单地说,malloc出来之后,他做的唯一的操作就是设置x+8的位置为QWORD 0
- 上述操作从后往前从前往后都能成功获得flag,验证了2中的论述
第二关有个奇怪的问题我暂时没解决,就是我发现我通过漏洞构造在0x422F20位置的malloc直接导致了segmentation fault
然而其他不对齐的没有任何问题,简直太奇怪。
from pwn import * |
level 10
相比之下这一题显得有点简单。
from pwn import * |
level 11
需要自己下泄露栈和text段地址。
利用echo新生成的堆中argv里的内容。其中有一个v4可以获得栈地址,而argv可以获得
其他的很简单 不说了
level 12
House Of Spirits!
一连串莫名奇妙的house of * 中的一个 。这个漏洞的名字没有任何意义。但是他非常牛逼,它的原理是在任意一个可写的位置伪造一个chunk。然后free掉它。
伪造chunk 需要的是熟悉他的元数据
position | a chunk |
---|---|
ptr-16 | PREV_SIZE |
ptr-8 | SIZE |
ptr | data |
… | data |
由于堆的利用机制,当前一个chunk在使用,SIZE部分的最低位会设置位1,这时候PREV_SIZE部分可以给前一个chunk利用。里面可以填入任何数据 | |
这一题要求malloc(47),对齐到48 加上16 SIZE就是p64(65) |
from pwn import * |
第二关数据变了,但是脚本一个字不用动,设计的有点失败。
level 13
栈溢出根本覆盖不到v14 不然这题显然是直接秒了的。
因此需要先在v13位置构造一个32字节的chunk(元数据16字节),然后free掉,通过泄露栈地址和修改地址,重新把他定位到v14,然后malloc
这时候只要读取8个字节。
from pwn import * |
level 14
这一题和之前的那个有echo的题很像,但有些许区别。
之前的echo里面给出了一个局部变量,这显得有点刻意,我们可以利用它轻松获得栈地址,但是这次就没有了。
泄露栈的地址方法和上一题相同。
大致思路
- 通过tcache泄露栈地址
- 通过echo获取代码段地址。
- 覆盖返回地址。
from pwn import * |
第二关不知道是不是设计上的问题,\x0a附近的一些字符不能被scanf接收。所以需要进行略微的调整
比如用0x00141D
level 15
这一题echo又可以泄露栈的值。但是和之前不同的是,free后指针会被重置成NULL。
但是利用前面说到的chunk的结构,我们仍然可以利用漏洞,就是在上一个chunk的read覆盖掉一个free掉的chunk的内容即可。
其他的和原来的差不多
from pwn import * |
这里面有很多前面用的函数。但是我懒得删了
level 16
启用了safe linking机制。
他的原理并不复杂,就是在free后的块的fd(指向后一个free的块的指针)本身地址右移12位 然后和fd里面存储的地址异或
也就是说,我们构造的部分还需要异或一个值,这个值大多数程度上是固定的,因为需要跨越0x1000字节才会改变
这题难度主要在于具有对齐检查又有malloc地址合法性的检查。如果像之前一样尝试key的重置机制覆盖两次是不可能的,程序会直接暴
这一题如果对于tcache机制不是非常深入了解的话,基本上没法做。
我们用之前的方法污染了tcache 让其中一个块指到key的部分之后,进行malloc 这时候链表删除了被malloc的那个部分(记为A),其实原理是用head指向了A->next
然后把A的key部分变成0
这时候即使tcache是空的 再malloc free 的块(记为B)的next也不会是Null 而就是A->next (当然是经过safelinking加密过后的)
于是我们就可以获得secret的前八个字节
同时还需要修正加密
一开始定位A到secret部分 这时候A->next原始的值应该是加密前的,也就是要xor secret地址>>12
然后puts出来的又是和堆地址>>12xor后过的。
两次异或回来就可以得到secret
from pwn import * |
level 17
这题太阴险了 原来试了半天一直在 malloc_usable_size 报错
因为canary会导致大小的未定义行为,具体函数的实现我猜测大概是判断在size的范围内是否是具有R/W权限的
于是乎这题卡了很久,后来看了下discord才有一点思路。
我的思路一开始是基于一个假设 我假设malloc_usable_size并没有对于对齐进行检查,后来发现确实如此。
然后发现在ida中ptr之前有八个字节没有初始化的内容,这很有可能可以提供一个合理的size值,让我们修改ptr[0]
然后我们再让ptr[0]指向ret_addr-1 注意不是rbp-8,因为在gdb中调试时我发现saved_rbp是1,这没法让我们覆盖返回地址,但是如果再加入一个canary里的随机字节,
这个值很大概率是合理的。
于是我们便可以覆盖返回地址。
from pwn import * |
level 18
house of spirit
感觉和之前的没任何区别,不说了
level 19
通过覆盖元数据,让一个chunk变得很大,然后再read_flag free掉那个chunk重新malloc 就可以包含到flag
level 20
这题难度瞬间上升。
我在discord里了解到gef gdb的一个命令 才知道怎么做。
gef有个scan 命令 可以搜索一个内存范围内是否有另一个内存范围的指针
指令可以是
scan libc stack / scan heap libc
这种设定好的。
也可以是
scan A-B C-D A B C D 均为地址
这题原来只能R/W heap部分的任意地址。我们利用的最开始突破点就是scan搜索heap上包含libc的地址
就和之前的echo函数一样 只要有用到malloc的函数 我们就可以利用。
在safe_write里面有两个fdopen和fwrite很有可能会有。
所以就在gef中运行一下,再scan heap libc
事实证明确实有。
有了libc之后,我们就可以去找libc里面包含的stack地址或者代码段地址。
但是libc里面的text段地址在动态链接的时候就可以确定,他会被放入libc不可写入的部分。
这样我们在泄露的时候由于key重置为0的行为会segmentation fault
而stack的地址会放到libc可以写的部分,我们可以利用那些地址。
有了这两个 我们就可以在栈上构造chmod /flag a+rwx
比较简单就不具体说明。
from pwn import * |
有几个好用的命令
- objdump -T 快速检索函数符号
- scan 扫描内存中的指针
- vmmap 看内存分配(gdb 中 info proc map)