首页
论坛
课程
招聘
雪    币: 3745
活跃值: 活跃值 (1265)
能力值: ( LV13,RANK:220 )
在线值:
发帖
回帖
粉丝

[原创]CUCKOO沙箱源码分析 后篇

2020-6-23 08:10 2258

[原创]CUCKOO沙箱源码分析 后篇

2020-6-23 08:10
2258

cuckoo沙箱源码分析后篇

前言

中篇文章分析了host端如何开始分析(在数据库), 包括: 开启虚拟机、上传样本、上传分析模块和分析配置文件、在数据库中记录虚拟机的状态等. 下面用一张图来说明host和client端的数据传输,agent.py代码就按照这个图来进行分析.

 

cuckoo_host_client_interaction

agent中的重要类

# 继承自SimpleHTTPServer.SimpleHTTPRequestHandler, 用作对不同路径的不同函数处理(响应)
class MiniHTTPRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
    server_version = "Cuckoo Agent"

    # 响应GET请求
    def do_GET(self):
        #self.client_address -> 基类中的(host, port)
        request.client_ip, request.client_port = self.client_address
        request.form = {}
        request.files = {}
        request.method = "GET"

        self.httpd.handle(self)

    # 响应POST请求
    def do_POST(self):
        environ = {
            "REQUEST_METHOD": "POST",
            "CONTENT_TYPE": self.headers.get("Content-Type"),
        }
        # host与client之间的数据传输, 格式为multipart/form-data
        # 你可以理解为key:value形式
        form = cgi.FieldStorage(fp=self.rfile,
                                headers=self.headers,
                                environ=environ)

        request.client_ip, request.client_port = self.client_address
        request.form = {}
        request.files = {}
        request.method = "POST"

        # 遍历传输的数据格式(key:value), 将带有filename字段的内容挑选出来, 以便后面写文件.
        # 如: analysis.conf, 分析模块, 样本等,这些数据在传输的时候, 都要带有filename字段
        # 但不同的filename字段对应的值不一样.
        if form.list:
            for key in form.keys():
                value = form[key]
                if value.filename:
                    request.files[key] = value.file
                else:
                    request.form[key] = value.value.decode("utf8")

        self.httpd.handle(self)

class MiniHTTPServer(object):
    def __init__(self):
        self.handler = MiniHTTPRequestHandler

        # Reference back to the server.
        self.handler.httpd = self

        self.routes = {
            "GET": [],
            "POST": [],
        }
    # 运行tcp服务器
    def run(self, host="0.0.0.0", port=8000):
        self.s = SocketServer.TCPServer((host, port), self.handler)
        self.s.allow_reuse_address = True
        self.s.serve_forever()

    # route作为一个装饰器, 修饰下面步骤中函数(@app.route(...))
    # self.routes:
    # ["GET"] --> [(path, function), (path, function)]
    # ["POST"]  --> [(path, function),(path, function)]
    def route(self, path, methods=["GET"]):
        def register(fn):
            for method in methods:
                self.routes[method].append((re.compile(path + "$"), fn))
            return fn
        return register

    # 执行相应的处理函数
    def handle(self, obj):
        if "client_ip" in state and request.client_ip != state["client_ip"]:
            if request.client_ip != "127.0.0.1":
                return
            if obj.path != "/status" or request.method != "POST":
                return

        # obj.command --> post or get
        # obj.path -> GET /path or POST /path
        for route, fn in self.routes[obj.command]:
            if route.match(obj.path):
                ret = fn()
                break
        else:
            ret = json_error(404, message="Route not found")

        ret.init()
        obj.send_response(ret.status_code)
        ret.headers(obj)
        obj.end_headers()

        if isinstance(ret, jsonify):
            obj.wfile.write(ret.json())
        elif isinstance(ret, send_file):
            ret.write(obj.wfile)

    def shutdown(self):
        # BaseServer also features a .shutdown() method, but you can't use
        # that from the same thread as that will deadlock the whole thing.
        self.s._BaseServer__shutdown_request = True

开机自启动

在进行cuckoo沙箱安装的时候, 需要将ageng.py拷贝至Windows的Startup文件夹, 并且修改后缀py为pyw, 实现开机自启动.

获取agent.py基本信息

