Empire/lib/common/packets.py

352 lines
12 KiB
Python

"""
Packet handling functionality for Empire.
Defines packet types, builds tasking packets and parses result packets.
Packet format:
RC4s = RC4 encrypted with the shared staging key
HMACs = SHA1 HMAC using the shared staging key
AESc = AES encrypted using the client's session key
HMACc = first 10 bytes of a SHA256 HMAC using the client's session key
Routing Packet:
+---------+-------------------+--------------------------+
| RC4 IV | RC4s(RoutingData) | AESc(client packet data) | ...
+---------+-------------------+--------------------------+
| 4 | 16 | RC4 length |
+---------+-------------------+--------------------------+
RC4s(RoutingData):
+-----------+------+------+-------+--------+
| SessionID | Lang | Meta | Extra | Length |
+-----------+------+------+-------+--------+
| 8 | 1 | 1 | 2 | 4 |
+-----------+------+------+-------+--------+
SessionID = the sessionID that the packet is bound for
Lang = indicates the language used
Meta = indicates staging req/tasking req/result post/etc.
Extra = reserved for future expansion
AESc(client data)
+--------+-----------------+-------+
| AES IV | Enc Packet Data | HMACc |
+--------+-----------------+-------+
| 16 | % 16 bytes | 10 |
+--------+-----------------+-------+
Client data decrypted:
+------+--------+--------------------+----------+---------+-----------+
| Type | Length | total # of packets | packet # | task ID | task data |
+------+--------+--------------------+--------------------+-----------+
| 2 | 4 | 2 | 2 | 2 | <Length> |
+------+--------+--------------------+----------+---------+-----------+
type = packet type
total # of packets = number of total packets in the transmission
Packet # = where the packet fits in the transmission
Task ID = links the tasking to results for deconflict on server side
Client *_SAVE packets have the sub format:
[15 chars] - save prefix
[5 chars] - extension
[X...] - tasking data
"""
import struct
import base64
import os
import hashlib
import hmac
from pydispatch import dispatcher
# Empire imports
import encryption
# 0 -> error
# 1-99 -> standard functionality
# 100-199 -> dynamic functionality
# 200-299 -> SMB functionality
PACKET_NAMES = {
"ERROR" : 0,
"TASK_SYSINFO" : 1,
"TASK_EXIT" : 2,
"TASK_SET_DELAY" : 10,
"TASK_GET_DELAY" : 12,
"TASK_SET_SERVERS" : 13,
"TASK_ADD_SERVERS" : 14,
"TASK_UPDATE_PROFILE" : 20,
"TASK_SET_KILLDATE" : 30,
"TASK_GET_KILLDATE" : 31,
"TASK_SET_WORKING_HOURS" : 32,
"TASK_GET_WORKING_HOURS" : 33,
"TASK_SHELL" : 40,
"TASK_DOWNLOAD" : 41,
"TASK_UPLOAD" : 42,
"TASK_GETJOBS" : 50,
"TASK_STOPJOB" : 51,
"TASK_GETDOWNLOADS" : 52,
"TASK_STOPDOWNLOAD" : 53,
"TASK_CMD_WAIT" : 100,
"TASK_CMD_WAIT_SAVE" : 101,
"TASK_CMD_JOB" : 110,
"TASK_CMD_JOB_SAVE" : 111,
"TASK_SCRIPT_IMPORT" : 120,
"TASK_SCRIPT_COMMAND" : 121,
"TASK_IMPORT_MODULE" : 122,
"TASK_VIEW_MODULE" : 123,
"TASK_REMOVE_MODULE" : 124,
"TASK_SWITCH_LISTENER" : 130
}
# build a lookup table for IDS
PACKET_IDS = {}
for name, ID in PACKET_NAMES.items(): PACKET_IDS[ID] = name
LANGUAGE = {
'NONE' : 0,
'POWERSHELL' : 1,
'PYTHON' : 2
}
LANGUAGE_IDS = {}
for name, ID in LANGUAGE.items(): LANGUAGE_IDS[ID] = name
META = {
'NONE' : 0,
'STAGE0' : 1,
'STAGE1' : 2,
'STAGE2' : 3,
'TASKING_REQUEST' : 4,
'RESULT_POST' : 5,
'SERVER_RESPONSE' : 6
}
META_IDS = {}
for name, ID in META.items(): META_IDS[ID] = name
ADDITIONAL = {}
ADDITIONAL_IDS = {}
for name, ID in ADDITIONAL.items(): ADDITIONAL_IDS[ID] = name
def build_task_packet(taskName, data, resultID):
"""
Build a task packet for an agent.
[2 bytes] - type
[2 bytes] - total # of packets
[2 bytes] - packet #
[2 bytes] - task/result ID
[4 bytes] - length
[X...] - result data
+------+--------------------+----------+---------+--------+-----------+
| Type | total # of packets | packet # | task ID | Length | task data |
+------+--------------------+--------------------+--------+-----------+
| 2 | 2 | 2 | 2 | 4 | <Length> |
+------+--------------------+----------+---------+--------+-----------+
"""
taskType = struct.pack('=H', PACKET_NAMES[taskName])
totalPacket = struct.pack('=H', 1)
packetNum = struct.pack('=H', 1)
resultID = struct.pack('=H', resultID)
length = struct.pack('=L',len(data))
return taskType + totalPacket + packetNum + resultID + length + data.decode('utf-8').encode('utf-8',errors='ignore')
def parse_result_packet(packet, offset=0):
"""
Parse a result packet-
[2 bytes] - type
[2 bytes] - total # of packets
[2 bytes] - packet #
[2 bytes] - task/result ID
[4 bytes] - length
[X...] - result data
+------+--------------------+----------+---------+--------+-----------+
| Type | total # of packets | packet # | task ID | Length | task data |
+------+--------------------+--------------------+--------+-----------+
| 2 | 2 | 2 | 2 | 4 | <Length> |
+------+--------------------+----------+---------+--------+-----------+
Returns a tuple with (responseName, length, data, remainingData)
Returns a tuple with (responseName, totalPackets, packetNum, taskID, length, data, remainingData)
"""
try:
responseID = struct.unpack('=H', packet[0+offset:2+offset])[0]
totalPacket = struct.unpack('=H', packet[2+offset:4+offset])[0]
packetNum = struct.unpack('=H', packet[4+offset:6+offset])[0]
taskID = struct.unpack('=H', packet[6+offset:8+offset])[0]
length = struct.unpack('=L', packet[8+offset:12+offset])[0]
if length != '0':
data = base64.b64decode(packet[12+offset:12+offset+length])
else:
data = None
remainingData = packet[12+offset+length:]
return (PACKET_IDS[responseID], totalPacket, packetNum, taskID, length, data, remainingData)
except Exception as e:
dispatcher.send("[*] parse_result_packet(): exception: %s" % (e), sender='Packets')
return (None, None, None, None, None, None, None)
def parse_result_packets(packets):
"""
Parse a blob of one or more result packets
"""
resultPackets = []
# parse the first result packet
(responseName, totalPacket, packetNum, taskID, length, data, remainingData) = parse_result_packet(packets)
if responseName and responseName != '':
resultPackets.append( (responseName, totalPacket, packetNum, taskID, length, data) )
# iterate 12 (size of packet header) + length of the decoded
offset = 12 + length
while remainingData and remainingData != '':
# parse any additional result packets
# (responseName, length, data, remainingData) = parse_result_packet(packets, offset=offset)
(responseName, totalPacket, packetNum, taskID, length, data, remainingData) = parse_result_packet(packets, offset=offset)
if responseName and responseName != '':
resultPackets.append( (responseName, totalPacket, packetNum, taskID, length, data) )
offset += 12 + length
return resultPackets
def parse_routing_packet(stagingKey, data):
"""
Decodes the rc4 "routing packet" and parses raw agent data into:
{sessionID : (language, meta, additional, [encData]), ...}
Routing packet format:
+---------+-------------------+--------------------------+
| RC4 IV | RC4s(RoutingData) | AESc(client packet data) | ...
+---------+-------------------+--------------------------+
| 4 | 16 | RC4 length |
+---------+-------------------+--------------------------+
RC4s(RoutingData):
+-----------+------+------+-------+--------+
| SessionID | Lang | Meta | Extra | Length |
+-----------+------+------+-------+--------+
| 8 | 1 | 1 | 2 | 4 |
+-----------+------+------+-------+--------+
"""
if data:
results = {}
offset = 0
# ensure we have at least the 20 bytes for a routing packet
if len(data) >= 20:
while True:
if len(data) - offset < 20:
break
RC4IV = data[0+offset:4+offset]
RC4data = data[4+offset:20+offset]
routingPacket = encryption.rc4(RC4IV+stagingKey, RC4data)
sessionID = routingPacket[0:8]
# B == 1 byte unsigned char, H == 2 byte unsigned short, L == 4 byte unsigned long
(language, meta, additional, length) = struct.unpack("=BBHL", routingPacket[8:])
if length < 0:
dispatcher.send('[*] parse_agent_data(): length in decoded rc4 packet is < 0', sender='Packets')
encData = None
else:
encData = data[(20+offset):(20+offset+length)]
results[sessionID] = (LANGUAGE_IDS.get(language, 'NONE'), META_IDS.get(meta, 'NONE'), ADDITIONAL_IDS.get(additional, 'NONE'), encData)
# check if we're at the end of the packet processing
remainingData = data[20+offset+length:]
if not remainingData or remainingData == '':
break
offset += 20 + length
return results
else:
dispatcher.send("[*] parse_agent_data() data length incorrect: %s" % (len(data)), sender='Packets')
return None
else:
dispatcher.send("[*] parse_agent_data() data is None", sender='Packets')
return None
def build_routing_packet(stagingKey, sessionID, language, meta="NONE", additional="NONE", encData=''):
"""
Takes the specified parameters for an RC4 "routing packet" and builds/returns
an HMAC'ed RC4 "routing packet".
packet format:
Routing Packet:
+---------+-------------------+--------------------------+
| RC4 IV | RC4s(RoutingData) | AESc(client packet data) | ...
+---------+-------------------+--------------------------+
| 4 | 16 | RC4 length |
+---------+-------------------+--------------------------+
RC4s(RoutingData):
+-----------+------+------+-------+--------+
| SessionID | Lang | Meta | Extra | Length |
+-----------+------+------+-------+--------+
| 8 | 1 | 1 | 2 | 4 |
+-----------+------+------+-------+--------+
"""
# binary pack all of the passed config values as unsigned numbers
# B == 1 byte unsigned char, H == 2 byte unsigned short, L == 4 byte unsigned long
data = sessionID + struct.pack("=BBHL", LANGUAGE.get(language.upper(), 0), META.get(meta.upper(), 0), ADDITIONAL.get(additional.upper(), 0), len(encData))
RC4IV = os.urandom(4)
stagingKey = str(stagingKey)
key = RC4IV + stagingKey
rc4EncData = encryption.rc4(key, data)
# return an rc4 encyption of the routing packet, append an HMAC of the packet, then the actual encrypted data
packet = RC4IV + rc4EncData + encData
return packet
def resolve_id(PacketID):
"""
Resolve a packet ID to its key.
"""
try:
return PACKET_IDS[int(PacketID)]
except:
return PACKET_IDS[0]