Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLV traffic obfuscation with 4-non-null-byte XOR #6480

Merged
merged 9 commits into from Feb 11, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Expand Up @@ -68,6 +68,8 @@ external/source/exploits/**/Release
# Avoid checking in Meterpreter binaries. These are supplied upstream by
# the metasploit-payloads gem.
data/meterpreter/*.dll
data/meterpreter/*.php
data/meterpreter/*.py
data/meterpreter/*.bin
data/meterpreter/*.jar
data/meterpreter/*.lso
Expand Down
38 changes: 38 additions & 0 deletions lib/rex/post/meterpreter/packet.rb
Expand Up @@ -665,6 +665,44 @@ def initialize(type = nil, method = nil)
end
end

#
# Override the function that creates the raw byte stream for
# sending so that it generates an XOR key, uses it to scramble
# the serialized TLV content, and then returns the key plus the
# scrambled data as the payload.
#
def to_r
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mainly a style nitpick, but this could be cleaner:

xor_key = rand(0x100000000)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That doesn't guarantee non-null bytes in all 4 positions.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would something like xor_key = rand(0xffffffff) +1 be better?

Copy link
Contributor Author

@OJ OJ Feb 15, 2016 via email

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is avoiding any single byte good or bad for heuristic detection? Would the chance of the first byte never being an 'R' be more of a tell than it being an 'R' in rare cases?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that as long as not all of the bytes are 0, nothing else really makes a difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I may just be overly paranoid, but I felt like it was a good thing to guarantee that every byte changed. That's really the only reasoning.

raw = super
xor_key = rand(254) + 1
xor_key |= (rand(254) + 1) << 8
xor_key |= (rand(254) + 1) << 16
xor_key |= (rand(254) + 1) << 24
result = [xor_key].pack('N') + xor_bytes(xor_key, raw)
result
end

#
# Override the function that reads from a raw byte stream so
# that the XORing of data is included in the process prior to
# passing it on to the default functionality that can parse
# the TLV values.
#
def from_r(bytes)
xor_key = bytes[0,4].unpack('N')[0]
super(xor_bytes(xor_key, bytes[4, bytes.length]))
end

#
# Xor a set of bytes with a given DWORD xor key.
#
def xor_bytes(xor_key, bytes)
result = ''
bytes.bytes.zip([xor_key].pack('V').bytes.cycle).each do |b|
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems overcomplex, why not something like bytes.unpack("C*").map{}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because I'm stupid? :)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LOL. This is post-merge, I'll PR something back if there is time.

result << (b[0].ord ^ b[1].ord).chr
end
result
end

##
#
# Conditionals
Expand Down
4 changes: 2 additions & 2 deletions lib/rex/post/meterpreter/packet_dispatcher.rb
Expand Up @@ -117,8 +117,7 @@ def on_passive_request(cli, req)

self.last_checkin = Time.now

# If the first 4 bytes are "RECV", return the oldest packet from the outbound queue
if req.body[0,4] == "RECV"
if req.method == 'GET'
rpkt = send_queue.shift
resp.body = rpkt || ''
begin
Expand Down Expand Up @@ -176,6 +175,7 @@ def send_packet(packet, completion_routine = nil, completion_param = nil)
end
end


if bytes.to_i == 0
# Mark the session itself as dead
self.alive = false
Expand Down
20 changes: 14 additions & 6 deletions lib/rex/post/meterpreter/packet_parser.rb
Expand Up @@ -12,6 +12,11 @@ module Meterpreter
###
class PacketParser

# 4 byte xor
# 4 byte length
# 4 byte type
HEADER_SIZE = 12

#
# Initializes the packet parser context with an optional cipher.
#
Expand All @@ -26,14 +31,17 @@ def initialize(cipher = nil)
#
def reset
self.raw = ''
self.hdr_length_left = 8
self.hdr_length_left = HEADER_SIZE
self.payload_length_left = 0
end

#
# Reads data from the wire and parse as much of the packet as possible.
#
def recv(sock)
# Create a typeless packet
packet = Packet.new(0)

if (self.hdr_length_left > 0)
buf = sock.read(self.hdr_length_left)

Expand All @@ -49,7 +57,10 @@ def recv(sock)
# payload length left to the number of bytes
# specified in the length
if (self.hdr_length_left == 0)
self.payload_length_left = raw.unpack("N")[0] - 8
xor_key = raw[0, 4].unpack('N')[0]
length_bytes = packet.xor_bytes(xor_key, raw[4, 4])
# header size doesn't include the xor key, which is always tacked on the front
self.payload_length_left = length_bytes.unpack("N")[0] - (HEADER_SIZE - 4)
end
elsif (self.payload_length_left > 0)
buf = sock.read(self.payload_length_left)
Expand All @@ -67,14 +78,11 @@ def recv(sock)
if ((self.hdr_length_left == 0) &&
(self.payload_length_left == 0))

# Create a typeless packet
packet = Packet.new(0)

# TODO: cipher decryption
if (cipher)
end

# Serialize the packet from the raw buffer
# Deserialize the packet from the raw buffer
packet.from_r(self.raw)

# Reset our state
Expand Down
2 changes: 1 addition & 1 deletion metasploit-framework.gemspec
Expand Up @@ -70,7 +70,7 @@ Gem::Specification.new do |spec|
# are needed when there's no database
spec.add_runtime_dependency 'metasploit-model', '1.0.0'
# Needed for Meterpreter
spec.add_runtime_dependency 'metasploit-payloads', '1.0.23'
spec.add_runtime_dependency 'metasploit-payloads', '1.0.24'
# Needed by msfgui and other rpc components
spec.add_runtime_dependency 'msgpack'
# get list of network interfaces, like eth* from OS.
Expand Down
2 changes: 1 addition & 1 deletion spec/lib/rex/post/meterpreter/packet_parser_spec.rb
Expand Up @@ -20,7 +20,7 @@

it "should initialise with expected defaults" do
expect(parser.send(:raw)).to eq ""
expect(parser.send(:hdr_length_left)).to eq 8
expect(parser.send(:hdr_length_left)).to eq 12
expect(parser.send(:payload_length_left)).to eq 0
end

Expand Down