夫天地者,万物之逆旅;光阴者,百代之过客。

0%

虎符ctf三道逆向中最简单的一道,比赛期间我只做出来这一道。题目对我来说挺新颖的,所以在这里也记录一下。刚写完vm的题解,有点累,所以这篇就简单点写。

只给出一个disasm.txt,部分如下:

1588254571359

查了下,是python的字节码。

pyc可以还原成py源代码,但是这个类似汇编的东西没办法还原出来(如果大佬们知道怎么还原,劳请告诉我一下)

dis模块

同时查到这个文本是怎么来的,python的dis模块。

下面有个小例子:

1
2
3
4
5
6
7
8
9
10
11
12
def f():
s = [90, 100, 87, 109, 86, 108, 86, 105, 90, 104, 88, 102]
b = [90, 100, 87, 109, 86, 108, 86, 105, 90, 104, 88, 102]
#a = s[6:30:3]
#
#s = 'xctf{1231231}'
#a = (((((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2]))*128)+ord(s[3]))*128) + ord(s[4])) * 128 + ord(s[5])
#print(a)
a = map(lambda x:x+1, zip(s, b[2:5]))

import dis
dis.dis(f)

输出:

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
  2           0 LOAD_CONST               1 (90)
2 LOAD_CONST 2 (100)
4 LOAD_CONST 3 (87)
6 LOAD_CONST 4 (109)
8 LOAD_CONST 5 (86)
10 LOAD_CONST 6 (108)
12 LOAD_CONST 5 (86)
14 LOAD_CONST 7 (105)
16 LOAD_CONST 1 (90)
18 LOAD_CONST 8 (104)
20 LOAD_CONST 9 (88)
22 LOAD_CONST 10 (102)
24 BUILD_LIST 12
26 STORE_FAST 0 (s)

3 28 LOAD_CONST 1 (90)
30 LOAD_CONST 2 (100)
32 LOAD_CONST 3 (87)
34 LOAD_CONST 4 (109)
36 LOAD_CONST 5 (86)
38 LOAD_CONST 6 (108)
40 LOAD_CONST 5 (86)
42 LOAD_CONST 7 (105)
44 LOAD_CONST 1 (90)
46 LOAD_CONST 8 (104)
48 LOAD_CONST 9 (88)
50 LOAD_CONST 10 (102)
52 BUILD_LIST 12
54 STORE_FAST 1 (b)

9 56 LOAD_GLOBAL 0 (map)
58 LOAD_CONST 11 (<code object <lambda> at 0x0000019F93CB0270, file "d:\桌面\20200419 虎符ctf\text.py", line 9>)
60 LOAD_CONST 12 ('f.<locals>.<lambda>')
62 MAKE_FUNCTION 0
64 LOAD_GLOBAL 1 (zip)
66 LOAD_FAST 0 (s)
68 LOAD_FAST 1 (b)
70 LOAD_CONST 13 (2)
72 LOAD_CONST 14 (5)
74 BUILD_SLICE 2
76 BINARY_SUBSCR
78 CALL_FUNCTION 2
80 CALL_FUNCTION 2
82 STORE_FAST 2 (a)
84 LOAD_CONST 0 (None)
86 RETURN_VALUE

Disassembly of <code object <lambda> at 0x0000019F93CB0270, file "d:\桌面\20200419 虎符ctf\text.py", line 9>:
9 0 LOAD_FAST 0 (x)
2 LOAD_CONST 1 (1)
4 BINARY_ADD
6 RETURN_VALUE

上面的输出的代码形式风格和题目给出的只有细微的差异(我用的是python3,而题目是python2,可能是这个原因)

然后可以去试着分析下题目给出的代码了,遇到不懂的点,可以自己写个函数,然后disasm,看看和题目中代码的差异。

分析字节码

首先是arr0,arr1,arr2这三个列表的赋值,很容易看出来。

然后有check0, check1,check2,check3四个检测函数。

check0

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
# <genexpr> line 6 of game.py

6 0 LOAD_FAST 0 '.0'
3 FOR_ITER 32 'to 38'
6 STORE_FAST 1 'x'
9 LOAD_GLOBAL 0 'ord'
12 LOAD_FAST 1 'x'
15 CALL_FUNCTION_1 1 None
18 LOAD_GLOBAL 1 'range'
21 LOAD_CONST 32
24 LOAD_CONST 128
27 CALL_FUNCTION_2 2 None
30 COMPARE_OP 6 in
33 YIELD_VALUE
34 POP_TOP
35 JUMP_BACK 3 'to 3'
38 LOAD_CONST None
41 RETURN_VALUE
//推测是判断字符ord(x) in (32, 128)

# check0 line 5 of game.py

6 0 LOAD_GLOBAL 0 'all'
3 LOAD_GENEXPR '<code_object <genexpr>>'
6 MAKE_FUNCTION_0 0 None
9 LOAD_FAST 0 's'
12 GET_ITER
13 CALL_FUNCTION_1 1 None
16 CALL_FUNCTION_1 1 None
19 RETURN_VALUE

//check0()判断字符介于(32,128)

配合着我代码中的注释就应该能看懂了吧,我就不多解释了。

check1

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
# check1 line 8 of game.py

9 0 LOAD_GLOBAL 0 'len'
3 LOAD_FAST 0 's'
6 CALL_FUNCTION_1 1 None
9 LOAD_CONST 100
12 COMPARE_OP 0 <
15 POP_JUMP_IF_FALSE 58 'to 58'
//if len(s)<100
18 LOAD_GLOBAL 0 'len'
21 LOAD_FAST 0 's'
24 CALL_FUNCTION_1 1 None
27 LOAD_GLOBAL 0 'len'
30 LOAD_FAST 0 's'
33 CALL_FUNCTION_1 1 None
36 BINARY_MULTIPLY //乘
//len(s) * len(s)
37 LOAD_CONST 777
40 BINARY_MODULO //取模
//(len(s) * len(s))%777
41 LOAD_CONST 233
44 BINARY_XOR //异或
//((len(s) * len(s))%777) ^ 233
45 LOAD_CONST 513
48 COMPARE_OP 2 ==
//if ((len(s) * len(s))%777) ^ 233 == 513
51_0 COME_FROM 15 '15'
51 POP_JUMP_IF_FALSE 58 'to 58'

10 54 LOAD_GLOBAL 1 'True'
57 RETURN_END_IF
58_0 COME_FROM 51 '51'

12 58 LOAD_GLOBAL 2 'False'
61 RETURN_VALUE
62 LOAD_CONST None
65 RETURN_VALUE

//check1()判断flag长度,经爆破得知len(flag) = 39

check1检测flag长度是否符合条件( if ((len(s) * len(s))%777) ^ 233 == 513

爆破可以求得长度为39

check2

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
# check2 line 14 of game.py

15 0 LOAD_GLOBAL 0 'ord'
3 LOAD_FAST 0 's'
6 LOAD_CONST 0
9 BINARY_SUBSCR //TOS = TOS1[TOS]
10 CALL_FUNCTION_1 1 None
13 LOAD_CONST 128
16 BINARY_MULTIPLY

//ord(s[0]) * 128

17 LOAD_GLOBAL 0 'ord'
20 LOAD_FAST 0 's'
23 LOAD_CONST 1
26 BINARY_SUBSCR
27 CALL_FUNCTION_1 1 None
30 BINARY_ADD
31 LOAD_CONST 128
34 BINARY_MULTIPLY

//(ord(s[0])*128+ord(s[1])) * 128

35 LOAD_GLOBAL 0 'ord'
38 LOAD_FAST 0 's'
41 LOAD_CONST 2
44 BINARY_SUBSCR
45 CALL_FUNCTION_1 1 None
48 BINARY_ADD
49 LOAD_CONST 128
52 BINARY_MULTIPLY

//(((ord(s[0])*128+ord(s[1]))*128) + ord(s[2])) * 128

53 LOAD_GLOBAL 0 'ord'
56 LOAD_FAST 0 's'
59 LOAD_CONST 3
62 BINARY_SUBSCR
63 CALL_FUNCTION_1 1 None
66 BINARY_ADD
67 LOAD_CONST 128
70 BINARY_MULTIPLY

//(((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2])) * 128) + ord(s[3])) * 128

71 LOAD_GLOBAL 0 'ord'
74 LOAD_FAST 0 's'
77 LOAD_CONST 4
80 BINARY_SUBSCR
81 CALL_FUNCTION_1 1 None
84 BINARY_ADD
85 LOAD_CONST 128
88 BINARY_MULTIPLY

//(((((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2]))*128)+ord(s[3]))*128) + ord(s[4])) * 128

89 LOAD_GLOBAL 0 'ord'
92 LOAD_FAST 0 's'
95 LOAD_CONST 5
98 BINARY_SUBSCR
99 CALL_FUNCTION_1 1 None
102 BINARY_ADD

//(((((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2]))*128)+ord(s[3]))*128) + ord(s[4])) * 128 + ord(s[5])

103 LOAD_CONST 3533889469877L
106 COMPARE_OP 2 ==
109 POP_JUMP_IF_FALSE 138 'to 138'
112 LOAD_GLOBAL 0 'ord'
115 LOAD_FAST 0 's'
118 LOAD_CONST -1
121 BINARY_SUBSCR
122 CALL_FUNCTION_1 1 None
125 LOAD_CONST 125
128 COMPARE_OP 2 ==
131_0 COME_FROM 109 '109'
131 POP_JUMP_IF_FALSE 138 'to 138'

16 134 LOAD_GLOBAL 1 'True'
137 RETURN_END_IF
138_0 COME_FROM 131 '131'

18 138 LOAD_GLOBAL 2 'False'
141 RETURN_VALUE
142 LOAD_CONST None
145 RETURN_VALUE

/*
if 表达式 == 3533889469877L:
if ord(s[-1]) == 125:
return True
else return false
else return false
*/
//check2()判断flag{}这个框架,以及flag{}主体的第一个字符flag[5]

check2用于判断前6个字符是否满足(((((((ord(s[0])*128+ord(s[1]))*128)+ord(s[2]))*128)+ord(s[3]))*128) + ord(s[4])) * 128 + ord(s[5]) == 3533889469877,同时判断最后一个字符是否是}

