对某端口转发工具的一次分析

0x00

在上上一篇文章中就计划对一个端口转发工具进行分析的,但是,由于自己懒的原因就一下子拖了好几个月,后来自己再分析的时候出现了分析的逻辑性错误,所以,求助了老P同志,所以转进入正轨。

工具名是 rtcp.py ,是2009年知道创宇当时写的一个端口转发工具,工具主要是利用python的socket端口转发,代码的整体编写思路非常值得学习。

0x01

我一开始是从模块开始分析的,忘记了从运行流程上来分析,所以出现卡壳了。

完整代码:

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
import socket
import sys
import threading
import time

streams = [None, None] # 存放需要进行数据转发的两个数据流(都是SocketObj对象)
debug = 1 # 调试状态 0 or 1

def _usage(): # 提示用法函数
print 'Usage: ./rtcp.py stream1 stream2\nstream : l:port or c:host:port'

def _get_another_stream(num):
'''
从streams获取另外一个流对象,如果当前为空,则等待
'''
if num == 0:
num = 1
elif num == 1:
num = 0
else:
raise "ERROR"

while True:
if streams[num] == 'quit': # 判断列表里是否 quit
print("can't connect to the target, quit now!")
sys.exit(1)

if streams[num] != None:
return streams[num]
else:
time.sleep(1)

def _xstream(num, s1, s2):
'''
交换两个流的数据
num为当前流编号,主要用于调试目的,区分两个回路状态用。
'''
try:
while True:
# 注意,recv函数会阻塞,直到对端完全关闭(close后还需要一定时间才能关闭,最快关闭方法是shutdow)
buff = s1.recv(1024)
if debug > 0:
print num,"recv"
if len(buff) == 0: # 对端关闭连接,读不到数据
print num,"one closed"
break
s2.sendall(buff)
if debug > 0:
print num,"sendall"
except :
print num,"one connect closed."

try:
s1.shutdown(socket.SHUT_RDWR)
s1.close()
except:
pass

try:
s2.shutdown(socket.SHUT_RDWR)
s2.close()
except:
pass

streams[0] = None
streams[1] = None
print num, "CLOSED"

def _server(port, num):
'''
处理服务情况,num为流编号(第0号还是第1号)
'''
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('0.0.0.0', port))
srv.listen(1)
while True:
conn, addr = srv.accept()
print "connected from:", addr
streams[num] = conn # 放入本端流对象
s2 = _get_another_stream(num) # 获取另一端流对象
_xstream(num, conn, s2)

def _connect(host, port, num):
''' 处理连接,num为流编号(第0号还是第1号)
@note: 如果连接不到远程,会sleep 36s,最多尝试200(即两小时)
'''
not_connet_time = 0
wait_time = 36
try_cnt = 199
while True:
if not_connet_time > try_cnt:
streams[num] = 'quit'
print('not connected')
return None

conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
conn.connect((host, port))
except Exception, e:
print ('can not connect %s:%s!' % (host, port))
not_connet_time += 1
time.sleep(wait_time)
continue

print "connected to %s:%i" % (host, port)
streams[num] = conn #放入本端流对象
s2 = _get_another_stream(num) #获取另一端流对象
_xstream(num, conn, s2)


if __name__ == '__main__':
if len(sys.argv) != 3:
_usage()
sys.exit(1)
tlist = [] # 线程列表,最终存放两个线程对象
targv = [sys.argv[1], sys.argv[2] ]
for i in [0, 1]:
s = targv[i] # stream描述 c:ip:port 或 l:port
sl = s.split(':')
if len(sl) == 2 and (sl[0] == 'l' or sl[0] == 'L'): # l:port
t = threading.Thread(target=_server, args=(int(sl[1]), i))
tlist.append(t)
elif len(sl) == 3 and (sl[0] == 'c' or sl[0] == 'C'): # c:host:port
t = threading.Thread(target=_connect, args=(sl[1], int(sl[2]), i))
tlist.append(t)
else:
_usage()
sys.exit(1)

for t in tlist:
t.start()
for t in tlist:
t.join()
sys.exit(0)

0x02

我们首先看一下用法:

1
2
Usage: ./rtcp.py stream1 stream2
stream : l:port or c:host:port

那以 python rtcp.py l:9999 c:192.168.1.1:8022 这样的形式全部分析一下代码的运行流程。

工具主要使用了 socket ,sys,threading,time,这四个模块。

1
2
streams = [None, None] 
debug = 1

这两个分别是存放数据转发的数据流和调试的开关,关于debug这个很值得学习。

