工具名是 rtcp.py ,是2009年知道创宇当时写的一个端口转发工具,工具主要是利用python的socket端口转发,代码的整体编写思路非常值得学习。
我一开始是从模块开始分析的,忘记了从运行流程上来分析,所以出现卡壳了。
完整代码:
import socket
import sys
import threading
import time
streams = [None, None] # 存放需要进行数据转发的两个数据流(都是SocketObj对象)
debug = 1 # 调试状态 0 or 1
def _usage(): # 提示用法函数
print 'Usage: ./rtcp.py stream1 stream2nstream : 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)
我们首先看一下用法:
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,这四个模块。
streams = [None, None]
debug = 1
这两个分别是存放数据转发的数据流和调试的开关,关于debug这个很值得学习。
分析的话,肯定是从 if __name__ == '__main__':
这里开始的,因为是逆向分析流程,肯定是不能从功能模块直接开始,这样很容易懵逼的。
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循环去分离出自己想要的参数。
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)
# 这里第二次循环
# 模块结束
这里比较简单就以注释的方法解释一遍。
在多线程那里,首先是进入了_server
这个模块,从上面的例子过来,我们知道它传入的是监听端口号,我们先看一下代码:
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]
中。
下面那两行代码一会再分析,继续回归上面那个第二个循环代码。
第二个就是循环的到_connect
,也就是客户端,先看下整体代码:
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地址,端口号,流编码,顺着咱们的预设下来,这三个应该分别是
host = 192.168.1.1
port = 8022
num = 1
函数中的not_connet_time,wait_time,try_cnt,这三个变量,主要是用来判别是否存在数据流,和挂载延迟时间,来重复连接的。
if not_connet_time > try_cnt:
streams[num] = 'quit'
print('not connected')
return None
从这几行就可以看到,如果没有连接的时间大于挂载的这个时间,就会把quit
这个字符串添加到streams[1]
里,然后并输出无法连接,然后退出该函数,不再执行下面的代码。
下面用了异常函数来处理端口IP绑定之间的错误,如果无法访问就会自动挂载sleep(36)秒。
连接成功的话,就会输出连接成功,并且print出IP地址和端口号,然后这个客户端的数据流对象会放到streams[1]
中。
都进行到这里后,就会进入下面两个函数中:
s2 = _get_another_stream(num) # 获取另一端流对象
_xstream(num, conn, s2)
在_get_another_stream
的后面的注释中,可以知道这是个一个获取另一端流对象的函数,具体是什么流程还是得看代码。
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)
这个函数,我们可以看到入口处有三个参数:
num = 传进来的num值[0,1]
s1 = 传入者自己的流量
s2 = 对方的流量
其实在上面传入的参数中是_xstream(num, conn, s2)
这样的,因为s2是交换数据流以后的函数,所以也就是对方的流量对象。
下面看一下整体函数:
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)来互相通信流量。
剩下的都是关闭数据流关闭端口,然后重置下数据,但是我个人太菜了,看这一部分的时候还是有点懵逼,估计我只是细看的原因吧。
整体分析一遍流程,通过外接参数,然后分析使用服务端函数还是使用客户端函数,然后加入到多线程中,如果进入服务端就先创建一个套接字,然后绑定下端口,把本端的流放入到一开始存在的那个列表里,然后使用交换两端的流函数,交换一下, 最后使用通信函数,互相交流流文件。如果是选择的客户端函数,就会先判断流文件中是否存在quit这个退出关键字,如果存在就退出,不存在就绑定IP地址和端口号,继续和上面的服务端函数差不多,互相通信流文件。最后退出时,关闭端口,清空存在流文件的列表。
这个端口转发脚本,真的值得去学习,感觉对我感触很多,在开发其他一些工具的时候,关于构造的思考。
参考链接
https://sh1yan.top/2018/07/09/Port-Forwarding-Tool-Analysis/#0x00
真心感觉自己要学习的知识好多,也有好多大神卧虎藏龙、开源分享。作为初学者,我们可能有差距,不论你之前是什么方向,是什么工作,是什么学历,是大学大专中专,亦或是高中初中,只要你喜欢安全,喜欢渗透,就朝着这个目标去努力吧!有差距不可怕,我们需要的是去缩小差距,去战斗,况且这个学习的历程真的很美,安全真的有意思。但切勿去做坏事,我们需要的是白帽子,是维护我们的网络,安全路上共勉。
本文版权归作者和微信公众号平台共有,重在学习交流,不以任何盈利为目的,欢迎转载。
由于传播、利用此文所提供的信息而造成的任何直接或者间接的后果及损失,均由使用者本人负责,文章作者不为此承担任何责任。公众号内容中部分攻防技巧等只允许在目标授权的情况下进行使用,大部分文章来自各大安全社区,个人博客,如有侵权请立即联系公众号进行删除。若不同意以上警告信息请立即退出浏览!!!
敲敲小黑板:《刑法》第二百八十五条 【非法侵入计算机信息系统罪;非法获取计算机信息系统数据、非法控制计算机信息系统罪】违反国家规定,侵入国家事务、国防建设、尖端科学技术领域的计算机信息系统的,处三年以下有期徒刑或者拘役。违反国家规定,侵入前款规定以外的计算机信息系统或者采用其他技术手段,获取该计算机信息系统中存储、处理或者传输的数据,或者对该计算机信息系统实施非法控制,情节严重的,处三年以下有期徒刑或者拘役,并处或者单处罚金;情节特别严重的,处三年以上七年以下有期徒刑,并处罚金。
原文始发于微信公众号(巢安实验室):对某端口转发工具的一次分析_
- 左青龙
- 微信扫一扫
- 右白虎
- 微信扫一扫
评论