浏览器输入192.168.56.2:8000, 返回, 获取agent的一些基本信息.

{"message": "Cuckoo Agent!", "version": "0.10", "features": ["execpy", "pinning", "logs", "largefile", "unicodepath"]}

AGENT_VERSION = "0.10"
AGENT_FEATURES = [
    "execpy", "pinning", "logs", "largefile", "unicodepath",
]

# app = MiniHTTPServer()
# 以json格式返回基本信息
@app.route("/")
def get_index():
    return json_success(
        "Cuckoo Agent!", version=AGENT_VERSION, features=AGENT_FEATURES
    )

是否固定ip

个人感觉没啥用, 既然已经能获取基本信息, 说明ip和端口都是正确的, 已经被固定使用了.

@app.route("/pinning")
def do_pinning():
    if "client_ip" in state:
        return json_error(500, "Agent has already been pinned to an IP!")

    state["client_ip"] = request.client_ip
    return json_success("Successfully pinned Agent",
                        client_ip=request.client_ip)

获取环境变量

获取client端环境变量,以便后面后续的一些命令执行.

  • mkdtemp
  • extract
  • store
  • execpy
@app.route("/environ")
def get_environ():
    return json_success("Environment variables", environ=dict(os.environ))

创建临时文件夹

其实ageng.py中存在两个创建临时文件夹的命令: mktemp和mkdtemp. 但二者创建的位置不一样:

  • mkdtemp --> 在%SYSTEMDRIVE%(C:\)下创建一个随机文件夹
  • mktemp --> 在%TEMP%(C:\Users\bill\AppData\Local\Temp)下创建一个随机文件夹
@app.route("/mktemp", methods=["GET", "POST"])
def do_mktemp():
    # 我抓的包中, 没有发现suffix和prefix这两个字段.
    # %TEMP%(C:\\Users\\bill\\AppData\\Local\\Temp)
    suffix = request.form.get("suffix", "")
    prefix = request.form.get("prefix", "tmp")
    dirpath = request.form.get("dirpath")

    try:
        fd, filepath = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dirpath)
    except:
        return json_exception("Error creating temporary file")

    os.close(fd)

    return json_success("Successfully created temporary file",
                        filepath=filepath)

@app.route("/mkdtemp", methods=["GET", "POST"])
def do_mkdtemp():
    # 我抓的包中, 没有发现suffix和prefix这两个字段.
    # %SYSTEMDRIVE%(C:\\)
    suffix = request.form.get("suffix", "")
    prefix = request.form.get("prefix", "tmp")
    dirpath = request.form.get("dirpath")

    try:
        dirpath = tempfile.mkdtemp(suffix=suffix, prefix=prefix, dir=dirpath)
    except:
        return json_exception("Error creating temporary directory")

    return json_success("Successfully created temporary directory",
                        dirpath=dirpath)

上传分析模块

中篇中提到,将分析模块以zip格式压缩,发送给client端. 发送extrac命令, 将分析模块解压到上一步创建的文件夹中.

@app.route("/extract", methods=["POST"])
def do_extract():
    # 上一步创建的随机文件夹C:\\tmppx7scxC:tmppx7scx
    if "dirpath" not in request.form:
        return json_error(400, "No dirpath has been provided")

    if "zipfile" not in request.files:
        return json_error(400, "No zip file has been provided")

    try:
        with zipfile.ZipFile(request.files["zipfile"], "r") as archive:
            archive.extractall(request.form["dirpath"])
    except:
        return json_exception("Error extracting zip file")

    return json_success("Successfully extracted zip file")

上传分析配置文件analysis.conf

执行store命令, 写入analysis.conf

@app.route("/store", methods=["POST"])
def do_store():
    # filepath: C:/tmppx7scx/analysis.conf
    if "filepath" not in request.form:
        return json_error(400, "No filepath has been provided")
    # file: analysis.conf
    if "file" not in request.files:
        return json_error(400, "No file has been provided")

    try:
        with open(request.form["filepath"], "wb") as f:
            shutil.copyfileobj(request.files["file"], f, 10*1024*1024)
    except:
        return json_exception("Error storing file")

    return json_success("Successfully stored file")

analysis.conf内容

