如何搭建TCP代理(三)

如何搭建TCP代理(一)

如何搭建TCP代理(二)

构建我们的代理

我已经为我们编写了一个使用Python的twisted网络框架的代理示例,我发现twisted提供了对代理内部的适当控制,而所需模板却很少。为了实现此目的,它引入了一些自己的新抽象。这些抽象使twisted代码非常简洁,但对于不熟悉的人来说又有点神秘。

Twisted是围绕“事件驱动的回调”而设计的。这意味着只要发生特定事件,它就会自动运行特定方法(或“回调”)。我们感兴趣的事件是“构建连接”和“接收数据”。我们可以通过使用称为connectionMade和dataReceived的方法定义一个Protocol类来告诉twisted事件发生时该怎么办。当twisted看到“连接已构建”事件时,它将运行我们的connectionMade方法,你可能会猜到看到“数据已接收”事件时它将做什么。

以下就是我们的代码,接下来是对不同组件的更详细说明。

from twisted.internet import protocol, reactor

from twisted.internet import ssl as twisted_ssl

import dns.resolver

import netifaces as ni

# Adapted from http://stackoverflow.com/a/15645169/221061

class TCPProxyProtocol(protocol.Protocol):

    """

    TCPProxyProtocol listens for TCP connections from a

    client (eg. a phone) and forwards them on to a

    specified destination (eg. an apps API server) over

    a second TCP connection, using a ProxyToServerProtocol.

    It assumes that neither leg of this trip is encrypted.

    """

    def __init__(self):

        self.buffer = None

        self.proxy_to_server_protocol = None

    def connectionMade(self):

        """

        Called by twisted when a client connects to the

        proxy. Makes an connection from the proxy to the

        server to complete the chain.

        """

        print("Connection made from CLIENT => PROXY")

        proxy_to_server_factory = protocol.ClientFactory()

        proxy_to_server_factory.protocol = ProxyToServerProtocol

        proxy_to_server_factory.server = self

        reactor.connectTCP(DST_IP, DST_PORT,

                           proxy_to_server_factory)

    def dataReceived(self, data):

        """

        Called by twisted when the proxy receives data from

        the client. Sends the data on to the server.

        CLIENT ===> PROXY ===> DST

        """

        print("")

        print("CLIENT => SERVER")

        print(FORMAT_FN(data))

        print("")

        if self.proxy_to_server_protocol:

            self.proxy_to_server_protocol.write(data)

        else:

            self.buffer = data

    def write(self, data):

        self.transport.write(data)

class ProxyToServerProtocol(protocol.Protocol):

    """

    ProxyToServerProtocol connects to a server over TCP.

    It sends the server data given to it by an

    TCPProxyProtocol, and uses the TCPProxyProtocol to

    send data that it receives back from the server on

    to a client.

    """

    def connectionMade(self):

        """

        Called by twisted when the proxy connects to the

        server. Flushes any buffered data on the proxy to

        server.

        """

        print("Connection made from PROXY => SERVER")

        self.factory.server.proxy_to_server_protocol = self

        self.write(self.factory.server.buffer)

        self.factory.server.buffer =

    def dataReceived(self, data):

        """

        Called by twisted when the proxy receives data

        from the server. Sends the data on to to the client.

        DST ===> PROXY ===> CLIENT

        """

        print("")

        print("SERVER => CLIENT")

        print(FORMAT_FN(data))

        print("")

        self.factory.server.write(data)

    def write(self, data):

        if data:

            self.transport.write(data)

def _noop(data):

    return data

def get_local_ip(iface):

    ni.ifaddresses(iface)

    return ni.ifaddresses(iface)[ni.AF_INET][0][addr]

FORMAT_FN = _noop

LISTEN_PORT = 80

DST_PORT = 80

DST_HOST = "nonhttps.com"

local_ip = get_local_ip(en0)

# Look up the IP address of the target

print("Querying DNS records for %s..." % DST_HOST)

a_records = dns.resolver.query(DST_HOST, A)

print("Found %d A records:" % len(a_records))

for r in a_records:

    print("* %s" % r.address)

print("")

assert(len(a_records) > 0)

# THe target may have multiple IP addresses - we

# simply choose the first one.

DST_IP = a_records[0].address

print("Choosing to proxy to %s" % DST_IP)