只要学过数学的小学生都知道6元一次等式根本不可能求出六个未知数,所以题目可能是假设了前五个字符是flag{,由此我们可以求出flag的第6个字符,也就是flag主体部分的第一个字符。

注意,我和炜昊都被这里坑到了,看到了flag[5],以为只有5个字符,恰好是flag{,忽略了第六个字符。

这个低级的下标错误平常不会错,但是每次到这个综合性题目中总会出错,还是要注意啊。

check3

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
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
# check3 line 20 of game.py

21 0 LOAD_GLOBAL 0 'map'
3 LOAD_GLOBAL 1 'ord'
6 LOAD_FAST 0 's'
9 CALL_FUNCTION_2 2 None
12 STORE_FAST 1 'arr'
//e.g: map(function, iterable, ...)
//arr = map(ord, s)

22 15 LOAD_FAST 1 'arr'
18 LOAD_CONST 6
21 LOAD_CONST 30
24 LOAD_CONST 3
27 BUILD_SLICE_3 3 //切片,3个一组
30 BINARY_SUBSCR
31 STORE_FAST 2 'a'

//a = arr[6:30:3]

23 34 SETUP_LOOP 62 'to 99'
37 LOAD_GLOBAL 2 'range'
40 LOAD_GLOBAL 3 'len'
43 LOAD_FAST 2 'a'
46 CALL_FUNCTION_1 1 None
49 CALL_FUNCTION_1 1 None
52 GET_ITER
53 FOR_ITER 42 'to 98'
56 STORE_FAST 3 'i'

//类似for i in range(len(a)) ,不过这里用的是迭代器

24 59 LOAD_FAST 2 'a'
62 LOAD_FAST 3 'i'
65 BINARY_SUBSCR
66 LOAD_CONST 17684
69 BINARY_MULTIPLY
70 LOAD_CONST 372511
73 BINARY_ADD
74 LOAD_CONST 257
77 BINARY_MODULO
78 LOAD_GLOBAL 4 'arr0'
81 LOAD_FAST 3 'i'
84 BINARY_SUBSCR
85 COMPARE_OP 3 !=
88 POP_JUMP_IF_FALSE 53 'to 53'

/*
if (a[i] * 17684 + 372511) % 257 == arr0[i]:(是==,我没搞错(因为jump if false))
goto 53(继续循环)
else return flase
*/

25 91 LOAD_GLOBAL 5 'False'
94 RETURN_END_IF
95_0 COME_FROM 88 '88'
95 JUMP_BACK 53 'to 53'
98 POP_BLOCK
99_0 COME_FROM 34 '34'

26 99 LOAD_FAST 1 'arr'
102 LOAD_CONST -2
105 LOAD_CONST 33
108 LOAD_CONST -1
111 BUILD_SLICE_3 3
114 BINARY_SUBSCR
115 LOAD_CONST 5
118 BINARY_MULTIPLY
119 STORE_FAST 4 'b'

//b = arr[-2:33:-1] * 5

27 122 LOAD_GLOBAL 0 'map'
125 LOAD_LAMBDA '<code_object <lambda>>'
128 MAKE_FUNCTION_0 0 None
131 LOAD_GLOBAL 6 'zip'
134 LOAD_FAST 4 'b'
137 LOAD_FAST 1 'arr'
140 LOAD_CONST 7
143 LOAD_CONST 27
146 SLICE+3
147 CALL_FUNCTION_2 2 None
150 CALL_FUNCTION_2 2 None
153 STORE_FAST 5 'c'
//c = map(lambda x:函数体, zip(b, arr[7,27]))
//即c[i] = b[i] ^ arr[7,27][i]

28 156 LOAD_FAST 5 'c'
159 LOAD_GLOBAL 7 'arr1'
162 COMPARE_OP 3 !=
165 POP_JUMP_IF_FALSE 172 'to 172'
29 168 LOAD_GLOBAL 5 'False'
171 RETURN_END_IF
172_0 COME_FROM 165 '165'

/*
if c == arr1:
goto 172
else return false
*/

30 172 LOAD_CONST 0
175 STORE_FAST 6 'p'

31 178 SETUP_LOOP 105 'to 286'
181 LOAD_GLOBAL 2 'range'
184 LOAD_CONST 28
187 LOAD_CONST 34
190 CALL_FUNCTION_2 2 None
193 GET_ITER
194 FOR_ITER 88 'to 285'
197 STORE_FAST 3 'i'

/*
for i in range(28, 34)
*/

32 200 LOAD_FAST 1 'arr'
203 LOAD_FAST 3 'i'
206 BINARY_SUBSCR
207 LOAD_CONST 107
210 BINARY_ADD
211 LOAD_CONST 16
214 BINARY_DIVIDE
215 LOAD_CONST 77
218 BINARY_ADD

// ((arr[i] + 107)//16)+77 (除法//还是/存疑)

219 LOAD_GLOBAL 8 'arr2'
222 LOAD_FAST 6 'p'
225 BINARY_SUBSCR
226 COMPARE_OP 3 !=
229 POP_JUMP_IF_TRUE 268 'to 268'
/*
if arr2[p] != ((arr[i] + 107)//16)+77 :
return false
*/
232 LOAD_FAST 1 'arr'
235 LOAD_FAST 3 'i'
238 BINARY_SUBSCR
239 LOAD_CONST 117
242 BINARY_ADD
243 LOAD_CONST 16
246 BINARY_MODULO
247 LOAD_CONST 99
250 BINARY_ADD
251 LOAD_GLOBAL 8 'arr2'
254 LOAD_FAST 6 'p'
257 LOAD_CONST 1
260 BINARY_ADD
261 BINARY_SUBSCR
262 COMPARE_OP 3 !=
265_0 COME_FROM 229 '229'
265 POP_JUMP_IF_FALSE 272 'to 272'
/*
if (arr[i]+117)%16+99 == arr2[p+1]:
goto 272
else return false
*/

33 268 LOAD_GLOBAL 9 'false'
271 RETURN_END_IF
272_0 COME_FROM 265 '265'

34 272 LOAD_FAST 6 'p'
275 LOAD_CONST 2
278 INPLACE_ADD //TOS = TOS1 + TOS
279 STORE_FAST 6 'p'
282 JUMP_BACK 194 'to 194'
285 POP_BLOCK
286_0 COME_FROM 178 '178'

//p += 2,然后继续循环

35 286 LOAD_GLOBAL 10 'True'
289 RETURN_VALUE

整理一下,check3的逻辑是这样的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def check3():
arr = map(ord, flag)
a = arr[6:30:3]
for i in range(len(a)):
if (a[i] * 17684 + 372511) % 257 != arr0[i]:
return False
b = arr[-2:33:-1] * 5
c = map(lambda x: x[0]^x[1], zip(b, arr[7,27]))
if c != arr1:
return False
p = 0
for i in range(28, 34):
if arr2[p] != (((arr[i]+107)//16)+77):
return False
if arr2[p+1] != ((arr[i]+117)%16+99):
return False
p += 2
return True

逆向

倒着逆,脚本如下:

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
arr0 = [249, 91, 149, 113, 16, 91, 53 ,41]
arr1 = [43, 1, 6, 69, 20, 62, 6, 44, 24, 113, 6, 35, 0 , 3, 6, 44, 20, 22, 127, 60]
arr2 = [90, 100, 87, 109, 86, 108, 86, 105, 90, 104, 88, 102]


def check3():
arr = map(ord, flag)
a = arr[6:30:3]
for i in range(len(a)):
if (a[i] * 17684 + 372511) % 257 != arr0[i]:
return False
b = arr[-2:33:-1] * 5
c = map(lambda x: x[0]^x[1], zip(b, arr[7,27]))
if c != arr1:
return False
p = 0
for i in range(28, 34):
if arr2[p] != (((arr[i]+107)//16)+77):
return False
if arr2[p+1] != ((arr[i]+117)%16+99):
return False
p += 2
return True


def solve_check1():
for i in range(100):
if ((i *i )%777) ^ 233 == 513:
print(i)

def solve_5():
for i,n in enumerate('flag{'):
flag[i] = ord(n)
flag[38] = ord('}')
sum = 0
for i in range(5):
sum = (sum + flag[i]) * 128
#print(sum)
for i in range(32,128):
if sum+i==3533889469877:
flag[5] = i

def solve_6_30_3():
for i in range(6, 30, 3):
for j in range(32, 128):
if (j * 17684 + 372511) % 257 == arr0[(i-6)//3]:
flag[i] = j
break

def solve_28_34():
p = 0
for i in range(28,34):
for j in range(32,128):
if (((j+107)//16)+77) == arr2[p] and ((j+117)%16+99) == arr2[p+1]:
flag[i] = j
break
p += 2

def solve_34_38():
flag[34] = flag[18] ^ arr1[11]
flag[35] = flag[9] ^ arr1[2]
flag[36] = flag[12] ^ arr1[5]
flag[37] = flag[15] ^ arr1[8]

def solve_7_27():
b = flag[37:33:-1]*5
for i in range(20):
flag[i+7] = b[i] ^ arr1[i]


def main():
solve_check1()
global flag
flag = [0] * 39
solve_5()
solve_6_30_3()
solve_28_34()
solve_34_38()
solve_7_27()
for i in flag:
print(chr(i),end='')

main()

注意flag[7, 27]的值需要依靠flag[34, 38]求出。

第一次接触vm这类的题型。一开始我以为vm和vmp是类似的,但是后来才知道vm指的是自己实现一套指令系统,然后在此基础上运行字节码。类似java的虚拟机。

其实也不算是第一次做vm的题,很久之前在南邮ctf平台上做过的WxyVM这道题也是vm。这题蛮简单的,以至于我一直都不知道它其实也是vm题。

不知道为什么,网上有关虎符ctf逆向的题解不多,vm这道题只找到一份题解,令则师傅的原创-虎符第三题 -vm 虚拟机逆向。令则师傅写得挺清楚的。不过,好歹是我第一次做出这种基于栈的vm题,还是写一下题解温习一下吧。

总述

题目给出了两个文件,

  • vm,解释器,解释字节码并执行,ELF程序
  • code,字节码,十六进制数据

如何运行?

linux终端内键入./vm ./code,回车后输入flag,若错误则输出n

vm的main函数:

1588243222122

VM结构体

上图21~28行的变量可以看作一个结构体,这不是写在程序里的,是等下分析33行的vm函数的代码时总结出来的。

1
2
3
4
5
6
7
8
struct VM{
dword vm_eip;
dword vm_sp; VMstruct[1]
qword* code; VMstruct + 1
qword* stack; VMstruct + 2
qword* c;
qword* d;
}VMstruct;

vm_eip是表示当前指令运行到哪里了,比如运行code文件的0x100处的指令时,vm_eip = 0x100。

vm_sp表示栈内有几个数据元素,配合下面的stack这个变量,可以表示栈顶元素(stack[sp-1])

code是code文件中的字节码

stack是一个数组,本程序通过数组来模拟了一个栈,所有的数据运算都通过这个模拟的栈进行。

c是用于存储数据的数组,本题中c存储了三组数据,c[i+0x64]存储输入的flag,c[i+0x32]存储预设的一组数据,c[i]存储flag加密后的数据。i取值均为0到0x29,含0x29(最后c[i]和c[i+0x32]相比较,相等则答对flag)

d存储两个循环变量i,j,i = d[0], j=d[1]

为什么上面的结构体c和d两个变量,我没有使用像vm_eip,stack这种明确的变量名?

因为单独分析vm程序的vm函数根本无法分析出c和d的作用。必须结合code文件中的字节码才能分析出其含义。

分析操作码

vm文件的vm函数(sub_400A50):

经典的while-switch结构选择opcode(操作码),不同的操作码模拟不同的操作。

1588244728385

将一些基础的变量改下名之后,代码中的变量和我们总结出的VM结构体有这样的关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
*(_BYTE *)(code + vm_eip)		
VMstruct.vm_eip。当前eip,程序运行到的地方

*(_BYTE *)(code + vm_eip + 1)
data。表示操作数。只有单操作数指令才会出现这个变量

(signed int)VMstruct[1]
VMstruct.vm_sp。表示栈内有几个数据元素。

*((_QWORD *)VMstruct + 1)
VMstruct.code。code字节码地址。

*((_QWORD *)VMstruct + 2)
VMstruct.stack。模拟栈

*((_QWORD *)VMstruct + 3)
VMstruct.c。存储数据的数组。

*((_QWORD *)VMstruct + 4)
VMstruct.d。存储两个循环变量

分析也没啥技巧,就慢慢来,以0xc的操作码为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
case 0xC:                         // case 0xc : pop a; pop b; b/a;
v58 = VMstruct[1]; //VMstruct.vm_sp
v59 = *((_QWORD *)VMstruct + 2); //VMstruct.stack
v60 = v58 - 1;
v58 -= 2;
VMstruct[1] = v60;
v61 = *(_BYTE *)(v59 + v60); //pop a
VMstruct[1] = v58;
v7 = (_BYTE *)(v58 + v59);
vm_eip_1 = (unsigned __int8)*v7; //pop b
if ( !v61 )
return vm_eip_1;
VMstruct[1] = v60;
v9 = (unsigned __int16)vm_eip_1 / v61;// temp = b/a
goto LABEL_8;
//(省略一些中间代码)
LABEL_8:
*v7 = v9; //push temp
LABEL_9:
code = *((_QWORD *)VMstruct + 1);
vm_eip_1 = *VMstruct + 1;
*VMstruct = vm_eip_1; //eip+=1
goto LABEL_2;

经过漫长的分析(里面的变量繁杂,代码可读性极低,需要一点一点看),可以整理出下面的指令,我基本上是用伪汇编表示的。

本题中的指令一部分是无操作数(单字节码)的,另一部分是单操作数(双字节码,即一个操作码加上一个操作数)的。如果下面的指令中含有data这个字眼,说明该指令是单操作数的。

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
1		getchar()
2 pop a; printf(a)
3 vm_eip ++
4 push data
5 push d[data]
6 pop d[data]
7 push c[data]
8 pop c[data]
9 pop a; pop b; push b+a
0xa pop a; pop b; push b-a
0xb pop a; pop b; push b*a
0xc pop a; pop b; push b/a
0xd pop a; pop b; push b%a
0xe pop a; pop b; push b^a
0xf pop a; pop b; push b&a
0x10 pop a; pop b; push b|a
0x11 pop a; push -a;
0x12 pop a; push ~a;
0x13 pop a; pop b; if (b!=a) {vm_eip+=2} else {vm_eip+=data}
0x14 pop a; pop b; if (b==a) {vm_eip+=2} else {vm_eip+=data}
0x15 pop a; pop b; if (b<=a) {vm_eip+=2} else {vm_eip+=data}
0x16 pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=data}
0x17 pop a; pop b; if (b>=a) {vm_eip+=2} else {vm_eip+=data}
0x18 pop a; pop b; if (b>a) {vm_eip+=2} else {vm_eip+=data}
0x19 pop a; push c[a]
0x1a pop a; pop b; c[a] = b
0x1b pop a; push d[a]
0x1c pop a; pop b; d[a] = b
0x1d vm_eip += data; if (*vm_eip>0x1d) return vm_eip
0x1e return vm_eip

0x9~0x12是一些简单的算数运算,无操作数,所涉及的数据都是从栈中弹出的。

0x13~0x18是进行判断、实现程序跳转的,类似jnz,jbe等。

0x1d其实就是jmp,注意data是char类型的(*(char *)(code + vm_eip + 1)),有正有负,比如0xe8是负数。

1
2
3
import ctypes
print(ctypes.c_byte(0xe8)).value
#输出-24

0x1e是ret

下面说一些组合指令:

0x4-0x8:向c数组写入一个数据

0x1-0x8:输入一个数据,并写入c数组

0x4-0x6:为循环变量i或j(d[0]或d[1])赋值

等等。。

分析字节码

首先很容易看出前两部分:

一:内置数据的赋值

这部分数据是code文件内含的,用于和加密后的字符串作比较。

第一部分字节码就是把这些数据赋值到c[0x32+i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def part1(self):
'''
验证flag的数据,存储在c[0x32]~c[0x32+0x29]
0x0 ~ 0xa7 :
04 66 08 32 04 4E 08 33 04 A9 08 34 04 FD 08 35
04 3C 08 36 04 55 08 37 04 90 08 38 04 24 08 39
04 57 08 3A 04 F6 08 3B 04 5D 08 3C 04 B1 08 3D
04 01 08 3E 04 20 08 3F 04 81 08 40 04 FD 08 41
04 36 08 42 04 A9 08 43 04 1F 08 44 04 A1 08 45
04 0E 08 46 04 0D 08 47 04 80 08 48 04 8F 08 49
04 CE 08 4A 04 77 08 4B 04 E8 08 4C 04 23 08 4D
04 9E 08 4E 04 27 08 4F 04 60 08 50 04 2F 08 51
04 A5 08 52 04 CF 08 53 04 1B 08 54 04 BD 08 55
04 32 08 56 04 DB 08 57 04 FF 08 58 04 28 08 59
04 A4 08 5A 04 5D 08 5B
'''
self.c = [0] * 0x100
with open(r'code', 'rb')as f:
self.b = f.read()
for i in range(0xa8//4):
group = self.b[i*4:i*4+4]
data, offset = group[1], group[3]
self.c[offset] = data

二:输入flag

输入flag,数据保存至c[0x64+i]

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def part2(self):
'''
输入flag,存储在c[0x64]~c[0x64+0x29]
0xa8 ~ 0x125 :
01 08 64 01 08 65 01 08 66 01 08 67 01 08 68 01
08 69 01 08 6A 01 08 6B 01 08 6C 01 08 6D 01 08
6E 01 08 6F 01 08 70 01 08 71 01 08 72 01 08 73
01 08 74 01 08 75 01 08 76 01 08 77 01 08 78 01
08 79 01 08 7A 01 08 7B 01 08 7C 01 08 7D 01 08
7E 01 08 7F 01 08 80 01 08 81 01 08 82 01 08 83
01 08 84 01 08 85 01 08 86 01 08 87 01 08 88 01
08 89 01 08 8A 01 08 8B 01 08 8C 01 08 8D
'''
flag = input('请输入长度为42的字符串:')
if len(flag) != 42:
print('字符串长度不合要求')
else:
for i in range((0x126-0xa8)//3):
group = self.b[0xa8+i*3 : 0xa8+i*3+3]
data, offset = ord(flag[i]), group[2]
self.c[offset] = data

三:加密部分

翻译字节码脚本

这部分不像前两部分的指令简单,所以我先写了个翻译字节码的脚本:

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
class Translate():
def opcode(self):
'''
得到一个字节码字典
'''
dic = r'''1 getchar()
2 pop a; printf(a)
3 vm_eip ++
4 push data
5 push d[data]
6 pop d[data]
7 push c[data]
8 pop c[data]
9 pop a; pop b; push b+a
0xa pop a; pop b; push b-a
0xb pop a; pop b; push b*a
0xc pop a; pop b; push b/a
0xd pop a; pop b; push b%a
0xe pop a; pop b; push b^a
0xf pop a; pop b; push b&a
0x10 pop a; pop b; push b|a
0x11 pop a; push -a;
0x12 pop a; push ~a;
0x13 pop a; pop b; if (b!=a) {vm_eip+=2} else {vm_eip+=data}
0x14 pop a; pop b; if (b==a) {vm_eip+=2} else {vm_eip+=data}
0x15 pop a; pop b; if (b<=a) {vm_eip+=2} else {vm_eip+=data}
0x16 pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=data}
0x17 pop a; pop b; if (b>=a) {vm_eip+=2} else {vm_eip+=data}
0x18 pop a; pop b; if (b>a) {vm_eip+=2} else {vm_eip+=data}
0x19 pop a; push c[a]
0x1a pop a; pop b; c[a] = b
0x1b pop a; push d[a]
0x1c pop a; pop b; d[a] = b
0x1d vm_eip += data; if (*vm_eip>0x1d) return vm_eip
0x1e return vm_eip'''
self.opcode_dict = {}
for i in dic.split('\n'):
num, opcode = int(i.split('\t')[0], 16), i.split('\t')[1]
self.opcode_dict[num] = opcode


def translate_part_3_opcode(self):
'''
翻译字节码
'''
self.opcode()

with open(r'code', 'rb')as f:
self.b = f.read()

eip = 0x126
while eip < 0x1e8:
op = self.b[eip]
if 'data' in self.opcode_dict[op]:
#将data替换为具体的操作数
data = self.b[eip+1]
print('{0:<8}{1:<5}{2:<7}{3}'.format(hex(eip), hex(op), hex(data), self.opcode_dict[op].replace('data', hex(data))))
eip += 2
else:
print('{0:<8}{1:<12}{2}'.format(hex(eip), hex(op), self.opcode_dict[op]))
eip += 1


def __init__(self):
self.opcode()
self.translate_part_3_opcode()


t = Translate()

本脚本没有写具体跳转的指令翻译(因为本来就是个辅助分析的程序而已)

比如这条输出:

0x178 0x1d 0xbc vm_eip += 0xbc; if (*vm_eip>0x1d) return vm_eip

需要手动计算出跳转的位置,手动替换成下面这条指令:

0x178 0x1d 0xbc jmp 0x134

又比如这条:

0x18c 0x16 0x34 pop a; pop b; if (b<a) {vm_eip+=2} else {vm_eip+=0x34}
需要手动翻译成:

0x18c 0x16 0x34 pop a; pop b; cmp a,b; jbe 0x1c0;

分析

最终我们得到了第三部分的伪汇编指令:

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
0x126   0x4  0x0    push 0x0
0x128 0x6 0x0 pop d[0x0]

0x12a 0x5 0x0 push d[0x0]
0x12c 0x4 0x7 push 0x7
0x12e 0x16 0x56 pop a; pop b; cmp a,b; jbe 0x184;
0x130 0x4 0x0 push 0x0
0x132 0x6 0x1 pop d[0x1]

0x134 0x5 0x1 push d[0x1]
0x136 0x4 0x6 push 0x6
0x138 0x16 0x42 pop a; pop b; cmp a,b; jbe 0x17a;
0x13a 0x5 0x0 push d[0x0]
0x13c 0x4 0x6 push 0x6
0x13e 0xb pop a; pop b; push b*a
0x13f 0x5 0x1 push d[0x1]
0x141 0x9 pop a; pop b; push b+a
0x142 0x4 0x64 push 0x64
0x144 0x9 pop a; pop b; push b+a
0x145 0x19 pop a; push c[a]
0x146 0x12 pop a; push ~a;
0x147 0x5 0x0 push d[0x0]
0x149 0x5 0x1 push d[0x1]
0x14b 0x4 0x2 push 0x2
0x14d 0x9 pop a; pop b; push b+a
0x14e 0xb pop a; pop b; push b*a
0x14f 0xf pop a; pop b; push b&a
0x150 0x4 0x64 push 0x64
0x152 0x5 0x0 push d[0x0]
0x154 0x4 0x6 push 0x6
0x156 0xb pop a; pop b; push b*a
0x157 0x5 0x1 push d[0x1]
0x159 0x9 pop a; pop b; push b+a
0x15a 0x9 pop a; pop b; push b+a

0x15b 0x19 pop a; push c[a]
0x15c 0x5 0x0 push d[0x0]
0x15e 0x5 0x1 push d[0x1]
0x160 0x4 0x2 push 0x2
0x162 0x9 pop a; pop b; push b+a
0x163 0xb pop a; pop b; push b*a
0x164 0x12 pop a; push ~a;
0x165 0xf pop a; pop b; push b&a
0x166 0x10 pop a; pop b; push b|a
0x167 0x5 0x1 push d[0x1]
0x169 0x4 0x7 push 0x7
0x16b 0xb pop a; pop b; push b*a
0x16c 0x5 0x0 push d[0x0]
0x16e 0x9 pop a; pop b; push b+a
0x16f 0x1a pop a; pop b; c[a] = b
0x170 0x5 0x1 push d[0x1]
0x172 0x4 0x1 push 0x1
0x174 0x9 pop a; pop b; push b+a
0x175 0x4 0x1 push 0x1
0x177 0x1c pop a; pop b; d[a] = b
0x178 0x1d 0xbc jmp 0x134


0x17a 0x5 0x0 push d[0x0]
0x17c 0x4 0x1 push 0x1
0x17e 0x9 pop a; pop b; push b+a
0x17f 0x4 0x0 push 0x0
0x181 0x1c pop a; pop b; d[a] = b
0x182 0x1d 0xa8 jmp 0x12a
;以上为第一部分,二重循环。设输入为x,求出f(x),存储在c[0]~c[0x29]


;第一个部分循环跳出至此
0x184 0x4 0x1 push 0x1
0x186 0x6 0x0 pop d[0x0]

0x188 0x5 0x0 push d[0x0]
0x18a 0x4 0x2a push 0x2a
0x18c 0x16 0x34 pop a; pop b; cmp a,b; jbe 0x1c0;
0x18e 0x5 0x0 push d[0x0]
0x190 0x4 0x2 push 0x2
0x192 0xd pop a; pop b; push b%a
0x193 0x4 0x0 push 0x0
0x195 0x14 0xf pop a; pop b; cmp a,b; jnz 0x1a4;
0x197 0x5 0x0 push d[0x0]
0x199 0x19 pop a; push c[a]
0x19a 0x5 0x0 push d[0x0]
0x19c 0x4 0x1 push 0x1
0x19e 0xa pop a; pop b; push b-a
0x19f 0x19 pop a; push c[a]
0x1a0 0x9 pop a; pop b; push b+a
0x1a1 0x5 0x0 push d[0x0]
0x1a3 0x1a pop a; pop b; c[a] = b


0x1a4 0x5 0x0 push d[0x0]
0x1a6 0x4 0x2 push 0x2
0x1a8 0xd pop a; pop b; push b%a
0x1a9 0x4 0x1 push 0x1
0x1ab 0x14 0xb pop a; pop b; cmp a,b; jnz 0x1b6;
0x1ad 0x4 0x6b push 0x6b
0x1af 0x5 0x0 push d[0x0]
0x1b1 0x19 pop a; push c[a]
0x1b2 0xb pop a; pop b; push b*a
0x1b3 0x5 0x0 push d[0x0]
0x1b5 0x1a pop a; pop b; c[a] = b


0x1b6 0x5 0x0 push d[0x0]
0x1b8 0x4 0x1 push 0x1
0x1ba 0x9 pop a; pop b; push b+a
0x1bb 0x4 0x0 push 0x0
0x1bd 0x1c pop a; pop b; d[a] = b
0x1be 0x1d 0xca jmp 0x188
;至此为第二部分,按照下标的奇偶,分别将上一步得到的数据进一步加密


;以下为第三部分,判断c[i]是否和c[i+0x32]是否相等,相等输出y,反之输出n
0x1c0 0x4 0x0 push 0x0
0x1c2 0x6 0x0 pop d[0x0]

0x1c4 0x5 0x0 push d[0x0]
0x1c6 0x4 0x29 push 0x29
0x1c8 0x18 0x4 pop a; pop b; cmp a,b; jae 0x1cc;
0x1ca 0x1d 0x1b jmp 0x1e5
;jmp to printf('y')


0x1cc 0x5 0x0 push d[0x0]
0x1ce 0x19 pop a; push c[a]
0x1cf 0x4 0x32 push 0x32
0x1d1 0x5 0x0 push d[0x0]
0x1d3 0x9 pop a; pop b; push b+a
0x1d4 0x19 pop a; push c[a]
0x1d5 0x14 0xc pop a; pop b; cmp a,b; jnz 0x1e1;
;jmp to printf('n')

0x1d7 0x5 0x0 push d[0x0]
0x1d9 0x4 0x1 push 0x1
0x1db 0x9 pop a; pop b; push b+a
0x1dc 0x4 0x0 push 0x0
0x1de 0x1c pop a; pop b; d[a] = b
0x1df 0x1d 0xe5 jmp 0x1c4


0x1e1 0x4 0x6e push 0x6e
;printf('n')
0x1e3 0x2 pop a; printf(a)
0x1e4 0x1e return vm_eip

;printf('y')
0x1e5 0x4 0x79 push 0x79
0x1e7 0x2 pop a; printf(a)

然后有需要慢慢分析。我是手动维护了一个栈,然后手动模拟了一遍程序。

最后整理出一下加密逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def part3(self):
'''
加密flag,加密后的数据放在c[0]~c[29]
'''
for i in range(7):
for j in range(6):
#self.c[j*7+i] = ((~self.c[6*i+j+0x64]) & ((j+2)*i)) | ((self.c[6*i+j+0x64]) & (~((j+2)*i)))
self.c[j*7+i] = self.c[6*i+j+0x64] ^ ((j+2)*i)
#因为a ^ b = (~a & b) | (a & ~b),所以上面两行代码等价

for i in range(1, 0x2a):
if i % 2 == 0:
self.c[i] = (self.c[i] + self.c[i-1]) & 0xff
elif i % 2 == 1:
self.c[i] = (self.c[i] * 0x6b) & 0xff

for i in range(0x29+1):
if self.c[i] != self.c[i+0x32]:
break

if i == 0x29:
print('y')
else:
print('n')

加密部分有两步,第一步逻辑很复杂,但是可以化简(如果我单独思考,肯定没法化简出来,这个化简是令则大佬在题解中说的)。

第二部根据奇偶分别运算。

特别注意循环的起始和终止条件!!!

特别注意c和python之间变量位数的不同,注意&0xff!!!

逆向

最后逆着写出解密脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Decode():
def __init__(self):
import ctypes
c_0x32 = [102, 78, 169, 253, 60, 85, 144, 36, 87, 246, 93, 177, 1, 32, 129, 253, 54, 169, 31, 161, 14, 13, 128, 143, 206, 119, 232, 35, 158, 39, 96, 47, 165, 207, 27, 189, 50, 219, 255, 40, 164, 93]
c = [0] * 0x2a
c_0x64 = [0] * 0x2a

c[0] = c_0x32[0]
for i in range(1,0x2a):
if i % 2 == 0:
for j in range(0x100):
if c_0x32[i] == (j + c_0x32[i-1]) & 0xff:
c[i] = j
elif i % 2 == 1:
for j in range(0x100):
if c_0x32[i] == (j * 0x6b) & 0xff:
c[i] = j

for i in range(7):
for j in range(6):
c_0x64[6*i+j] = c[j*7+i] ^ ((j+2)*i)

print(''.join(map(chr, c_0x64)))

总体难度不是很大,第三道题我赛后搞了六七个小时才做出,不是因为难,只是因为我对c语言的指针很不熟悉,影响了对验证逻辑的理解。

RE1 签到

1587878803272

v6[i-112]v5[i]

1
2
3
4
s = list('akhb~chdaZrdaZudqduvdZvvv|')
for i in range(len(s)):
s[i] = chr((ord(s[i])-1) ^ 6)
print(''.join(s))

encrypt3

1587879014441

1587879028636

首先输入一个即将输入字符串的长度n

然后输入字符串,记为input

然后程序使用enc_flag(网上查了查,意思是encrypt flag)对input加密(说解密也行叭~)

将结果输出,记为output.

分析关键代码:

v10=input[0] ^ input[1] ^ input[2] ^ ... ^ input[n-1]

output[i] = v10 ^ enc_flag[i]

enc_flag[i]已知。

可以看出,v10与input有关,但不管input的内容是什么,v10只是个char。所以可以爆破出所有的output。

程序中没有提示flag是input还是output,但是我们求出output后就已经发现了flag了。

我觉得input没法求,毕竟只需要逐位异或满足v10是某一特定值(爆破求出是64)即可,input的可能的字符串取值空间极大。

1
2
3
4
5
6
7
8
9
10
11
enc_flag = [38,44,33,39,59,35,34,115,117,114,113,33,36,117,118,119,35,120,38,114,117,113,38,34,113,114,117,114,36,112,115,118,121,112,35,37,121,61]
for i in range(0, 0x7f):
output = ''
for j in range(len(enc_flag)):
if not(32 <= (enc_flag[j] ^ i) <= 127):
break
else:
output += chr(enc_flag[j] ^ i)
if 'flag' in output:
print('v10=%d' % i)
print(output)

RE3 (忘了本题名称了)

输入四个int,经过sub_401863()进行变换,得到4个int,并和v9~v12比较数值,相等则输入的4个int的十六进制形式就是flag.

1587880072350

sub_401863() 函数:

第一部分:求出dword_408A40[]

1587880207049

虽然参数里有input,但是函数内部没用到input。用到了main中的v15~v18.

值是固定的,所以动态调试拿到,一共32个int.

之后进入一个32次的循环,循环内部有第2,3,4,5部分。

第二部分:求出v12

1587880490649

1587880769689

函数内部是一个4次循环,每次循环得到一个byte,合起来即一个INT。

对于每个byte:

v12[j] = input[i+2][j]^input[i+1][j]^input[i+3][j]^dword_408A40[i][j],

所以v12在形式上为:v12[0]v12[1]v12[2]v12[3]

按字节异或和按int异或其实是等同的,所以简化为:

v12 = input[i+2]^input[i+1]^input[i+3]^dword_408A40[i]

第三部分:求出v11

1587880746304

1587880788680

得到v11,4个_BYTE,即INT

v11[j] = byte_404100[16*(__int8(v12[j]>>4)) + (v12[j]&0xF)]

可以归纳为v11[j] = byte_404100[v12[j]]

byte_404100为已知的长度为0xff的数组,可以使用idc脚本导出二进制流文件。

导出后(记文件名称为dump),可以使用下面的代码提取成数组:

1
2
3
4
5
6
7
8
9
10
11
import binascii

with open('dump','rb')as f:
data = f.read()
num = []
for i in range(len(data)):
d = data[i : i+1][::-1] #b'\x00\x00C\xdf'
d = str(binascii.hexlify(d))[2:-1] #000043df
d = int(d,16) #17375
num.append(d)
print(num)

代码没优化,其实不需要这么多行,先这样放在这里吧。

第四部分:求出v10

1587880914872

__ROL4__是uint32循环左移,__ROL2__是uint16循环左移,__ROL1__是uint8循环左移

__ROR4__是uint32循环右移,__ROR2__是uint16循环右移,__ROR1__是uint8循环右移

这些名称都是ida的定义,可以在ida根目录\plugins\defs.h中详细查看:

1587881337330

(我tm一开始没注意v10的计算同时包含左移和右移,我眼瞎,以为都是__ROL4__

第五部分:求a[i+4]

1587881415207

1587881426743

a[i+4][j] = a[i][j] ^ v10[j]

求四个byte,合起来一个int.

按字节异或和按int异或其实是等同的,所以简化为:

a[i+4] = a[i] ^ v10

归纳

程序输入4个int,然后通过32次循环,每次循环求出一个input[i+4]

这里的input不完全代表输入,input[0]到input[3]是输入,但是此后的input[4]到input[35]都是求出来的。

验证input[32],input[33],input[34],input[35]是否和main中的v9~v12相等。

为了不影响理解,我把input[]这个数组起个别名:数组a[]

我们把代数式整合一下:

1
2
3
4
5
6
7
v12 = a[i+1] ^ a[i+2] ^ a[i+3] ^ dword_408A40[i]

v11[j] = byte_404100[v12[j]]

v10 = __ROR4__(v11, 14) ^ __ROL4__(v11, 10) ^ v11 ^ __ROL4__(v11, 2) ^ __ROR4__(v11, 8)

a[i+4] = a[i] ^ v10

对于上面的算式,我们已知的有dword_408A40[],byte_404100[]a[32]~a[35]

我们需要求出的是a[0]~a[3]

所以先求v12,再求v11,再求v10,最后求a[i],直到倒着求出a[0]~a[3]

脚本

脚本没整理,有些臃肿,大家将就看吧。

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
'''
循环左移和循环右移
from https://blog.csdn.net/C_chuxin/article/details/83691674
我嫌弃这两函数不够简洁,自己实现了这个需求,所以此段代码作废。
def circular_shift_left(int_value,k,bit = 8):
bit_string = '{:0%db}' % bit
bin_value = bit_string.format(int_value) # 8 bit binary
bin_value = bin_value[k:] + bin_value[:k]
int_value = int(bin_value,2)
return int_value

def circular_shift_right (int_value,k,bit = 8):
bit_string = '{:0%db}' % bit
bin_value = bit_string.format(int_value) # 8 bit binary
bin_value = bin_value[-k:] + bin_value[:-k]
int_value = int(bin_value,2)
return int_value
'''

def __ROL4__(value, k):
k %= 32
return (((value << k)&0xffffffff) | ((value >> (32-k))&0xffffffff))&0xffffffff

def __ROR4__(value, k):
k %= 32
return (((value >> k)&0xffffffff) | ((value << (32-k))&0xffffffff))&0xffffffff

byte_404100 = [214, 144, 233, 254, 204, 225, 61, 183, 22, 182, 20, 194, 40, 251, 44, 5, 43, 103, 154, 118, 42, 190, 4, 195, 170, 68, 19, 38, 73, 134, 6, 153, 156, 66, 80, 244, 145, 239, 152, 122, 51, 84, 11, 67, 237, 207, 172, 98, 228, 179, 28, 169, 201, 8, 232, 149, 128, 223, 148, 250, 117, 143, 63, 166, 71, 7, 167, 252, 243, 115, 23, 186, 131, 89, 60, 25, 230, 133, 79, 168, 104, 107, 129, 178, 113, 100, 218, 139, 248, 235, 15, 75, 112, 86, 157, 53, 30, 36, 14, 94, 99, 88, 209, 162, 37, 34, 124, 59, 1, 33, 120, 135, 212, 0, 70, 87, 159, 211, 39, 82, 76, 54, 2, 231, 160, 196, 200, 158, 234, 191, 138, 210, 64, 199, 56, 181, 163, 247, 242, 206, 249, 97, 21, 161, 224, 174, 93, 164, 155, 52, 26, 85, 173, 147, 50, 48, 245, 140, 177, 227, 29, 246, 226, 46, 130, 102, 202, 96, 192, 41, 35, 171, 13, 83, 78, 111, 213, 219, 55, 69, 222, 253, 142, 47, 3, 255, 106, 114, 109, 108, 91, 81, 141, 27, 175, 146, 187, 221, 188, 127, 17, 217, 92, 65, 31, 16, 90, 216, 10, 193, 49, 136, 165, 205, 123, 189, 45, 116, 208, 18, 184, 229, 180, 176, 137, 105, 151, 74, 12, 150, 119, 126, 101, 185, 241, 9, 197, 110, 198, 132, 24, 240, 125, 236, 58, 220, 77, 32, 121, 238, 95, 62, 215, 203, 57, 72]

dword_408A40 = [0xF9,0x86,0x21,0xF1,0x61,0x2B,0x66,0x41,0x9A,0xB1,0x6A,0x5A,0x77,0x20,0xA9,0x7B,0xF4,0x60,0x73,0x36,0x61,0x0C,0x6A,0x77,0xB3,0x89,0xBB,0xB6,0x51,0x31,0x76,0x24,0x7C,0x30,0x20,0xA5,0xBD,0x4D,0x58,0xB7,0xED,0x53,0x07,0xC3,0x57,0x5B,0xE5,0x7E,0x8C,0x60,0x88,0x69,0xB7,0x95,0xD8,0x30,0xAF,0x14,0xBA,0x44,0xA1,0x95,0x44,0x10,0x28,0xB4,0x20,0xD1,0xA3,0x5F,0xB5,0x73,0x66,0x49,0x87,0xCC,0x39,0x44,0x24,0x92,0x1F,0x64,0x9E,0xE8,0x5A,0x01,0xCA,0x98,0x60,0x90,0x15,0xC7,0x2E,0xFD,0xE1,0x99,0x0C,0xD8,0x9B,0xB7,0xB0,0x15,0x21,0x1D,0xEB,0x8A,0x22,0x0E,0x81,0x0C,0x78,0xF1,0x54,0x36,0x8D,0x42,0x96,0x34,0x29,0x62,0xE5,0x72,0xCF,0x01,0x12,0xA0,0x24,0x91]
dword_408A40_2 = [0] * 32
for i in range(32):
#四个byte合并成一个int
#dword_408A40_2[i] = hex(dword_408A40[i*4+3] * 2**24 + dword_408A40[i*4+2] * 2**16 + dword_408A40[i*4+1] * 2**8 + dword_408A40[i*4])
dword_408A40_2[i] = dword_408A40[i*4+3] * 2**24 + dword_408A40[i*4+2] * 2**16 + dword_408A40[i*4+1] * 2**8 + dword_408A40[i*4]
dword_408A40 = dword_408A40_2
#print(dword_408A40)

global a
a = [0] * 36
a[35] = 3229185894
a[34] = 2011540633
a[33] = 835020779
a[32] = 1191593383


def get_v12(i):
return a[i+1] ^ a[i+2] ^ a[i+3] ^ dword_408A40[i]

def get_v11(v12):
#print(hex(v12))
v12 = hex(v12)[2:]
if len(v12)<8:
v12 = (8-len(v12))*'0' + v12
v11 = ''
for j in range(4):
v12_j = int('0x' + v12[j*2:j*2+2], 16)
v11_j = hex(byte_404100[v12_j])[2:]
if len(v11_j)<2:
v11_j = (2-len(v11_j))*'0' + v11_j
#print(byte_404100[v12_j], ' ', v12_j, ' ', v11_j)
v11 += v11_j
#v11 = '0x' + v11[6:8] + v11[4:6] + v11[2:4] + v11[0:2]
#print(v11)
#print()
return int(v11, 16)

def get_v10(v11):
#return circular_shift_right(v11, 14) ^ circular_shift_left(v11, 10) ^ v11 ^ circular_shift_left(v11, 2) ^ circular_shift_right(v11, 8)
return __ROR4__(v11, 14) ^ __ROL4__(v11, 10) ^ v11 ^ __ROL4__(v11, 2) ^ __ROR4__(v11, 8)

def get_a_i(i):
return a[i+4] ^ get_v10(get_v11(get_v12(i)))

def solve():
import ctypes
for i in range(31, -1, -1):
a[i] = get_a_i(i)
for i in range(4):
#a[i] = ctypes.c_int(a[i]).value
print(a[i])
print('或')
for i in range(4):
a[i] = ctypes.c_int(a[i]).value
print(a[i])
print('一个是int,一个是unsigned int,反正内存中存储的十六进制是相同的')

solve()

结果有两组,分别是int和uint(内存中存储的十六进制是相同的)

img

本文转自https://wsxq2.55555.io/blog/2019/07/07/%E7%A7%91%E5%AD%A6%E4%B8%8A%E7%BD%91/,是我见过的讲解科学上网最全面的一篇文章。

Posted by wsxq2 on 2019-07-07

TAGS:  VPSGFWproxysshshadowsocksshadowsocksrV2Ray代理科学上网

本文最后一次编辑时间:2020-03-19 22:00:07 +0800

本文说明:本文最初写的是在 Kali Linux 中如何实现科学上网(只讲解了客户端的操作),但是后来压根儿就没怎么用 Kali Linux 了,而且发现科学上网这块内容是真的复杂,只讲解客户端的操作非常不完整。由于随着时间的推移,对科学上网的了解也逐渐增加,于是萌生了一个大胆的想法——从头梳理一下科学上网的知识。于是便有了本文

温馨提示:对于文中的维基百科链接,建议好好读一下,尤其是相应的英文页面,内容超级丰富,满满的干货

众所周知,科学上网是每个学计算机的人员的必备技能。本文从科学上网的基本概念开始,简要讲解科学上网的各种方法以及相关的原理。

概述

本部分主要讲述“翻墙”和“墙”的真正含义以及为什么要翻墙。其中“翻墙”和“墙”的定义均引用自“万能”的维基百科,强烈建议概览一下相关的维基百科页面,会让你受益匪浅;为什么要翻墙部分则是我的个人理解,欢迎大家补充

何为翻墙(科学上网)?

  突破网络审查或突破网络封锁,俗称翻墙、穿墙、爬墙、科学上网、魔法上网、爱国上网、自由上网、正常上网等。由于“翻墙”在中国大陆境内成为敏感词汇,现在更多的使用科学上网来代替“翻墙”,通常特指在中国大陆绕过互联网审查封锁技术(IP封锁、端口封锁、关键词过滤、域名劫持等),突破防火长城,实现对网络内容的访问。

  突破网络审查的软件通常被称作翻墙软件,俗称梯子。翻墙软件并不只是VPN、代理软件。它们着眼于获得被屏蔽的网站内容,并在访问受限网站时向ISP隐藏自己的真实地址信息。

  ——引用自突破网络审查 - 维基百科,自由的百科全书

何为墙?

  防火长城[1](英语:Great Firewall,常用简称:GFW,中文也称中国国家防火墙[2],中国大陆民众俗称墙、防火墙、功夫网[3]等等),是对中华人民共和国政府在其互联网边界审查系统(包括相关行政审查系统)的统称。此系统起步于1998年[4],其英文名称得自于2002年5月17日Charles R. Smith所写的一篇关于中国网络审查的文章《The Great Firewall of China》[5],取与Great Wall(长城)相谐的效果,简写为Great Firewall,缩写GFW[6]。随着使用的拓广,中文“墙”和英文“GFW”有时也被用作动词,网友所说的“被墙”即指网站内容被防火长城所屏蔽或者指服务器的通讯被封阻,“翻墙”也被引申为突破网络审查浏览境内外被屏蔽的网站或使用服务的行为。

  ——引用自防火长城 - 维基百科,自由的百科全书

为什么要翻墙?

有人常常问我翻墙有什么好处,为什么要翻墙,现我将其总结如下:

  • 搜索引擎:作为搞机人员最重要的工具,国外的 Google 比国内的 百度 好用得不要太多。
  • 维基百科:比国内的百度百科等更客观,更合理,更准确。其底蕴(维基百科推出时间 2001 年,百度百科 2006 年)是国内任何百科都无法比拟的。详情参见 维基百科 - 维基百科,自由的百科全书百度百科 - 维基百科,自由的百科全书
  • GitHub:作为全球最大的“同性交友网站”(大误),这是一个开发者流连忘返的地方,然而在国内时而能访问,时而不能访问
  • 问答网站:如 Stack Overflow比国内百度大力推荐的百度知道靠谱得不要太多。
  • 官方网站:很多好的软件的官网都在国外,如 Windows,Ubuntu,Kali Linux,CentOS,VirtualBox,7zip,Java,Google Chrome等,在国内访问它们要么访问很慢要么完全无法访问
  • 学习环境:国内不少论坛贴吧等交流网站日常进行着无意义的争辩,且常有学到一点东西就得意忘形的人在其中炫耀,少有技术干货。
  • “翻墙”与“墙”的博弈的趣味性:在学习“翻墙”和“墙”的过程可以收获大量知识
  • ……

方法

既然知道了是什么和为什么,那么接下来便是怎么做了。本部分简要阐述现今(2019-07-01)常用的方法。

方法可粗略地分为免费付费两大部分,其中由于免费的方法大多存在各种问题,所以只是简单地提一下,而付费的方法则是重点阐述的对象。

付费方法中又大体分为使用“机场”使用 VPS 自己搭建代理服务器这两种方法,前者没有技术难度,故只是简单提一下,而后者则难度较大,其中涉及的内容也较多,是本文的核心内容。也正因为后者内容较多,所以在这里的内容只是一个概述,后文会详细讲解

当然,由于翻墙技术的复杂性和不宜公开性,这里的方法可能只是冰山一角。

免费

  • 网上找免费的 VPN、SS、SSR 账号
  • 使用免费工具。如蓝灯(Lantern)等
  • 网上查找免费的 SSR 订阅
  • 免费蹭好友的 VPS

付费

“机场”

购买 VPN, SS, SSR 账号,也被称为“机场”。比较便宜的推荐如下:

  • yizhihongxing(需要科学上网):我最初科学上网时用得便是它,毕竟那个时候啥也不会。99¥/年。当时的使用体验还可以,现在不知道如何

  • justmysocks:亲测好用。237¥/年

      目前只有三个方案可选,支持月/季/半年/年付,付款周期越长价格越便宜,年付只需付10个月,目前支持paypal和支付宝付款,带宽分1G/2.5G/5G,对于GIA来说带宽相当大了。

方案 流量/设备数 带宽 优惠码(5.2%) 价格 购买链接
Just My Socks 100 100G/月 3台设备 GIA 1G JMS9272283 2.88$/月 点击购买
Just My Socks 500 500G/月 5台设备 GIA 2.5G JMS9272283 5.88$/月 点击购买
Just My Socks 1000 1000G/月 无限设备 GIA 5G JMS9272283 9.88$/月 点击购买

  ——引用自好用的机场推荐 Atrandys

常用链接:

  • buyV

  • kdatacenter

VPS

自己租用 VPS 搭建代理服务器。

VPS 提供商
  • 搬瓦工:笔者一直用的它,它价格便宜(笔者当时的是200元/年左右)、速度较快(200KB/s+),但是延迟较高(200ms左右)。近期搬瓦工的大量 VPS IP地址被封,笔者就是其中的一员,但是亲测可以通过套 cf(cloudflare) 的方法继续使用。

    常用链接:

  • vultr:曾经尝试过使用它,但是其 IP 地址大多被封(因为我试了好几个节点就没有一个成功的 :joy:),于是就放弃了

  • virmach:一个好友用过,价格也比较便宜(200元/年),但是速度较慢(20Mbps 带宽下20KB/s)、延迟较高(376ms)。以上是我的测试,我的好友的测试结果和我截然相反,他那边速度较快(100Mbps 带宽下1MB/s)、延迟较低(250ms左右)

  • 其他:hostdare cloudcone hosthatch anynode hostsolutions hostflyte justhost sentris gullo cheapnat GCP xenspec。以上 VPS 提供商笔者和笔者的好友都没有用过,它们是热心网友推荐的

使用的方案

本部分是本文的核心纲要,通过点击相应的方案链接可直接跳转到本文的相应位置。

这里所谓的方案其实实质都是一样的,那便是代理,因为从租用 VPS 的那一刻起,便走上了使用代理的道路

以下方案按出现时间排序(笔者推测的,如有错误欢迎留言)。关于 GFW 的发展史,可参见 道高一尺,墙高一丈:互联网封锁是如何升级的|大陆|互联网审查|端传媒Initium Media

  • 通用代理服务器:起初 GFW 没有现在这么强大的时候,大家可以通过简单的通用代理服务器上网。这里的通用代理服务器指的是 HTTP 代理服务器和 SOCKS 代理服务器。其中 HTTP 代理服务器最为常用。

    虽然该方案对于现今的 GFW 已经没有作用,但其原理和相关协议(例如 socks5 )依然在使用。比如大家熟知的 SS 便在本地主机上搭建了一个 SOCKS 代理服务器(监听地址通常为127.0.0.1 1080)。

    后文将会花不少篇幅讲解这方面的内容

  • SSH 的-D参数:使用 OpenSSH 软件包中ssh命令的 -D 参数即可实现科学上网。该方法的前提是你可以通过 SSH 成功连上你的服务器,且你的服务器能访问国外网站(如谷歌等)。优点在于简单方便,缺点在于不适合多人使用,且可能易被 GFW 识别。

  • VPN:未曾尝试过。请自行研究。后面有空的话可能会补充相关内容

  • Shadowsocks:使用 shadowsocks 服务器端软件和客户端软件即可实现科学上网。虽然不少人说它凉了,但其开发依然活跃,尤其是 shadowsocks-libev(C语言版)。

  • shadowsocksr:使用 shadowsocksr 服务器端软件和客户端软件即可实现科学上网。虽然它好久没有更新了,但是依然能用。

  • V2Ray:使用 V2ray 服务器端软件和客户端软件即可实现科学上网。新兴势力,开发活跃。

  • V2Ray + CDN:即“俗称”的“套cf”。由于用到的 CDN 通常是免费的 cloudflare,所以被网友戏称为“套cf”。它能让被墙 IP 的 VPS 继续使用(SS、SSR、V2ray 能用的前提是 IP 没被封)。

  • 通过已经可以科学上网的电脑实现科学上网:和已经可以科学上网的电脑位于同一局域网并开启它的允许来自局域网的连接(实质是让本地代理的监听地址从127.0.0.1 1080变成0.0.0.0 1080)即可

小结

须知,天下没有免费的午餐(就算有也很少)。上述的方法中,免费的那几种方法大多存在不安全、有流量限制、有速度限制、有广告、不稳定等问题。而付费的机场虽然体验极佳,但是价格昂贵,且通常只能 1~3 人使用。

而自己租用 VPS 则不然,相对安全、流量上限高(如 500G/月,你几乎不可能用完)、无广告、相对稳定,此外如果某个方法无效了换个方法即可,同时会学到更多知识,尤其是网络方面的知识。但是凡事皆有利弊,自己租用 VPS 的弊端在于你会遇到大量问题,花费大量时间。

至于如何选择,可以结合你的需求、兴趣、未来的研究方向综合考虑。我之所以从一开始就果断选择了自己租用 VPS 这条不归路(好吧,事实上最初我也用了将近一年的机场),是出于较大的科学上网需求、对科学上网的兴趣和所选择的研究方向——计算机科学与技术之网络安全。

因此,本文着重讲解,如何通过自己租用的 VPS 搭建代理服务器实现科学上网

自主搭建代理服务器实现科学上网大致流程

自主搭建代理服务器实现科学上网总体流程如下:

  1. 选择 VPS 提供商
  2. 租用 VPS
  3. 在 VPS 上安装操作系统
  4. 连接到你的 VPS
  5. 配置公私钥登录
  6. 选择科学上网方案
  7. 配置你的服务器(VPS)
  8. 配置你的客户端
  9. 测试

如前所述,本文重点在于方法-付费-VPS,即使用 VPS 自主搭建代理服务器实现科学上网。但是事实上,由于篇幅原因,本文侧重于讲解总体流程中的后面三个内容——配置你的服务器(VPS)(方案相关)、配置你的客户端(方案相关) 、测试(方案无关,只讲一般思路)。因为它们才是重难点

选择 VPS 提供商

VPS 简介

VPS,全称 Virtual Private Sever,即虚拟专用服务器。它是一个在真实的服务器中虚拟出来的一个服务器环境,你可以在这个服务器上安装你喜欢的服务器端操作系统,然后做你想做的事,如搭建代理服务、搭建 Web 站点、搭建 VPN 服务等。维基百科中的介绍如下:

  虚拟专用服务器(英语:Virtual private server,缩写为 VPS),是将一台服务器分割成多个虚拟专享服务器的服务。实现VPS的技术分为容器技术和虚拟机技术 。在容器或虚拟机中,每个VPS都可分配独立公网IP地址、独立操作系统、实现不同VPS间磁盘空间、内存、CPU资源、进程和系统配置的隔离,为用户和应用程序模拟出“独占”使用计算资源的体验。VPS可以像独立服务器一样,重装操作系统,安装程序,单独重启服务器。

  ——引用自虚拟专用服务器 - 维基百科,自由的百科全书

其中可安装的操作系统有很多,如 Ubuntu Server、CentOS、Windows Server等,笔者建议使用 CentOS,一个字——“稳”。当然,Ubuntu Server也不错,它有如下优势:

  • 使用体验和 Ubuntu(这里指 Bash)几乎完全一致。故适合用过 Ubuntu 的新手
  • 如果命令未找到,会提示你安装相应的软件包(CentOS可以直接使用yum provides <commandname>查找相应的命令的软件包)
  • 强大的自动补全功能。比 CentOS 强大。因为它甚至可以补全二级命令(如apt-get后的install命令),好像是因为它默认安装了一个叫bash_completion的软件包。

注意,几乎所有的 Linux 服务器版本都不会带图形界面,事实上,Linux 本身就不需要图形界面,请萌新尽早学会使用 Shell(如 Bash)

选择 VPS 提供商

由于提供上述 VPS 的商家很多,如 搬瓦工 vultr virmach hostdare cloudcone hosthatch anynode hostsolutions hostflyte justhost sentris gullo cheapnat GCP xenspec等等。如何选择成为一个比较复杂的问题。需要从价格、速度、稳定性、延迟、易用性等方面来考虑。

笔者使用的是搬瓦工(29.9$/年的那个套餐,不过现在已经没有,曾经还有一个 19.9$/年的套餐),但是近期(2019-06-03)搬瓦工的 IP 被封得有些厉害,故现在不是很推荐,当时选择它是因为它便宜且速度和稳定性尚可。

现在比较好用的 VPS 笔者也不清楚,建议使用搜索引擎搜索

2019-07-21 更新:笔者的搬瓦工 VPS IP 已于 2019-07-06 解封(被封了一个月左右)

租用 VPS

选择好了 VPS 提供商后,便可租用一个 VPS。租用时间可自行斟酌。如果未用过,网上也没有相关的测评,则建议租用时间短些(如 1 个月);如果网上好评如潮且不差钱,则直接 1 年起步即可

温馨提示:租用的 VPS 可以在某些渠道转卖

完成这一步后你需要对你的 VPS 有个大概的认识,主要包括如下几个方面的内容:

  • 你的 VPS 提供商是?这个问题的答案可以帮助你在遇到问题使用正确的关键字搜索。对于搬瓦工而言就是搬瓦工

  • 如何进入你的VPS的控制面板?这个问题最为重要,你必须知道VPS提供商为你提供了怎样的接口来访问和控制你的VPS,以及如何使用这些接口。对于搬瓦工而言是Bandwagon Host - Client Area中的KiwiVM Control Panel。其中有如下几个重要的功能(以搬瓦工为例,其它VPS提供商类似):

    • Main controls:在这里你可以查看你的服务器的概况。包括所在位置、公网 IP、SSH端口、状态、内存和 SWAP 使用情况、磁盘使用情况、带宽使用情况、操作系统、主机名、PTR记录
    • Root - shell interactive:这个接口在你使用 SSH 连不上 VPS 时非常有用。它让你可以直接通过 Web 使用 Shell 对你的 VPS 进行控制
    • Install new OS:这个接口允许你重新安装操作系统。注意,重装操作系统,你之前操作系统上的所有更改过的数据将清空
    • Root password modification:搬瓦工直接提供了重新生成 root 密码的接口。如果你不知道 root 初始密码的话,你可以在这里生成后使用生成的新密码
  • 如何续费?这个问题在你完成一个租用周期依然想继续使用时显得格外重要。对于搬瓦工而言是 Bandwagon Host - Client Area - addfunds

在 VPS 上安装操作系统

租用了 VPS 后,有的商家可能直接给你安装了一个默认的操作系统,有的可能会让你自己部署。无论如何,你可能都会想要安装一个自己喜欢的操作系统。如上所述,笔者建议安装 CentOS 或者 Ubuntu Server。安装方法通常很简单,往往只需要点击一个按钮就可以了(如搬瓦工)

安装好你喜欢的操作系统后,你需要知道如下相关信息:

  • IP 地址
  • SSH 端口
  • root 密码(或者配置 SSH 公/私钥)

从而为下一步的连接做准备

连接到你的 VPS

在这一步中,你需要使用 SSH 工具连接到你的服务器。在 Windows 中,你需要安装一个 SSH 客户端,例如 PuTTY、Xshell、Mobaxterm 等(对于 Windows 10 较新版本,自带 OpenSSH 客户端,可在 Powershell 或 CMD 中直接使用,如ssh -p 22 wsxq2@192.168.56.11,不过使用体验远不如 PuTTY 等软件);而 MacOS 则不需要,其终端支持良好,默认 Shell 为 Bash,ssh 应该是默认安装了的,所以直接使用ssh -p <port> <username>@<server ip>即可,如ssh -p 22 wsxq2@192.168.56.11

笔者使用的是 PuTTY

(我超级喜欢的 PuTTY 竟然更新了?今天点进去一看,我的天,时隔将近两年,PuTTY 终于更新了,现在(2019-07-06)最新版是 2019-03-16 发布的 0.71 版本 :sob:

(2019-07-21 更新:PuTTY竟然又更新了?:scream: 当前最新版是 2019-07-20 发布的 0.72 版本)

安装好 PuTTY 后,你便可以连接到你的 VPS 了:在Host Name中填写你的服务器的 IP 地址,在Port处填写你的服务器的 SSH 服务使用的端口,在Connection type处选择SSH(默认使用的就是SSH,这里提一下是为了以防万一)

如果你的服务器的 IP 没被封、TCP 没被阻断的话,应该就能连上。

第一次连接会提示The authenticity of host...balabala...之类的东东,这是为了防止有人冒充你的服务器接受你的连接从而套取你的密码,但是这是低概率事件,所以输入yes即可。

之后便会要求你输入用户名和密码,通常用户名默认为root,密码应该能在你的 VPS 的控制面板处找到,如果找不到,你可以在控制面板的 shell 接口处(对于搬瓦工而言是KiwiVM Control Panel中的Root - shell interactive)直接修改你的 root 密码(在 shell 中使用passwd命令即可)

登录成功后,你就可以对你的服务器进行配置了。

配置公私钥登录

上传 SSH 客户端的公钥到服务器以方便以后登录(即使用公私钥认证而非用户密码认证,公私钥认证的优点在于不需要每次手动输入密码)。具体方法如下(以 Windows10 上的 PuTTY 客户端为例):

  1. 使用

    1
    PuTTYgen

    工具生成公私钥对。具体步骤如下:

    1. 打开 PuTTYgen。按快捷键Win+S->输入puttygen->回车
    2. 生成公私钥对。点击 Generate->随意晃动鼠标以生成随机参数->生成完成
  2. 复制公钥到剪贴板。全选Public key for pasting into OpenSSH authorized_keys file下面的文本框中的内容,按Ctrl+C复制到剪贴板

  3. 保存私钥到安全的位置。点击 Save private key->点击确定(为了方便不设置 passphrase,passphrase 相当于一个用于保护私钥文件的密码,如果设置了的话,每次使用私钥文件时都会要求输入 passphrase,除非使用 Pageant 工具)->选定要保存到的目录(建议放到个人目录C:\Users\<username>\putty中,注意其中<username>要替换成你自己的用户名)->确定

  4. 上传公钥到VPS。使用 PuTTY 以用户密码的登录方式登录你的VPS。登录成功后,使用如下命令:

    1
    echo '<公钥>' >> ~/.ssh/authorized_keys

    其中<公钥>是你之前生成的公私钥对中的Public key,在 PuTTY 中可以使用Shift+Insert粘贴剪贴板的内容(Ctrl+Insert是复制)

  5. 配置 PuTTY。具体步骤如下:

    1. 打开 PuTTY。Win+S->输入putty->回车
    2. 设置主机和端口。在主界面输入Host NamePort
    3. 设置默认登录用户名。在 PuTTY 主界面点击左侧的Connection中的Data,在Login Details处的Auto-login username处输入root(通常是使用root登录 VPS)
    4. 设置使用的私钥文件。点击左侧的Connection中的SSH中的Auth,在Authentication Parameters处点击Browse,选择你刚刚保存的私钥文件
    5. 设置字体和大小(可选)。PuTTY 默认的字体和字体大小过于丑陋,可以简单设置一下。点击Window中的ApprearanceFont setting处的ChangeFont处输入consolasSize选择14,点击OK
    6. 保存为一个会话。点击左侧的Session回到主界面,在Saved session处输入bwg(或者一个你喜欢的名字),然后点击右下侧的Save
  6. 使用公私钥登录。打开 PuTTY,双击Saved session中的bwg即可

选择科学上网方案

方法-付费-VPS-使用的方案处已经提到了现今比较流行的方案。从中选择一个即可。

如果 IP 没被封,笔者建议使用 Shadowsocks,因为其历史悠久(相对于 SSR 和 V2Ray),操作简单,文档丰富(V2Ray 文档也挺好);如果 IP 被封了,建议使用 V2Ray + CDN 的方案

选择的方案不同,配置服务器和客户端的步骤也不同。

配置你的服务器(VPS)

根据你选择的方案参见后文相应的内容

配置你的客户端

根据你选择的方案参见后文相应的内容

对于大多有图形界面的客户端软件(如 SSR for Windows——shadowsocksr-csharp)而言,配置都相当简单,网上很容易搜到图文教程,本文会一笔带过。

本文着重讲解没有图形界面的客户端软件(如 SSR for Linux——shadowsocksr(Python版))

测试

测试是个非常重要的环境,很多人在失败之后一脸懵逼,只知道自己没成功,但不知道为什么没成功

由于测试(调试)自古以来是难点,因为导致出错的原因很多,让人不知从何下手。然而本文针对的就是这种难点。因此后文将大致介绍测试思路和测试工具

通用代理

温馨提示:从这一部分开始,本文就开始对具体的科学上网方案(参见方法-付费-VPS-使用的方案)作详细的讲解。由于内容庞杂,可能会不断扩充

如前文所述,起初 GFW 没有现在这么强大的时候,大家可以通过简单的通用代理服务器实现科学上网。这里的通用代理服务器指的是 HTTP 代理服务器和 SOCKS 代理服务器。其中 HTTP 代理服务器最为常用。

虽然通用代理对于现今的 GFW 已经没有作用,但其原理和相关协议(例如 socks5 )依然在使用,包括现在的 SS、SSR、V2Ray,都和通用代理实现科学上网使用了几乎相同的原理。比如大家熟知的 SS 方案的服务器端便是相当于一个使用 SS 协议的代理(监听地址通常为<VPS ip> 8388),而其客户端则在本地主机上搭建了一个 SOCKS 代理服务器(监听地址通常为127.0.0.1 1080),通过这种双代理的方式实现科学上网

在了解通用代理之前,我们需要对代理有一些了解。因为代理这个概念是自主搭建代理服务器实现科学上网的核心

代理简介

代理本身是一个很宽泛的概念,维基百科对它的解释如下:

  代理(英语:Proxy)也称网络代理,是一种特殊的网络服务,允许一个网络终端(一般为客户端)通过这个服务与另一个网络终端(一般为服务器)进行非直接的连接。一些网关、路由器等网络设备具备网络代理功能。一般认为代理服务有利于保障网络终端的隐私或安全,防止攻击。

  ——引用自代理服务器 - 维基百科,自由的百科全书

事实上,代理这一块的内容相当多,也相当复杂。就连代理的分类都比较复杂,相关的英文维基百科页面中将代理分为开放代理反向代理,其中开放代理又分为匿名代理透明代理(参见 Proxy server - Wikipedia(英文版的代理服务器页面))。然而并不知道其是否正确。因此,本文只涉及科学上网过程中可能用到的代理相关的知识。

代理的一般原理:客户端(如你的浏览器)<->代理服务器(如你的 VPS)<->目标站点(如 www.google.com )

其中客户端代理服务器之间可以使用各种代理协议,例如 HTTP、SOCKS、SS、SSR、Vmess(V2Ray) 等等。其中 HTTP 和 SOCKS 是早期使用的代理协议,现今因为其极易被 GFW 侦测和识别,所以已经不会用在客户端代理服务器之间,而是用于本地代理(如 SS、SSR、V2Ray);相应地,SS、SSR、Vmess 是现在主流的用于穿墙的代理协议。

2019-08-04 更新:这里将 SS/SSR 理解为独立的新的协议是有问题的,因为维基百科中说它们基于 SOCKS5 协议,或者说就协议而言用的就是 socks5 协议,只是在发送数据前进行了加密和混淆而已(待引证或分析 SS 等软件的源码以证明)。文中的其它部分也存在这个问题

常见的代理方法如下(按出现时间先后顺序排列,但没有严格依据):

代理方法 说明
No Proxy 最初是没有代理的,所有应用都是直连到目的站点
HTTP 后来为了访问直连访问不了的网站,出现了 HTTP 代理
PAC 再后来为了提高代理的灵活性,实现只代理该代理的,不代理不必要代理的 Web 站点这一目标,出现了 PAC (代理自动配置)。它实质上是一个 JS 脚本
WPAD 更后来,为了配置上的方便,即懒得在每个客户端主机上设置 PAC 代理,于是出现了 WPAD (网络代理自动发现协议)
Use System settings 更更后来,为了避免对每一个 Web 客户端进行代理配置,于是出现了系统代理配置(全局),一次配置,多个应用程序使用,方便、快捷。系统代理设置通常支持 HTTP 代理和 PAC 这两种(可能还支持 WPAD)
SOCKS 更更更后来,为了能够高效代理非 Web 应用(如 SSH),于是发明了 SOCKS 协议。需要注意的是,HTTP 代理也支持非 Web 应用(使用 CONNECT),但是不够高效
HTTPS(SSL/TLS) 更更更更后来,为了能够实现更加安全的代理,出现了基于传输层安全(TLS)之上的 HTTP 代理——HTTPS
其它 更更更更更后来,为了对付日渐强大的 GFW,实现科学上网,出现以 SS 为首的各种代理协议,如 SS、SSR、Vmess 等

后文将讲解上述代理方法中的 HTTP、PAC、WPAD、SOCKS、其它,其中着重讲解 HTTPSOCKS。对于其它代理协议(如 SS、SSR、Vmess),将在更后面详细讲解,但着重于应用而非协议细节

版权相关:本部分后面的部分内容(HTTP 代理,SOCKS 代理)修改自 有关几种网络代理协议的探讨 - 图文 - 百度文库。若侵权请联系wsxq2@qq.com删除

HTTP 代理

HTTP 本身是一个用于 Web 的协议,但是其具有代理功能。为了使用它的代理功能,我们需要对 HTTP 协议有个大概的了解

HTTP 协议

HTTP 协议是一个属于应用层(参见 OSI模型 - 维基百科,自由的百科全书)的协议。它于1990年提出,经过十几年的使用与发 展,得到不断地完善和扩展。目前在 WWW 中主要使用的是 HTTP/1.1 版。

在 HTTP 协议中.主要有 GET、POST、CONNECT 和 HEAD 等请求方法。为了把服务器的请求信息传递给客户.HTTP 定义了以下四部分工作过程:

  1. 客户机建立起与服务器的连接。建立一次连接的过程是这样的.客户机打开一个套接字(Socket)并把它约束在一个端口上。打开一个套接字就是建立一个虚拟文件,当向文件上写完数据后,数据在网络上传输。在这之前,HTTP 服务器已在运行,监听某个端口(通常是 80 )等待连接的建立。它们通过 TCP 三次握手进行连接
  2. 客户机向服务器发出请求,指出要检索的文档。连接建立以后,客户机就可以发出请求将请求数据发送到服务器指定的端口。例如:GET <URI> HTTP/1.1。其中GET是方法。用于从服务器请求一个由<URI>标识的资源对象。如果对象是文档或文件,GET 将请求其内容,如果对象是程序或脚本.GET 将请求程序的运行结果或脚本的输出。
  3. 服务器发出响应,包括状态码和文档的正文。当服务器接收到浏览器发出的请求时,搜索客户所需资源并响应。
  4. 客户机或者服务器任一方断开连接。

有关 HTTP 协议的更多信息,请参见 超文本传输协议 - 维基百科,自由的百科全书

基于 GET、POST 方法的代理

如前所述,HTTP 协议具有代理功能。其中最常用的便是基于 GET 和 POST 方法的代理。它们主要用于代理 HTTP 协议,结合缓存可以大大提高效率

在 HTTP 协议中规定了与代理相关的三个实体:

  1. 客户机(Client):一个用于发送 HTTP 请求数据的应用程序。例如浏览器
  2. 服务器(Server):一个接受 HTTP 请求数据并返回相应响应数据的应用程序。例如 Apache、Nginx 等
  3. 代理(Proxy):一个中间程序,它可以充当一个服务器(接受客户机的请求).也可以充当一个客户机(向服务器发起请求)。

当客户端是用通常的代理模式时(即使用 GET 或者 POST 方法发出请求),在客户端与代理服务器建立连接后,代理服务器将收到请求命令。这时代理服务器应该截取主机名部分进行域名解析,并同该主机建立连接,将去掉主机名部分的请求命令转发给它,等待它做出响应,然后将得到的响应转发给客户端,最后断开连接。其工作下图所示:

基于GET-POST方法的HTTP代理原理.png

注:

  • 1.客户连接代理服务器,并发出客户请求;
  • 2.在本地缓存中无此资源时。连接到Internet;
    1. 从Internet上获得所请求的资源;
  • 4.将客户所请求的资源发送给客户;
  • 2’.代理服务系统检索缓存数据库;
  • 3’.如果客户请求的资源在数据库中.则直接将请求的资源发给代理服务器。

由于 GET 和 POST 方法是工作在 HTTP 本层内,对代理服务器而言,接受或发送的数据都是可以理解的,这样它就可以将服务器应答的网页信息存储起来,当再有相同请求的时候,代理就可以快速的将客户端所需内容发送给客户端了。这个方法极大的提高了代理服务器的效率。

基于 CONNECT 方法的代理

在 HTTP 代理中,还有一种隧道代理方式,那就是使用 CONNECT 方法来请求并建立隧道。CONNECT 方法请求代理服务器为客户端和目标服务器建立一个连接通道。这种方法可以用来代理任意应用层协议,如 SSH 等。类似于 SOCKS 协议

在 CONNECT 方法中,请求命令行的请求 URI 部分总是指明请求连接的目的主机名和端口号,由冒号分隔。例如:

1
2
CONNECT example.com:80 HTTP/1.1
Host:example.com:80

在代理服务器作出成功的响应后,由客户端到服务器的隧道就被建立起来了。这种代理方式实际上是工作在应用层之下,因此代理服务器并不能对客户端与服务器发送来的数据进行理解.而只是简单的转发,所以基于 CONNECT 方法的代理方法不能进行缓存,但是它能够进行级联,也就是可以连接到下一个代理服务器进行中转。

在 VPS 上搭建 HTTP 代理

使用 apache、Nginx 均可,不过如果只是为了实现 HTTP 代理,它们显得过于庞大了。作为替代,可以选择使用 tinyproxy。tinyproxy 是一个小巧且高效的 HTTP/HTTPS 代理服务器。

但是由于这种方法已经过时,故本文不会细讲

通过 HTTP 代理进行 DNS 查询

HTTP 代理 DNS 的解析必然是通过代理的:

  对于 HTTP/HTTPS 类型的代理服务器而言,请求的域名会作为 HTTP 协议的一部分直接发往代理服务器,不会在本地进行任何解析操作。也就是说,域名的解析与连接目标服务器,是代理服务器的职责。浏览器本身甚至无须知道最终服务器的 IP 地址。据我所知,此行为无法通过浏览器选项等更改。

  也就是说,理论上使用 HTTP/HTTPS 类型的代理服务器时,本地的 DNS 解析、缓存、 hosts 文件等都不使用,与本地设置的 DNS 服务器地址无关。DNS 解析完全在代理服务器上进行。

  ——引用自请问在设置http/https代理后DNS的解析还是通过proxy吗?? · Issue #963 · FelisCatus/SwitchyOmega

所以无需这方面花太多时间。与之形成对比的是后文阐述的 SOCKS 代理,其对于 DNS 查询的处理就比较复杂

在本地代理中的应用

这里的本地代理指的是 SS 等方案中应用程序(如浏览器)和 SS 客户端程序(监听端口通常为 1080 )之间的连接。需要注意的是,在本地代理中,客户端应用程序通常是 Web 客户端(包括但不限于浏览器、浏览器插件、curl、wget、git),服务器应用程序通常是 SS 或 SSR 或 V2Ray 客户端程序(如 SS 的 shadowsocks-libev 中的 ss-local、SSR 中的 shadowsocksr-csharp 等等)。

HTTP 代理出现得在代理中出现最早(待引证),所以应用最为广泛。因此几乎所有支持代理的客户端应用程序都支持 HTTP 代理。如下表所示:

客户端应用程序 测试平台 是否支持 HTTP 代理 备注
Firefox Windows 10
Firefox 的 FoxyProxy 插件 Windows 10
curl Linux
Chrome 的 SwitchyOmega 插件 Windows 10
Kali 系统代理设置 Kali Linux 2.0
git Linux
IE(系统代理设置) Windows 10 注意,Windows 10 系统代理设置实质上和 IE 的代理设置是一样的
wget Linux
Chrome Windows 10 Chrome 使用系统代理设置(即 IE 代理设置,见前面相应的行)

需要注意的是,Windows 10 的设置中的手动代理设置使用的是 HTTP 代理,所以对于只支持 SOCKS 代理的客户端(如 SSH 的-D参数),在这里设置是行不通的

此外,对于服务器应用程序,通过查阅相关文献,得出了如下结论:

对于 SS 等工具而言,原始的服务器应用程序(实际上是 SS 等工具的客户端)不支持 HTTP 代理,只支持 socks5 代理;而 Windows 上的通常都支持 HTTP 代理,且都通常是使用 privoxy 工具转换的,其它衍生的带图形化界面的客户端可能支持 HTTP 代理

如下表所示:

服务器应用程序 测试平台 是否支持 HTTP 代理 备注
shadowsocksr-csharp Windows 10 它使用了 privoxy 工具将 SOCKS 代理转换为 HTTP 代理
shadowsocks-windows Windows 10 同样使用了 privoxy 工具
shadowsocks-qt5 未测试 参见 使用手册 · shadowsocks/shadowsocks-qt5 Wiki
shadowsocks-libev 中的 ss-local Linux
shadowsocksr-python 中的 local.py Linux

由于大多客户端应用程序默认的代理均指 HTTP 代理,所以其配置相当简单。下面将以 Windows10 的设置为例,简单说明一下

Windows 10 系统代理设置

前提:你在127.0.0.1 1080处配置了个 HTTP 代理服务器(如使用后文所述的 shadowsocks-csharp 即可,不过这样得到的不是纯 HTTP 代理,它还支持 SOCKS)

  1. 打开设置。使用快捷键Win+I即可
  2. 找到代理相关的设置。点击右上角的网络和 Internet->点击左下角的代理
  3. 关闭自动代理配置并开启手动代理配置。在自动代理配置部分下将自动检测设置(对应 WPAD)和使用配置脚本(对应 PAC)关掉,将手动代理配置下的使用代理服务器开启
  4. 输入IP端口。在IP输入框中输入127.0.0.1,在端口输入框中输入1080
  5. 点击下方的保存。滑动到最下方,点击保存

完成!

现在让我们看个有趣的事情。打开 IE 中的代理设置,如下图所示:

系统设置中的手动代理设置为127.0.0.1时IE中代理设置的变化.png

由此可知 Windows 10 系统设置中的代理设置默认设置为 HTTP 代理(访问 HTTP,Secure(HTTPS),FTP 站点时均使用 HTTP 代理,而 Socks 为空),且会和 IE 设置同步。即 Windows 10 的系统设置中的代理设置是 IE 中代理设置的上层,它用过调用 IE 的代理设置来完成代理设置,且默认使用 HTTP 代理

让 UWP 应用走系统设置中的代理

有的 UWP 应用自身支持设置代理,例如 Telegram UWP 版。但大多数应用不支持在应用中设置代理,如微软的邮件和日历。对于不支持自己设置代理的 UWP 应用,可以使用全局代理(即系统设置中的代理)

  • 首先,UWP是可以使用全局代理的。也就是说,必须从系统设置里面设置代理才会有效。
  • 第二,系统屏蔽UWP的loopback是出于安全性考虑。虽然这可以通过win32提供的api禁止,但UWP应用不能更改这项屏蔽,微软也没有考虑将loopback作为权限开放。
  • 第三,就算解除了loopback的系统限制,UWP可以使用的loopback也是有限的。即无论是tcp还是udp,win32或uwp应用都无法连接到UWP应用上打开的服务器(绑定的端口)。对于udp,则UWP应用向loopback发送的任何消息都会被系统屏蔽。当然还是有方法解除这一限制的。

  ——引用自 为什么以 Windows 应用商店为代表的 UWP 不能使用代理? - 知乎中 Afanyiyu 的回答

要想使用全局代理,需要将对应的 UWP 应用添加到屏蔽 loopback 的排除列表(因为我们设置的是本地代理,监听地址是环回地址)。一个简单的方法是使用 Fiddler。具体步骤如下:

  1. 打开 Fiddler。按Win+S->输入fiddler->回车

  2. 打开 WinConfig。点击 Fiddler 中左上角的WinConfig,并允许管理员权限申请

  3. 选择正确的 UWP 应用。在弹出的AppContainer Loopback Exemption Utility窗口中选择要使用全局代理的 UWP 应用

    这里所谓的“正确”非常重要。比如对于邮件和日历这个 UWP 应用,实际需要勾选以下三个应用:

    • 邮件和日历
    • microsoft.windows.authhost.a_8wekyb3d8bbwe
    • email and accounts

    如何知道哪些是“正确的”?答案是试。想个比较快的方法很容易将它试出来

  4. 保存。点击Save Changes

详情参阅 如何为 Windows 10 UWP 应用设置代理 - 知乎

Windows 真·系统全局代理(透明代理)

由于我们科学上网的主要目的在于访问国外的 Web 网站,即客户端是使用 HTTP 协议的浏览器。众所周知,HTTP 协议是基于 TCP 的,上述方法对于基于 TCP 的 Web 浏览器而言自然没有问题。

但是在玩游戏时,可能会大量用到 UDP 协议(就算不玩游戏,DNS 也是基于 UDP 的,虽然 DNS 有很多其它的解决办法)。

由前所述,HTTP 基于 TCP 协议,虽然有 CONNECT 方法可以用于代理非 Web 应用,但是它支持代理使用 UDP 协议的流量吗?

此外有的游戏(或应用)可能不会理会 Windows 系统设置中的代理设置。那么问题来了,如何让所有应用强制走代理?甚至包括使用 UDP 协议的应用?

答案是劫持流量

进一步的分析日后进行(又挖了一个大坑)

PAC

简介

  代理自动配置(英语:Proxy auto-config,简称PAC)是一种网页浏览器技术,用于定义浏览器该如何自动选择适当的代理服务器来访问一个网址。

  一个PAC文件包含一个JavaScript形式的函数“FindProxyForURL(url, host)”。这个函数返回一个包含一个或多个访问规则的字符串。用户代理根据这些规则适用一个特定的代理器或者直接访问。当一个代理服务器无法响应的时候,多个访问规则提供了其他的后备访问方法。浏览器在访问其他页面以前,首先访问这个PAC文件。PAC文件中的URL可能是手工配置的,也可能是是通过网页的网络代理自动发现协议(WPAD)自动配置的。

  ——引用自 代理自动配置 - 维基百科,自由的百科全书

PAC 实现了自动代理,即只代理需要代理的,不代理不需要代理的,而且还可以提供多个代理服务器,在某个失效时自动切换至另一个。其核心是 PAC 脚本,它实质上是一个 JS 文件(也被称为 PAC 文件)。该文件只有FindProxyForURL函数是必需的。详细内容请点击上面给出的维基百科的链接以查看。

一个最简单的 PAC 文件长这样:

1
2
3
function FindProxyForURL(url, host) {
return "SOCKS5 127.0.0.1:1088; DIRECT";
}

代表的含义也非常明显,即优先使用127.0.0.1 1080这个 SOCKS 代理进行访问,如果访问失败再通过直连访问。

对于 SOCKS 代理而言,使用 PAC 有两个好处,第一个好处是可以让手动代理不支持 socks5 的浏览器支持 socks5 代理(如 Windows 下的 Chrome,它使用系统的代理设置,而系统的代理设置只支持 socks4);第二个好处在于可以避免 DNS 污染,因为在 PAC 文件中像上面那样写的话,DNS 查询必然会通过 socks5 代理服务器进行(参见 关于 SOCKS 代理的远端 DNS 解析 | 边际效应 - 杨文博

其中返回的值除了SOCKS5 127.0.0.1:1088DIRECT之外,还可以为如下形式:

最初:

  • DIRECT

    直接连接

  • PROXY host:port

    指定 HTTP 代理服务器

  • SOCKS host:port

    指定 SOCKS 代理服务器

后来新增:

  • HTTP host:port

    明确指定使用 HTTP 代理服务器

  • HTTPS host:port

    明确指定使用 HTTPS 代理服务器(然而我到现在也没弄懂 HTTPS 代理服务器是啥)

  • SOCKS4 host:port

    明确指定使用 SOCKS 代理服务器,且使用版本 4 的协议(具体是 socks4 还是 socks4a 依然不清楚)

  • SOCKS5 host:port

    明确指定使用 SOCKS 代理服务器,且使用版本 5 的协议(即 socks5)

PAC 脚本通常保存为 proxy.pac,且其 MIME 类型通常设置为application/x-ns-proxy-autoconfig(在 Apache 中设置 MIME 类型超级简单,只需要在其主配置文件/etc/httpd/conf/httpd.conf中添加AddType application/x-ns-proxy-autoconfig .pac即可,也可直接修改/etc/mime.types文件)

需要注意的是 PAC 脚本实现代理只适用于浏览器(毕竟是 JS 脚本嘛),不能用于其它应用

关于 PAC 的更多信息请参见 breakwa11/gfw_whitelist: gfw_whitelistProxy servers and tunneling - HTTP | MDN 中的 Proxy Auto-Configuration (PAC) 部分

应用举例

Kali Linux 通过系统设置配置 PAC 实现自动科学上网
  • 前提:有个 SS/SSR 账号,且已经配置好客户端(即已在127.0.0.1 1080处搭建了 socks5 代理)。如何在 VPS 上搭建 SS/SSR 服务器及配置相应的客户端请参见后文
  • OS:Kali 2.0

此方法主要使用了genpac(GENerate PAC file)生成 PAC 文件,并将系统设置中的网络代理方式改为自动,将其Configuration URL指向相应的 PAC 文件位置。具体过程如下:

  1. 安装

    1
    genpac

    1
    2
    3
    4
    #安装
    pip install genpac
    #更新
    pip install --upgrade genpac
  2. 生成

    PAC

    文件:

    1
    2
    3
    4
    mkdir ~/.pac
    cd ~/.pac
    touch user-rules.txt #可在此文件中添加用户自定义规则,此处省略
    genpac --pac-proxy "SOCKS5 127.0.0.1:1080" --output="ProxyAutoConfig.pac" --user-rule-from="user-rules.txt"
  3. 设置系统自动代理:设置->网络->网络代理,方式改为自动Configuration URL改为file:///root/.pac/ProxyAutoConfig.pac(注意我用的是root用户,如果非root用户请将/root改为/home/<your username>

  4. 测试:打开浏览器,输入网址www.google.com看是否访问成功

2019-07-03 更新:该方法存在的问题是如果你的 PAC 文件失效了,可能需要重新下载 PAC 文件,即重新执行第 2 步中的genpac步骤。

Windows 上使用 PAC 实现科学上网——情形一
  • 前提:有个 SS/SSR 账号,且已经配置好 Windows 客户端(shadowsocks-windows、shadowsocksr-csharp)。如何在 VPS 上搭建 SS/SSR 服务器及配置相应的客户端请参见后文
  • OS:Windows 10

对于 SS 的 Windows 客户端 shadowsocks-windows,它直接提供了 PAC 支持,不需要自己生成 PAC 文件。具体获取方法如下:

右键任务栏右边的小飞机图标->鼠标移至PAC->点击Copy Local PAC URL

然后在可以设置 PAC 的地方设置即可。如 Windows 10 系统设置中的代理设置处、Chrome 浏览器的 SwitchyOmega 插件的新建 PAC 情景模式处、Firefox 浏览器的设置中的网络设置处、Firefox 浏览器的 FoxyProxy 插件的 Add Proxy 处等。

SSR 类似。至于 V2Ray,其本身并不支持 PAC(事实上,SS/SSR 本身也不支持 PAC ,只是其部分带 GUI 的客户端支持,尤其是 Windows),所以需要自己写 PAC 文件或者根据已有的 PAC 文件进行修改。在这里就不详述了

Windows 上使用 PAC 实现科学上网——情形二
  • 前提:有个国外的VPS,该VPS可直接访问谷歌,你的笔记本可通过SSH连上该VPS,且使用了SSH的

    1
    -D

    参数(参见后文SSH 的-D参数)。具体的命令如下:

    1
    ssh -N -D 1088 -p26635 root@wsxq21.55555.io
  • OS:Windows 10

SSH 的-D参数只支持 socks5 代理,且没有直接的 PAC 支持,所以需要我们自己写 PAC 脚本。之所以会提到这个情形,是因为 Windows 系统代理设置不支持 socks5,而通过 PAC 脚本,即可让 Windows 上的浏览器支持 socks5

现在我们写个简单的脚本:

1
2
3
function FindProxyForURL(url, host) {
return "SOCKS5 127.0.0.1:1088; DIRECT";
}

将它保存到C:\Users\wsxq2\Desktop\proxy.pac。然后在系统代理设置中的自动代理配置下的使用配置脚本处填写file:///C:/Users/wsxq2/Desktop/proxy.pac。理论上是可以的,然而事实上去不行。究其原因,在于:

  This issue occurs because Internet Explorer and Edge on Windows 10-based computers use the WinHttp proxy service to retrieve proxy server information. The WinHttp Proxy service does not support using the ftp:// or file:// protocol for a PAC file.

  ——引用自 Windows 10 does not read a PAC file referenced by a file protocol

有关 Windows 的代理设置详细信息,可参见 Understanding Web Proxy Configuration – IEInternals

虽然在 Windows 10 下, IE、Edge 和 Chrome(Chrome 使用系统的代理设置,即 IE 的代理设置)这几个浏览器不支持本地文件的 PAC 代理,但是有个浏览器却支持,它就是 Firefox。所以在 Firefox 的设置网络设置处,将自动代理配置的 URL 设置为file:///C:/Users/wsxq2/Desktop/proxy.pac是可行的。

值得注意的是 Chrome 浏览器的 SwitchyOmega 插件也不支持file://的 PAC 文件地址,但是它可以直接录入 PAC 脚本,更为方便可靠

另外 Firefox 浏览器的插件 FoxyProxy 并不支持 PAC, WAPD, System Settins。当选择上述之一时会出现如下错误提示:

1
2
Not supported
Due to several Firefox bugs, PAC, WPAD, and System Settings are not yet supported.

WPAD

先放个维基百科的链接镇一下场面,回头有空再填这个坑:网络代理自动发现协议 - 维基百科,自由的百科全书

SOCKS 代理

Socks 代理协议是从 1990 年开始发展的,此后,就一直作为 Internet RFC 中的开放标准。目前,主要有两个版本的 Socks 协议,版本4和版本5(中间还有个 socks4a,但似乎应用不广,故不做阐述)。Socks版本4被简写为“Socks4”,Socks版本5被简写为“Socks5”。与 HTTP 协议不同,Socks 协议实际上是一个纯代理协议。Socks 协议从概念上来讲是介于应用层和传输层之间的“中介层(shim—layer)”,因而实际上是一个“全能”的代理协议,可以代理各种应用层协议,而不仅仅局限于代理 HTTP 协议。

Socks 协议模型

Socks4 协议执行三个功能:连接请求、代理链路的建立和应用数据中转。Socks5协议增加了鉴别功能。其控制流程图如下所示:

SOCKS协议控制流程图.png

上图显示了 Socks5 控制流模型,虚线框中的部分代表 Socks4 的功能。

当应用客户在需请求外部网络的应用服务器服务时,首先与 Socks 服务器建立连接。它向 Socks 服务器发出连接请求及相关的信息,如所支持的鉴别方法列表。Socks 服务器接到消息后,检查安全配置策略,返回服务器选择的安全鉴别方法。Socks 客户再对服务器所作选择进行验证.Socks 客户及服务器分别根据选择的鉴别方法进行处理。Socks 客户向 Socks 服务器发送代理请求。Socks 服务器处理客户的请求,设置代理链路,建立与应用服务器的连接,并向 Socks 客户发送设置状态。而后。Socks 服务器在 Socks 客户与应用服务器之间中转数据。

更多相关信息请参见 SOCKS - 维基百科,自由的百科全书

Socks 协议的优缺点

优点:

  1. Socks 协议是一个纯代理协议,因此比 HTTP 更高效。它可以进行级联。
  2. Socks4 协议由于较为简单.只能简单的按源地址进行访问控制;而 Socks5 协议可以有多种访问控制机制,还可以进行域名地址解析,更进一步减少了客户端的工作量。

缺点:

  1. Socks协议不是应用层代理协议.它并不能理解所代理的网络协议内容,因此不能通过进行本地缓存来提高访问速度。

在 VPS 上搭建 SOCKS 代理

使用 SS5 即可

但是由于这种方法已经过时,故本文不会细讲

通过 SOCKS 代理进行 DNS 查询

使用 socks4a 新增的 HOSTNAME 字段

该方法也叫做 “远程 DNS 解析”。使用最多

  socks4a 是 socks4 协议的简单扩展,允许客户端对无法解析的目的主机,进行自行规定。

  客户端对 DSTIP 的头三个字节设定为 NULL,最后一个字节为非零;对应的IP地址就是 0.0.0.x,其中 x 是非零,这当然不可能是目的主机的地址,这样即使客户端可以解析域名,对此也不会发生冲突。USERID 以紧跟的 NULL 字节作结尾,客户端必须发送目的主机的域名,并以另一个 NULL 字节作结尾。CONNECT 和 BIND 请求的时候,都要按照这种格式(以字节为单位):

VN CD DSTPORT DSTIP 0.0.0.x USERID NULL HOSTNAME NULL
1 1 2 4 variable 1 variable 1

  使用 socks4a 协议的服务器必须检查请求包里的 DSTIP 字段,如果表示地址 0.0.0.x,x是非零结尾,那么服务器就得读取客户端所发包中的域名字段,然后服务器就得解析这个域名,可以的话,对目的主机进行连接。

  ——引用自 SOCKS # socks4a - 维基百科,自由的百科全书

使用 socks5 新增的 UDP 转发

DNS 查询通常通过 UDP/53 进行(有时会用到 TCP),而 SOCKS 客户端通常是基于 TCP 的。所以这里通过 SOCKS 代理进行 DNS 查询也被称为 “UDP Over TCP”

  SOCKS5请求格式(以字节为单位):

VER CMD RSV ATYP DST.ADDR DST.PORT
1 1 0x00 1 动态 2
  • VER 是 SOCKS 版本,这里应该是0x05
  • CMD 是 SOCKS 的命令码
  • 0x01表示 CONNECT 请求
  • 0x02表示 BIND 请求
  • 0x03表示 UDP 转发
  • RSV 0x00,保留
  • ATYP DST.ADDR类型
    • 0x01 IPv4地址,DST.ADDR 部分4字节长度
    • 0x03 域名,DST.ADDR 部分第一个字节为域名长度,DST.ADDR 剩余的内容为域名,没有\0结尾。
    • 0x04 IPv6地址,16个字节长度。
  • DST.ADDR 目的地址
  • DST.PORT 网络字节序表示的目的端口

  ——引用自 SOCKS # SOCKS5 - 维基百科,自由的百科全书

通过使用 UDP 转发,你就可以在本机设置一个国外的 DNS 服务器,每次向它发起 DNS 查询时都会通过代理进行(理论上,个人理解,待验证)

关于 shadowsocks-libev 的 UDP 转发是如何实现的可参考 ss-libev 源码解析udp篇 (1) - 技术无界 - CSDN博客

结合其它工具

对于这部分的内容,原理尚待分析

使用 dns2socks(Github 上也有其项目地址https://github.com/qiuzi/dns2socks)或 overture

此外,shadowsocks-libev 中的 ss-tunnel 也能实现该功能。和 dns2socks 相比,ss-tunnel 走 TCP(即 socks5),而 dns2socks 走 UDP

具体步骤就不详述了

转换 SOCKS 代理为 HTTP 代理

2019-07-04 更新:相关的工具有很多,例如 Polipo、privoxy等。这里使用的工具是 Polipo。需要注意的是 Polipo 最后一次更新是在 2014-05-15(参见 Polipo - Wikipedia),因此更建议使用 privoxy(最后一次更新是在 2016-08-27)

如前所述,通用代理只有两种:HTTP 代理(使用 HTTP 协议)和 SOCKS 代理(使用 socks4/socks4a/socks5 协议)。最常见且最普及的是前者,有的应用不支持后者。所以为了让那些不支持 SOCKS 代理的应用程序使用代理,需要将 SOCKS 转换为 HTTP 代理。

Polipo可以用来将SOCKS的代理转换为HTTP的代理,从而使那些只支持HTTP代理的软件(如wget,部分浏览器,部分操作系统(如 Windows 就只支持 http 代理和 socks4 代理,这是我通过抓包分析发现的))也可以科学上网

以下操作是在 Kali 上进行的,其它基于 Debian 的 Linux 类似:

  1. 安装

    1
    polipo

    :

    1
    apt install polipo
  2. 修改

    1
    /etc/polipo/config

    文件为如下内容:

    1
    2
    3
    4
    5
    6
    7
    logSyslog = true
    logFile = /var/log/polipo/polipo.log

    socksParentProxy = "127.0.0.1:1080"
    socksProxyType = socks5
    proxyAddress = "0.0.0.0"
    proxyPort = 8123
  3. 重启

    1
    polipo

    (安装后它会自动启动,故这里说重启):

    1
    systemctl restart polipo
  4. 验证 polipo 是否正常工作:

    1
    2
    export http_proxy=http://127.0.0.1:8123/
    curl www.google.com

    如果正常,就会返回抓取到的 Google 网页内容。可通过man polipo查看其帮助文档。

在本地代理中的应用

这里的本地代理指的是 SS 等方案中应用程序(如浏览器)和 SS 客户端程序(监听端口通常为 1080 )之间的连接。需要注意的是,在本地代理中,客户端应用程序通常是 Web 客户端(包括但不限于浏览器、浏览器插件、curl、wget、git),服务器应用程序通常是 SS 或 SSR 或 V2Ray 客户端程序(如 SS 的 shadowsocks-libev 中的 ss-local、SSR 中的 shadowsocksr-csharp 等等)。

对于客户端应用程序,通过艰辛的抓包实验(使用工具 WireShark),得出常见客户端应用程序对于 SOCKS 代理的支持度如下(按对 SOCKS 代理的支持度排序):

客户端应用程序 测试平台 支持的 SOCKS 代理 备注
Firefox Windows 10 socks4/socks4a/socks5 手动代理配置中,其 SOCKS v4 选项既支持 socks4,又支持 socks4a
Firefox 的 FoxyProxy 插件 Windows 10 socks4/socks4a/socks5 Proxy Type中的 SOCKS4 同时既支持 socks4,又支持 socks4a
curl Linux socks4/socks4a/socks5 curl 是一个 Linux 下的命令行工具
Chrome 的 SwitchyOmega 插件 Windows 10 socks4/socks5 不支持 socks4a
Kali 系统代理设置 Kali Linux 2.0 socks5 未测试是否支持 socks4/socks4a
git Linux socks5
IE(系统代理设置) Windows 10 socks4 不支持 socks4a/socks5。注意,Windows 10 系统代理设置实质上和 IE 的代理设置是一样的
Chrome Windows 10 不支持 Chrome 使用系统代理设置(即 IE 代理设置,见上一行)
wget Linux 不支持 wget 是一个 Linux 下的命令行工具

由于测试内容过多,难免有疏忽之处,如有错误,还请指正。

需要注意的是,Windows 10 的设置中的手动代理设置使用的是 HTTP 代理,所以对于只支持 SOCKS 代理的客户端(如 SSH 的-D参数),在这里设置是行不通的

此外,对于服务器应用程序,通过查阅相关文献,得出了如下结论:

Windows上的服务器应用程序(即 SS/SSR/V2Ray 客户端程序)通常实现了全能代理(即 http + socks4 + socks4a + socks5),而在 Linux 中的命令行工具(如 shadowsocks-libev 中的 ss-local)中则通常只实现了 socks5 代理

服务器应用程序 测试平台 支持的 SOCKS 代理 备注
shadowsocksr-csharp Windows 10 socks4/socks4a/socks5
shadowsocks-windows Windows 10 socks5 socks4/socks4a未测试
shadowsocks-libev 中的 ss-local Linux socks5 socks4/socks4a未测试
shadowsocksr-python 中的 local.py Linux socks5 socks4/socks4a未测试

下面将根据上文的结论对经典的使用场景进行演示。需要注意的是,你需要一个能连上的 SOCKS 代理服务器,如果你按照后文所述的那样在你的服务器(VPS)上配置了 shadowsocksr-python,在你的客户端(PC)上配置了 shadowsocks-csharp(for Windows)或 shadowsocksr-python(for Linux),那么你的客户端自己(即 IP 地址127.0.0.1,端口1080)就是一个代理服务器。而且如上所述,它至少支持 socks5 代理。

Firefox 浏览器中的代理设置

本部分的目标在于让 Firefox 浏览器浏览所有网页时都走代理。只需在 Firefox 浏览器中设置手动代理即可。

  1. 找到 Firefox 浏览器中的手动代理设置的位置:点击右上角的菜单,选择Preferences,选择General,滑到最下面,选择Network Proxy标签下的Settings,选择Manual proxy configuration

  2. 配置:找到SOCKS Host一栏,填入127.0.0.11080,在下面选择SOCKS v5,其它栏留空(如HTTP Proxy)。并在之后的No Proxy for中填入不需要代理的网址或 IP 地址或网段(例如 127.0.0.1、192.168.0.0/16等)。

    2019-07-04 更新:记得勾选下方的Proxy DNS when using SOCKS v5以防止 GFW 的 DNS 污染。事实上,这里的Proxy DNS when using SOCKS v5设置和 Firefox 中的 中的network.proxy.socks_remote_dns设置是一样的。此外,对连接页面中的手动代理配置中的内容说明如下:

    • HTTP Proxy:HTTP 代理服务器地址。须知,使用前面的方法在127.0.0.1 1080处搭建的代理服务器是 SOCKS 代理服务器(不是 HTTP 代理服务器),且使用的是 socks5 协议(不是 socks4/socks4a 协议)。所以在这里填写127.0.0.1 1080将无法访问(即必须留空)
    • SSL Proxy:访问 HTTPS 站点时使用的代理服务器地址,其实就是 HTTP 代理服务器地址。留空原因同上
    • FTP Proxy:访问 FTP 站点时使用的代理服务器地址,其实就是 HTTP 代理服务器地址(不过需要注意的是 FTP 代理是存在的,例如使用工具ftp.proxy)。留空原因同上
    • SOCKS Host:访问任意站点时使用的代理服务器地址,使用 SOCKS 协议,即 SOCKS 代理服务器地址。

    2019-08-04 更新:需要注意的是,在 Windows 10 中,对于 socks5 代理常常出现失败的情况,即便勾选了Proxy DNS when using SOCKS v5,如果这样,请重启电脑试试;如果依然不行,勾选Enable DNS over HTTPS试试(关于 DoH 的更多信息可参见 在Firefox上开始使用DNS over HTTPS - 知乎

  3. 测试。打开新标签页->输入->回车

Google Chrome 浏览器的 SwitchyOmega 插件
  1. 安装 SwitchyOmega。请前往 SwitchyOmega - Chrome 网上应用商店(需要科学上网),对于不需要科学上网的安装方法,请使用必应百度搜索
  2. 新建类型为代理服务器情景模式。点击 Chrome 右上角的 SwitchyOmega 图标->点击选项->点击新建情景模式->输入一个你喜欢的情景模式名称->选择代理服务器->点击创建->选择代理协议SOCKS5->代理服务器中输入127.0.0.1->代理端口中输入1080->点击应用选项
  3. 使用刚刚创建的情景模式。点击 SwitchyOmega 图标(在浏览器右上角)->选中刚刚创建的情景模式
  4. 测试。打开新标签页->地址栏中输入->回车

笔者在 Windows 10 上测试,通过这种方法会默认使用 socks5 中的 HOSTNAME 字段,即默认会将 DNS 解析交给 SOCKS 代理服务器进行。所以通常可以成功访问谷歌

Firefox 浏览器的 FoxyProxy 插件

2019-07-04 更新:本部分的目标在于让浏览器根据浏览的网站的不同自动选择是否走代理,例如对于国外网站走代理,对于国内网站不走代理。方法也很简单,使用浏览器插件即可,FireFox 用 FoxyProxy,Chrome 用 SwitchyOmega。使用的过滤规则不是 PAC,而是 FoxyProxy 自制的规则

FoxyProxy 是 Firefox 浏览器中的一个非常好用的代理插件。因为有科学上网需求的主要是浏览器,且通常访问 Google 等国外网站较多,则可直接使用全局代理,对那少部分的需要访问的国内网站添加过滤规则(即 FoxyProxy 中的 Patterns)

  1. 安装FoxyProxy插件:https://addons.mozilla.org/en-US/firefox/addon/foxyproxy-standard/

  2. 设置

    1
    FoxyProxy

    选项:

    1. Add Proxy: Proxy TypeSOCKS5IP address127.0.0.1Port1080,记得最后点下Save
    2. 添加Patterns: 在选项主界面,点击刚刚添加的ProxyPatterns,根据自己的需要添加Patterns
  3. 启用FoxyProxy:单击浏览器中右上角相应的图标,选择Use Enabled Proxies By Patterns and Priority

  4. 测试:输入网址www.google.com看是否访问成功

Kali 通过系统设置实现全局代理

2019-07-28 更新:本部分讲解在 Kali 中如何通过系统代理设置设置 socks5 代理

在系统设置中的网络代理方式设为手动,并将相应的Socks Host改为127.0.0.1 1080即可。具体步骤如下:

  1. 设置系统手动代理:设置->网络->网络代理,方式改为手动SOCKS Host改为127.0.0.1 1080,其它留空(留空的理由和前文类似)

    • HTTP Proxy:HTTP 代理服务器地址。须知,使用前面的方法在127.0.0.1 1080处搭建的代理服务器是 SOCKS 代理服务器(不是 HTTP 代理服务器),且使用的是 socks5 协议(不是 socks4/socks4a 协议)。所以在这里填写127.0.0.1 1080将无法访问(即必须留空)
    • SSL Proxy:访问 HTTPS 站点时使用的代理服务器地址,其实就是 HTTP 代理服务器地址。留空原因同上
    • FTP Proxy:访问 FTP 站点时使用的代理服务器地址,其实就是 HTTP 代理服务器地址(不过需要注意的是 FTP 代理是存在的,例如使用工具ftp.proxy)。留空原因同上
    • SOCKS Host:访问任意站点时使用的代理服务器地址,使用 SOCKS 协议,即 SOCKS 代理服务器地址。

    温馨提示:该处的设置依赖于network-manager服务,应确保其正在运行。(有的人因为network-manager.servicenetworking.service冲突所以采取网上的建议将network-manager.service给禁用了,结果导致系统设置中和网络相关的设置均不可用。好吧,说的就是我自己-_-)可以使用systemctl命令查看network-manager.service的状态,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    root@kali:~# systemctl status network-manager.service 
    ● NetworkManager.service - Network Manager
    Loaded: loaded (/lib/systemd/system/NetworkManager.service; enabled; vendor preset: enabled)
    Active: active (running) since Thu 2019-07-04 23:30:33 CST; 19h ago
    Docs: man:NetworkManager(8)
    Main PID: 396 (NetworkManager)
    Tasks: 3 (limit: 4695)
    Memory: 13.8M
    CGroup: /system.slice/NetworkManager.service
    └─396 /usr/sbin/NetworkManager --no-daemon

    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.6270] manager: NetworkManager state is now CONNECTED_GLOBAL
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.7143] modem-manager: ModemManager available
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.7188] device (eth0): state change: disconnected -> prepare (reason 'none', sys-iface-state: 'external')
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.7203] device (eth0): state change: prepare -> config (reason 'none', sys-iface-state: 'external')
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.7210] device (eth0): state change: config -> ip-config (reason 'none', sys-iface-state: 'external')
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.7215] device (eth0): state change: ip-config -> ip-check (reason 'none', sys-iface-state: 'external')
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.7226] device (eth0): state change: ip-check -> secondaries (reason 'none', sys-iface-state: 'external')
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.7233] device (eth0): state change: secondaries -> activated (reason 'none', sys-iface-state: 'external')
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.8065] device (eth0): Activation: successful, device activated.
    7月 04 23:30:35 kali.abc.com NetworkManager[396]: <info> [1562254235.8200] manager: startup complete
    root@kali:~#

    另外,这一步中的设置完成后,每次打开新终端的时候,检查代理相关的环境变量,你会发现:

    1
    2
    3
    4
    5
    6
    root@kali:~# env|grep -i proxy
    ALL_PROXY=socks://127.0.0.1:1080/
    no_proxy=
    NO_PROXY=
    all_proxy=socks://127.0.0.1:1080/
    root@kali:~#

    所以这个步骤的实质只是设置了下环境变量 :joy:

    假如你在系统的网络代理设置中将所有代理均设置为127.0.0.1 1080Ignore Hosts设置为127.0.0.1, 192.168.0.0/16,那么在新打开的终端中检查代理相关的环境变量,你会发现:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    root@kali:~# env|grep -i proxy
    HTTP_PROXY=http://127.0.0.1:1080/
    FTP_PROXY=http://127.0.0.1:1080/
    https_proxy=http://127.0.0.1:1080/
    http_proxy=http://127.0.0.1:1080/
    ALL_PROXY=socks://127.0.0.1:1080/
    no_proxy=127.0.0.1,192.168.0.0/16
    NO_PROXY=127.0.0.1,192.168.0.0/16
    HTTPS_PROXY=http://127.0.0.1:1080/
    all_proxy=socks://127.0.0.1:1080/
    ftp_proxy=http://127.0.0.1:1080/
    root@kali:~#

    同样印证了上述结论——在 Kali 系统设置中的网络代理设置处进行手动代理设置实质上是修改了代理相关的环境变量

    需要注意的是(如前文所述),通用代理只有两种:HTTP 代理(使用 http 协议)和 SOCKS 代理(使用 socks4/socks4a/socks5 协议)。即代理相关环境变量中,每个变量=后面的协议部分必需是http/socks4/socks4a/socks5之一(对于curl还支持socks5h,详情参见man curl中的--socks5-hostname参数)

  2. 测试:打开浏览器,输入网址www.google.com看是否访问成功

2019-07-05 更新:如果使用的浏览器是 FireFox,则还需要将网络代理设置为Use system proxy settings(默认是这个,如果改过记得改回去)。同样要记得勾选下方的Proxy DNS when using SOCKS v5以防止 GFW 的 DNS 污染

Kali 真·系统全局代理(透明代理)

2019-07-28 更新:本部分讲解在 Kali 中如何设置真正意义上的系统全局代理(即透明代理)。未实践过

如前所述,在 Kali 系统设置中的网络代理设置处进行手动代理设置实质上是修改了代理相关的环境变量。对于 linux 下不支持代理的程序而言,前面的设置并没有什么用,即并非真的实现了全局代理。那么如果要实现真正意义上的全局代理,即让所有应用都经过代理服务器该怎么办?答案是使用 tsocks:

  tsocks 利用 LD_PRELOAD 机制,代理程序里的connect函数,然后就可以代理所有的 TCP 请求了。不过对于 dns 请求,默认是通过 udp 来发送的,所以 tsocks 不能代理 dns 请求。

  默认情况下,tsocks 会先加载~/.tsocks.conf,如果没有,再加载/etc/tsocks.conf。对于 local ip 不会代理。

  使用方法:

  &emsp;&emsp;sudo apt-get install tsocks &emsp;&emsp;LD_PRELOAD=/usr/lib/libtsocks.so wget http://www.facebook.com &emsp;&emsp;

  ——引用自科学上网的一些原理 | 横云断岭的专栏

关于 Linux 终端下程序的代理设置
终端代理环境变量

如前文所述,通用代理只有两种:HTTP 代理(使用 http 协议)和 SOCKS 代理(使用 socks4/socks4a/socks5 协议)。即代理相关环境变量中,每个变量=后面的协议部分必需是http/socks4/socks4a/socks5之一(对于curl还支持socks5h)。如下所示则全将其设置为了socks5

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Set Proxy
function sp(){
export all_proxy=socks5://127.0.0.1:1080/
export ALL_PROXY=socks5://127.0.0.1:1080/ #有的命令行工具使用大写的变量名,如 curl
export http_proxy=socks5://127.0.0.1:1080/ #有的命令行工具使用小写的变量名,如 curl、wget
export ftp_proxy=socks5://127.0.0.1:1080/ #有的命令行工具使用小写的变量名,如 wget
export FTP_PROXY=socks5://127.0.0.1:1080/ #有的命令行工具使用大写的变量名,如 curl
export https_proxy=socks5://127.0.0.1:1080/ #有的命令行工具使用小写的变量名,如 wget
export HTTPS_PROXY=socks5://127.0.0.1:1080/ #有的命令行工具使用大写的变量名,如 curl
export no_proxy=localhost,127.0.0.1,192.168.0.0 #有的命令行工具使用小写的变量名,如 wget
export NO_PROXY=localhost,127.0.0.1,192.168.0.0 #有的命令行工具使用大写的变量名,如 curl
}

# Unset Proxy
function up() {
unset all_proxy ALL_PROXY http_proxy ftp_proxy FTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY
}

其中的http_proxy表示访问http协议站点使用的代理,而不是使用http代理访问http协议站点。同理ftp_proxy表示访问ftp站点时使用的代理

使用程序的代理相关的参数
  1. git
    
    1
    2
    3
    4
    5

    :已知(亲测)支持

    ```plaintext
    socks5
    1
    http
    这两种代理方式,支持上述的终端代理环境变量。也可单独设置代理以覆盖全局设置:
    1
    2
    3
    4
    5
    6
    # 设置`socks5`代理
    git config --global http.proxy socks5h://127.0.0.1:1080
    # 设置`http`代理
    git config --global http.proxy http://127.0.0.1:1080
    # 取消代理
    git config --global --unset http.proxy
    对于`socks5`代理执行对应的`git config`命令后`.gitconfig`配置文件内容如下:
    1
    2
    [http]
    proxy = socks5h://127.0.0.1:1080
    其中`socks5h`(socks5 hostname)代表远程解析 DNS,这在大多数时候都是推荐选项,当然你也可以选择使用`socks5`。详情参见`man curl`中的`--socks5-hostname`介绍 但是事实上,更建议这样设置:
    1
    git config --global http.https://github.com.proxy socks5h://127.0.0.1:1080
    这时配置文件`~/.gitconfig`内容如下:
    1
    2
    [http "https://github.com"]
    proxy = socks5h://127.0.0.1:1080
    另外需要注意的是,上述设置只能代理 remote-url 使用 HTTPS 协议的流量,如果你采用的是 SSH 协议,则上述设置无效,但是可通过修改 SSH 客户端配置文件来代理 SSH 流量(建议修改`~/.ssh/config`而非全局文件`/etc/ssh/ssh_config`)。添加如下内容即可::
    1
    2
    Host github.com
    ProxyCommand /usr/bin/ncat --proxy-type socks5 --proxy 127.0.0.1:1080 %h %p
    注意我使用的是 nmap 重新开发的工具`nmap-ncat-6.40-16.el7.x86_64`,而非原始的`nc`工具。如果使用原始的`nc`工具(netcat),可以这样:
    1
    2
    Host github.com
    ProxyCommand /usr/bin/nc -X 5 -x 127.0.0.1:1080 %h %p
    当然,Windows通常不会有`nc`命令,即便你安装了 Git Bash,但是有一个`connect`命令作为替代,因此可以这样:
    1
    2
    Host github.com
    ProxyCommand connect -S 127.0.0.1:1080 %h %p
    关于 git 设置代理的更多内容可参见 [git 设置和取消代理](https://gist.github.com/laispace/666dd7b27e9116faece6)
  2. curl:支持socks4socks4asocks5http这几种代理方式,支持上述的终端代理环境变量。也可单独设置代理以覆盖全局设置:

    1
    2
    curl -i4 -m3 -x socks5://192.168.56.11:1080 https://www.google.com
    curl -i4 -m3 -x socks5h://192.168.56.11:1080 https://www.google.com

    注意socks5socks5h的区别,前者解析域名时不使用代理,后者解析域名时要使用代理,由于国内 DNS 可能被污染,故建议使用socks5h。详情参见man curl

    –socks5-hostname <host[:port]>

      Use the specified SOCKS5 proxy (and let the proxy resolve the host name). If the port number is not specified, it is assumed at port 1080. (Added in 7.18.0)

    –socks5 <host[:port]>

      Use the specified SOCKS5 proxy - but resolve the host name locally. If the port number is not specified, it is assumed at port 1080.

    正因为 curl工具支持所有常用代理,我们可以利用它写个用于测试代理服务器是否正常工作的脚本:

    1
    2
    3
    4
    # test proxy
    function tp() {
    curl -i -4 -m10 -s -x socks5h://$1:$2 https://www.google.com
    }

    将其添加至~/.bashrc末尾,通过如下方法使用:

    1
    tp 127.0.0.1 1080
  3. wget: 似乎只支持http协议代理,支持上述的终端代理环境变量。不可单独设置代理以覆盖全局变量

其它代理

该部分是笔者自己的理解,为了为后文作铺垫,仅供参考。

上述的两种代理(HTTP 和 SOCKS)是极为常见的代理方式。但是它们都不提供额外加密(更不提供混淆)的功能,即如果要访问的目标服务器使用的是 HTTP 协议(明文传输,不安全),则客户端和代理服务器间的数据也是明文。因此如果在你(客户端)和你的 VPS(代理服务器)间使用 HTTP 或 SOCKS 协议,那么 GFW 很容易检测到相关特征(如对于明文可以直接进行内容过滤、对于 HTTPS 可以使用 SNI 阻断等)。因此,就需要能对流量进行额外加密的代理协议,比如 SSH、SS、SSR、V2Ray 等等。这些正是后文要讲的内容

SSH, SS, SSR, V2Ray 在的原理总体上和上述的两种代理一样,都是由客户端发送数据到代理服务器,再由代理服务器向目标站点发出请求。但是在客户端这个地方有个本地代理(127.0.0.1 1080),这是上述的两种代理所没有的。原因也很简单,因为上述协议都比较复杂,浏览器等应用程序不可能实现它们的客户端(而浏览器等应用通常实现了 SOCKS 代理客户端,即将 HTTP 请求数据转换为 SOCKS 数据再发送给 SOCKS 代理服务器),所以需要一个独立的客户端应用程序作为中介,而这个中介就是所谓的本地代理(该客户端应用程序作为本地代理服务器)

对于 SSH (主要指它的动态端口转发,即-D参数)而言,这个中介就是 SSH 客户端,如 OpenSSH Client、PuTTY 等。由浏览器发出的数据通过浏览器本身将 HTTP 请求转换为 SOCKS 数据再发送给 SSH 客户端,再由 SSH 客户端将发来的数据加密通过 SSH 隧道传输到代理服务器,再由代理服务器向目标站点请求数据。

类似地,对于 SS 而言,这个中介就是 SS 客户端,例如用于 Windows 的 shadowsocks-windows,用于 Linux 的 shadowsocks-libev 中的ss-local等。由浏览器发出的数据通过浏览器本身将 HTTP 请求转换为 SOCKS 数据再发送给 SS 客户端,再由 SS 客户端将发来的数据加密,然后将加密后的数据发送到代理服务器,再由代理服务器向目标站点请求数据。

而 SSR、V2Ray 也类似,故不再赘述

SSH 的-D参数

简介

OpenSSHPuTTY都支持端口动态转发(即-D参数)。事实上,在 SSH 中,转发共有如下四种:

  • 本地端口转发:将发往本地端口的数据包通过 SSH 隧道转发到远程服务器的端口中,数据包由远程服务器处理。远程服务器可以 SSH 服务器自身,也可以是其它服务器(例如未加密的 HTTP 服务器,加密的 HTTPS 貌似会出于安全考虑拒收这样的数据包)。在sshplink/putty中均使用-L参数

  • 远程端口转发:将发往 SSH 服务器指定端口的数据通过 SSH 隧道转发到本地主机(客户端)的指定端口中,数据包由本地主机进行处理,然后反方向返回响应数据。在sshplink/putty中均使用-R参数

  • 动态端口转发:将发往本地端口的数据包通过 SSH 隧道发送到 SSH 服务器上,再由 SSH 服务器转发到目的地。具体流程如下:

    1. 在本地应用程序和本地端口间使用SOCK4/5协议进行通信(即本地 SOCKS 代理)
    2. 发送到本地端口的数据经过客户端程序ssh加密后发送到 SSH 服务器(即 SSH 加密后的数据通过网络传输)
    3. 服务器端程序sshd将其解密后再转发出去。

    在这种转发中,SSH 服务器用作代理服务器,即转发由客户端发送的数据包。这种转发在sshplink/putty中均使用-D参数

  • X 协议转发:以 GUI(图形用户界面)方式运行一些程序时会用到

关于它们的详细讲解请参见 实战 SSH 端口转发 。下面我们把重心放在第 3 种转发上(即动态端口转发),因为它完美满足了我们科学上网的需求。让我们先来看看它的运行原理

原理

  动态端口转发原理图解  ——引用自 实战 SSH 端口转发

仔细看一下上面的图,让我们再来模拟一遍整个流程(假设浏览器的代理设置为socks5://127.0.0.1:7001,访问的 Web 服务器使用的是 HTTP 协议):

  1. 浏览器发送请求数据到本地主机的7001端口;
  2. 由本地主机上的ssh(客户端)处理该请求数据,将其加密;
  3. ssh(客户端)将加密后的数据发送给 SSH 服务器;
  4. 这个加密后的数据历经千难万险(如经过 GFW)到达服务器程序sshd
  5. sshd(服务器)将该数据解密;
  6. sshd(服务器)选择一个可用的端口(通常 10000+,因此被称为动态端口转发)将解密后的数据转发给目标服务器(在这里是 Web 服务器);
  7. Web 服务器收到解密后的请求数据返回相应的响应数据给 SSH 服务器;
  8. SSH 服务器收到响应数据后将其交给sshd处理;
  9. sshd将其加密后发送给客户端(期间经过 GFW);
  10. ……

清楚了这个 SSH -D参数的原理后对于后文理解 shadowsocks, shadowsocksr, V2ray 很有帮助。事实上,它们的基本原理几乎完全一样

现在再让我们来看一下 Linux 中的 manual 手册对该参数的说明:

  -D port

  This works by allocating a socket to listen to port on the local side, and whenever a connection is made to this port, the connection is forwarded over the secure channel, and the application protocol is then used to determine where to connect to from the remote machine. Currently the SOCKS4 and SOCKS5 protocols are supported, and ssh will act as a SOCKS server. Only root can forward privileged ports. Dynamic port forwardings can also be specified in the configuration file.

  ——引用自man ssh

操作步骤

了解原理后,我们便可以使用如下命令实现动态端口转发:

1
2
ssh -N -D 1080 -p 22  <user>@<hostname> #for Linux/Windows + OpenSSH
plink -batch -N -D 1080 -load <your_saved_session> #for Windows + PuTTY

其中-N参数:

  -NDo not execute a remote command. This is useful for just forwarding ports.

  ——引用自man ssh

执行完上述命令后,本地主机会在本地环回地址(127.0.0.1)的1080端口(当然,你也可以更换成其它端口)监听数据。如果监听到数据,就会将收到的数据加密并发送给 SSH 服务器,然后……(如原理中所述)

那么如何让1080端口监听到数据呢?答案很简单,只需设置全局代理(在 Windows 设置中),并将地址设置为127.0.0.1端口设置为1080即可让所有程序的数据都发送到1080端口

2019-07-18 更新:上述方法有问题。在 Windows 中,设置中的代理设置是通过修改 IE 中的代理设置实现的,且默认使用的是 HTTP 代理(不支持 sock5 代理,IE 代理设置处也只支持 socks4 代理)。作为替代,可使用 PAC 脚本。具体方法参见前文中的Windows 上使用 PAC 实现科学上网——情形二。另一个简单的方法是使用 Chrome + SwitchyOmega

当然你也可以使用前文提到的代理-SOCKS 代理中的在本地代理中的应用一节中的其它方法

可能遇到的问题

channel 1: open failed: administratively prohibited: open failed

  These options can be found in /etc/ssh/sshd_config. You should ensure that:

  • AllowTCPForwarding is either not present, is commented out, or is set to yes
  • PermitOpen is either not present, is commented out, or is set to any[1]

  Additionally, if you are using an SSH key to connect, you should check that the entry corresponding to your SSH key in ~/.ssh/authorized_keys does not have no-port-forwarding or permitopen statements[2].

  ——引用自 SSH tunneling error: “channel 1: open failed: administratively prohibited: open failed” - Unix & Linux Stack Exchange

小结

该方法非常简单,你甚至不需要在服务器上做任何配置,客户端的话ssh+一个浏览器插件即可,但前提在于你有一个在国外的服务器,并且每次使用都需要使用 ssh 连接你的服务器,故只适合特殊情况使用(比如你刚买/租用一个国外的服务器,并且迫切需要科学上网)。

2019-07-18 更新:事实上,你可以使用 Shell 脚本实现自动化。不过 SSH 的特征应该比 SS 等专用翻墙工具好检测

Shadowsocks

重要相关网站:

此方法截止 2018-10-20 已不可用。建议使用本文中的其它方法(可以逐个尝试)

2019-06-30 更新: 事实上,shadowsocks并非真的不可用了,毕竟现在 shadowsocks-libev 还在更新(其开发相当活跃),而且不少机场都是用的shadowsocks(如搬瓦工官方机场justmysocks)。所以当时我那个服务器上的shadowsocks不可用了应该另有其它原因。另外 2017 年 IHMSC 上发表的使用机器学习探测 shadowsocks 流量的论文存在争议

是什么

  Shadowsocks(简称SS)是一种基于Socks5代理方式的加密传输协议,也可以指实现这个协议的各种开发包。当前包使用Python、C、C++、C#、Go语言等编程语言开发,大部分主要实现(iOS平台的除外)采用Apache许可证、GPL、MIT许可证等多种自由软件许可协议开放源代码。Shadowsocks分为服务器端和客户端,在使用之前,需要先将服务器端程序部署到服务器上面,然后通过客户端连接并创建本地代理。

  在中国大陆,本工具广泛用于突破防火长城(GFW),以浏览被封锁、遮蔽或干扰的内容。2015年8月22日,Shadowsocks原作者Clowwindy称受到了中国政府的压力,宣布停止维护此计划(项目)并移除其个人页面所存储的源代码。

  为了避免关键词过滤,网民会根据谐音将ShadowsocksR称为“酸酸乳”(SSR),将Shadowsocks称为“酸酸”(SS)。

  ——引用自Shadowsocks - 维基百科,自由的百科全书

运行原理

  Shadowsocks的运行原理与其他代理工具基本相同,使用特定的中转服务器完成数据传输。例如,用户无法直接访问Google,但代理服务器可以访问,且用户可以直接连接代理服务器,那么用户就可以通过特定软件连接代理服务器,然后由代理服务器获取网站内容并回传给用户,从而实现代理上网的效果。另外用户在成功连接到服务器后,客户端会在本机构建一个本地Socks5代理(或VPN、透明代理等)。浏览网络时,网络流量需要先通过本地代理传递到客户端软件,然后才能发送到服务器端,反之亦然。

  为防止流量被识别和拦截,服务器和客户端软件会要求提供密码和加密方式,并且在数据传输期间会对传入和传出流量进行加密。

  ——引用自Shadowsocks - 维基百科,自由的百科全书

图解:

  简单理解的话,shadowsocks 是将原来 ssh 创建的 Socks5 协议拆开成 server 端和 client 端,所以下面这个原理图基本上和利用 ssh tunnel 大致类似

  shadowsocks原理图解

  其中:

  • 1、6) 客户端发出的请求基于 Socks5 协议跟 ss-local 端进行通讯,由于这个 ss-local 一般是本机或路由器或局域网的其他机器,不经过 GFW,所以解决了上面被 GFW 通过特征分析进行干扰的问题
  • 2、5) ss-local 和 ss-server 两端通过多种可选的加密方法进行通讯,经过 GFW 的时候是常规的TCP包,没有明显的特征码而且 GFW 也无法对通讯数据进行解密
  • 3、4) ss-server 将收到的加密数据进行解密,还原原来的请求,再发送到用户需要访问的服务,获取响应原路返回

  ——引用自写给非专业人士看的 Shadowsocks 简介 | 綠茶如是说

因此,如果要使用 VPS 自行搭建 SS 服务并使用它,我们需要做如下几件事情:

  • 在 VPS 服务器上搭建 SS 服务(对应上图中的SS Server):这个步骤可简单可复杂。想简单的话使用一键安装脚本即可,但是缺点在于你遇到问题后会一脸懵逼;复杂点的方法则是自己一步一步的根据官网指导,结合别人的博客来安装,缺点在于费时
  • 在客户端上进行配置(对应上图中的PCSS Local):对于 Windows, MacOS, Android 这几个操作系统来说这个过程是非常简单的。因为它们的客户端几乎都是带图形界面的(MacOS 也可以使用命令行),且近乎傻瓜式的操作在网上很容易找到带图的教程。而在 Linux 上则稍微麻烦些,不过现在也变得简单了,因为它也有了图形界面的客户端——shadowsocks-qt5,而且是跨平台的(因为 Qt5 这个图形界面接口是跨平台的)。但是,使用命令行更有助于了解原理

安全性

  Shadowsocks的最初设计目的只是为了绕过GFW,而不是提供密码学意义的安全,所以Shadowsocks自行设计的加密协议对双方的身份验证仅限于预共享密钥,亦无完全前向保密,也未曾有安全专家公开分析或评估协议及其实现。如果是在监听类型的国家内想更加安全的上网,基本上Shadowsocks功能不够完善,应该使用隐密性更高的工具。[8]

  Shadowsocks本质上只是设置了密码的专用网络代理协议,不能替代TLS或者VPN,不能用作匿名通信方案,该协议的目标不在于提供完整的通信安全机制,主要是为了协助上网用户在严苛的网络环境中突破封锁。

  在某些极端的环境下,通过深度包检测(DPI)也有可能识别出协议特征。为了确保安全,用户应做好额外的加密和验证措施,以免泄露信息,无论使用的服务器来源是否可靠。2017年9月21日,一篇名为《The Random Forest based Detection of Shadowsock’s Traffic》的论文在IEEE发表,该论文介绍了通过随机森林算法检测Shadowsocks流量的方法,并自称可达到85%的检测精度[9],虽然该论文的有效性遭到网友质疑[10],但机器学习配合GFW已经实现的深度数据包检测来识别网络流量特征的做法是实际可行的,而且还适用于任何网络代理协议而不仅仅局限于Shadowsocks。[11]

  ——引用自Shadowsocks - 维基百科,自由的百科全书

实现

  当前Shadowsocks有多个实现支持,以自由软件形式发布的主要有原始Shadowsocks(以Python语言编写)、Shadowsocks-libev(分支项目openwrt-Shadowsocks)、Shadowsocks-rust、Shadowsocks-go/go-Shadowsocks2、libQtShadowsocks、Shadowsocks-qt5(仅作为客户端)、Shadowsocks-android(仅作为客户端)、Shadowsocks-windows(仅作为客户端)、ShadowsocksX-NG(仅作为客户端)、Outline[12]、V2Ray、Brook[13]等等,还有为数甚多的免费软件及专有软件。

  ——引用自Shadowsocks - 维基百科,自由的百科全书

相对官方的实现请参见: Shadowsocks - Implementations

使用方法

环境说明

  • 服务器操作系统:CentOS7.2
    • 使用的 shadowsocks 实现(服务器):shadowsocks-libev
    • 使用的用户:root
  • 客户端操作系统:Kali-Linux
    • 使用的 shadowsocks 实现(客户端):shadowsocks-qt5
    • 使用的用户:root

值得注意的是上面的服务器和客户端用的软件不一样,但是重要的不是软件表面,而是它们的“内涵”,它们都是使用的 shadowsocks 协议,只不过 shadowsocks-libev(C语言、轻量、快速、开发活跃) 通常用作服务器端(也可用作客户端),shadowsocks-qt5(C++、图形界面、跨平台) 只能用做客户端

另外,对于其它操作系统和实现,参考相应的官网文档操作即可。它们的官网链接在Shadowsocks - Implementations页面中

这里还有份来自别的博客的对shadowsocks-libev的简介:

  shadowsocks-libev 是一个 shadowsocks 协议的轻量级实现,是 shadowsocks-android, shadowsocks-ios 以及 shadowsocks-openwrt 的上游项目。其具有以下特点:

  • 体积小巧,静态编译并打包后只有 100 KB。
  • 高并发,基于 libev 实现的异步 I/O,以及基于线程池的异步 DNS,同时连接数可上万。
  • 低资源占用,几乎不占用 CPU 资源,服务器端内存占用一般在 3MB 左右。
  • 跨平台,适用于所有常见硬件平台,已测试通过的包括 x86,ARM 和 MIPS。也适用于大部分 POSIX 的操作系统或平台,包括 Linux,OS X 和 gwin 等。
  • 协议及配置兼容,完全兼容 shadowsocks 协议,且兼容标准实现中的 JSON 风格配置文件,可与任意实现的 shadowsocks 端或服务端搭配使用。

  ——引用自CentOS 7 配置 shadowsocks-libev 服务器端进行科学上网 | 鸣沙山侧 月牙泉畔

服务器配置(SS Server)

两个方法,使用脚本一键安装和手动安装。前者非常简单,后者较为困难,且不同 Linux 发行版可能不一样

一键安装脚本可在 teddysun/shadowsocks_install at master 获得。注意,因为“某些”原因,该一键安装脚本已经停止更新,参见 Shadowsocks非官方网站

下面详细讲解手动安装的方法(注意操作系统为 CentOS7.2,使用的实现为 shadowsocks-libev)

安装shadowsocks-libev
方法一:添加 librehat-shadowsocks-epel 源
1
2
3
4
5
cd /etc/yum.repos.d/ #进入 CentOS 软件源目录
wget https://copr.fedorainfracloud.org/coprs/librehat/shadowsocks/repo/epel-7/librehat-shadowsocks-epel-7.repo #下载librehat-shadowsocks 软件源
yum install epel-release #安装额外的软件源(shadowsocks-libev的依赖 libsodium 和 mbedtls 要用)
yum makecache fast #更新软件缓存信息(类似 apt update)
yum install shadowsocks-libev #安装

其中,如果安装了 epel-release 依然显示找不到 libsodiummbedtls ,则可以使用命令yum reinstall epel-release重新安装

详情参见Install from repository

温馨提示:通过这种方法安装的shadowsocks-libev的版本为3.2.0(2018年5月发布(参见shadowsocks/shadowsocks-libev at v3.2.0))。如果想要安装最新版本(当前(2019-07-01)为3.3.0)),建议使用从源码编译的方法,即后文中的方法二

方法二:从源码编译安装(推荐)

参考自 shadowsocks libev · iMeiji/shadowsocks_install Wiki

1
2
3
4
5
6
yum install epel-release -y #安装额外的软件源(shadowsocks-libev的依赖 libsodium 和 mbedtls 要用)
yum install gcc gettext autoconf libtool automake make pcre-devel asciidoc xmlto c-ares-devel libev-devel libsodium-devel mbedtls-devel -y #安装依赖
git clone https://github.com/shadowsocks/shadowsocks-libev.git #克隆源代码到本地
cd shadowsocks-libev #进入目录
./autogen.sh && ./configure && make #开始构建(编译)
sudo make install #安装
方法三:生成 RPM 文件安装(推荐)
1
2
3
4
5
git clone https://github.com/shadowsocks/shadowsocks-libev.git #克隆源代码到本地
cd shadowsocks-libev/rpm/ #进入 rpm 目录
./genrpm.sh -b #可通过 -h 参数查看帮助
cd RPMS/x86_64/
yum install shadowsocks-libev-3.3.0-1.21.gite3c6c80.el7.x86_64.rpm #从生成的 RPM 文件安装
方法四:从 SRPM 文件安装(不推荐)

该方法主要是为了涨知识,较繁琐,不实用

1
2
3
git clone https://github.com/shadowsocks/shadowsocks-libev.git #克隆源代码到本地
cd shadowsocks-libev/rpm/ #进入 rpm 目录
./genrpm.sh #可通过 -h 参数查看帮助

此时在SRPMS/目录中便有相应的 SRPM 文件了,通过我另一篇博客中的方法将其转换为 RPM 文件以安装——rpm相关#重建-srpm

概览shadowsocks-libev包中的文件

安装好后让我们来看一下shadowsocks-libev软件包展开后都有哪些文件(使我们对这个软件包有着更清晰的认识):

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
# rpm -ql shadowsocks-libev
/etc/shadowsocks-libev/config.json
/etc/sysconfig/shadowsocks-libev
/usr/bin/ss-local
/usr/bin/ss-manager
/usr/bin/ss-nat
/usr/bin/ss-redir
/usr/bin/ss-server
/usr/bin/ss-tunnel
/usr/lib/systemd/system/shadowsocks-libev-local.service
/usr/lib/systemd/system/shadowsocks-libev-local@.service
/usr/lib/systemd/system/shadowsocks-libev-redir@.service
/usr/lib/systemd/system/shadowsocks-libev-server@.service
/usr/lib/systemd/system/shadowsocks-libev-tunnel@.service
/usr/lib/systemd/system/shadowsocks-libev.service
/usr/lib64/libshadowsocks-libev.la
/usr/lib64/libshadowsocks-libev.so
/usr/lib64/libshadowsocks-libev.so.2
/usr/lib64/libshadowsocks-libev.so.2.0.0
/usr/lib64/pkgconfig
/usr/lib64/pkgconfig/shadowsocks-libev.pc
/usr/share/doc/shadowsocks-libev
/usr/share/doc/shadowsocks-libev/shadowsocks-libev.html
/usr/share/doc/shadowsocks-libev/ss-local.html
/usr/share/doc/shadowsocks-libev/ss-manager.html
/usr/share/doc/shadowsocks-libev/ss-nat.html
/usr/share/doc/shadowsocks-libev/ss-redir.html
/usr/share/doc/shadowsocks-libev/ss-server.html
/usr/share/doc/shadowsocks-libev/ss-tunnel.html
/usr/share/man/man1
/usr/share/man/man1/ss-local.1.gz
/usr/share/man/man1/ss-manager.1.gz
/usr/share/man/man1/ss-nat.1.gz
/usr/share/man/man1/ss-redir.1.gz
/usr/share/man/man1/ss-server.1.gz
/usr/share/man/man1/ss-tunnel.1.gz
/usr/share/man/man8
/usr/share/man/man8/shadowsocks-libev.8.gz

可以看到其最主要的配置文件(/etc/...)应该是/etc/shadowsocks-libev/config.json,其最常用的命令(/usr/bin/...)应该是ss-server(用于服务器),其次应是ss-local(用于客户端)。此外,可以通过systemctl命令(/usr/lib/systemd/system/...)实现对所有命令的控制,如:

1
2
systemctl enable shadowsocks-libev-local@client
systemctl enable shadowsocks-libev-server@server

其中client表示/etc/shadowsocks-libev/client.jsonserver类似,不使用config是为了避免混乱,毕竟服务器和客户端所使用的配置文件还是不一样的

另外其还有静态和动态运行库(/usr/lib64/...,其中*.la表示静态库,*.so表示动态库)。最后其文档也是相当完善的,有用于man命令的在线手册(/usr/share/man/...),也有html格式的网页(/usr/share/doc/...)。因此,强烈建议至少通读一下man 8 shadowsocks-libev

配置shadowsocks-libev

由于在服务器端只使用ss-server,所以我们直接修改默认的配置文件/etc/shadowsocks-libev/config.json即可:

1
2
3
4
5
6
7
8
{
"server":"0.0.0.0",
"server_port":1234,
"local_address": "0.0.0.0",
"local_port":1080,
"password":"shadowsocks",
"timeout":60,
"method":"aes-256-cfb" }

其中local_address字段和local_port字段可省略(它们是用于客户端的)。需要注意的是端口千万不要设置为默认的8388,因为现在的 GFW 好像会阻断端口8388的连接(毕竟用 shadowsocks 的人太多了)

控制shadowsocks-libev

使用如下命令启动:

1
systemctl start shadowsocks-libev-server@config

使用如下命令设置开机自启:

1
systemctl enable shadowsocks-libev-server@config

使用如下命令查看状态:

1
systemctl status shadowsocks-libev-server@config

使用如下命令停止:

1
systemctl stop shadowsocks-libev-server@config
查看日志

可以在/var/log/messages中查看相关日志,也可以使用journalctl -u shadowsocks-libev-server@config命令查看

关于将shadowsocks-libev用作客户端

其实如果使用shadowsocks-libev作为客户端也是极为简单的,其配置文件只需更改server字段即可。如对于上述配置的服务器,客户端可以使用如下配置文件(假设命名为client.json):

1
2
3
4
5
6
7
8
{
"server":"serverip",
"server_port":1234,
"local_address": "0.0.0.0",
"local_port":1080,
"password":"shadowsocks",
"timeout":60,
"method":"aes-256-cfb" }

使用如下命令启动:

1
systemctl start shadowsocks-libev-local@client

使用如下命令设置开机自启:

1
systemctl enable shadowsocks-libev-local@client

……

客户端配置(SS Local + PC)

  • 客户端操作系统:Kali Linux 2.0
  • 使用的 shadowsocks 实现(客户端):shadowsocks-qt5
  • 使用的用户:root

2019-07-02更新: 客户端选择shadowsocks-qt5是因为它简单,界面友好(好吧,是因为当初只听说过它)。现在的我更倾向于使用shadowsocks-libev作为客户端,因为它最近一次更新在 2019 年 7 月,而 shadowsocks(即原始的 Python 版)最后更新时间是 2018 年 10 月,shadowsocks-qt5 最近一次更新则是在 2018 年 8 月

安装并配置shadowsocks-qt5(SS Local 连接到 SS Server)
  1. /etc/apt/sources.list文件末尾添加: deb http://ppa.launchpad.net/hzwhuang/ss-qt5/ubuntu devel main

  2. 更新 apt 软件列表:

    1
    apt update #这里会提示错误,以下两步解决该错误 gpg --keyserver keyserver.ubuntu.com --recv 6DA746A05F00FA99 gpg --export --armor 6DA746A05F00FA99 | sudo apt-key add - apt update #这一步成功后便可安装shadowsocks-qt5了
  3. 安装shadowsocks-qt5: apt install shadowsocks-qt5

  4. 安装后在bash中输入ss-qt5, 完成配置, 配置好后的图如下:

    ss-qt5

设置 PAC 自动代理(PC 连接到 SS Local)
  1. 获得 pac 文件:

    1
    2
    3
    4
    5
    6
    pip install genpac
    pip install --upgrade genpac
    mkdir ~/vpnPAC
    cd ~/vpnPAC
    touch user-rules.txt
    genpac -p "SOCKS5 127.0.0.1:1080" --gfwlist-proxy="SOCKS5 127.0.0.1:1080" --output="autoproxy.pac" --gfwlist-url="https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt" --user-rule-from="user-rules.txt"
  2. 系统设置自动代理: 设置->网络->网络代理方式改为自动配置URL改为:file://root/vpnPAC/autoproxy.pac

2019-08-04 更新:关于 PAC 的更多内容可以参见前文的 PAC 部分。此外,除了这里的 PAC 自动代理,还可参见前文的 代理-SOCKS 代理中的在本地代理中的应用部分达到配置本地代理的目的

优化
  1. 设置开机启动:通过kali linux自带的优化工具实现: Win+a, 直接输入优化工具,出现优化工具图标(当然你也可以自己找),双击,找到开机启动程序,添加shadowsock-qt5
  2. 自动连接某个节点:打开bash,输入ss-qt5右键某个节点->编辑->程序启动时自动连接
  3. 通过快捷键开启或关闭shadowsocks-qt5: 设置->键盘->添加自定义快捷键(滑到最下面你会看到一个+), 名字可以随意,命令输入ss-qt5(关闭时输入pkill ss-qt5),按键设置成你喜欢的即可。

shadowsocksr

  • 该方法最初发布时间:2018-10-20
  • 该方法最后更新时间:2019-07-03。此次更新添加了一些新内容(重要相关网站、简介、基本原理、安全性、实现、服务器配置、2019-07-03 更新),并对之前的部分内容进行了优化

重要链接:

主要是因为 SS 不能用了,所以把服务器改成了 SSR 的。而 Linux 下的 SSR 客户端配置起来有点麻烦,故更新此文,添加了该内容。

起初我以为最简单的方法为使用electron-ssr,因为它看起来那么的棒,结果安装后却只有第一次可以成功打开,不知是它的 BUG 还是kali的 BUG 。

2019-07-03 更新:2019 年 05 月 15 日 electron-ssr 原作者 erguotou520 因为在 Telegram 群里听说 Doubi 被抓,自行删库。参见 format · erguotou520/bye@7541a9aShadowsocks非官方网站。现在其项目由 shadowsocksrr 维护,且最近一次更新是在2019 年 5 月,所以可能还能用,大家可以低调地试试

后来根据Python版SSR客户端4 - Ubuntu 16.04 + SSR翻墙这两个参考链接才成功,下面简要总结如下:

2019-07-03 更新:上面的两个参考链接中,第二个参考链接已失效

简介

  ShadowsocksR(简称SSR)是网名为breakwa11的用户发起的Shadowsocks分支,在Shadowsocks的基础上增加了一些数据混淆方式,称修复了部分安全问题并可以提高QoS优先级。[20]后来贡献者Librehat也为Shadowsocks补上了一些此类特性,[21]甚至增加了类似Tor的可插拔传输层功能。[22]

  ——引用自Shadowsocks#ShadowsocksR - 维基百科,自由的百科全书

需要注意的是其原开发者 breakwa11 已删除了 GitHub 上的所有 ShadowsocksR 代码、解散了相关 Telegram 交流群组,停止开发 ShadowsocksR 项目。但该项目已被多人 fork,并有人在其基础上继续发布新的版本,例如较为知名的 SSRR (ShadowsocksRR)。而本部分(shadowsocksr)讲解的也正是 SSRR

基本原理

由于其前身是 shadowsocks,所以它的基本原理和 shadowsocks 相同,依然可以用前文所述的方法解释,只是将 SS 替换为 SSR 即可:

简单理解的话,shadowsocks 是将原来 ssh 创建的 Socks5 协议拆开成 server 端和 client 端,所以下面这个原理图基本上和利用 ssh tunnel 大致类似

shadowsocks原理图解

其中:

  • 1、6) 客户端发出的请求基于 Socks5 协议跟 ss-local 端进行通讯,由于这个 ss-local 一般是本机或路由器或局域网的其他机器,不经过 GFW,所以解决了上面被 GFW 通过特征分析进行干扰的问题
  • 2、5) ss-local 和 ss-server 两端通过多种可选的加密方法进行通讯,经过 GFW 的时候是常规的TCP包,没有明显的特征码而且 GFW 也无法对通讯数据进行解密
  • 3、4) ss-server 将收到的加密数据进行解密,还原原来的请求,再发送到用户需要访问的服务,获取响应原路返回

——引用自写给非专业人士看的 Shadowsocks 简介 | 綠茶如是说

  因此,如果要使用 VPS 自行搭建 SS 服务并使用它,我们需要做如下几件事情:

  • 在 VPS 服务器上搭建 SS 服务(对应上图中的SS Server):这个步骤可简单可复杂。想简单的话使用一键安装脚本即可,但是缺点在于你遇到问题后会一脸懵逼;复杂点的方法则是自己一步一步的根据官网指导,结合别人的博客来安装,缺点在于费时
  • 在客户端上进行配置(对应上图中的PCSS Local):对于 Windows, MacOS, Android 这几个操作系统来说这个过程是非常简单的。因为它们的客户端几乎都是带图形界面的(MacOS 也可以使用命令行),且近乎傻瓜式的操作在网上很容易找到带图的教程。而在 Linux 上则稍微麻烦些,不过现在也变得简单了,因为它也有了图形界面的客户端——shadowsocks-qt5,而且是跨平台的(因为 Qt5 这个图形界面接口是跨平台的)

  ——引用自前文的SS 运行原理

安全性

简介中所述:

  在 Shadowsocks 的基础上增加了一些数据混淆方式,称修复了部分安全问题并可以提高 QoS 优先级

实现

和 shadowsocks 类似,不过其开发并不活跃。结合 Shadowsocks - ImplementationsGitHub - shadowsocksrr可以知晓它的实现和 SS 的对应关系。

需要注意的是有的实现很长时间没有更新了,下表列出其各个实现的最近一次更新时间(2019-07-03):

实现 编程语言 适用平台 最近一次更新 服务器 or 客户端
shadowsocksr Python Linux, OSX 2018-05 both
shadowsocksr-libev C Linux, OSX, openwrt 2018-03 both
shadowsocksr-android Java,Go Android 2018-03 client
shadowsocks-csharp C# Windows 2019-04 client
SSR-Windows(forked from shadowsocks-csharp) C# Windows 2018-08 client
electron-ssr JavaScript, Vue Linux, OSX, Windows 2019-05 client

上述所有信息来源于 shadowsocksrr

温馨提示:对于 shadowsocksr-csharp(Windows SSR 客户端)而言,它的本地代理是个全能代理,同一端口支持socks4/socks4a/socks5/http(通过 privoxy)(参见 BRITE’S BLOG.人生在世,看得穿,又看得远者prevail everywhere.: ShadowsocksR CSharp

注意其中的服务器和客户端一栏,通常而言,可用于服务器端的话就能用于客户端,所以 shadowsocksr 和 shadowsocks-libev 均为both。此外令人震惊的是,上述所有软件的最近一次更新全是由 Akkariiin (Akkariiin) 完成的,当然,也可能只是因为他恰好负责打包和发布这一工作。无论如何,在此表示敬意和感谢

使用方法

服务器配置(SSR Server)

使用一键安装脚本 teddysun/shadowsocks_install at master

回头有空再补充手动安装的方法

客户端配置(SSR Local + PC)

环境说明
  • 操作系统:Kali Linux。实际上只要是 Linux 将本文内容稍加修改也适用。毕竟核心的东西是不变的
  • 使用的 SSR 客户端:shadowsocksr(Python 版)。
SSR Local 连接到 SSR Server

2019-07-05 更新: 该部分阐述了如何让 SSR Local 连接到 SSR Server。在 SSR Local 和 SSR Server 通信的过程中使用的协议为 SSR

之所以使用 Python 版,是因为我只找到 Python 版的,/笑哭。 这一步是最重要的,后面的方法都建立在这个基础之上

2019-07-03 更新:事实上,SSR 现在可用的 Linux 客户端有三个,shadowsocksr, shadowsocksr-libev, electron-ssr,参见前面的 SSR 实现部分

  1. 获得 Python 版

SSR

的相关文件:

1
2
cd ~/
git clone https://github.com/shadowsocksrr/shadowsocksr

经测试,其实只有 shadowsocksr 下的 shadowsocks 目录是必须的

  1. 根据你的服务器配置修改配置文件

    1
    ~/shadowsocksr/config.json

    :

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    {
    "server": "<ip address>",
    "server_ipv6": "::",
    "server_port": 8388,
    "local_address": "127.0.0.1",
    "local_port": 1080,

    "password": "password",
    "method": "none",
    "protocol": "auth_chain_a",
    "protocol_param": "",
    "obfs": "plain",
    "obfs_param": "",
    "speed_limit_per_con": 0,
    "speed_limit_per_user": 0,

    "additional_ports" : {},
    "additional_ports_only" : false,
    "timeout": 120,
    "udp_timeout": 60,
    "dns_ipv6": false,
    "connect_verbose_info": 0,
    "redirect": "",
    "fast_open": false }
  2. 启动

    SSR

    客户端:

    1
    2
    cd ~/shadowsocksr/shadowsocks
    python2 local.py -c ~/shadowsocksr/config.json
  3. 测试:暂时无法测试(欢迎知道的人告诉我此处应如何测试)

  4. 自动化:将如下bash脚本添加到

    1
    ~/.bashrc

    文件中(我自己写的,欢迎提建议):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function pg(){
    ps aux | grep -v "grep" |grep $1
    }
    function ssr(){
    ps aux |grep "[l]ocal\.py" > /dev/null
    if [ $? -eq 1 ]; then
    python ~/shadowsocksr/shadowsocks/local.py -c ~/shadowsocksr/config.json -d start
    else
    if [ -n "$1" ]; then
    kill `pg "local\.py" | awk '{print $2}'`
    else
    echo "ssr has been run!"
    fi
    fi
    }
    ssr

    简要说一下上面那个函数ssr的用法:直接在 bash 中输入ssr后回车则后台启动(关闭终端也能继续运行)ssr客户端,输入ssr <任意字符>则关闭已启动的ssr客户端。

PC 连接到 SSR Local

实际上就是设置 SOCKS 本地代理,参见前文中的代理部分的 PACSOCKS 代理 的相关内容

V2Ray

V2Ray 是一个非常强大的存在,其多入口多出口的设计理念导致它可以衍变出很多种科学上网方案,并且易于拓展,原理清晰。是深入学习和研究的首选工具

重要链接:

简介

与 SS 和 SSR 不同,V2Ray 是新兴势力(2015.11.30 v1.0),因此其开发活跃,文档丰富

  Project V 是一个工具集合,它可以帮助你打造专属的基础通信网络。Project V 的核心工具称为V2Ray,其主要负责网络协议和功能的实现,与其它 Project V 通信。V2Ray 可以单独运行,也可以和其它工具配合,以提供简便的操作流程。

  ——引用自Project V · Project V 官方网站

V2Ray 自行设计了Vmess应用层协议和mKCP传输层协议,所以建议使用它们。当然,你也可以选择它支持的其它协议(如 SS)

主要特性

  • 多入口多出口: 一个 V2Ray 进程可并发支持多个入站和出站协议,每个协议可独立工作。
  • 可定制化路由: 入站流量可按配置由不同的出口发出。轻松实现按区域或按域名分流,以达到最优的网络性能。
  • 多协议支持: V2Ray 可同时开启多个协议支持,包括 Socks、HTTP、Shadowsocks、VMess 等。每个协议可单独设置传输载体,比如 TCP、mKCP、WebSocket 等。
  • 隐蔽性: V2Ray 的节点可以伪装成正常的网站(HTTPS),将其流量与正常的网页流量混淆,以避开第三方干扰。
  • 反向代理: 通用的反向代理支持,可实现内网穿透功能。
  • 多平台支持: 原生支持所有常见平台,如 Windows、Mac OS、Linux,并已有第三方支持移动平台。
  • 总体原理和 SS 类似,不再赘述。不过它更复杂,更灵活,但是却不混乱,模块划分很合理

  ——引用自Project V · Project V 官方网站

工作原理

  在配置 V2Ray 之前,不妨先来看一下 V2Ray 的工作原理,以下是单个 V2Ray 进程的内部结构示意图。多个 V2Ray 之间互相独立,互不影响。

  V2Ray工作原理.png

  • 需要配置至少一个入站协议(Inbound)和一个出站协议(Outbound)才可以正常工作。协议列表见第二章节。
    • 入站协议负责与客户端(如浏览器)通信:
      • 入站协议通常可以配置用户认证,如 ID 和密码等;
      • 入站协议收到数据之后,会交给分发器(Dispatcher)进行分发;
    • 出站协议负责将数据发给服务器,如另一台主机上的 V2Ray。
  • 当有多个出站协议时,可以配置路由(Routing)来指定某一类流量由某一个出站协议发出。
    • 路由会在必要时查询 DNS 以获取更多信息来进行判断。

  ——引用自Project V · Project V 官方网站

安全性

TODO

实现

官方实现 + 其它实现。

官方实现的核心是 V2Ray,没有图形界面,但基本够用;其它实现参见 神一样的工具们 · Project V 官方网站

使用方法

参见官方文档 下载安装 · Project V 官方网站

由于其相关的文档(主要是指官方文档白话文教程)做得很好,所以没有必要再赘述一遍,重复造轮子

需要注意的是,官方安装脚本不支持 CentOS 6.x 等较老版本的基于 redhat 的操作系统,但是只需稍作更改即可。主要问题出在官方安装脚本go.sh中的installInitScript函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
installInitScript(){
if [[ -n "${SYSTEMCTL_CMD}" ]];then
if [[ ! -f "/etc/systemd/system/v2ray.service" ]]; then
if [[ ! -f "/lib/systemd/system/v2ray.service" ]]; then
cp "${VSRC_ROOT}/systemd/v2ray.service" "/etc/systemd/system/"
systemctl enable v2ray.service
fi
fi
return
elif [[ -n "${SERVICE_CMD}" ]] && [[ ! -f "/etc/init.d/v2ray" ]]; then
installSoftware "daemon" || return $?
cp "${VSRC_ROOT}/systemv/v2ray" "/etc/init.d/v2ray"
chmod +x "/etc/init.d/v2ray"
update-rc.d v2ray defaults
fi
return
}

在执行installSoftware "daemon"时会出现找不到安装包的情况,daemon应该是 Ubuntu 等基于 debian 的发行版才有的软件包。所以剩下的步骤需要手动:

  1. 安装

    1
    chkconfig

    。CentOS 6.x 不使用

1
systemctl

,也不使用

1
update-rc.d

管理自启动的服务,它使用的是

1
chkconfig

,且通常已经安装好了,如果没有安装,执行如下命令安装;

1
yum install chkconfig
  1. 获取正确的init.d相关文件。参见 https://github.com/v2ray/v2ray-core/issues/101#issuecomment-214670792。将其复制为/etc/init.d/v2ray(而不是使用${VSRC_ROOT}/systemv/v2ray

  2. 1
    /etc/init.d/v2ray

    添加可执行权限。

    1
    chmod +x "/etc/init.d/v2ray"
  3. 设置开机自启。

    1
    chkconfig v2ray on

注意事项

  1. 客户端与服务器的时间相差不能超过90秒钟,否则可能出现VMess: Invalid UserERR_CONNECTION_CLOSED等错误

V2Ray + WS + CDN

直接封 VPS 的 IP 地址是 GFW 的一大杀手锏,因为 IP 被封后,VPS 上搭建的所有服务都不能被直接访问(事实上连 ping 都 ping 不通)。好在,使用 CDN 可以完美地解决这个问题

(主要是因为 SSR 不能用了(原因在于 2019-06-01 左右 IP 被封),所以在网上找到了这个方案)

重要链接:

简介

该方案的核心是 CDN。因此我们先来看看什么是 CDN

CDN

  内容分发网络(英语:Content Delivery Network或Content Distribution Network,缩写:CDN)是指一种透过互联网互相连接的计算机网络系统,利用最靠近每位用户的服务器,更快、更可靠地将音乐、图片、影片、应用程序及其他文件发送给用户,来提供高性能、可扩展性及低成本的网络内容传递给用户。

  ——引用自内容分发网络 - 维基百科,自由的百科全书

CDN 最初用于加速访问 Web 网站的静态内容,如 HTML 文档、图片等。而下面我们将要使用的 CDN 算是某种程度上的妙用

除了 CDN 外,该方案还会用到 WS(WebSocket)。为什么要使用 WS 呢?如前所述,CDN 主要用于加速访问 Web 网站的静态内容,而这些内容通常都是使用 Web 相关的协议(如 HTTP, HTTP/2, WebSocket 等)进行访问,为了使用 CDN,我们需要伪装为 Web 相关的流量。

那么我们为什么不伪装成 HTTP 或者 HTTP/2 呢?对于 HTTP,我也不清楚为什么;而对于 HTTP/2,则是因为 cloudflare 不完全支持 HTTP/2 1

总之,我们选择了 WebSocket,那么 WebSocket 是什么呢?

WebSocket

  WebSocket是一种通信协议,可在单个TCP连接上进行全双工通信。WebSocket协议在2011年由IETF标准化为RFC 6455,后由RFC 7936补充规范。Web IDL中的WebSocket API由W3C标准化。

  WebSocket使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在WebSocket API中,浏览器和服务器只需要完成一次握手,两者之间就可以创建持久性的连接,并进行双向数据传输。

  ——引用自WebSocket - 维基百科,自由的百科全书

关于 WebSocket 的更多内容,还可以参考 WebSocket 教程 - 阮一峰的网络日志

具体步骤

参见 拯救被墙的IP,CDN + v2ray,安全的科学上网方法 | sprov(以 cloudflare 为例)。

我使用的便是该教程。只不过其中的二级域名我用的是花生壳(oray)的(因为之前我用过它的壳域名)而不是 godaddy 的。花生壳注册域名需要实名认证,而 godaddy 不需要

下面根据该教程总结了一下大概步骤:

  1. 购买一个二级域名。例如

    1
    wsxq2.top

    。下面列几个购买站点:

    1. https://www.godaddy.com/
    2. https://domain.oray.com/domain
  2. 选择一个CDN服务提供商并添加二级域名站点。例如在添加站点处输入

    1
    wsxq2.top

    。下面列几个CDN服务提供商站点:

    1. https://www.cloudflare.com/
  3. 在购买域名的站点配置 DNS。设置域名服务器为 CDN 服务提供商提供的域名服务器地址。例如将 orayNS 管理 处的 设置自定义 DNS 配置为derek.ns.cloudflare.comgail.ns.cloudflare.com

  4. 在CDN服务提供商处配置DNS解析记录。例如在cloudflare的DNS处添加如下记录:

    1
    A www 93.179.128.98 Auto Proxied
  5. 配置 V2Ray 服务端。配置文件样例(放在

    1
    inbounds

    数组中):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    {
    "protocol": "vmess",
    "port": 80,
    "streamSettings":{
    "wsSettings":{
    "path":"/",
    "headers":{}
    },
    "network":"ws"
    },
    "settings": {
    "clients": [
    {
    "id": "使用自己的 ID",
    "level": 1,
    "alterId": 4
    }
    ]
    }
    }
  6. 配置 V2Ray 客户端。配置文件样例(放在

    1
    outbounds

    数组中):

    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
    {
    "protocol": "vmess",
    "tag": "out_vmess",
    "streamSettings": {
    "wsSettings": {
    "path": "/",
    "headers": {}
    },
    "network": "ws"
    },
    "settings": {
    "vnext": [
    {
    "address": "www.wsxq2.top",
    "port": 80,
    "users": [
    {
    "id": "使用自己的 ID",
    "alterId": 4,
    "security": "auto"
    }
    ]
    }
    ]
    }
    }

    注意配置相应的 inboundsrouting

事实上,支持套 CDN 的工具有除了 V2Ray 之外还有 GoFlyway。关于使用 GoFlyway 套 CDN 的具体步骤可参见 GoFlyway 进阶教程:免费域名+免费CDN+HTTP伪装=被墙的IP继续做代理 | 逗比根据地IP被墙怎么办?利用Goflyway+CDN救活你的被墙IP! - flyzy小站

V2Ray + WS + TLS + CDN

简介

V2RAY + WS + TLS + CDN 被称为目前科学上网最安全的手段。虽然其原理复杂,但实现方法非常简单

该方案与上一个方案的区别在于新增了 TLS,因此我们需要先了解 TLS 是什么

TLS

  传输层安全性协议(英语:Transport Layer Security,缩写:TLS)及其前身安全套接层(英语:Secure Sockets Layer,缩写:SSL)是一种安全协议,目的是为互联网通信提供安全及数据完整性保障。网景公司(Netscape)在1994年推出首版网页浏览器-网景导航者时,推出HTTPS协议,以SSL进行加密,这是SSL的起源。IETF将SSL进行标准化,1999年公布第一版TLS标准文件。随后又公布RFC 5246 (2008年8月)与 RFC 6176 (2011年3月)。在浏览器、电子邮件、即时通信、VoIP、网络传真等应用程序中,广泛支持这个协议。主要的网站,如Google、Facebook等也以这个协议来创建安全连线,发送数据。当前已成为互联网上保密通信的工业标准。

  SSL包含记录层(Record Layer)和传输层,记录层协议确定传输层数据的封装格式。传输层安全协议使用X.509认证,之后利用非对称加密演算来对通信方做身份认证,之后交换对称密钥作为会谈密钥(Session key)。这个会谈密钥是用来将通信两方交换的数据做加密,保证两个应用间通信的保密性和可靠性,使客户与服务器应用之间的通信不被攻击者窃听。

  ——引用自传输层安全性协议 - 维基百科,自由的百科全书

具体步骤

这里所谓的具体步骤是在前述的 V2Ray + WS + CDN 的基础上进行的,即假设你已经有了一个二级域名,并使用了一个 CDN。

下面给出步骤:

  1. 使用 Certbot 一键获取证书并配置服务器端。

    进入 Certbot 官网。选择 SoftwareNone of the aboveSystemCentOS/RHEL 7。然后根据它的提示操作即可

    对于其中的第 6 步——Install your certificate,由于我们这里的“webserver”是 v2ray,所以这里修改 v2ray 服务端配置文件为如下内容即可:

    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
    {
    "port":443,
    "protocol":"vmess",
    "settings":{
    "clients":[{
    "id":"使用自己的 ID",
    "alterId":64
    }]
    },
    "streamSettings": {
    "network":"ws",
    "security": "tls",
    "wsSettings":{
    "path":"/",
    "headers":{}
    },
    "tlsSettings": {
    "serverName": "ja.wsxq2.top",
    "certificates": [{
    "certificateFile": "/etc/letsencrypt/live/ja.wsxq2.top/fullchain.pem",
    "keyFile": "/etc/letsencrypt/live/ja.wsxq2.top/privkey.pem"
    }]
    }
    }
    }
  2. 配置客户端。配置文件样例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "protocol": "vmess",
    "tag": "out_vmess_ws_tls_ja",
    "settings": {
    "vnext": [{
    "address": "ja.wsxq2.top",
    "port": 443,
    "users": [{
    "id": "使用自己的 ID",
    "alterId": 4,
    "security": "auto"
    }]
    }]
    }
    }

题外话:我是怎么找到上述获取 TLS 证书的方法的:阅读 V2Ray 官方文档 -> 在 以及广告 · Project V 官方网站 处点击 Let’s Encrypt -> 点击 Getting Started -> 点击 Certbot。选择 SoftwareNone of the aboveSystemCentOS/RHEL 7。根据提示操作即可

通过已经可以科学上网的电脑实现科学上网

也就是说,如果你有一台设备通过上述的方法(SSH/SS/SSR/V2Ray/V2Ray+CDN/V2Ray_WS+TLS+CDN)之一实现了科学上网,那么你就可以借助那台设备轻松地让其它和那台设备属于同一局域网的设备实现科学上网。比如你的实体机(如 Windows)实现了科学上网,那么对于你的 kali 虚拟机你就没必要想尽各种办法让它与你的实体机进行类似的配置以实现科学上网,你只需让虚拟机和 Windows 主机处于同一局域网下即可(对于 Virtual Box 可使用仅主机网络)。

前提条件

  • 已经实现科学上网的主机使用了 SSH/SS/SSR/V2Ray/V2Ray+CDN/V2Ray_WS+TLS+CDN 这几个方法之一。它们的共同点在于都有一个本地代理,且对于这个本地代理,可以将监听地址从127.0.0.1 1080改为0.0.0.0 1080
  • 和可以科学上网的主机处于同一局域网

基本原理

因为默认情况下,出于安全性考虑,我们通常将本地代理中的监听地址设为127.0.0.1 1080。而事实上,在局域网内是相对安全的,如果想让别人能够访问你的本地代理,可以将其改为0.0.0.0 1080

其中 127.0.0.1(即网卡 loopback 的 IP 地址) 是本地环回地址,同一局域网内的其它主机无法访问,只有它自己能访问;0.0.0.0 则不然,它用于表示本机上的任意网卡(除了 loopback)。如果需要让同一局域网内的其它主机访问,必需指定具体的网卡的 IP 地址或者使用 0.0.0.0(这个 IP 用于表示本机上的任意网卡)。

例如,你有两个网卡,一个是用于连接 Virtual Box 中的虚拟机的仅主机网络(IP 地址为192.168.56.100),另一个是用于上网的 WIFI(IP 地址为192.168.2.102),出于安全性考虑,你只打算和虚拟机共享本地代理,不打算让连接到同一 WIFI 中的主机共享本地代理,那么你可以让本地代理的监听地址为192.168.56.100 1080

如果想让任意网卡共享本地代理,即可配置为0.0.0.0 1080

具体方法

实验环境:主机 Windows10 (已实现科学上网),虚拟机 Kali Linux(需要实现科学上网),对于虚拟机 Kali,我使用了两个网卡,网络地址转换仅主机网络,前者保证能连上 Internet,后者保证让 主机和虚拟机处于同一局域网(网段为 192.168.56.0/24)

实现步骤(以 SSR 为例):

  1. 配置主机的 SSR 客户端,使其允许来自局域网的连接。右键任务栏最右边的小飞机图标->点击选项设置->勾选来自局域网的连接

    此时,在 Powershell 或 CMD 中输入netstat -ano | findstr "LISTENING"即可看到本地代理的监听地址从127.0.0.1 1080变为了0.0.0.0 1080

  2. 在虚拟机中,配置 FireFox 浏览器中的网络代理或系统代理,选择手动代理,在所有代理中填入主机的 IP 地址和其默认的端口(我的是192.168.56.1001080

  3. 完成

测试

  • TODO: 本部分需要完善 <2019-07-07>

最基本的测试是在配置好后,打开你的浏览器,在地址栏输入,如果访问成功,则恭喜你!

当然,通常不会一次性成功。那么失败时我们该从何下手呢?

让我们从通用测试方法开始

通用测试方法

  • ping。ping 命令可以用于检测本机和目的主机是否相通。当然,防火墙可能阻止 ping 命令的成功,即 ping 不通不代表连不上(但通常不会出现这种情况)。同时 ping 得通也不代表连得上(例如 TCP 阻断,此时需要更换端口或 IP)。ping 得通说明你的 IP 没被封
  • tracert(Linux 中为traceroute)。用于追踪 IP。可以用来追踪你的 VPS 的 IP 地址,从而得出途径的网络节点。除了使用该工具达到这个目的,还可以使用在线工具达到同样目的,且发出tracert命令的主机为各地的主机
  • nc。作为网络调试中的瑞士军刀,其功能非常强大。初始版本过旧,建议使用衍生版本,如ncat(Nmap 官方出品)。它最基本的一个用法是测试目标端口是否打开或是否连得上
  • telnettelnet具有基本的 TCP 连接的能力,可用于测试目标端口是否打开或是否连得上
  • 抓包。但凡涉及到网络,抓包总是不会错的。推荐的抓包工具为 Wiresharktcpdump。前者适用于有图形界面的,后者适用于无图形界面的(即 Shell)。再简单点地说,tcpdump 用于在服务器(例如 CentOS )上抓包,Wireshark 用于在带图形界面的客户端(例如 Windows)上抓包。

常用工具

本地工具

  • arp(二层——数据链路层):ARP
  • ifconfig, ip, route(三层——网络层):MAC, IPv4, IPv6
  • ping, traceroute(三层——网络层):ICMP
  • netstat, ss(四层——传输层):TCP, UDP
  • telnet, nc(四层——传输层):TCP
  • nslookup, dig(五层——应用层):DNS
  • dhcpclient(五层——应用层):DHCP
  • wget, lynx: HTTP, HTTPS, FTP
  • curl:FILE, FTP, FTPS, HTTP, HTTPS, IMAP, IMAPS, POP3, POP3S, RTMP, RTSP, SCP, SFTP, SMTP, SMTPS, TELNET, TFTP…
  • tcpdump: all
  • whois: ?

其中lynx是个终端下使用的轻量浏览器,更多轻量浏览器请参见 Comparison of lightweight web browsers - Wikipedia

更多 Linux 下的网络工具请参见: Linux常用网络工具总结 - int32bit的博客 | int32bit Blog

在线工具

科学上网问题测试思路

可以按如下顺序正向进行,也可反向进行:

  1. 确保你的服务器可以访问谷歌,使用如下命令:

    1
    curl -s -i -4 -m 10 www.google.com | less

    简要解释下:curl是一个强大的用于在服务器和客户端间传输数据的工具。上述命令使用的是其最常用的一个能力,获取 Web 网页。其中:

    • -s参数是为了防止出现进度条,影响阅读;
    • -i参数是为了让其显示响应头(Response header);
    • -m 10表示最多只等 10s,否则可能等很长时间才返回失败信息;
    • www.google.com参数用于指明从http://www.google.com获取数据;
    • |是管道,用于将curl命令的输入重定向为less命令的输入;
    • less命令的作用是分页显示输入的内容(通常用于原文过长,一个屏幕显示不全的情况)。

    如果成功,其输出应当如下:

    1
    2
    3
    4
    5
    6
    7
    HTTP/1.1 200 OK
    Date: Thu, 04 Jul 2019 03:17:26 GMT
    Expires: -1
    Cache-Control: private, max-age=0 Content-Type: text/html; charset=ISO-8859-1
    P3P: CP="This is not a P3P policy! See g.co/p3phelp for more info."
    Server: gws
    ......
  2. 确保你可以连接到你的服务器(如果IP已经被封(即使用了 V2Ray +CDN等方案)这点不做要求)。先使用 ping 看下IP是否被封(注意使用你的服务器的IP替换下面的演示IP):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    PS C:\Users\wsxq2> ping 104.27.157.73
    Pinging 104.27.157.73 with 32 bytes of data:
    Reply from 104.27.157.73: bytes=32 time=185ms TTL=52
    Reply from 104.27.157.73: bytes=32 time=186ms TTL=52
    Reply from 104.27.157.73: bytes=32 time=186ms TTL=52
    Reply from 104.27.157.73: bytes=32 time=187ms TTL=52

    Ping statistics for 104.27.157.73:
    Packets: Sent = 4, Received = 4, Lost = 0 (0% loss),
    Approximate round trip times in milli-seconds:
    Minimum = 185ms, Maximum = 187ms, Average = 186ms

    上面的是 ping 成功的例子(用的命令行是 Windows 10 中 Powershell),说明 IP 没被封。如果 ping 失败,则很有可能 IP 被封了。下面测试 TCP 连接是否正常(尝试使用 SSH 连接到你的服务器,注意替换master):

    1
    2
    PS C:\Users\wsxq2> ssh master
    The authenticity of host 'master (192.168.56.11)' can't be established. ECDSA key fingerprint is SHA256:0Y6BmNB1vsQiK2RSf9Ux9qcPlESud8C3UYIvtMZeKGs. Are you sure you want to continue connecting (yes/no)? no Host key verification failed. PS C:\Users\wsxq2>

    如果出现上面的提示(The authenticity of host...),则说明可以连接。如果出现如下提示(注意替换 IP):

    1
    2
    3
    PS C:\Users\wsxq2> ssh 192.168.56.13
    ssh: connect to host 192.168.56.13 port 22: Connection timed out
    PS C:\Users\wsxq2>

    则说明连接不上

    除了使用 ssh 本身测试 SSH 服务是否可用外,还可以使用 telnet:

    1
    PS C:\Users\wsxq2> telnet master 22

    如果 Powershell 界面刷新,并显示如下信息:

    1
    SSH-2.0-OpenSSH_7.4

    则说明连得上,否则说明连不上

  3. 测试你和你的服务器上的用于科学上网的端口是否可以连通。这个测试的前提是上面的 SSH 连接测试成功了

    先打开已经安装在 Windows 上的 Wireshark,选择你和服务器之间连接使用的网卡进行数据包嗅探。然后使用 Windows 自带的 telnet 连接到你的服务器上的用于科学上网的端口(注意替换 IP(master)和端口(1234)):

    1
    PS C:\Users\wsxq2> telnet master 1234

    在 Wireshark 中查看是否抓到如下数据包(即查看 TCP 三次握手是否成功):

    1
    2
    3
    1	0.000000	192.168.56.100	192.168.56.11	TCP	66	8754 → 1234 [SYN] Seq=0 Win=64240 Len=0 MSS=1460 WS=256 SACK_PERM=1
    2 0.000306 192.168.56.11 192.168.56.100 TCP 66 1234 → 8754 [SYN, ACK] Seq=0 Ack=1 Win=29200 Len=0 MSS=1460 SACK_PERM=1 WS=128
    3 0.000369 192.168.56.100 192.168.56.11 TCP 54 8754 → 1234 [ACK] Seq=1 Ack=1 Win=525568 Len=0

    如果失败,说明你和你的服务器上的用于科学上网的端口无法连接。可能是因为端口没有开启、TCP 被阻断之类的原因导致的。对于前者,可在服务器上使用如下命令查看端口是否开启:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    root@wsxq21:~# ss -ntl
    State Recv-Q Send-Q Local Address:Port Peer Address:Port
    LISTEN 0 50 127.0.0.1:3306 *:*
    LISTEN 0 128 *:26635 *:*
    LISTEN 0 100 127.0.0.1:25 *:*
    LISTEN 0 128 *:12635 *:*
    LISTEN 0 128 :::26635 :::*
    LISTEN 0 128 :::80 :::*
    LISTEN 0 128 :::8080 :::*
    LISTEN 0 128 :::11635 :::*
    LISTEN 0 100 ::1:25 :::*
    root@wsxq21:~#

    对于 TCP 阻断,可更换一个端口试试

  4. 测试你的客户端软件是否开启。使用如下命令:

    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
    PS C:\Users\wsxq2> netstat -ano|findstr "LISTENING"
    TCP 0.0.0.0:135 0.0.0.0:0 LISTENING 576
    TCP 0.0.0.0:445 0.0.0.0:0 LISTENING 4
    TCP 0.0.0.0:1080 0.0.0.0:0 LISTENING 11328
    TCP 0.0.0.0:1536 0.0.0.0:0 LISTENING 684
    TCP 0.0.0.0:1537 0.0.0.0:0 LISTENING 1484
    TCP 0.0.0.0:1538 0.0.0.0:0 LISTENING 1368
    TCP 0.0.0.0:1539 0.0.0.0:0 LISTENING 2928
    TCP 0.0.0.0:1545 0.0.0.0:0 LISTENING 828
    TCP 0.0.0.0:1547 0.0.0.0:0 LISTENING 840
    TCP 0.0.0.0:5040 0.0.0.0:0 LISTENING 5416
    TCP 0.0.0.0:5938 0.0.0.0:0 LISTENING 3708
    TCP 0.0.0.0:7680 0.0.0.0:0 LISTENING 13036
    TCP 0.0.0.0:23443 0.0.0.0:0 LISTENING 11196
    TCP 10.177.15.93:139 0.0.0.0:0 LISTENING 4
    TCP 127.0.0.1:1081 0.0.0.0:0 LISTENING 11528
    TCP 127.0.0.1:1562 0.0.0.0:0 LISTENING 9864
    TCP 127.0.0.1:4300 0.0.0.0:0 LISTENING 10268
    TCP 127.0.0.1:4301 0.0.0.0:0 LISTENING 10268
    TCP 127.0.0.1:5939 0.0.0.0:0 LISTENING 3708
    TCP 127.0.0.1:10000 0.0.0.0:0 LISTENING 3448
    TCP 127.0.0.1:28317 0.0.0.0:0 LISTENING 3696
    TCP 127.0.0.1:41830 0.0.0.0:0 LISTENING 16080
    TCP 127.0.0.1:54530 0.0.0.0:0 LISTENING 9072
    TCP 127.0.0.1:62783 0.0.0.0:0 LISTENING 14884
    TCP 127.0.0.1:65000 0.0.0.0:0 LISTENING 3220
    TCP 127.0.0.1:65001 0.0.0.0:0 LISTENING 3220
    TCP 169.254.64.217:139 0.0.0.0:0 LISTENING 4
    TCP 192.168.56.100:139 0.0.0.0:0 LISTENING 4
    TCP [::]:135 [::]:0 LISTENING 576
    TCP [::]:445 [::]:0 LISTENING 4
    TCP [::]:1080 [::]:0 LISTENING 11328
    TCP [::]:1536 [::]:0 LISTENING 684
    TCP [::]:1537 [::]:0 LISTENING 1484
    TCP [::]:1538 [::]:0 LISTENING 1368
    TCP [::]:1539 [::]:0 LISTENING 2928
    TCP [::]:1545 [::]:0 LISTENING 828
    TCP [::]:1547 [::]:0 LISTENING 840
    TCP [::]:5938 [::]:0 LISTENING 3708
    TCP [::]:7680 [::]:0 LISTENING 13036
    TCP [::1]:1546 [::]:0 LISTENING 2488
    PS C:\Users\wsxq2>

    在其中找到你设置的本地端口号(SS Local),通常为 1080(可以直接使用netstat -ano|findstr ":1080"命令)

  5. 检查你本地代理配置是否正确。一个简单可靠的方法是使用 Chrome + SwitchyOmega,新建一个类型为代理服务器情景模式代理协议设置为 socks5,代理服务器设置为本地代理的 IP (通常为 127.0.0.1),代理端口设置为本地代理配置的端口(通常为 1080)。

如果上述测试均成功,那可能是你的配置文件有问题,包括服务器和客户端,请仔细检查。

一些尝试

分析 SSR 科学上网失败原因

这个问题发生在我才接触科学上网不久后,那时我一直用的 SSR(因为有一次 SS 抽风了我就认为 SS 已经不能用了)。在 2019-06-01 左右的某一天,SSR 也抽风了。经过抓包分析,最后发现是因为三次握手失败。更加准确地说,是因为服务端能收到客户端的数据包,但是客户端收不到服务端的响应包,即客户端反复地重发 SYN,但是总是收不到回复。

此外,ping 和 ssh 均失败、使用手机依然失败、重启客户端和服务器无效、检查服务器开放端口正常、检查服务器防火墙状态正常、使用其它 VPS 能连上、等了一个月依然存在上述现象,这些现象足以证明是 IP 被封了

由于服务端能收到客户端的数据包,所以我当时有个大胆而简单的想法,即修改一下服务端 SSR 软件,在收到客户端请求后返回响应数据时将其源 IP 修改为其它 IP 地址(因为 GFW 封的 IP 是源 IP 为国外的 IP的数据包),只要再修改一下客户端 SSR 软件,让其能还原会话就可以成功使用。

如今想来,这个解决方法事实上还是挺复杂的。因为每次从服务端发来的包都需要修改源 IP ,而标识一个会话的关键因素就是双方IP,双方端口,使用的协议(五元组)。故而修改一方 IP 后还能还原会话是一大挑战。而且 GFW 想破解该方法不要太简单,它直接封了目的 IP 为你的 VPS IP 的数据包就行

分析 SS 科学上网失败原因

这是由于最近(2019-12-05) SS 虽然可以使用,但是经常间歇性抽风。在抽风时,我试图通过抓包分析问题所在。易知,问题主要出在客户端和服务器端之间的流量上,因此需要抓取两端的流量包以进行综合分析。

在客户端上抓取以太网接口(或 WIFI 接口)的流量即可。然后使用如下显示过滤器(这里及后面用的均是 Wireshark):

1
tcp.port==16834 and tcp.flags.syn==1 and tcp.flags.ack==0

其中注意将16834改为你自己的 SS 服务器端口。

与此同时,在服务器上抓取相应的网卡(也叫接口)流量(通常是eth0),但是由于服务器端通常没有图形界面,所以无法直接使用 Wireshark。但是,可以使用 Wireshark 的 sshdump 模块,直接在你的客户端上抓取服务器上的流量(这个功能超级强大有木有)。它需要在服务端上安装 tcpdump,然后客户端上使用 SSH 连接并传输流量信息。具体操作步骤可自行谷歌

在客户端上,选定一条记录,按Ctrl+Shift+Alt+T以 Follow TCP Stream;在服务端上,选定对应的记录(可根据客户端的端口来找到对应的记录),按Ctrl+Shift+Alt+T以 Follow TCP Stream。将两端显示的包记录分别保存为notebook_ssserver.pcapngnotebook_ssclient.pcapng,以待后续对比分析

通过对比分析得到的两个包,最终发现问题出在第二轮数据交换(客户端发送数据,服务端响应算一轮)中,即在客户端三次握手成功并经历了一次数据交换后,在客户端再次向服务端发送数据时,出现了服务端接收失败的现象,服务端因为没有接收到数据,所以在等待了 10 秒后发送了一个 FIN 包企图结束此次会话,但是客户端不死心,反复重发,最后依然没有得到希望的回复,只能发送 FIN 和 RST 结束会话

(突然感觉客户端和服务器是两个相亲相爱的人,但是 GFW 拆散了它们,它让它们产生误会,产生隔阂,导致客户端郁郁而终的悲剧。妈蛋,单身久了抓个包都觉得眉清目秀。:sob:

在此我有点好奇,为何在第二次客户端请求时会被 GFW 拦截?难道是因为该数据包有什么显而易见的特征被 GFW 检测到了?怀着疑问,我试图将该数据包解密,先看看数据包的明文内容再说。

因为 openssl 有巨多的加解密算法,于是我开始研究如何使用 openssl 解密一段密文。

最终,由于我的 SS 使用的是 aes-128-gcm 加密方式,但是 openssl 不支持该加密方式的加解密,因此只能不了了之

国内服务器上搭建 v2ray 作为中转

  • 时间:2019-12-05 左右

这个尝试源于我所在的网络环境比较糟糕,我所在的网络环境是 NAT 中的 NAT 中的 NAT……(我也不清楚嵌套了多少层,可以肯定的是至少两层)。因此,如果大家都在使用科学上网,那么被 GFW 检测的概率会大大增加,就可能会出现短暂地封源 IP 的结果。面对这个问题,一个简单的解决方法便是使用国内服务器进行中转。

为了提到效率,可以将客户端到中转服务器间的协议设为 HTTP,即将中转服务器作为一个 HTTP 代理服务器使用。这样一来,客户端的配置会简单很多,简单到不需要安装客户端软件了。

甚至你还可以将一个 PAC 放到你的 HTTP 代理服务器上,这样还能实现自动代理。客户端几乎不用任何配置(只需在网络设置处配置一个 PAC 文件 URL)

缺点是性能可能会下降

具体步骤就不赘述了

在执行这个想法的过程中,遇到了一个万万没想到的问题:Chrome 竟然不支持 SOCKS5 的用户l密码认证 2。并且使用 PAC 也达不到想要的结果,即因为 Chrome 不支持 SOCKS5 的用户密码认证,所以在 PAC 文件中包含认证信息也是毫无意义的。不仅如此,Firefox 和 MSIE 也是不支持的 3

用七牛云的 CDN 以提高科学上网的速度

  • 时间:2019-12-05 左右

由于听闻国内 CDN 速度更快(相比于 Cloudflare),于是我试图使用七牛云的 CDN 以提高科学上网的速度,但是由于以下原因最终放弃了:

  1. (最关键)使用七牛云的 CDN 需要域名备案。
  2. 使用 HTTPS 要收费
  3. 不一定能起到加速效果,毕竟 VPS 在国外

进一步阅读

总结

本文花了我整整一个周的时间,且期间没有偷懒,几乎每天除了吃饭睡觉和少有的看小说、看动漫、玩游戏(怎么突然感觉自己还是不够努力 :joy: )的时间都是在写这个博客。如今虽然还有很多内容需要完善,但是我还是打算暂时先放下了。主要是因为太累了 :sob:

由此可见,翻墙是门大学问。的确,在写这个博客的过程中我也学到了不少新知识,尤其对其原理和测试方法更加清晰。其中体会最深的莫过于抓包的作用之大。因此,我得出了如下结论:但凡涉及网络,抓包总是没错的。

另外非常感谢网上的各种教程,为我指明了方向。他们勇于探索、敢于创新、刻苦钻研、无私奉献、崇尚自由的精神让我非常钦佩。

  最后引用 SS 作者 clowwindy 的一段话:

  维护这个项目到现在大概总共回复过几千个问题,开始慢慢想清楚了一件事,为什么会存在GFW。从这些提问可以看出,大部分人的自理能力都很差,只是等着别人帮他。特别是那些从AppStore下载了App用着公共服务器的人,经常发来一封只有四个字的邮件:“不能用了?”我觉得这是一个社会常识,花一分钟写的问题,不能期待一个毫无交情的陌生人花一个小时耐心地问你版本和操作步骤,模拟出你的环境来帮你分析解决。Windows版加上GFWList功能以来,我反复呼吁给GFWList提交规则,但是一个月过去了竟然一个提交都没有。如果没有人做一点什么,它自己是不会更新的啊,没有人会义务地帮你打理这些。我觉得,政府无限的权力,都是大部分人自己放弃的。假货坑爹,让政府审核。孩子管不好,让政府关网吧。房价太高,让政府去限购。我们的文化实在太独特,创造出了家长式威权政府,GFW正是在这种背景下产生的,一个社会矛盾的终极调和器,最终生活不能自理的你每天做的每一件事情都要给政府审查一遍,以免伤害到其他同样生活不能自理的人。这是一个零和游戏,越和这样的用户打交道,越对未来持悲观态度,觉得GFW可能永远也不会消失,而墙内的这个局域网看起来还似乎生机勃勃的自成一体,真是让人绝望。

  ——引用自Linux 科学上网指南 | Firmy’s blog

更新记录

这里指的是重大更新记录。

更新时间 版本 耗时 变动
2019-07-07 1.0 7 天 初稿。
2019-08-04 2.0 好几个晚上 + 2 个白天 添加了配置公私钥的部分,极大地完善了代理部分,添加了 SS 服务器配置手动安装方法(shadowsocks-libev),修正了部分错误(包括 SSH 那部分的),移除了PC 连接到 SSR Local这一部分的内容(它被移动到了代理部分)。在前面的多个小节前添加了说明文字
2019-11-29 3.0 一个下午 完善了 V2Ray + WS + CDN 部分和 V2Ray + WS + TLS + CDN 部分
2019-12-05 4.0 一个上午(120min) 添加了一些尝试部分

链接

下面总结了本文中使用的所有链接:

缩略语

  • ACK: ACKnowledgement
  • API: Application Programming Interface
  • ARM: Advanced RISC Machines
  • ARP: Address Resolution Protocol
  • BT: BitTorrent
  • CDN: Content Delivery Network
  • CMD: CoMmanD
  • CPU: Central Processing Unit
  • CST: China Standard Time
  • DHCP: Dynamic Host Configuration Protocol
  • DNS: Domain Name System
  • DPI: Deep Packet Inspection
  • ECDSA: Elliptic Curve Digital Signature Algorithm
  • FTP: File Transfer Protocol
  • FTPS: File Transfer Protocol over TLS
  • GCP: Google Cloud Platform
  • GFW: Great Firewall
  • GIA: Global Internet Access
  • GMT: Greenwich Mean Time
  • GPL: General Public License
  • GUI: Graphical User Interface
  • HTTP: Hypertext Transfer Protocol
  • HTTPS: HTTP Secure
  • ICMP: Internet Control Message Protocol
  • ICP: Internet Content Provider
  • ID: Identifier
  • IDL: Interface Definition Language
  • IEEE: Institute of Electrical and Electronics Engineers
  • IETF: Internet Engineering Task Force
  • IHMSC: International Conference on Intelligent Human-Machine Systems and Cybernetics
  • IMAP: Internet Message Access Protocol
  • IMAPS: IMAP Secure
  • IP: Internet Protocol
  • ISO: International Organization for Standardization
  • ISP: Internet Service Provider
  • JSON: JavaScript Object Notation
  • KB: Kilobyte
  • KCP: A Fast and Reliable ARQ Protocol
  • MAC: Media Access Control
  • MB: Megabyte
  • MIPS: Microprocessor without Interlocked Pipeline Stages
  • MIT: Massachusetts Institute of Technology
  • MSS: Maximum Segment Size
  • NG: Next Generation
  • OS: Operating System
  • OSI: Open Systems Interconnection
  • OSX: macOS
  • PAC: Proxy auto-config
  • PC: Personal Computer
  • PID: Process ID
  • POSIX: Portable Operating System Interface, formerly IEEE-IX
  • PS: PowerShell
  • PTR: PoinTeR
  • RFC: Request For Comments
  • RTMP: Real-Time Messaging Protocol
  • RTSP: Real Time Streaming Protocol
  • SCP: Secure Copy
  • SFTP: SSH File Transfer Protocol
  • SMTP: Simple Mail Transfer Protocol
  • SMTPS: SMTP over TLS
  • SNI: Server Name Indication
  • SOCKS: SOCKetS
  • SS: shadowsocks
  • SSH: Secure Shell
  • SSL: Secure Socket Layer
  • SSR: shadowsocksr
  • SSRR: shadowsocksrr
  • TCP: Transmission Control Protocol
  • TFTP: Trivial File Transfer Protocol
  • TLS: Transport Layer Security
  • TTL: Time To Live
  • TTY: Teletype
  • UDP: User Datagram Protocol
  • URI: Uniform Resource Identifier
  • URL: Uniform Resource Locator
  • VM: Virtual Machine
  • VPN: Virtual Private Network
  • VPS: Virtual Private Server
  • WPAD: Web Proxy Autodiscovery Protocol
  • WS: WebSocket
  • WWW: World Wide Web

引用

  1. https://github.com/v2ray/v2ray-core/issues/1769
  2. https://bugs.chromium.org/p/chromium/issues/detail?id=256785
  3. https://stackoverflow.com/a/1983813

本文转自知乎用户@冯庆,网页链接

哈哈,居然真有这个题目,如果不让我回答,真是所有知友的极大损失!如果让我回答,其他回答就不用看了!

贴一篇本人名作《马千里:中国的胡锡进和胡锡进的中国》:

胡锡进老师自称是一位“复杂中国的报道者”,其实,在我看来,更准确地说应该是“复杂中国的评论者”。在环球时报,他不仅长于社论,还以“单仁平”笔名发表评论文章。而这些文章汇集起来就是最近出版的《胡锡进论复杂中国》。

这本书的意义在于,正式确定了胡锡进老师“复杂中国论”的特定概念和系统观点。

在胡老师的言论里(我没有说心目中或眼中),中国是复杂的。“人口这么多,发展阶段这么特殊,发展又这么快,全世界二百年的成果压缩在几十年,几百年的问题也是压缩在几十年中”,这是胡老师的总结。其实“复杂”还远不止于此,我们可以从国际形势、漫长历史、民族结构、思想多元化、各种阶层与势力的交织等等,来证明现阶段中国的“复杂性”。

然而,哪个国家承认自己是“简单的”呢?我们可以用胡老师的语言风格轻松化解这个质问:“是的,各个国家都有自己的复杂性,但中国更加复杂。”

“复杂中国”是个聪明的提法,却不是个科学的定论。正如每个民族都说自己勤劳,每个国家都说自己伟大,但并不以此来确证别的民族和国家不勤劳、不伟大。我们还常常在外交场合用同样的言辞来恭维对方,并不确证我们比他们差。

其实,胡老师的“复杂中国论”另有所指,是特别给那些把中国简单化的人说的,驳斥的是“非黑即白”的观点。中国“既不是天堂,也不是地狱”,此之谓也。其含义是全盘西化不行,不拿来主义也不行;完全法制无人情不行,完全德治无规矩也不行。——这是说到哪里都不会错的。

这个逻辑是胡老师的精神支柱,贯穿于他的所有思考,形成了他独特的论述特点。执其两端,左右逢源,两边都对,但都不全面。“你说的很有道理,他说的也都不错,不过……”,所以胡老师的文章,哪怕140字的微博,都不可能少了“但是”。几个“但是”下来,你的大脑会一片空白,想说点什么,一时又不知从何处反驳。

他曾说,环球时报既要符合党的利益,也要符合人民的利益。一方面做不好就不行,或者“政治死”,或者“市场死”。他自豪地说环球时报做到了,并因此证明党和人民的利益是一致的。当然,胡老师在这里把自己绕进去了,这只能证明党和人民的利益有一定的交叉,恰恰不是完全重合。否则,就不必处心积虑,而只需从心所欲即可做到两个“利益”都符合。——这不是重点,我们仅以此来说明胡老师这种特有的思维方式——左边捧一捧,右边摸一摸。

胡老师之所以会这么做,是因为他聪明地知道,谁也是得罪不得的。他的言论,总是看似站在民众立场上,然后话锋一转悄悄地说出政府爱听的话;有时候又正相反。这个技巧掌握的很纯熟,但是也很无效。不管你的身子探出多远,屁股在哪里坐着,还是很容易被看出来的。

事情本来不复杂,胡老师硬是将其绕复杂,有时候自己都钻不出来。但这种态度让胡老师很受用而视为珍宝,从而“复杂中国”成为胡老师的理论核心。复杂的好处在于,可以成为任何变革、任何建议、任何说法的挡箭牌。比如,你说要财产公示,他可以说“财产公示是国际惯例,是反腐的有效办法,但中国的情况很复杂,所以……”;再比如你说中国需要宪政,他可以说:“宪政有其好处,在一些国家也得到了成功的尝试,但中国的情况很复杂,所以……”。这些话不仅可以应付国内,也一样可以应付国外的任何人、任何事。程咬金还有三板斧,胡老师一招制敌。

其实,胡老师越是说复杂,事情就越是简单,简单到你什么也不要做、什么也不要想。这,正是他要的结果。当任何的道路都被“复杂”堵死的时候,你会发现他特特地给你敞开着一扇大门。只要坚持中国共产党的领导,坚持社会主义,坚持现有体制不变,坚持按现任领导的意思办,然后就任何的复杂都消失得无影无踪,所有事情都显得顺理成章、符合国情且破解了一切的难题。

说到底,胡老师的“复杂”,就是这么“简单”。胡锡进的中国,就是“复杂中国”,而胡锡进的“复杂中国”,简单到了极点。

中国缺少的资源太多,但惟独不缺胡锡进。现行的新闻体制,必然催生大批胡锡进。胡老师是党报的子报主编,是环球时报的领导人,也是如椽巨笔。胡老师有文采,这不是重点,重点是他有自己熟练掌握的逻辑思维,那就是在“复杂”的概念下,破解所有难题。

这些难题有的来自民间,有的来自地方政府的愚蠢行为,有的还来自更高层面欠思考的言行,有的还来自国际事件。胡老师都能用他特有的逻辑来轻松地化解,至少在表面上如此。这些“难题”被网友俏皮地称为“飞盘”。飞盘扔出,有的角度真的很刁。

胡老师的努力并没有说服他的读者,相反被读者看穿了他的手段。因此,胡老师微博的跟帖,可以说是清一色的质问、嘲讽甚至谩骂。甚至发展到不需要胡老师说什么,仅仅发一张风景图片,都能引来“骂声”。但这并不重要,重要的是胡老师表现出来的忠心耿耿、处心积虑、百般维护恰恰是有关方面所喜闻乐见。而什么更重要,胡老师心如明镜。

有些同行也参与到揶揄的队伍。我要说,你们真的没资格这么做。就理论水平而言,胡老师是我见到的优秀者甚至卓越者,非一般宣传从业人员所能望其项背。就忠于职业而言,胡老师也是从来没有说过一句一字的违背职业的话。他的职业是宣传。

有些媒体从业者,自以为是记者,其实你们也只是宣传机器的一个零件,你们缺乏宣传职业道德的一些言行给你们带来一些社会声誉,但你确实违背了职业承诺。因为,中国没有媒体,只有宣传品。胡老师的报纸明确地说:“媒体就是国家利益的看门狗。”这个理解非常准确、到位而贴切。

胡老师是忠诚于职业的人,他的精神世界未必精深但很博大。我相信,今天他能把黑说出白,如果需要,明天就可以把白说出黑。你必须这么理解:这是职业行为。人,首先要对得起自己的职业,除非你辞职不干。

有人说胡老师属于“五毛”或高级“五毛”,这一点我绝对不同意。胡老师绝无强词夺理,绝无一味维护着什么,甚至面对谩骂也从不反击曝出,表现出的是长者的谦和涵养。他已经构建了一套理论和逻辑的体系,在此支撑下进行自己的判断和评说,形成独有的个人风格。这套理论和逻辑并不涉及主义或意识形态,在缺乏思想指导下依然可以顺畅地空转。正因此,无论世道如何变幻,他都能游刃有余而不会有任何的磕绊。

这并不说明胡老师缺乏坚定的政治立场,或者缺乏真实的政治观点,而只能说胡老师是一个聪明人。他把一些不能显示或者没有必要显示的东西,有意地收藏起来。他更加高明的是,他的所有空转式的的文章,无论谁看起来都会觉得舒服,只要他是掌权者。

我对胡老师敬重到敬畏,就是因为他深不可测,你只能看到他的言论,而看不到他真实的思想结构和内心世界——而这一点,他在口头上绝不会承认。真正复杂的不是中国,是胡锡进。

队里的ljzjsc大佬近期在研究脱壳,而我自己反思了一下,开学两个月以来,基本没有学太多东西,一直在玩《我的世界》(有些自闭症的我第一次在网上结交了一些朋友),太颓废了。今天开始着手研究脱壳。萌新一个,笔记比较浅显,望大佬们指教~~

单步调试法

善用ctrl+f8和f4多次调试,善于跳出循环,直到找到OEP(original entry point,程序入口)。

对于简单的壳,如果善于静态调试的话,我个人认为哪怕错过了OEP,只要程序完全解压了,完全可以把任意一处当作OEP,然后把程序dump出来,放到ida中静态分析。缺点是程序无法运行,无法动态调试。

以《加密与解密》第十六章的随书文件RebPE.exe为例。

将文件拖进OD:

1586076232964

f8运行到413001处按f7进入函数:

1586076324838

ctrl+f8自动单步步过运行,直到看到一个大循环,按f8停止自动单步步过运行。

此时手动f8运行一遍这个循环,找一下循环出口。

下图蓝色标记处即为循环出口。单击此处,f4运行到此处。

1586076495538

然后f8一直运行到401130.

1586077022071

这里就是OEP.

工作区内右键 -> 用ollydump脱壳调试程序

1586077347367

即可完成脱壳。

两次内存断点法

一般的壳会依次对.text(代码)、.rdata、.data、.rsrc(资源)区块进行解压处理,所以可以现在比如.rsrc区块处设置内存访问断点,当程序中断时,便已经完成了对.text的解压。

然后此时我们再对.text区块设断点,当外壳跳到OEP返回代码段时即可触发内存访问断点。

仍然以RebPE.exe为例。

alt+m来到内存页,点击图中蓝色处(RebPE的.rsrc区段),按下f2设置断点。

1586077762109

f9(运行)或者shift+f9(忽略错误运行),中断在此处:

1586077851757

再次alt+m来到内存页,点击图中蓝色处(RebPE的.text区段),按下f2设置断点。

1586077879595

再次f9(运行)或者shift+f9(忽略错误运行),中断在此处:

1586077941776

按两次f8,来到此处:

1586077978881

这就是OEP.

ESP定律

原理:外壳初始化的现场环境(各寄存器的值)与原程序的现场环境要相同(主要是esp,ebp等重要的寄存器值)。

通常用pushad/popad(push/pop eax,ecx,edx,ebx,esp,ebp,esi,edi)和pushfd/popfd(push/pop 各标志寄存器)来保存和恢复现场环境,标志寄存器不太重要,一般不处理它。

用法:把esp入栈时的地址设硬件断点,之后恢复现场的时候寄存器的值一定是从栈中恢复的,当访问栈中此处时,运行中断,此处离OEP不远了。

下面是做法,仍以RebPE为例。

程序拖进OD:

1586079213664

f8一次,pushad,各寄存器入栈:

1586079444954

此时的ESP为19ff54,我们在此处设置硬件访问断点,在下图箭头处输入hr 19ff54并回车,hr是硬件访问断点,hw是硬件写断点。

1586079502442

如果你想取消该断点,可以:调试 -> 硬件断点

设置好断点后f9,中断在此处:

1586079126981

然后f8两次,到达OEP.

前两天把家里的老电脑xp系统改成了win7,我妈嫌弃速度太慢了,于是买了一台新电脑,替换下来的那台旧电脑我便打算刷成linux。

一开始是想centos7的,因为我的几台小鸡大多都是centos7的,对这个系统熟悉。然而centos7只支持64位电脑,我的电脑不允许。centos6可以支持32位,但是我嫌弃centos6和centos7之间的差异有些大,所以也放弃了。

然后考虑ubuntu,最新版也是只支持64位,所以我选择了ubuntu server 14.04。选择server而不是desktop,主要是因为电脑性能的限制,毕竟不要对十年前的老电脑的性能有太高的幻想。

下面先说一下一整个流程,遇到的坑放到最后一起来说。

为了写这篇文章,安装时,我中英文各走了一遍流程,希望可以让大家更加理解各个环节的细节。

流程图都是手机拍摄的,毕竟没法截图,总不能让我买个采集卡吧,哈哈~

下载镜像

清华ubuntus-releases/14.04

选择ubuntu-14.04.6-server-i386.iso下载。

i386是32位。

烧录镜像

下载一个叫UltraISO的软件。

准备一个空U盘。

下面的图片是我烧录win7镜像时的截图,其实不同的镜像的烧录都差不多。

1585042805014

1585042827186

1585042840797

BOIS中设置U盘为第一启动

重启电脑,在电脑品牌的logo出现的时候按下进入BIOS的按键。如果觉得自己把握不好按下按键的时机,可以在电脑开机时一直按着BIOS按键。

不同品牌的电脑进入BIOS界面的按键不同,我家这台的按键是del键。具体的按键可以去网上查一下,查不到的话可以尝试以下按键:

F1, F2, F5, F8, F10, F12, ESC, DEL

反正把常用的都试一试。

不同的主板的BIOS界面不同,设置的选项也不同,这里说一下我家电脑的步骤。

选择高级BIOS功能:

IMG_20200324_175708

选择硬盘启动优先级:

IMG_20200324_175722

将USB-HDD那项调至首位(加减号可以调节顺序):

IMG_20200324_175737

然后按下F10,弹窗询问是否保存,回车即可保存。

参考http://www.boot-disk.com/boot_priority.htm

  • Start the computer and press ESC, F1, F2, F8 or F10 during the initial startup screen. Depending on the BIOS manufacturer, a menu may appear.

  • Choose to enter BIOS setup. The BIOS setup utility page appears.

  • Use the arrow keys to select the BOOT tab. System devices appear in order of priority.

  • To give a CD or DVD drive boot sequence priority over the hard drive, move it to the first position in the list.

  • To give a USB device boot sequence priority over the hard drive, do the following:

    • Move the hard drive device to the top of the boot sequence list.
      Expand the hard drive device to display all hard drives.
    • Move the USB device to the top of the list of hard drives.
    • Save and exit the BIOS setup utility.
  • The computer will restart with the changed settings.

安装

语言,地区,键盘布局,字符集

选择英语:

IMG_20200329_133849

选择第一项(如果要建立出两台以上的服务器,可以选择「Multiple server install with MAAS」):

IMG_20200329_133854

再选一次英语:

IMG_20200329_133908

选择地区(Other->Asia->China):

IMG_20200329_133914

IMG_20200329_133919

IMG_20200329_133925

选择字符集:

IMG_20200329_133930

是否调整键盘布局,选否就行。

IMG_20200329_133938

English(US)键盘布局:

IMG_20200329_134000

IMG_20200329_134006

主机名,用户名,密码

主机名:

IMG_20200329_134103

用户名:

IMG_20200329_134113

IMG_20200329_134117

密码:

IMG_20200329_134139

IMG_20200329_134146

会提醒弱密码,选yes继续:

(我这台服务器只在内网使用,所以就图方便使用弱密码了)

IMG_20200329_134159

加密/home,时区

是否加密/home目录。非必要,可以不用加密。

IMG_20200329_134206

确认所在时区:

IMG_20200329_134224

磁盘分区

取消挂载硬盘:

IMG_20200329_134249

Server的话建议使用LVM(Logical Volume Manager)来管理硬碟空间,方便日后的扩充:

IMG_20200329_134256

IMG_20200329_134259

移除已有数据:

IMG_20200329_134309

写入分区变化并配置LVM:

IMG_20200329_134320

IMG_20200329_134327

LVM自动给出了合适的分区:

IMG_20200329_134335

安装系统,代理,自动更新

IMG_20200329_134448

设置代理,留空则不代理:

IMG_20200329_134556

设置更新类型,我设置了不自动更新:

IMG_20200329_134905

安装软件,GRUB引导

安装openssh-server(不过我之后发现好像没安上,又手动安装的):

IMG_20200329_134915

安装GRUB引导,开机的引导就靠它。

IMG_20200329_135437

结束安装

选择continue之前请确保U盘已拔出,否则会又来一遍安装程序(因为U盘是第一启动器)

IMG_20200329_135512

补充

如果有哪一步不小心选错了,大多数情况下可以按esc,或是选择“返回”,进入此界面,选择相应的项目再次修改:

IMG_20200328_222936

运行

登录:

IMG_20200329_135645

1
2
3
4
5
dpkg -l | grep ssh
#发现只有ssh客户端,而没有服务端

#手动安装ssh
sudo apt-get install openssh-server

IMG_20200329_135828

dpkg查询一下,现在成功安装了ssh服务端。

然后ifconfig查看ip,192.168.0.100

IMG_20200329_135938

然后就可以在其他机子上远程登录了。

1585469114630

(上图的ip和上上图查询到的ip不同,因为上上图拍摄于更换ip之前,请不要感到疑惑)

一些我自己常用的服务

适应于32位系统的宝塔:

1
wget -O install.sh http://download.bt.cn/install/install-ubuntu.sh && sudo bash install.sh

待补充

无法挂载CD-ROM

英文信息忘了拍下来了,记得大体是说cannot unmount CD-ROM。

IMG_20200328_221521

IMG_20200328_221543

这个问题由来已久啊,找到了两种解决方案:

方法一:

简单粗暴的方法,出现上面的信息后,拔出U盘5秒钟,然后再插上,选则继续,并再次尝试,发现此时成功继续流程。

此方案的发现者表示,他也搞不清楚为什么这样可以解决问题。

我猜测可能是系统检测不到光驱,于是弹出失败的消息,当U盘拔出又插上后,系统再次检测,并把U盘当作光驱??(仅是猜测而已)

方法二:

既然检测不到CD-ROM,那就手动将U盘挂载到/cdrom,让系统误以为U盘就是光驱。

按esc,或是选择“返回”,进入此界面,选择运行shell:

IMG_20200328_222936

1585471290401

最后输入exit,回车退出。

发现不了硬盘

这个是天坑啊!!!我昨晚一夜没睡,一直到今天早上7点才得以解决这个问题。

最终也不知道是什么原因,我把我的所有尝试都记录下来吧,仅供大家参考。

一开始,磁盘分区的时候发现:

IMG_20200329_020159

31.5GB正好是我u盘的大小。

后来通过拔U盘再插U盘的方法,使得可以发现CD-ROM后,到了这一步,连31.5GB的U盘这个选项也没了。

换了几个其他的系统,有ubuntu-14.04.6-desktop-i386lubuntu-16.04.3-desktop-i386CentOS-6.10-i386-minimal,都指向同一个问题,发现不了硬盘!

lubuntu16.04.3是live版的(可以在u盘里试用,好用的话再安装),我运行它自带的disk,发现可以识别到这个320G的硬盘,但仍然安装不了。

最后运行ubuntu-14.04.6-desktop-i386(也是live版),ctrl+alt+t调出终端,输入gparted并回车,这是一个窗口化的分区程序,选择320G的硬盘,右键,找到格式化为ext4的选项。

然后重新来一遍,发现可以识别硬盘了,并成功的安装了ubuntu-14.04.6-desktop-i386!!

然后换成ubuntu server 14.04,也可以正常识别硬盘,并成功安装。

我以为是因为文件系统的问题,因为我原先硬盘格式为NTFS,换成linux支持的ext4就成功安装。

但是接下来的操作令我的推测破灭了。

我接着又把电脑刷成了win7,硬盘的格式自然又变回了NTFS.

然后再次安装ubuntu server 14.04 ,令我吃惊的是,这次居然仍然正常识别了硬盘!!

所以应该并不是硬盘格式的问题。

最后,我推测,可能是之前多次分区,导致分区表异常,因此产生了未知的bug。当然这也没法验证我的推测了。

网上的未能识别硬盘的问题,大多(我见过的)归纳为以下几种:

  • BIOS内设置IDE,SATA,AHCI
  • 分区表异常
  • 硬盘格式NTFS,fat32,ext4
  • SSD和HDD共存问题
  • 双系统分区问题

太高深了,惹不起惹不起~

应用密码学:协议、算法与C源程序(原书第2版)

本文仅仅记录我读此书时的一些旁注笔记,阅读本文需要对照原著。

未覆盖所有知识,因为书中很容易懂的知识点我不会挑出来做笔记,做笔记的部分也未必是重点。

第一章 基础知识

古典密码学

  • 代替密码
    • 简单代替密码(明文的每一个字符被替换成密文中的另一个字符)
    • 多名码代替密码(与简单代替密码类似,唯一不同在于明文的一个字符可以映射成密文的多个字符之一)
    • 多字母代替密码(字符块成组代替,如ABA对应RTQ)
    • 多表代替密码(由多个简单的代替密码构成)
  • 换位密码(明文的字母不改变,但是顺序被打乱)
  • 简单异或
  • 一次一密乱码本
  • 隐写术

现代密码学

  • 对称算法
    • 序列密码(每次只对明文中的一个字节或一个bit运算)
    • 分组密码(每次对明文的一组bit进行运算)
  • 公开密钥算法

密码分析

  • 唯密文攻击(只有密文)
  • 已知明文攻击(已知一些密文及其对应的明文)
  • 选择明文攻击(在已知明文攻击的条件基础上,攻击者可以选择一部分明文,获取其对应的密文)
  • 自适应选择明文攻击(在选择明文攻击的条件基础上,攻击者可以基于以前的加密的结果修正这个选择)
  • 选择密文攻击(攻击者可以选择不同的密文,并可得到其对应的明文)
  • 选择密钥攻击(攻击者具有不同密钥之间关系的有关知识)
  • 软磨硬泡攻击(社工)

第二章 协议结构模块

Merkle的难题

P25,书中的翻译不太容易理解,所以自己重新描述了一下该流程。

  1. Bob产生约2^20(约一百万)个(明文x,密钥y)序列对,x各不相同,y各不相同。通过对称算法分别使用密钥y加密其对应的明文x,得到密文E(x),将这2^20个密文E(x)发给Alice
  2. Alice随机从2^20个E(x)中选一个,暴力破解出该密文采用的密钥y,顺便可以解得明文x.
  3. 采用刚刚暴力破解出的密钥y,使用对称算法,将Alice想要传达给Bob的消息x’加密成E(x’)。Alice将之前爆破解得的明文x和这一步加密的密文E(x’),发给Bob.
  4. Bob收到x和E(x’),从数据库中找到明文x对应的密钥y,使用y解密E(x’),得到消息x’.
  5. x’就是本轮工作,Alice想要传递给Bob的信息。之后Alice再次发送消息,需要再次暴力破解出一个新的密钥。

本方法的安全性在于攻击者不知道Alice从2^20个密文中随机选取了哪个密文。Alice只需要爆破解得一个密文,而攻击者需要爆破全部密文。

第九章 算法类型和模式

电子密码本模式(ECB)

electronic code book

特征

一个明文分组加密成一个密文分组。

相同的明文分组永远被加密成相同的密文分组。

由于各分组之间没有联系,所以加密解密工作可以并发进行。

填充

最后一个分组多数情况下不会正好是完整的。

处理该问题的方法是填充。

方案一:填充

在最后一个分组,不足的地方填充0,最后一个字节存储填充字节数。解密后删掉尾部填充字节。

最后一个分组如果恰好是完整分组,则需要再新建一个分组作为最后一个分组。

方案二:分组挪用

对照P136的图片。

概述的话就是,最后一个完整分组(一般是倒数第二个分组)加密后的密文一分为二,一部分留作密文,另一部分与最后一个分组(不完整分组)合并,组成的新分组的长度刚好满足完整分组的长度,对此新分组加密。

问题:分组重放

ECB模式的问题在于:攻击者无需知道密钥(只需要知道明文和密文的对应关系,比如5e081bc5加密为7ea593a4),就可以重组消息(重组:类似于把一个一个汉字从书上剪下来,重新用胶水组合成一封全新的信)。

书上的一个例子是银行的消息格式,讲得很清楚,不多赘述。

密码分组链接模式(CBC)

cipher block chaining

特征

前面提到ECB面临分组重放的问题,其根本原因在于各分组之间相互独立,所以CBC模式提供了反馈的机制。前一个分组的加密结果反馈到后一个分组的加密中。

对某一个分组加密前,需要首先将明文与前一个分组的密文进行异或,然后加密。

初始化向量(IV)

虽然反馈可以解决分组重放的问题,但是新的问题来了(其实ECB也有这个问题):任意两则消息在他们出现第一处不同之前,将被加密成相同的结果。

书中这句话不太容易理解(完全是翻译的锅),其实很简单,两个类似的消息,比如同一场考试的准考证信息,不管你是谁,准考证开头的注意事项都是完全一致的,在分组加密这些信息时,得到的密文都是一样的,直到遇到姓名、准考证等这些不同的信息时,密文才开始不同。

攻击者可能会利用这些相同的密文来分析密码。

方法也是有的,那就是给出一个初始化向量IV,这个IV每次都是随机的(书中说应为唯一的,我感觉有点怪,如果IV是唯一的,那么还引入IV干嘛?相同的明文还是加密为相同的密文。查阅了一些资料,上面说IV是随机的)

IV与第一个明文分组异或,后面的流程和上面的一样。

IV与密文一同传递给第二方,IV可以不必保密。

为什么不需要?因为IV仅仅是为了保证对相同密钥相同明文的情况下,加密得到不同的密文(得益于IV的每次加密都随机),而没有其他作用,IV对密文的破译没有任何影响。

退一万步,IV用于与第一个明文异或,然而后面的n-1个分组密文都是暴漏的,这n-1个分组密文与其后的分组明文分别异或,IV的作用其实就相当于这n-1个分组密文,都是与分组明文异或。既然n-1个分组密文都是暴漏的(因为传输啊!密文肯定是允许泄漏的,不然你加密他干嘛?),那就没必要把IV保密。

cycle graph

1583763508542

32位PE程序,使用ida分析。

1583763607306

通过string定位到主函数。

这一段代码在输入字符串之前,进行数据的处理。其值与输入无关,所以可以直接使用OD提取相关数据。

1583763679302

输入字符串之后的关键代码:

1583763977716

v7为指针,指向前面所说的在输入字符串之前进行的运算的一片区域中的某个值。开始时v7指向0x403378.

v5初始值为48,之后的第i次循环都将赋值为flag[i-1]的值。

v12是输入的flag的基址。

v11是flag中的字符。

进行16次循环(v6=5~20),每次循环主要有如下运算:

  • 若v7指向的数值加v5等于flag[i],则v7赋值为v7地址+4处的指针指向的一个地址
  • 若v5减去v7指向的数值等于flag[i],则v7赋值为v7地址+8处的指针指向的一个地址
  • v5赋值为flag[i](但是赋值后的v5是在下一次循环中参与运算,所以其实相当于flag[i-1])

在OD中看一下运算过程:

1583765113101

最初v7(eax)=0x9f3380,其值为0x34,该值用于第一次循环中的运算判断。我们手动构造了满足地址+4条件的flag(第一个字符为d即可),v7+4后的地址处的数据是0x9f3398,其值为0x2c,该值用于下一个循环。

以上过程循环十六次。

我们在hex区提取出该区域的所有值。

通过脚本可以得到整理后的数据:

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
import re


def little_egg(s):
return s[10:12] + s[7:9] + s[4:6] + s[1:3]

def hex_format(num):
return '00'+hex(num)[2:].upper()


data = '''00983378 98 33 98 00 00 00 00 00 34 00 00 00 98 33 98 00
00983388 8C 33 98 00 02 00 00 00 98 33 98 00 E0 33 98 00
00983398 2C 00 00 00 8C 33 98 00 D4 33 98 00 2A 00 00 00
009833A8 58 34 98 00 94 34 98 00 06 00 00 00 D4 33 98 00
009833B8 EC 33 98 00 2A 00 00 00 98 33 98 00 64 34 98 00
009833C8 2F 00 00 00 B8 34 98 00 F4 34 98 00 2A 00 00 00
009833D8 1C 34 98 00 94 34 98 00 33 00 00 00 B0 33 98 00
009833E8 EC 33 98 00 03 00 00 00 F8 33 98 00 1C 34 98 00
009833F8 02 00 00 00 B0 33 98 00 10 34 98 00 32 00 00 00
00983408 7C 34 98 00 DC 34 98 00 32 00 00 00 28 34 98 00
00983418 F8 33 98 00 32 00 00 00 8C 33 98 00 A0 34 98 00
00983428 30 00 00 00 80 33 98 00 EC 33 98 00 03 00 00 00
00983438 28 34 98 00 A0 34 98 00 01 00 00 00 BC 33 98 00
00983448 AC 34 98 00 32 00 00 00 D4 33 98 00 EC 33 98 00
00983458 2B 00 00 00 D0 34 98 00 B8 34 98 00 02 00 00 00
00983468 10 34 98 00 A4 33 98 00 2E 00 00 00 D0 34 98 00
00983478 88 34 98 00 01 00 00 00 34 34 98 00 C8 33 98 00
00983488 02 00 00 00 34 34 98 00 4C 34 98 00 2D 00 00 00
00983498 98 33 98 00 1C 34 98 00 32 00 00 00 40 34 98 00
009834A8 D4 33 98 00 04 00 00 00 94 34 98 00 34 34 98 00
009834B8 2D 00 00 00 E8 34 98 00 70 34 98 00 30 00 00 00
009834C8 94 34 98 00 8C 33 98 00 31 00 00 00 64 34 98 00
009834D8 40 34 98 00 2F 00 00 00 EC 33 98 00 B0 33 98 00
009834E8 33 00 00 00 88 34 98 00 04 34 98 00 05 00 00 00
009834F8 F4 34 98 00 F4 34 98 00 02 00 00 00 00 00 00 00'''
data_dict = {}
visit_dict = {}
for i, line in enumerate(data.split('\n')):
for j in range(4):
data_dict[hex_format(0x983378+16*i+4*j)] = little_egg(line[9+12*j : 9+12*j+12])

for key, value in data_dict.items():
print(key, value)

节选一部分数据以供观察其格式:

1
2
3
4
5
6
7
00983380 00000034
00983384 00983398
00983388 0098338C

0098338C 00000002
00983390 00983398
00983394 009833E0

其实就是三个一组,第一个是v7的取值,后二者分别是地址+4和地址+8的两种情形。

最后,程序要求v7的最终值必须是0x4034f4(为方便表述不提及变动的基址)

1583765337191

所以本题的算法可以总结为:

给定一些节点和节点之间的连通关系,寻找一条从0x380到0x4f4的路径。

一旦找到这条路径,我们就可以通过flag[i] = v7+flag[i-1]解得flag.

由于数据量不大,我直接手动寻路。

下面脚本中1表示地址+4的情形,2表示地址+8的情形。

1
2
3
4
5
6
7
8
9
10
v7 = [0x34, 0x2c,0x2a,0x32,0x32,0x01,0x2a,0x02,0x2a,0x2b,0x2d,0x33,0x32,0x01,0x2f,0x05]
op = [1,2,1,2,1,1,2,2,1,2,1,2,1,2,2,1]
v5 = 48
for i in range(len(v7)):
if op[i] == 1:
print(chr(v5+v7[i]),end='')
v5 = v5+v7[i]
elif op[i] == 2:
print(chr(v5-v7[i]),end='')
v5 = v5-v7[i]

一步到位的python代码

参考了一个c代码,写出了这个脚本。思想就是dfs。参考的网址找不到了。

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
import string

def little_egg(s):
return s[10:12] + s[7:9] + s[4:6] + s[1:3]

def hex_format(num):
return '00'+hex(num)[2:].upper()


data = '''00983378 98 33 98 00 00 00 00 00 34 00 00 00 98 33 98 00
00983388 8C 33 98 00 02 00 00 00 98 33 98 00 E0 33 98 00
00983398 2C 00 00 00 8C 33 98 00 D4 33 98 00 2A 00 00 00
009833A8 58 34 98 00 94 34 98 00 06 00 00 00 D4 33 98 00
009833B8 EC 33 98 00 2A 00 00 00 98 33 98 00 64 34 98 00
009833C8 2F 00 00 00 B8 34 98 00 F4 34 98 00 2A 00 00 00
009833D8 1C 34 98 00 94 34 98 00 33 00 00 00 B0 33 98 00
009833E8 EC 33 98 00 03 00 00 00 F8 33 98 00 1C 34 98 00
009833F8 02 00 00 00 B0 33 98 00 10 34 98 00 32 00 00 00
00983408 7C 34 98 00 DC 34 98 00 32 00 00 00 28 34 98 00
00983418 F8 33 98 00 32 00 00 00 8C 33 98 00 A0 34 98 00
00983428 30 00 00 00 80 33 98 00 EC 33 98 00 03 00 00 00
00983438 28 34 98 00 A0 34 98 00 01 00 00 00 BC 33 98 00
00983448 AC 34 98 00 32 00 00 00 D4 33 98 00 EC 33 98 00
00983458 2B 00 00 00 D0 34 98 00 B8 34 98 00 02 00 00 00
00983468 10 34 98 00 A4 33 98 00 2E 00 00 00 D0 34 98 00
00983478 88 34 98 00 01 00 00 00 34 34 98 00 C8 33 98 00
00983488 02 00 00 00 34 34 98 00 4C 34 98 00 2D 00 00 00
00983498 98 33 98 00 1C 34 98 00 32 00 00 00 40 34 98 00
009834A8 D4 33 98 00 04 00 00 00 94 34 98 00 34 34 98 00
009834B8 2D 00 00 00 E8 34 98 00 70 34 98 00 30 00 00 00
009834C8 94 34 98 00 8C 33 98 00 31 00 00 00 64 34 98 00
009834D8 40 34 98 00 2F 00 00 00 EC 33 98 00 B0 33 98 00
009834E8 33 00 00 00 88 34 98 00 04 34 98 00 05 00 00 00
009834F8 F4 34 98 00 F4 34 98 00 02 00 00 00 00 00 00 00'''
data_dict = {}
visit_dict = {}
for i, line in enumerate(data.split('\n')):
for j in range(4):
data_dict[hex_format(0x983378+16*i+4*j)] = little_egg(line[9+12*j : 9+12*j+12])
del data_dict['00983378']
del data_dict['0098337C']
del data_dict['00983500']
del data_dict['00983504']


class C:
def __init__(self,data,p1,p2,n,visit=False):
self.data = data
self.p1 = p1
self.p2 = p2
self.n = n
self.visit = visit


data_list = []
for i in range(32):
data = int(data_dict[hex_format(0x983380 + i * 12)], 16)
p1 = (int(data_dict[hex_format(0x983380 + i * 12 + 4)],16) - 0x983380) // 12
p2 = (int(data_dict[hex_format(0x983380 + i * 12 + 8)],16) - 0x983380) // 12
n = i
c = C(data,p1,p2,n)
#print(c.data,c.p1,c.p2,c.visit)
data_list.append(c)


def visible(n):
#return True if chr(n) in string.printable else False
return True if chr(n) in string.ascii_letters+string.digits else False

def dfs(c,step):
i = c.n
if step == 17 and i == data_list[31].p1:
return True

if c.visit:
return False
data_list[i].visit = True

if visible(flag[step-1] + c.data):
flag[step] = flag[step-1] + c.data
if dfs(data_list[c.p1], step+1):
return True
elif visible(flag[step-1] - c.data):
flag[step] = flag[step-1] - c.data
if dfs(data_list[c.p2], step+1):
return True
else:
data_list[i].visit = False
return False

flag = [48] + [0]*23
dfs(data_list[0], 1)
print('flag{%s}' % ''.join(list(map(chr,flag[1:17]))))

hackim20的另一道逆向题,从逆向的角度来说不是很难,关键是如何将操作自动化。通过本题初步地学习了题目的部署和angr的使用。

misc猜测

给出了3000个二进制文件,观察可知文件分两大类,大小分别是5.34K和5.99K。很容易得知二者分别是32位和64位程序。

首先猜测是否是32表示0,64表示1,构成了二进制串

(虽然这是逆向题,misc的可能性很小,但我还是首先尝试了这种可能)。

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
import os,struct, binascii

def get_FileSize(filePath):
'''
网上找的小轮子
'''
#filePath = unicode(filePath,'utf8')
fsize = os.path.getsize(filePath)
#fsize = fsize/float(1024*1024)
# 注释的是MB,下一行是KB
fsize = fsize/float(1024)
return round(fsize,2)

dirpath = r'D:\桌面\hackim20\RE\year3000\year3000'
files = os.listdir(dirpath)
# 去掉.bin的后缀,按照数字将文件排序
files.sort(key=lambda x:int(x[:-4]))
_bin = ''
for file in files:
path = os.path.join(dirpath, file)
#print(path)
size = get_FileSize(path)
if str(size) == '5.34':
_bin += '0'
elif str(size) == '5.99':
_bin += '1'
o = b''
for i in range(len(_bin)//8):
char = int(_bin[i*8:i*8+8], 2) & 0x7f
# struct,划知识点啦!!
o += struct.pack('b', char)
print(o)

输出是

1
b'R\x08!N4\x051J<8wsA8v"EZY&;`]Cv*;=\x11%C\x00C\x0ej\x08\x00?\x0f$ZP\x0e-BB\x1f\x15doY!\x15\x08C\x16J\x0ev\x0e\x00\x0cd\x12+\x1f1p.zXq\x16`\x14& G\\\x04\x1e\x1a\x05!>Z\x161z\x12\x13\x0ej\x1b(<(-Dsz@>4]A\x19~\x08+\x13Bs`X\x7f\x12/\x1d8.\x1f\x0b(\x0fD17\x05\x14`\x1a\x02&m[WhB+=8\x03}\x15-)s6_h"C\x12iA\x196enf.%\x0b!kSQ;3*(gLkB\x1c"g\x07"\x083\x1eA5\x05\x0cYc04\x0e3_!1\t_\x05z$\x10$`\x07\x15qD\x07Kt\x07\x0357Kd0bd2}Y\x15$\x10.zxU=Ke0F\x18Rpj\\H|34/Kk6e\x079k$\x0fkc\x16T\x14\x00y.\x13R\x07^#g\x1b%]\x1ak\x06\\`+\x12 r\x1dV\x7f0K.67c\x17P[gSb{k\x14@H\x7fHyB\x11}];o:B!Oo\x1cf_]\x18\x1b\x1f{Uo;*\r9k_}4L\x10\'\x01"GS\x03N\x1a\x1cL;S\\`\x04AM\x1am}5B\x18\x08>f\x06"d\r\ngno9eWAt2'

好吧,果然不是misc

比较文件差异

猜测同一类的文件应该存在细小的差异。

1.bin和3.bin都是5.34KB的文件。

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
[root@izwdpnodbapihwz dist]# cmp -l 2.bin 4.bin
645 40 26
646 321 354
647 200 321
648 331 77
649 343 35
650 230 101
651 114 33
652 155 37
653 152 71
654 144 222
655 40 252
656 73 65
657 112 153
658 277 176
659 242 305
660 271 7
661 13 315
662 334 265
663 131 150
664 221 143
2074 123 115
2081 116 167
4113 224 363
4114 54 143
4115 211 171
4116 342 134
4117 342 321
4118 250 216
4119 301 77
4120 54 312

使用cmp命令比较。

第一列是序号(偏移文件首部的字节数),表示两个二进制文件何处的数据存在差异,是十进制数。

第二三列分别表示两文件在差异处的数值分别是多少。注意,是八进制数

可以发现有三处,分别是605附近,2074附近,4113附近。

手动payload

下图是程序主体:

1583010382760

进入判断函数sub_80A:

1583010580765

unk_201010的数据为:

1583010532503

所以就是判断输入是否为0x53个0x4e对应的字符拼接上unk_201010对应的字符串。

写个小脚本测试一下2.bin的输入:

1
2
3
4
5
6
import struct

with open('2','wb')as f:
f.write(b'N'*83 + struct.pack('>Q', 0x942c89e2e2a8c12c))
# >代表大端序,好好想想我为啥用大端?因为我希望的到的字符串的顺序和程序中的顺序是相同的,用小端就反转了。
# Q代表无符号long long

1583011044412

因为python3print的二进制字节流本质上是个字符串,所以我先把二进制字节流保存到文件中,再通过管道命令重定向输出到2.bin中。得到了well done的回执,说明没毛病。

题目的线上部署

由于题目需要nc连接,而比赛通道早已关闭,幸好出题人赛后给出了docker,我们需要自行部署。

我fork的:https://github.com/iyzyi/hackim-2020

docker都给出了,其实部署就很简单了。

1
2
3
4
git clone https://github.com/iyzyi/hackim-2020
cd /root/tmp/hackim-2020/re/year3000
docker build -t year3000 .
docker run -it --name year3000 -p 50011:1234 year3000

50011换成自己的宿主机开放端口。

nc连接一下:
1583011907534

无意间发现:

题目是随机选取几个*.bin,要求你输入base64编码后的payload

自动化

可以通过上面说的cmp的命令,也可以在ida内结合下面两张图来确定偏移量。

1583011377151

1583011412733

(鼠标点击汇编代码,hex窗口会自动跟随定位位置)

经过对照发现:

1
2
2074处的八进制数值123即为0x53
2081处的八进制数值116即为0x4e

不过注意unk_201010在ida中显示的地址是0x201010,但是二进制程序没有此地址,最大地址才0x1500多一点。结合cmp的结果,我发现其在文件中的地址为4112,注意不是cmp结果中的第三部分的第一个差异的地方,这个要结合ida的unk_201010的值来看。

以上是64的分析,32位同理。

以下是我的脚本:

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
import os, struct, re, base64
from pwn import *

def get_FileSize(filePath):
#filePath = unicode(filePath,'utf8')
fsize = os.path.getsize(filePath)
#fsize = fsize/float(1024*1024)
fsize = fsize/float(1024)
return round(fsize,2)

def payload_32(filepath):
count_addr = 0x661
char_addr = 0x668
#data_addr = 0x2008
data_addr = 4104

file = open(filepath, 'rb').read()
count = struct.unpack('b', file[count_addr])[0]
char = file[char_addr]
data = file[data_addr:data_addr+4]
print count*char + data
return count*char + data

def payload_64(filepath):
count_addr = 0x819
char_addr = 0x820
#data_addr = 0x201010
data_addr = 4112

file = open(filepath, 'rb').read()
count = struct.unpack('b', file[count_addr])[0]
char = file[char_addr]
data = file[data_addr:data_addr+8]
print count*char + data
return count*char + data

def payload(filepath):
if get_FileSize(filepath) == 5.34:
return payload_32(filepath)
elif get_FileSize(filepath) == 5.99:
return payload_64(filepath)


def main():
context.log_level = 'debug'
i = remote('iyzyi.com', 50011)

'''
file = '2.bin'
dirpath = '/root/tmp/dist'
filepath = os.path.join(dirpath, file)
payload(filepath)
'''
recv = ''
while 'hackim' not in recv:
file = i.recvuntil('>')
r = re.search(r'(\d+?)\.bin', file)
if r:
file = r.group(1) + '.bin'
dirpath = '/root/tmp/dist'
filepath = os.path.join(dirpath, file)
i.sendline(base64.b64encode(payload(filepath)))


main()

脚本和3000个*.bin放在一起,而且要修改dirpath为相应的值哦。

1583014473886

思路相近的大佬:

以下脚本摘录自https://x3ero0.tech/posts/year3000/,纯英文,也是难为我了。

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
from pwn import *
import base64


context.log_level = "critical"

def solve(filename):

file = open(filename, "rb").read()

ELF64_Coun = 0x816
ELF64_Valu = 0x81d

ELF32_Coun = 0x65e
ELF32_Valu = 0x665


ELF64_Addr = 0x1010
ELF32_Addr = 0x1008


Format = ord(file[0x04])
if (Format == 2):

context.update(arch='amd64', os='linux')

code_count = file[ELF64_Coun : ELF64_Coun+7]
code_value = file[ELF64_Valu : ELF64_Valu+4]

disa_count = disasm(code_count)
disa_value = disasm(code_value)

code_count = int(disa_count.split('0x')[-1], 16)
code_value = chr(int(disa_value.split('0x')[-1], 16))

#value = u64(file[ELF64_Addr : ELF64_Addr+8])

value = file[ELF64_Addr : ELF64_Addr+8]
value_n = u64(value)

flag = code_value * code_count + value

elif(Format == 1):

context.update(arch='i386', os='linux')

code_count = file[ELF32_Coun : ELF32_Coun+7]
code_value = file[ELF32_Valu : ELF32_Valu+4]

disa_count = disasm(code_count)
disa_value = disasm(code_value)

code_count = int(disa_count.split('0x')[-1], 16)
code_value = chr(int(disa_value.split('0x')[-1], 16))

#value = u32(file[ELF32_Addr:ELF32_Addr+4])

value = file[ELF32_Addr:ELF32_Addr+4]
value_n = u32(value)
flag = code_value * code_count + value

#print "[!] file : " + filename

#print "[!] count: " + hex(code_count)

#print "[!] value: " + code_value

#print "[!] numbe: " + hex(value_n)


return flag


def main():


dump = ""
i = 0
p = remote("re.ctf.nullcon.net", 1234)
a = ""
temp = ""
while('hackim' not in dump):

# i noticed that only 10 files were asked

if(i==10):
print "Flag: " + p.recvline()
p.close()
exit()
file = p.recvline().strip()

a = p.recvuntil("> ")
solver = solve(file)
# .encode('base64') doesnt work with bytes

p.sendline(base64.b64encode(solver))
#print str(solver).encode('base64')

temp = p.recvline()
if("Well done" in temp):
print "[0x%.4x]\t" % i+ file + '\t: Well Done'
else:
print "[+] Failed:\t" + file + "\tdumping recieved data"
exit()
i += 1

if __name__ == "__main__":
main()

我使用文件大小来区分32位和64位没有普适性,这位大佬采用的Format = ord(file[0x04])区分。受教了。

他脚本的disasm其实是类似于ida中一整行的汇编代码,左边是行号,中间是汇编代码(对于此偏移量处的汇编代码,其实就是一个单纯的数字),右侧是啥我忘了,好像是注释来着??反正我觉得他的提取方法不如我的好,直接从文件中读偏移。

angr

参考自https://mrt4ntr4.github.io/Nullcon-HackIM-Year3000/

源代码是python2,我改成了python3

当然本代码只是能暴力出所有的payload而已,没有nc的功能。仅作演示angr的使用吧。

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
import angr,claripy
import sys,csv,time
from base64 import b64encode
import logging

logging.getLogger('cle.loader').setLevel('ERROR')
w = csv.writer(open("output.csv", "a"))

def solve(path):
begin = time.time()
proj = angr.Project(path)
state = proj.factory.entry_state(add_options={angr.options.LAZY_SOLVES})
simulation = proj.factory.simgr(state)

def success(state):
output = state.posix.dumps(sys.stdout.fileno())
return b'Well done' in output

def failed(state):
output = state.posix.dumps(sys.stdout.fileno())
return b'You have failed' in output

simulation.explore(find=success, avoid=failed)

if simulation.found:
sol_state = simulation.found[0]
sol = sol_state.posix.dumps(sys.stdin.fileno())
print ("({} secs)".format(round(time.time()-begin,2)))
enc = b64encode(sol)
w.writerow([path, enc])
print (enc)
else:
print( "No Sol found for {}".format(path))

if __name__ == '__main__':
for x in range(1,3000):
solve("{}.bin".format(x))

一些问题

没搭建成功,可能是python版本冲突??待排查TODO

然后找到了官方的docker,然鹅一直出问题,好不容易才跑成功。

下面的命令用于启动:

1
docker run -it --name angr -v /root/tmp/dist:/home/angr/dist --privileged=true angr/angr

docker挂载数据卷

不加上--privileged=true的话,关于文件的读写都会报权限不足的错误,su root也不行,因为挂载了数据卷,docker的root相对于宿主机仍然是普通用户。

angr模块没有’Project’

这个问题把我逼疯惹~

我还以为是docker的锅,没想到最后无意间发现,我的脚本的名字是angr.py。

和模块的名字的命名空间发生了冲突。

这个问题我之前写re.py的时候就被坑过,只能说太容易出问题了。

1583014599087

No module named ‘angr’

请切换到angr用户运行脚本

1583014562506

运行

1583014700452

只运行到第二条payload,仅作演示。