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] debug = 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': 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: 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] sl = s.split(':') if len(sl) == 2 and (sl[0] == 'l' or sl[0] == 'L'): 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'): 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:9999
和sys.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] sl = s.split(':') ''' sl[0] = l sl[1] = 9999 ''' if len(sl) == 2 and (sl[0] == 'l' or sl[0] == 'L'): 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'): t = threading.Thread(target=_connect, args=(sl[1], int(sl[2]), i)) tlist.append(t) else: _usage() sys.exit(1)
s = targv[i] 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'): 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'): t = threading.Thread(target=_connect, args=(sl[1], int(sl[2]), i)) tlist.append(t) 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': 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: 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地址和端口号,继续和上面的服务端函数差不多,互相通信流文件。最后退出时,关闭端口,清空存在流文件的列表。
这个端口转发脚本,真的值得去学习,感觉对我感触很多,在开发其他一些工具的时候,关于构造的思考。