print("""

#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#

-#-#-#-#-#-RUNNING  TCP PROXY-#-#-#-#-#-

#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#

Dst IP:\t%s

Dst port:\t%d

Dst hostname:\t%s

Listen port:\t%d

Local IP:\t%s

""" % (DST_IP, DST_PORT, DST_HOST, LISTEN_PORT, local_ip))

print("""

Next steps:

1. Make sure you are spoofing DNS requests from the

device you are trying to proxy request from so that they

return your local IP (%s).

2. Make sure you have set the destination and listen ports

correctly (they should generally be the same).

3. Use the device you are proxying requests from to make

requests to %s and check that they are logged in this

terminal.

4. Look at the requests, write more code to replay them,

fiddle with them, etc.

Listening for requests on %s:%d...

""" % (local_ip, DST_HOST, local_ip, LISTEN_PORT))

factory = protocol.ServerFactory()

factory.protocol = TCPProxyProtocol

reactor.listenTCP(LISTEN_PORT, factory)

reactor.run()

让我们仔细看看这段代码,你可能会发现在GitHub上打开代码很有用。

TCPProxyProtocol是我们的主要协议类,它处理与智能手机的通信,并将与远程服务器的通信委托给ProxyToServerProtocol类。我们通过实例化其中一个TCPProxyProtocol对象来初始化代理服务器,并告诉twisted使用它来监听端口80(按照规定,这个端口是未加密的HTTP端口)。接下来,什么都不会发生,直到你的笔记本电脑在端口80(可能是通过智能手机)上收到TCP连接为止。当twisted看到此“连接构建”事件时,它将在我们的TCPProxyProtocol上调用connectionMade回调。至此,我们的代理已与你的智能手机构建连接,并且完成了4步过程的第1步。

从代理到远程服务器的第2步由ProxyToServerProtocol类处理,调用我们的TCPProxyProtocol#connectionMade方法时,它将创建ProxyToServerProtocol的实例,并指示该实例连接到端口80上的目标远程服务器。

如果我们的TCPProxyProtocol在ProxyToServerProtocol与远程服务器的连接完成之前从你的手机接收到任何数据,它会将数据添加到缓冲区中,以确保数据不会被删除。一旦连接就绪,ProxyToServerProtocol将缓冲区收集的所有数据发送到远程服务器。此时,我们的代理已经打开了与你的智能手机和远程服务器的独立连接,并将数据从你的智能手机发送到远程服务器。此时,步骤2完成。

最后,当ProxyToServerProtocol从远程服务器接收回数据时,twisted会调用ProxyToServerProtocol自己的dataReceived回调。此回调中的代码指示原始TCPProxyProtocol将ProxyToServerProtocol从远程服务器接收的数据发送回手机。此时,步骤3和4完成。

测试我们的代理

由于我们还没有为我们的代理实现TLS支持,我们需要使用一个没有启用HTTPS的网站来测试我们的代理。我建议使用nonhttps.com,这是一个非常方便的开发主机名,正如承诺的那样,它不使用HTTPS。

在开始测试之前,请确保:

1. 你的DNS欺骗脚本指向nonhttps.com;

2. 你的智能手机将其DNS服务器设置为笔记本电脑的IP地址;

3. 你的TCP代理脚本指向nonhttps.com;

4. 你的TCP代理脚本设置为监听端口80。

然后启动这两个脚本,并在手机上访问nonhttps.com。你应该看到你的虚假DNS服务器欺骗了DNS请求,并返回了笔记本电脑的IP地址。然后,你应该看到TCP代理从智能手机接收HTTP数据,并将其内容记录到终端。接下来,它将记录从nonhttps.com返回的相应HTTP响应。最后,nonhttps.com应该会加载到手机的浏览器中,仿佛什么事都没有发生。

如果这不起作用,则需要进行一些调试。

你是否可以使用DNS服务器和TCP代理日志来准确指出问题出在哪里?也许你的DNS欺骗失败了,或者除了从远程服务器接收回数据之外,一切都在正常工作?

打开Wireshark并使用过滤器tcp端口80运行它,你看到任何看起来像错误的东西吗?你看到什么了吗?