分析的话,肯定是从 if __name__ == '__main__': 这里开始的,因为是逆向分析流程,肯定是不能从功能模块直接开始,这样很容易懵逼的。

1
2
3
4
5
if len(sys.argv) != 3:
_usage()
sys.exit(1)
tlist = [] # 线程列表,最终存放两个线程对象
targv = [sys.argv[1], sys.argv[2] ]

先是判断参数是否为三位,如果不是就直接退出,如果是三位就继续进入下面的流程,

targv = [sys.argv[1], sys.argv[2] ] 这里其实是把sys.argv[1] = l:9999sys.argv[2] = c:192.168.1.1:8022分别放进 targ 这个列表里。

然后开始使用一个for循环去分离出自己想要的参数。

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
for i in [0, 1]:
开始两次循环。
# 这里第一次循环
s = targv[i] # s = targv[0] = sys.argv[1] = l:9999
sl = s.split(':') # 以:号进行分割
'''
sl[0] = l
sl[1] = 9999
'''
if len(sl) == 2 and (sl[0] == 'l' or sl[0] == 'L'):
# 这里判断分割后的sl长度等于2并且sl[0]等于'l'或者等于'L'的话就进入以下代码
t = threading.Thread(target=_server, args=(int(sl[1]), i))
#启动线程,线程目标为:_server,传入的参数为sl[1] = 9999,i = 0
tlist.append(t) #将线程对象放入tlist中

elif len(sl) == 3 and (sl[0] == 'c' or sl[0] == 'C'):
# 这里因为循环第一次sl长度等于2,因此表达式不成立。
t = threading.Thread(target=_connect, args=(sl[1], int(sl[2]), i))
tlist.append(t)
else:
_usage()
sys.exit(1)
# 这里第一次循环

# 这里第二次循环
s = targv[i] # s = targv[1] = sys.argv[2] = c:192.168.1.1:8022
sl = s.split(':') # 以:号进行分割
'''
sl[0] = c
sl[1] = 192.168.1.1
sl[2] = 8022
'''
if len(sl) == 2 and (sl[0] == 'l' or sl[0] == 'L'):
# 这里循环第二次sl长度等于3,因此不成立
t = threading.Thread(target=_server, args=(int(sl[1]), i))
#启动线程,线程目标为:_server,传入的参数为sl[1],与i
tlist.append(t) #将线程对象放入tlist中

elif len(sl) == 3 and (sl[0] == 'c' or sl[0] == 'C'):
# 这里判断分割后的sl长度等于3并且sl[0]等于'c'或者等于'C'的话就进入以下代码
t = threading.Thread(target=_connect, args=(sl[1], int(sl[2]), i))
#启动线程,线程目标为:_connect,传入的参数为sl[1] = 192.168.1.1,int(sl[2]) = 8022,i = 1
tlist.append(t)#将线程对象放入tlist中
else:
_usage()
sys.exit(1)
# 这里第二次循环
# 模块结束

这里比较简单就以注释的方法解释一遍。

0x03

在多线程那里,首先是进入了_server这个模块,从上面的例子过来,我们知道他传入的是监听端口号,我们先看一下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def _server(port, num):
'''
处理服务情况,num为流编号(第0号还是第1号)
'''
srv = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
srv.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
srv.bind(('0.0.0.0', port))
srv.listen(1)
while True:
conn, addr = srv.accept()
print "connected from:", addr
streams[num] = conn # 放入本端流对象
s2 = _get_another_stream(num) # 获取另一端流对象
_xstream(num, conn, s2)

传入的是两个参数一个是端口号,一个是流编号,也是就是一开始的第一次循环和第二次循环的那个[0,1],conn, addr = srv.accept()这里的conn是套接字对象,addr是IP地址,streams[num] = conn这个也就是把这个流的套接字放入streams[0]中。

下面那两行代码一会再分析,继续回归上面那个第二个循环代码。

0x04

第二个就是循环的到_connect,也就是客户端,先看下整体代码:

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
def _connect(host, port, num):
''' 处理连接,num为流编号(第0号还是第1号)
@note: 如果连接不到远程,会sleep 36s,最多尝试200(即两小时)
'''
not_connet_time = 0
wait_time = 36
try_cnt = 199
while True:
if not_connet_time > try_cnt:
streams[num] = 'quit'
print('not connected')
return None

conn = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
try:
conn.connect((host, port))
except Exception, e:
print ('can not connect %s:%s!' % (host, port))
not_connet_time += 1
time.sleep(wait_time)
continue

