Pwnable.kr unlink
预习知识
什么是unlinked
unlinked 是堆溢出中的一种常见形式,通过将双向列表中的空闲块拿出来与将要free的物理相邻的块进行合并。(将双向链表上的chunk卸载下来与物理chunk合并)
unlink漏洞条件
有3个以上的空闲chunk链表,其中最前面的chunk存在有堆溢出
unlink的触发
当使用free函数释放正在使用的chunk时,会相应地检查其相邻的chunk是否空闲。如果空间则将相邻的chunk与free的chunk进行合并。
unlink有两个安全检测机制,都是针对chunk的header部分。
- prev_size:记录上一chunk的大小
- size:记录当前chunk的大小
- fd:在链表中指向下一个空闲chunk
- bk:在链表中指向上一个空闲chunk
判断一
1 | if(chunksize (p) != prev_size (next_chunk (p))) |
此判断所代表的含义为检查将从链表中卸下的chunk其size是否被恶意的修改。记录当前size的地方有两处一个是为当前chunk的size字段和下一个chunk(物理地址上相邻的高地址的chunk)的prev_size字段如果这两个字段的值不等,则unlink会抛出异常。
判断二
1 | if(__builtin_expect (fd->bk != p || bk->fd != p, 0)) |
这是检查当前chunk的前面一个chunk的bk、后面一个chunk的fd是否指向当前chunk,如果有其中之一不符合,则unlink会抛出异常。
unlink解链原理
FD=P->fd即当前空闲chunk所指向的下一个空闲chunk
BK=P->bk即当前空闲chunk所指向的上一个chunk
FD->bk=BK<=>P->fd->bk= P->bk
BK->fd=FD<=>P->bk->fd= P->fd
unlink引发漏洞原理
Dword shoot一种漏洞溢出技巧。漏洞是在双向链表chunk删除时,出现的一种漏洞类型。在进行双向链表的操作过程中,有溢出等的情况下,删除的chunk的fd、bk两个指针被恶意的改写的话,就会在链表删除的时候发生的漏洞。
unlink 卸载chunk时,会对chunk 的fd、bk指针进行操作,就是因为这个操作我们可以利用堆溢出控制,进行unlink的chunk的fd、bk指针。
如果双向链表的2个指针被修改的话,一般由于修改数据,或者控制程序的跳转。shellcode无法放入这么小的空间中。
题目
1 |
|
概述一下程序运行思路:申请ABC三个大小为0x10的chunk;将ABC之间相互串联起来,形成一个空闲chunk链表(链接方式看下图)。接着答应出chunk A的栈地址、堆地址。之后从A buf后写入我们输入的数据,对B调用unlink函数,将B在链表上卸载下来。
思路
最明显的一点就是get(A->buf)
存在堆溢出的情况。结合题目给予我们的提示:unlink。存在溢出和有unlink出现,我们就能构造出两个任意指向的指针(fd、bk)。
我们先来看看unlink函数:
1 | void unlink(OBJ* B){ |
我们先来解读一下unlink是怎么释放一个chunk的。我们就用下图中堆块解释:
我们先将四条转换方程式化简一下:
1 | B->fd->bk = B->bk |
B->bk
= 0x1030
;B->fd
= 0x1010
;都是存储着内存地址的变量。换句话说就是B->bk
B->fd
都是地址值。
B->fd->bk
= *((B->fd)+bk)
=*(0x1010+4)
;B->bk->fd
= *((B->bk)+fd)
= *(0x1030+0)
;可以看见现在变成了指针,而不是上面的地址值,而是一个指针。
那么最后的运算结果是:
1 | *(0x1010+4) = 0x1030//0x1014(前一个chunk的bk)的值被覆盖为0x1010 |
unlink之后的chunk指针就如同最下行所示。
我们初定的利用思路是:利用unlink和堆溢出,创造出两个所我们所控制的任意指针,然后将程序控制流控制到shell上。将shellcode写到堆上,然后覆盖返回指针到堆上的方法不可行。因为程序开启了NX,堆栈上数据不能运行。而且程序中也有现成的shell。
做到这里就有点进行不下去,主要是不知覆盖那个函数的返回地址,来控制程序流。查看别人writeup,发现关键点在这里:
retn
retn指令作用是栈顶字单元出栈,其赋值给EIP寄存器,只要能够修改ESP寄存器的内容修改为shellcode的地址就能执行shellcode。
lea esp,[ecx-4]
lea指令将存储器操作数mem的4位16进制偏移地址送到指定的寄存器。eg:SI=1000H,执行指令 LEA BX , [SI] 后,BX=1000H。也就是说esp的值来自[ecx-4]指向的值 。
leave
leave指令相当于mov esp ebp,pop ebp,对esp数据的来源无影响。
mov ecx,[ebp+var_4]
相当于mov ecx,[ebp-4]
ecx等于[ebp-4]指向的值,也就是我们需要将[ebp-4]指向的值修改为shellcode+4
还记得我们已经分析得到的条件:可以利用堆溢出和unlind创造出两个指针。(ebp的值在程序运行时值不变)
我们就将其中的一个任意指针修改为*(ebp-4)
,将ebp-4的值覆写为shellcode+4。
那我们先找出shellcode 和 ebp的内存地址:
IDA函数表中找到shellcode地址:
程序会将chunk A的栈地址和堆地址,而栈地址是用ebp计算偏移得到的。IDA中查看chunk A栈地址的偏移算法:
chunk A stack addr = ebp+var_14 = ebp - 0x14
从这里就可以推算出ebp - 0x4 相对于的chunk A 栈地址的相对位置,进而推算出实际内存地址。
shellcode的地址,我们需要找一个地址放置。方便我们构建的任意地址指针读取到ebp-0x4中。那么,我们就一齐写入到chunk A 堆中。
接下来就是看看构造指针的fd、bk需要填入什么数。unlink 的计算方程:
1 | B->bk->fd = B->fd |
我们利用第一个指针进行写入,也就是:*(bk+0)=fd
。换而言之就是*(ebp-4)=shellcode-4
。
得出我们需要填入的是fd = chunk A堆地址+12;bk = chunk A栈地址 + 0x10
初始情况下的chunk A :
data A on heap:
——————————— <=A
| FD | BK |
——————————— <=A->buf , A+0x8
| put shellcode here |
| length of buf is 0x8 |
———————————
| presizeB | sizeB |
——————————— <=B , A+0x8+0x8+0x8
| FD | BK |
——————————— <=B->buf
| |
———————————
好,我们现在构造payload:
1 | # 'a'用作填充chunk A剩余空间到达chunk B堆顶; |
溢出覆写之后的chunk A:
data A on heap:
——————————— <=A
| FD | BK |
——————————— <=A->buf , A+0x8
| shellcode (4 byte) |
| AAAA (4 byte junk) |
———————————
| AAAA | AAAA |
——————————— <=B , A+0x8+0x8+0x8
|A heap +12|A stack+0x10|
——————————— <=B->buf
| |
———————————
接下来调用unlink及其后面指令的原理简单记录:
1 | //unlink之后得到的两条方程 |
脚本
1 | #coding=utf-8 |