[analysis]
category = file
target = /tmp/cuckoo-tmp-pwnmelife/tmpZ3SA0v/maze.exe (host端的样本地址)
package = exe
file_type = PE32 executable (GUI) Intel 80386, for MS Windows
file_name = maze.exe
clock = 20200620T09:28:00
id = 1
terminate_processes = False
options = apk_entry=:,procmemdump=yes,route=none
enforce_timeout = False
timeout = 120
ip = 192.168.56.1
pe_exports = 
port = 2042

上传样本

执行store命令, 写入maze.exe

@app.route("/store", methods=["POST"])
def do_store():
    # filepath: C:\Users\bill\AppData\Local\Temp\maze.exe
    if "filepath" not in request.form:
        return json_error(400, "No filepath has been provided")
    # file: sample.bin
    if "file" not in request.files:
        return json_error(400, "No file has been provided")

    try:
        with open(request.form["filepath"], "wb") as f:
            shutil.copyfileobj(request.files["file"], f, 10*1024*1024)
    except:
        return json_exception("Error storing file")

    return json_success("Successfully stored file")

执行分析脚本

def do_execpy():
    # filepath: C:/tmppx7scx/analyzer.py
    if "filepath" not in request.form:
        return json_error(400, "No Python file has been provided")

    # Execute the command asynchronously? As a shell command?
    # async: yes
    async = "async" in request.form
    # cwd : C:/tmppx7scx
    cwd = request.form.get("cwd")
    stdout = stderr = None

    args = [
        sys.executable,
        request.form["filepath"],
    ]
    # async = yes, 不返回执行结果
    # async = false, 返回执行结果
    try:
        if async:
            subprocess.Popen(args, cwd=cwd)
        else:
            p = subprocess.Popen(args, cwd=cwd,
                                 stdout=subprocess.PIPE,
                                 stderr=subprocess.PIPE)
            stdout, stderr = p.communicate()
    except:
        return json_exception("Error executing command")

    return json_success("Successfully executed command",
                        stdout=stdout, stderr=stderr)

不断获取样本分析状态

当返回complete状态时, host端要关闭虚拟机.

@app.route("/status")
def get_status():
    return json_success("Analysis status",
                        status=state.get("status"),
                        description=state.get("description"))

HWS计划·2020安全精英夏令营来了!我们在华为松山湖欧洲小镇等你

最后于 2020-6-23 08:13 被baolongshou编辑 ,原因:
上传的附件:
最新回复 (5)
雪    币: 3745
活跃值: 活跃值 (1265)
能力值: ( LV13,RANK:220 )
在线值:
发帖
回帖
粉丝
baolongshou 活跃值 2 2020-6-23 08:19
2
0

附件cuckoo.vsdx是Visio 2019画的图, 比较清楚的显示了host与client之间的数据交互.

filename_key_value附件帮助大家理解do_POST函数中挑选filename字段的.

本来还应该上传一个数据包, 但由于论坛附件大小限制,无法上传. 有需要的留一下邮箱,看到的就会发过去的.

不知道为什么,上传的图片附件老是被显示在文章最后.


最后于 2020-6-23 08:21 被baolongshou编辑 ,原因:
雪    币: 12642
活跃值: 活跃值 (203)
能力值: (RANK:648 )
在线值:
发帖
回帖
粉丝
KevinsBobo 活跃值 8 2020-6-23 08:46
3
1
感谢楼主的分享,三篇文章值得一个精华!
雪    币: 3745
活跃值: 活跃值 (1265)
能力值: ( LV13,RANK:220 )
在线值:
发帖
回帖
粉丝
baolongshou 活跃值 2 2020-6-23 09:30
4
0
谢谢坛主鼓励,我会把这一系列更完的。
雪    币: 4774
活跃值: 活跃值 (2520)
能力值: (RANK:65 )
在线值:
发帖
回帖
粉丝
Editor 活跃值 2020-6-23 09:52
5
0
赞!感谢分享!
雪    币: 174
活跃值: 活跃值 (35)
能力值: ( LV2,RANK:15 )
在线值:
发帖
回帖
粉丝
lracker 活跃值 2020-6-23 15:35
6
0
感谢楼主的分享。赞
游客
登录 | 注册 方可回帖
返回