Listener & Stager & Agent

第二篇开始,我们来探究empire中的listener, stager和agent。首先来明确下这三个组件的定位:

listener中文译为监听器,CC要想接收被控端发来的信息或者向其发布命令,必须要与之建立连接,此时会开启一个端口来等待被控端连接。在empire中,我们的http listener其实就是启动了一个flask web应用,通过flask内置的WSGI来作server

stager是empire的最终payload,在RAT程序中,常常会使用payload分离的手段。目的之一是躲避杀毒软件,目的之二是减少投放payload的体积。首先释放一个体积较小的程序,该程序一般会判断环境、检查杀毒软件和系统信息,当判读可以继续执行时,到CC服务器上下载更大体积的恶意程序执行。体积较小的程序通常称为dropper/launcher/downloader,体积较大的程序可以称为stager。此外还有一种payload不分离的情况,称为standalone,此时不会向服务器再请求新的payload,所有功能都在一个文件里,直接进入与CC通信执行命令的阶段

agent在empire中代指被控端,这个没什么需要细说的,等下继续看源码

下面我们按照时间顺序,从建立监听开始,一直到执行命令,一起按照数据流串一遍流程,深入细节揭秘一下empire的主要逻辑

Debug In Docker With Pycharm

为了更清晰的分析整个过程,我们用Pycharm进行调试。为了方便,这里选用了Docker+Remote Debug的方式,使用了empire docker中的python解释器+本地源码,具体设置如下:

设置好之后即可开始调试

在调试的时候遇到的一个坑:使用uselistener http命令发现没有反应,经过debug发现原因在于我之前在本机上试着初始化过empire.db,而在初始化的时候会将安装目录的路径写进数据库,在load_listeners操作的时候又会按照这个路径去取listener文件,从docker container按照这个路径去取自然是取不到的,因此会按照处理不存在的listener的流程继续进行

解决方案也很简单,在程序启动前打个断点,然后开个终端进入docker container,在docker container内重新执行一下setup/setup_database.py即可

Listener

开启Listener

这一部分从建立Listener开始,首先我们建立一个最基础的HTTP Listener

使用set指令为其中的必须项设置具体值,这里需要设定IP和端口。⚠️注意,由于这里的empire是放在docker容器中的,我们的Host地址添了容器宿主机的地址(同时做了端口映射)

(Empire: listeners/http) > set Port 443
(Empire: listeners/http) > set Host 172.16.132.1

之后启动listener

(Empire: listeners/http) > execute
[*] Starting listener 'http'
 * Serving Flask app "http" (lazy loading)
 * Environment: production
   WARNING: Do not use the development server in a production environment.
   Use a production WSGI server instead.
 * Debug mode: off
[+] Listener successfully started!

在这一过程中,调用的是lib/common/empire.py中的do_uselistener方法

def do_uselistener(self, line):
    "Use an Empire listener module."

    parts = line.split(' ')

    if parts[0] not in self.mainMenu.listeners.loadedListeners:
        print
        helpers.color("[!] Error: invalid listener module")
    else:
        listenerMenu = ListenerMenu(self.mainMenu, parts[0])
        listenerMenu.cmdloop()

这里对我们使用的listener进行了查询,当存在对应listener时新建了ListenerMenu实例,进入其命令循环

在设置完Listener的相应值后,使用execute命令其实调用的是:

def do_execute(self, line):
    "Execute the given listener module."
    
    self.mainMenu.listeners.start_listener(self.listenerName, self.listener)

跟进到start_listener函数,位于lib/common/listeners.py

def start_listener(self, moduleName, listenerObject):
        ...

    try:
        print helpers.color("[*] Starting listener '%s'" % (name))
        success = listenerObject.start(name=name)

        if success:
            listenerOptions = copy.deepcopy(listenerObject.options)
            self.activeListeners[name] = {'moduleName': moduleName, 'options':listenerOptions}
            pickledOptions = pickle.dumps(listenerObject.options)
            cur = self.conn.cursor()
            cur.execute("INSERT INTO listeners (name, module, listener_category, enabled, options) VALUES (?,?,?,?,?)", [name, moduleName, category, True, pickledOptions])
            cur.close()

            # dispatch this event
            message = "[+] Listener successfully started!"
            signal = json.dumps({
                'print': True,
                'message': message,
                'listener_options': listenerOptions
            })
            dispatcher.send(signal, sender="listeners/{}/{}".format(moduleName, name))
        else:
            print helpers.color('[!] Listener failed to start!')

    except Exception as e:
        ...

绕来绕去还是最终调用了listener对象内的start()函数,当启动成功时,empire还会保存一份listener对象中配置选项的副本,将其序列化保存到数据库中,这是为了在程序再次启动时能够恢复之前启动的listener。另外一个值得一提的细节,之前提到过empire在界面的展示是通过dispatcher,这里就是一个很好的例子,将signal以json的形式发过去,并决定是否打印出来

下面我们深入到HTTP Listener对象,看一下它到底是什么。我们选择的listener,它的位置在lib/listeners/http.py。其实可以看到这里还有很多其他的listener,我们随后再进行分析,先从基础款开始

这个listener中主要是一个Listener类,都是用统一的模板写的,这样能够做到插件化开发,当我们理解这个构成后应该也可以自定义自己的Listener。其中变量有:

  • self.info 介绍作者和模块信息
  • self.options 关键参数
  • self.mainMenu 主菜单对象
  • ...

一个好玩的事:其中self.options中有一个StagingKey参数

'StagingKey' : {
    'Description'   :   'Staging key for initial agent negotiation.',
    'Required'      :   True,
    'Value'         :   '2c103f2c4ed1e59c0b4e2e01821770fa'
}

