构建我们的代理
我已经为我们编写了一个使用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 app's 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的安全来说肯定不是一个好兆头吗?
本文翻译自:https://robertheaton.com/2018/08/31/how-to-build-a-tcp-proxy-4/如若转载,请注明原文地址: