又找到了一个简单(才怪)的题目,这里刷一刷:
hacknote [200 pts]
首先打开,和之前的题目类似,都是一个nemu,基本逻辑是:建立笔记,可以将笔记打印,同时也可以将笔记删除。然后发现有一些细节有点问题:
当指定了大小的时候,我们再输入数据的时候,发现我们已经进行了输入(?)仿佛是因为输入流的原因未将上一个内容的/n读入导致。
再多次输入数据之后,我们就发现之后的【输出流会被原先的输入流干扰】,这种现象,显然不是正常的。所以我们优先怀疑一下在建立笔记的时候出现的问题:
从函数中可以看出来,此时存在一个结构体:note,大致是如下
1 | struct note{ |
其中,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 | .text:08048923 mov eax, [ebp+index] |
仔细看这一部分会发现,此时的栈中存放的居然是【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 |
|
最终得到
1 | FLAG{Us3_aft3r_fl3333_in_h4ck_not3} |
完结撒花!!!
调试的技巧
这里由于给了libc,本地测试的话可能libc是不一样的,可以通过设置环境变量将libc设置为题目所给的
1 | export LD_PRELOAD="/home/xxxx/libc_32.so.6" |