接下来,你可以代理任何不使用TLS加密的TCP请求。即使我们一直在使用HTTP请求进行测试以简化操作,但请注意,在我们的代码中甚至没有提到HTTP。我们只看到一个通用的tcp传输的字节流,它可以具有任何结构并使用其喜欢的任何应用程序协议。

剩下的,就是使用构建的代理能够处理使用TLS加密的TCP请求。

这是构建通用TCP代理的最后一部分,该代理将能够处理任何基于TCP的协议,而不仅仅是HTTP。

伪造证书颁发机构

如上所述,我们已经完成了可处理未加密协议的基本TCP代理的构建。我们使用此代理来拦截和检查你手机发送的纯文本HTTP请求,并且效果很好。

但是,我们的代理仍然无法处理加密,包括TLS,这是互联网上最常见的加密形式。你手机上任何需要TLS加密连接的应用程序,比如连接到只支持https的网站的移动浏览器,都将拒绝与我们的代理进行业务往来。因此,我们需要向我们的代理展示如何协商TLS连接。

现在,我们将建立一个伪造的证书颁发机构。这将帮助我们诱骗你的手机相信它应该信任我们的代理,并帮助我们的代理与你的手机建立TLS连接。

现在,让我们看看以前所介绍的基本代理请求加密连接时出了什么问题?

错误内容

让我们尝试通过基本代理发送HTTPS请求,将虚拟的DNS服务器中的目标主机名从第2部分更改为google.com。同样,将我们的代理服务器中的主机名也从第3部分更改为google.com,并将其监控和发送的端口设置为443(按照规定,为HTTPS端口)。将你手机的DNS服务器设置为你笔记本电脑的本地IP地址,然后同时启动我们的DNS服务器和TCP代理。

在手机上访问google.com,如上所述使用nonhttps.com执行这个技巧时,会话劫持会成功发生。你的手机浏览器将其对nonhttps.com的未加密请求发送到我们的代理,没此时,代理将此请求转发到nonhttps.com本身。

但是,Google非常明智地坚持通过HTTPS提供服务。当你手机的浏览器发现我们的代理不知道如何协商TLS连接时,它将立即放弃并关闭与它的TCP连接。此时,浏览器将显示一个错误。

理解代理TLS

首先,我们列出需要解决的挑战。我们将从代理的当前状态开始,然后向前回溯。这将帮助我们确切地了解为什么每一步都是必要的,如果我们将其遗漏,将会发生什么。

对SSL进行监控

现在,我们需要对代理进行一些重新编程。

如上所述,我们的代理使用twisted Python网络库的listenTCP方法监听来自手机的TCP连接。twisted还有一个方法叫做listenSSL。listenSSL和listenTCP都监视和等待传入的TCP连接,它们在概念上非常相似。

这些方法的不同之处在于它们与客户端建立TCP连接后如何进行,listenTCP立即开始接受应用程序层数据(例如未加密的HTTP请求)。但是,在listenSSL接受任何应用程序层数据之前,它首先尝试与客户端执行TLS握手。仅在成功完成此握手之后,listenSSL才开始接受(现已加密)应用程序层数据。

为了了解我们的代理TLS,我们将需要使用listenSSL而不是listenTCP。但是,除了在我们的方法调用的末尾添加SSL外。

生成TLS证书

listenSSL为我们处理TLS握手和解密的底层算法机制,但是为了做到这一点,需要向它传递一个表示TLS证书的对象和一个私有密钥作为它的一个参数。我们将会看到,这些对于我们来说很容易创建,但正确率不敢保证。

服务器使用TLS证书来证明其身份,当客户端(例如你的手机)要求服务器(例如我们的代理)执行TLS握手时,服务器首先向客户端提供其TLS证书。你的手机将拒绝与我们的代理进行TLS握手,除非此证书的公用名(证书中的字段)与你认为手机正在与之交谈的主机名匹配。因此,我们将需要能够生成和使用我们自己的TLS证书,将它们的公共名称设置为目标应用程序的主机名(如api.targetapp.com)。

这听起来可能有些奇怪,TLS的全部意义在于,当服务器向客户端提供api.targetapp.com的证书时,客户端可以非常确定它正在与真正的TargetApp对话,而不是某个虚假的恶意中间人。首先,我们不是恶意中间人,但是如果我们能在舒适的家里为api.targetapp.com生成一个证书,那么这对TLS的安全来说肯定不是一个好兆头吗?

参考及来源: