HITCTF的pwn部分
DragonBall
先按一般的流程来,首先分析一下程序功能:
通过买卖龙珠来集齐7颗龙珠,就可以许愿。
所以程序包含买、卖、显示龙珠数、许愿和退出五个功能
可以看到,buy函数有一个很明显的缺陷,就是在购买龙珠时,只判断钱数是否为零,所以只要钱数不为零,就可以无限购买,所以通过卖一颗来使钱数恒不等于零,从而买到七颗龙珠,可以许愿。
买到七颗龙珠之后就可以许愿,可以很明显的看到wish函数中的两次input有问题,第一次刚好没有覆盖到EBP,而第二次则刚好覆盖到返回地址,可以劫持程序流,我们就可以通过这两次写入进行程序控制。
可以看到几乎没有任何保护措施,因此我们可以有多种思路可选。
由于只能覆盖到返回地址,则需要跳转到栈中,或者可控制的内存区域,来布置空间,从而可以执行函数或者通过ROP链来直接构造汇编语句getshell,我们可以找一下相应的gadgets:
并没有直接跳转到栈里gadgets
也没有足够多的gadgets可以构造出ROP链
所以我们只能换一种思路:
由于未开启NX和PIE保护措施,所以栈中的指令可执行,且内存中的地址固定,因此我们只要泄露出栈的基址,就可以在栈中执行shellcode,从而getshell。
通过调试可以发现,栈的返回地址始终不变,而返回地址之前的就是栈基址,而wish函数中的第一个input,如果刚好占满整个空间,就会在其后的printf函数中将这两个地址都打印出来,我们就可以通过截取字符串获得栈基址。
获取栈基址后,就可以将shellcode写入栈中,截取程序流,getshell。
nodes
首先用IDA查看程序,看到这么多未命名的函数,我们可以通过修改名称将它们变成我们熟悉的写法,方便我们后续的阅读。
通过对程序的了解,为程序中的函数以及全局变量加上注释,如果有结构体也可以为结构体加上注释。
该程序通过看代码看不出什么问题,只是可以发现不少的全局变量,而程序也在全局变量中存储了不少内容,我们可以通过动态调试可以看一下。
可以看出蓝色笔圈的就是堆块的大小,而黄色的就是插入在全局变量取的一句话。
当我们插入十个堆块是,很显然,原本存放堆块大小的地方被之前的字符串给覆盖了,不过目前覆盖的是零,所以我们申请一百个堆块试试。
堆块的大小被覆盖为0x73,也就是说我们可以在0x73范围内修改堆块,从而造成堆溢出。
有库文件,所以我们选择第一个堆块,将紧接着的下一个堆块地址修改为puts函数的got表地址,从而泄漏puts函数的地址,从而计算出库基址和system函数的地址。
本题可以有两种思路,泄露方式相同,只是利用方式略有不同:
这两种都是在泄露出puts函数和system函数地址后,通过edit将puts函数的地址改写。
第一种,将puts函数地址改写为system函数。
可以看到,在SHOW函数中,存在一个puts的参数是堆中的数据,所以我们只要将这个数据改写为"/bin/sh",在执行到这里的时候,就相当于执行system("/bin/sh"),从而getshell。
这是出题人的思路,按照程序源码确实可解,但是:
无论是IDA还是GDB,在反编译的时候,都在可以利用的puts前加入了一个puts函数,而源代码中这里应该是printf函数,所以造成了该题在本地调试无法getshell。
第二种,将puts函数地址改成one_gadget地址,从而再次执行puts函数的时候,就可以直接getshell。
这些地址是知道达到相对应的条件,就可以直接getshell。
但是,可能是由于库文件不同,在本地并没有找到合适的one_gadget,也无法getshell。
babynote
用IDA打开,可以看到程序开启了PIE,所以很显然如果要getshell,必须要先泄露出关键地址。
该程序最多只能申请3个node:
可以看出,堆块是通过堆数组来直接访问的,先通过堆基址访问每个堆最开始的12字节内容,然后通过这里的指针去访问content的内容。
每个node在申请的时候都会先申请一个12字节大小的堆块,里面存放三个4字节大小的数据,第一个是node大小size,指的是content的大小,第二个是指针,指向content堆块,第三个是一个函数指针,指向一个函数,可以通过这个指针直接调用函数。
然后申请一个堆块,大小和size相同,用来存放content。
在delete里有很明显的UAF漏洞,每个node的两个堆块都是直接释放,但是指针并未清零,所以指针都可以再次利用。
还有一个有问题的地方就是,在print函数里面读取content的时候,直接调用的存放在堆中的函数指针来读取堆的内容,所以如果这块堆可控,则可以泄漏我们需要的地址。
我们可以利用典型的UAF利用思路:
先申请两个堆块,然后释放,再申请一个堆块,跟之前的指针指向同一个地址,从而通过指针控制内存。
这一题,我们先申请node0,node1两个,两个node的content大小要不等于12字节,我们先释放node0,后释放node1,在申请node2,并且设node2的content大小为12字节,则由于fastbin的LIFO可以知道,此时node2的第一个堆块应该与node1的第一个堆块指向同一个地方,node2的第二个堆块,也就是centent,则与node0的第一个堆块指向同一个地方。
也就是说,node0的指针字段已经可以被我们控制,接下来只要泄漏函数地址,就可以实现getshel。
首先,我们看一下官方思路:
申请两个content都是100字节,这里是smallbin,主要是为了不影响对每个content头部的fastbin的利用,所以错开大小,方便我们利用。然后释放,很显然,释放后content的两个头部会被加入到fastbin队列,而两个大的堆块会被链入unsortes bin。
当我们再次申请时,此时申请的时小堆块,大小都正好为12字节,就正好将fastbin中原有的两块给申请来了,又因为本题存在UAF漏洞,所以最后申请的node2的头部就应该是node1的头部,而node2的content就是node0的头部。
这里顺序跟释放顺序有关,如果释放node1,在释放node0则正好相反,正反不影响利用。
我们来看一下当前的堆情况。
如图所示,黄色的部分就是我们的node2,分别对应node0和node1的头部,可以看出当我们print(2)时,可以看到会把node0头部的内容打印出来,可以看到除了我们填充的a之外,紧接着就是一个地址,就是我们用来调用输出函数的地址, 我们就可以通过打印获得该地址。
泄露出来后我们就相当于知道了一个ELF文件内的实际地址,又因为我们可以知道该地址相对于ELF文件的偏移,则可以知道加载进内存时,我们的ELF基址是多少,通过该基址和库函数相对于ELF的偏移,可以求得库函数的got表的地址,当然,这个库函数必须是程序调用的函数。
获得got表地址后,只需要通过修改node0的头部中content指针指向该地址,就可以通过print(0)泄露出该函数的真实地址。
获得某一个库函数的实际地址后,通过在libc文件中的偏移,可以知道libc的基址,从而就可以计算出我们所以要的system函数在程序中的实际地址。
通过上述思路,我们可以求得system函数的地址,只要我们利用同样的方式,将原本调用输出函数的函数地址,改成system函数地址,然后通过该函数的调用配置好system函数的参数,就可以getshell。
edit(‘2‘,"/bin/sh\x00" + p32(system_addr))
但是实际上是利用不成功的,通过调试我们就可以发现,我们计算出的库的基址是错误的,与实际的库机制有不定长的偏移,而每个函数相对于我们计算出的基址也都不相同,所以利用失败。
如图所示,我们最后调用的函数地址实际上是do_system函数处,然而该函数并没有类似于"/bin/sh"的参数,所以利用失败。
接下来我们看看另一种思路:
在前一种思路里我们说过,为了不影响利用,我们的两个content都会被释放到unsorted bin里,所以我们看一下unsorted bin里会有什么可以利用的地方,不过我们这次只将一个堆块释放至unsorded bin,同样是方便利用:
可以看到此时的unsorted bin的前后指针都指向了main_arena的固定位置,同样在堆中如下:
我们可以通过print(0)直接获得这个地址,然后求出main_arena的地址,这个怎么利用呢:
main_rena往往和__malloc_hook在相对固定的位置,本题中就偏移0x18
在libc符号表中,没有main_arena的符号,但该地址与__malloc_hook很近,通常利用 __malloc_hook来定位main_arena
所以我们就可以通过泄露出的地址,获得__malloc_hook的实际地址,从而根据在libc中的偏移,获得libc的基址,从而获得system函数的真实地址,造成利用。
每一步的具体实现方式,同思路一相同。
注:
以上题目及解题脚本,见https://github.com/LJRosemary/ctf/tree/master/HITCTF