Value的值其实是Password123!的md5值,不过里面的设定并不会使用这个默认值,而是在按照empire的时候会在数据库随机生成一个,之后都是通过get_config从数据库里取的

类中主要的方法有:

  • default_response IIS 7.5 404 not found page
  • Index_page HTTP默认页面
  • validate_options 检查是否必须项都设置了内容
  • generate_launcher 生成启动器
  • generate_stager 生成stager相关代码
  • generate_agent 生成agent代码
  • generate_comms 生成通信相关代码
  • start_server 启动服务

start_server函数其实启动了一个Flask Werkzeug服务器,然后上面挂了一个Flask应用,里面定义的路由有:

  • /download/<stager> 用于下载stager
  • / & /index.htm 用于展示首页
  • /welcome.png 用于展示图片
  • /<path:request_uri> 用于真正进行CC与agent之间的通信

建立连接

从代码注释来看,建立连接分为以下几步:

  1. client requests staging code【client - GET】
  2. return stager.ps1 (stage 1)【server - RESPONSE】
  3. client posts public key【client - POST】
  4. server returns RSA(nonce+AESsession)) / server returns HMAC(AESn(nonce+PUBs))【server - RESPONSE】
  5. client posts nonce+sysinfo and requests agent【client - POST】
  6. server sends patched agent.ps1/agent.py【server - RESPONSE】

STAGE0

当agent回连后,首先会发送GET请求到一个任意页面(这里可以在选项中配置,本次发送到的是/new.php页面)

之后会获取agent的IP、listener信息,以及从header中获取cookie。这里决定通信状态的,其实是cookie中的值,当然这些值有被编码和加密过,我们继续看

这里在解析cookie,试图从中获取session的值,并且使用base64解码

之后开始用stagingKey解密routingPacket,这里先不看具体的解密过程,只需要知道向某个函数传入了key和encrypted_info进行解密,解密后的具体信息是这样的

其中一个重要信息,STAGE0,标志着目前建立连接处于哪个阶段,在STAGE0这一阶段,会生成stager并发送给agent,当然生成的stager也是加密和混淆过的,请看lib/listeners/http.py文件中的generate_stager函数

获取配置信息,随机选取两个url作为stage1和stage2的地址。之后打开data/agent/stagers/http.ps1模板文件,替换其中的相关配置信息

之后进行混淆操作,随机变换大小写

这里最终使用了RC4进行加密

此时完成了STAGE0阶段的信息交换

STAGE1

STAGE1阶段是agent向CC发送通信加密用的RSA公钥,处理函数在lib/common/agents.py中,重点看handle_agent_staging函数

这里主要进行的操作是:

  1. 判断信息格式是否完整
  2. 判断agent使用的语言
  3. 获取rsaKey
  4. 向全局变量mainMenu.agents中注册(字典),并且将相关信息添加到数据库。此时如果没有sessionKey,会在入库的时候自动生成一个随机值
  5. 返回加密后的nonce+clientSessionKey

STAGE2

之后进入STAGE2阶段,client再次使用POST请求CC,这次传输的是agent的基本信息

可以看到这里传输了nonce、CC地址、主机名、用户、IP、操作系统等等。获取完之后,会更新数据库,将信息写入。另外还可以看到这里为slack接口预留了逻辑,如果填写了slack api,还会向slack发送通知

还有autoruns功能,用于在上线时自动执行脚本

最终返回值会存入dataResults,具体内容为agent语言和该agent的sessionID

然后进入step 6,返回给agent修改后的agent.ps1或agent.py文件

跟进self.generate_agent函数,进行的具体操作是:

  1. 更新通信相关代码(看了一下,重点是CC服务器地址和通信时的profile)
  2. 去除注释和空行
  3. 更新delay, jitter, lost limit, comms profile的值
  4. 更新killDate, obfuscate选项,如果开启混淆,则将代码替换成混淆后的值

命令获取 & 结果上报

我们以whoami命令为例,跟进下命令发放和结果上报的全过程

将待执行命令写入数据库的操作在lib/common/empire.pyPowerShellAgentMenu类中

不具体展开了,里面有个处理细节是限定了命令队列的最大长度是65535,填满后会覆盖

等到建立连接之后,agent会周期性的向CC发起GET请求,检查是否有需要执行的命令。再次重申下,所有的GET请求中,需要传递的数据都是加密后放在cookie中的

在解析完数据包之后,请求的内容是这样的:

之后会进入lib/common/agents/py文件中的handle_agent_request函数,并完成以下操作:

  1. update_lastsee 更新beacon时间
  2. self.get_agent_tasks_db(sessionID) 从数据库中获取该agent需要执行的命令
  3. 如果有命令需要执行,构建相关packet

构建数据包相关的操作都在lib/common/packets.py里,之后应该还会再分析,最终生成的是这样的一个数据包

+------+--------------------+----------+---------+--------+-----------+
| Type | total # of packets | packet # | task ID | Length | task data |
+------+--------------------+--------------------+--------+-----------+
|  2   |         2          |    2     |    2    |   4    | <Length>  |
+------+--------------------+----------+---------+--------+-----------+

等发放完命令之后,我们在lib/common/agents.py中这里打断点,等待结果上报

agent使用POST请求,将结果发送给CC,详情可以跟一下lib/listeners/http.py中的handle_post方法

跟进self.handle_agent_response

在经过解包后,responsePackets中可以发现命令的执行结果,这里没有什么需要讲的,结果已经传回来了,只需要处理和格式化展示即可

这就是从开启Listener,建立连接,到命令获取与结果上报的全过程。用一张图来展示会更清楚:

Stager

Agent

标签: Empire

添加新评论