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

/ 0评 / 0

0x00

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

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

0x01

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

完整代码:

python
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

我们首先看一下用法:

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)
# 这里第二次循环
# 模块结束

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

0x03

在多线程那里,首先是进入了 _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]`中。

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

0x04

第二个就是循环的到 _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] 中。

0x05

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

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)来互相通信流量。

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

0x06

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

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

发表评论

电子邮件地址不会被公开。 必填项已用*标注