print "connected to %s:%i" % (host, port)
streams[num] = conn #放入本端流对象
s2 = _get_another_stream(num) #获取另一端流对象
_xstream(num, conn, s2)

参数主要为三个,分别是host,port,num,也就是IP地址,端口号,流编码,顺着咱们的预设下来,这三个应该分别是

1
2
3
host = 192.168.1.1
port = 8022
num = 1

函数中的not_connet_time,wait_time,try_cnt,这三个变量,主要是用来判别是否存在数据流,和挂载延迟时间,来重复连接的。

1
2
3
4
if not_connet_time > try_cnt:
streams[num] = 'quit'
print('not connected')
return None

从这几行就可以看到,如果没有连接的时间大于挂载的这个时间,就会把quit这个字符串添加到streams[1]里,然后并输出无法连接,然后退出该函数,不再执行下面的代码。

下面用了异常函数来处理端口IP绑定之间的错误,如果无法访问就会自动挂载sleep(36)秒。

连接成功的话,就会输出连接成功,并且print出IP地址和端口号,然后这个客户端的数据流对象会放到streams[1]中。

0x05

都进行到这里后,就会进入下面两个函数中:

1
2
s2 = _get_another_stream(num)  # 获取另一端流对象
_xstream(num, conn, s2)

_get_another_stream的后面的注释中,可以知道这是个一个获取另一端流对象的函数,具体是什么流程还是得看代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def _get_another_stream(num):
'''
从streams获取另外一个流对象,如果当前为空,则等待
'''
if num == 0:
num = 1
elif num == 1:
num = 0
else:
raise "ERROR"

while True:
if streams[num] == 'quit': # 判断列表里是否 quit
print("can't connect to the target, quit now!")
sys.exit(1)

if streams[num] != None:
return streams[num]
else:
time.sleep(1)

num就是一开始我们的 0,1这两个流编号,前面几个判断就是替换下编号,把1改成0,把0改成1,如果都不是就爆异常,raise这个用法执行后,后面的代码将不会继续执行了。

下面的死循环,显示判断数据流中是否存在 quit ,如果存在就退出,不存在执行下面的代码,如果流对象不为空,就返回交换后的那个流对象。

_xstream(num, s1, s2)这个函数,我们可以看到入口处有三个参数:

1
2
3
num = 传进来的num值[0,1]
s1 = 传入者自己的流量
s2 = 对方的流量

其实在上面传入的参数中是_xstream(num, conn, s2)这样的,因为s2是交换数据流以后的函数,所以也就是对方的流量对象。

下面看一下整体函数:

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
def _xstream(num, s1, s2):
'''
交换两个流的数据
num为当前流编号,主要用于调试目的,区分两个回路状态用。
'''
try:
while True:
# 注意,recv函数会阻塞,直到对端完全关闭(close后还需要一定时间才能关闭,最快关闭方法是shutdow)
buff = s1.recv(1024)
if debug > 0:
print num,"recv"
if len(buff) == 0: # 对端关闭连接,读不到数据
print num,"one closed"
break
s2.sendall(buff)
if debug > 0:
print num,"sendall"
except :
print num,"one connect closed."

try:
s1.shutdown(socket.SHUT_RDWR)
s1.close()
except:
pass

try:
s2.shutdown(socket.SHUT_RDWR)
s2.close()
except:
pass

streams[0] = None
streams[1] = None
print num, "CLOSED"

到这里的时候代码就很简单明了了,传入者自己的流量赋值给变量 buff,然后通过s2.sendall(buff)来互相通信流量。

剩下的都是关闭数据流关闭端口,然后重置下数据,但是我个人太菜了,看这一部分的时候还是有点懵逼,估计我只是细看的原因吧。

0x06

整体分析一遍流程,通过外接参数,然后分析使用服务端函数还是使用客户端函数,然后加入到多线程中,如果进入服务端就先创建一个套接字,然后绑定下端口,把本端的流放入到一开始存在的那个列表里,然后使用交换两端的流函数,交换一下, 最后使用通信函数,互相交流流文件。如果是选择的客户端函数,就会先判断流文件中是否存在quit这个退出关键字,如果存在就退出,不存在就绑定IP地址和端口号,继续和上面的服务端函数差不多,互相通信流文件。最后退出时,关闭端口,清空存在流文件的列表。

这个端口转发脚本,真的值得去学习,感觉对我感触很多,在开发其他一些工具的时候,关于构造的思考。


对某端口转发工具的一次分析
https://sh1yan.top/2018/07/09/Port-Forwarding-Tool-Analysis/
作者
shiyan
发布于
2018年7月9日
许可协议