Skip to content

Instantly share code, notes, and snippets.

@wxsBSD
Last active April 3, 2020 01:21
Show Gist options
  • Save wxsBSD/0c6584913bcc5e6da31b to your computer and use it in GitHub Desktop.
Save wxsBSD/0c6584913bcc5e6da31b to your computer and use it in GitHub Desktop.

SSL Profiling in Bro

I wrote profiling applications over SSL recently and this is my attempt at doing so in Bro. I haven't written a Bro script before this one so I'm betting I've got a bunch of things wrong here. The code comes in two parts. The first is the main script which has the core logic. The second part is the "local" script which defines the application profiles you are interested in.

The Main Script

@load base/protocols/conn
@load base/protocols/ssl
@load base/frameworks/notice

module SSLProfiler;

export {
    type profile: record {
        orig: vector of count;
        resp: vector of count;
        name: string;
        orig_idx: count &default=0 &optional;
        resp_idx: count &default=0 &optional;
        skip: bool &default=F &optional;
        orig_match: bool &default=F &optional;
        resp_match: bool &default=F &optional;
    };
    type profiles: vector of profile;
    type profile_table: table[string] of profiles;

    redef enum Notice::Type += {
        SSL_Application_Profile
    };

    # Applications will look the same across given groups of ciphers. For
    # example, a reverse shell with TLS_RSA_WITH_AES_128_CBC_SHA will look the
    # same as a reverse shell with TLS_DH_DSS_WITH_AES_128_CBC_SHA.
    const cipher_groups: table[string] of string = {
        ["TLS_RSA_WITH_RC4_128_SHA"] = "STREAM_SHA",
        ["TLS_RSA_WITH_RC4_128_MD5"] = "STREAM_MD5",
        ["TLS_RSA_WITH_AES_256_CBC_SHA"] = "BLOCK_16_SHA"
    } &redef;

    const ssl_profiles: profile_table = {
    } &redef;
}

redef SSL::disable_analyzer_after_detection = F;
redef record SSL::Info += {
    application_data_count: count &default = 0;
    found_ssl_profile: bool &default = F;
    prof_table: profile_table &default = ssl_profiles;
};

event ssl_encrypted_data(c: connection, is_orig: bool, content_type: count, length: count) {
    c$ssl$application_data_count += 1;

    # Skip the first two records as they are the encrypted handshake, not
    # relevant to our profiling.
    if (!(c ?$ ssl) ||
        !(c$ssl ?$ cipher) ||
        c$ssl$found_ssl_profile ||
        c$ssl$application_data_count <= 2 ||
        c$ssl$cipher !in cipher_groups) {
        return;
    }

    local group: string = cipher_groups[c$ssl$cipher];

    if (group !in c$ssl$prof_table) {
        return;
    }

    local app_profiles: profiles = c$ssl$prof_table[group];

    for (i in app_profiles) {
        local app_profile = app_profiles[i];

        # Skip the profile if we can't possibly match it.
        if (app_profile$skip ||
            (is_orig && app_profile$orig_match) ||
            (!is_orig && app_profile$resp_match)) {
            next;
        }

        local idx: count;
        local vect: vector of count;

        if (is_orig) {
            idx = app_profile$orig_idx;
            vect = app_profile$orig;
        } else {
            idx = app_profile$resp_idx;
            vect = app_profile$resp;
        }

        if (vect[idx] == 0 || vect[idx] == length) {
            if (is_orig) {
                app_profile$orig_idx += 1;
                if (app_profile$orig_idx >= |app_profile$orig|) {
                    app_profile$orig_match = T;
                }
            } else {
                app_profile$resp_idx += 1;
                if (app_profile$resp_idx >= |app_profile$resp|) {
                    app_profile$resp_match = T;
                }
            }

            if (app_profile$orig_match && app_profile$resp_match) {
                NOTICE([$note=SSL_Application_Profile,
                        $msg=fmt("Possible SSL application profile: %s",
                                 app_profile$name),
                        $conn=c]);
                c$ssl$found_ssl_profile = T;
            }
        } else {
            app_profile$skip = T;
        }
    }
}

Data Structures and Types

The export section is used to define the various data structures in use. The most basic of which is called profile, which is a Bro record (think of it like a struct in C). The profile consists of three required parts: orig, resp, and name. The orig and resp members are vectors of type count. These are where you can put in the sizes of application records you are interested in for your application profile. The name is just a string which you can fill in to make notices more useful. The other parts of the record are not required to be filled in, and are used for internal state tracking.

The next two types are straight-forward: profiles are a vector of profile records and profile_table maps a given string to profiles.

All of the data structures mentioned above are used in the ssl_profiles table, which is of type profile_table. This is the main data structure used throughout the event handler.

The last table is cipher_groups which maps the cipher string to a shortened version. This table is used as a way to group cipher suites.

The Event Handler

The ssl_encrypted_data event is called once per encrypted message (in TLS terms it is an Application Data Record). The first message sent after a Change Cipher Spec message must be an Encrypted Handshake message. Because this is not relevant to our profiling needs we can simply discard it. This is why we track the application_data_count and only handle the event if it is > 2.

Once we have determined that the application_data_count has passed the Encrypted Handshake messages we must look up the negotiated cipher in the cipher_groups table. This is the table which maps the cipher suite (TLS_RSA_WITH_AES_256_CBC_SHA) to a generic grouping (BLOCK_16_SHA). This step is useful because applications encrypted with different cipher suites can have the same profiles if the cipher suite is similar enough. A good example of this is an application encrypted TLS_RSA_WITH_AES_256_CBC_SHA will look the same as TLS_DH_WITH_AES_256_CBC_SHA because they are the same symmetric cipher, just the key exchange is different. This allows us to group similar profiles easily.

With the grouping key we can look up the profiles from the ssl_profiles table. This gives us a vector of profile objects (the records discussed before) which we can iterate over to determine if we have a matching profile. To determine a matching profile we use orig_idx and resp_idx as indices into the orig and resp vectors, respectively. If a value in the vector is 0 or matches the length of the current application data record then corresponding index is incremented. The special value of 0 is used to indicate that the length of the application record in that spot is not relevant to the profile. If the end of the vector in a particular direction is reached then that direction is "matched". When both vectors are matched then the entire profile is matched and a NOTICE is raised.

If at any point the length of the current application data record does not match the current index into the appropriate array the entire profile is marked as a failure and skipped in the future.

The Local Script

@load ./ssl-profiling.bro

const reverse_shell_stream_sha = SSLProfiler::profile(
    $orig = vector(148),
    $resp = vector(34),
    $name = "Reverse Shell (STREAM SHA)"
);

const reverse_shell_block_16_cbc_sha = SSLProfiler::profile(
    $orig = vector(32, 160),
    $resp = vector(32, 48),
    $name = "Reverse Shell (BLOCK 16 SHA)"
);

const reverse_shell_stream_md5 = SSLProfiler::profile(
    $orig = vector(144),
    $resp = vector(30),
    $name = "Reverse Shell (STREAM MD5)"
);

redef SSLProfiler::ssl_profiles: SSLProfiler::profile_table = {
    ["STREAM_SHA"] = vector(reverse_shell_stream_sha),
    ["STREAM_MD5"] = vector(reverse_shell_stream_md5),
    ["BLOCK_16_SHA"] = vector(reverse_shell_block_16_cbc_sha)
};

The above illustrates profiles for a reverse shell with a typical banner as discussed in my earlier post, followed by executing "ipconfig /all" within three different cipher suites.

Execution

Here is my execution of the ssl-profiling-local.bro script on a PCAP which contains two reverse shells, each with a different cipher suite.

wxs@psh bro % ls
reverseshell-both.pcap   ssl-profiling-local.bro  ssl-profiling.bro
wxs@psh bro % bro -b -r reverseshell-both.pcap ssl-profiling-local.bro
wxs@psh bro % ls -l
total 768
-rw-r--r--  1 wxs  staff     788 Jun 30 13:46 conn.log
-rw-r--r--  1 wxs  staff     965 Jun 30 13:46 files.log
-rw-r--r--  1 wxs  staff    1124 Jun 30 13:46 notice.log
-rw-r--r--  1 wxs  staff  360558 Jun 29 11:07 reverseshell-both.pcap
-rw-------  1 wxs  staff     729 Jun 30 13:23 ssl-profiling-local.bro
-rw-------  1 wxs  staff    3901 Jun 30 13:23 ssl-profiling.bro
-rw-r--r--  1 wxs  staff    1093 Jun 30 13:46 ssl.log
-rw-r--r--  1 wxs  staff    1244 Jun 30 13:46 x509.log
wxs@psh bro % cat notice.log
#separator \x09
#set_separator	,
#empty_field	(empty)
#unset_field	-
#path	notice
#open	2015-06-30-13-46-13
#fields	ts	uid	id.orig_h	id.orig_p	id.resp_h	id.resp_p	fuid	file_mime_type	file_desc	proto	note	msg	sub	src	dst	p	n	peer_descr	actions	suppress_for	droppedremote_location.country_code	remote_location.region	remote_location.city	remote_location.latitude	remote_location.longitude
#types	time	string	addr	port	addr	port	string	string	string	enum	enum	string	string	addr	addr	port	count	string	set[enum]	interval	bool	string	string	string	double	double
1434735285.374838	CFvgM51TNJJtpi6dj9	192.168.1.214	65411	192.168.1.209	9999	-	-	-	tcp	SSLProfiler::SSL_Application_Profile	Possible SSL application profile: Reverse Shell (STREAM SHA)	-	192.168.1.214	192.168.1.209	9999	-	bro	Notice::ACTION_LOG	3600.000000	F	-	-	-	-	-
1434744729.751189	CgmPbH3O1D42wtt4xh	192.168.1.214	49533	192.168.1.209	9999	-	-	-	tcp	SSLProfiler::SSL_Application_Profile	Possible SSL application profile: Reverse Shell (BLOCK 16 SHA)	-	192.168.1.214	192.168.1.209	9999	-	bro	Notice::ACTION_LOG	3600.000000	F	-	-	-	-	-
#close	2015-06-30-13-46-13
wxs@psh bro % 

Future Improvements

In some cases it is reasonable to express a range for a length. Currently you have to know the exact length of a given application data record, which is rather limiting. For example, in the case of a reverse shell the prompt may not always be C:\windows\system32 >, which would mean our profile for stream ciphers would be incorrect, and depending upon the length of the path may be incorrect for block ciphers. Changing the code to use a vector for each length element would be one way to solve this. The first element in the vector would be a minimum length and the second could be a maximum. In python this would look something like:

orig = [(150, 160)]

There are also other things that can be done to make this nicer, like being able to specify the negotiated TLS version or automatically handling block ciphers in CBC mode in TLS1.2 where the first block is discarded, thereby inflating otherwise similar profiles.

So What?

If you like this idea and want to contribute PCAPs of applications inside SSL so they can be profiled please get in touch with me. In particular I'm interested in malicious protocols and detecting those using this technique.

@load ./ssl-profiling.bro
const reverse_shell_stream_sha = SSLProfiler::profile(
$orig = vector(148),
$resp = vector(34),
$name = "Reverse Shell (STREAM SHA)"
);
const reverse_shell_block_16_cbc_sha = SSLProfiler::profile(
$orig = vector(32, 160),
$resp = vector(32, 48),
$name = "Reverse Shell (BLOCK 16 SHA)"
);
const reverse_shell_stream_md5 = SSLProfiler::profile(
$orig = vector(144),
$resp = vector(30),
$name = "Reverse Shell (STREAM MD5)"
);
redef SSLProfiler::ssl_profiles: SSLProfiler::profile_table = {
["STREAM_SHA"] = vector(reverse_shell_stream_sha),
["STREAM_MD5"] = vector(reverse_shell_stream_md5),
["BLOCK_16_SHA"] = vector(reverse_shell_block_16_cbc_sha)
};
@load base/protocols/conn
@load base/protocols/ssl
@load base/frameworks/notice
module SSLProfiler;
export {
type profile: record {
orig: vector of count;
resp: vector of count;
name: string;
orig_idx: count &default=0 &optional;
resp_idx: count &default=0 &optional;
skip: bool &default=F &optional;
orig_match: bool &default=F &optional;
resp_match: bool &default=F &optional;
};
type profiles: vector of profile;
type profile_table: table[string] of profiles;
redef enum Notice::Type += {
SSL_Application_Profile
};
# Applications will look the same across given groups of ciphers. For
# example, a reverse shell with TLS_RSA_WITH_AES_128_CBC_SHA will look the
# same as a reverse shell with TLS_DH_DSS_WITH_AES_128_CBC_SHA.
const cipher_groups: table[string] of string = {
["TLS_RSA_WITH_RC4_128_SHA"] = "STREAM_SHA",
["TLS_RSA_WITH_RC4_128_MD5"] = "STREAM_MD5",
["TLS_RSA_WITH_AES_256_CBC_SHA"] = "BLOCK_16_SHA"
} &redef;
const ssl_profiles: profile_table = {
} &redef;
}
redef SSL::disable_analyzer_after_detection = F;
redef record SSL::Info += {
application_data_count: count &default = 0;
found_ssl_profile: bool &default = F;
prof_table: profile_table &default = ssl_profiles;
};
event ssl_encrypted_data(c: connection, is_orig: bool, content_type: count, length: count) {
c$ssl$application_data_count += 1;
#print fmt("Orig: %s Orig pkts: %s Resp pkts: %s CT: %s Length: %s", is_orig, c$orig$num_pkts, c$resp$num_pkts, content_type, length);
# Skip the first two records as they are the encrypted handshake, not
# relevant to our profiling.
if (!(c ?$ ssl) ||
!(c$ssl ?$ cipher) ||
c$ssl$found_ssl_profile ||
c$ssl$application_data_count <= 2 ||
c$ssl$cipher !in cipher_groups) {
return;
}
local group: string = cipher_groups[c$ssl$cipher];
if (group !in c$ssl$prof_table) {
return;
}
local app_profiles: profiles = c$ssl$prof_table[group];
for (i in app_profiles) {
local app_profile = app_profiles[i];
# Skip the profile if we can't possibly match it.
if (app_profile$skip ||
(is_orig && app_profile$orig_match) ||
(!is_orig && app_profile$resp_match)) {
next;
}
local idx: count;
local vect: vector of count;
if (is_orig) {
idx = app_profile$orig_idx;
vect = app_profile$orig;
} else {
idx = app_profile$resp_idx;
vect = app_profile$resp;
}
if (vect[idx] == 0 || vect[idx] == length) {
if (is_orig) {
app_profile$orig_idx += 1;
if (app_profile$orig_idx >= |app_profile$orig|) {
app_profile$orig_match = T;
}
} else {
app_profile$resp_idx += 1;
if (app_profile$resp_idx >= |app_profile$resp|) {
app_profile$resp_match = T;
}
}
if (app_profile$orig_match && app_profile$resp_match) {
NOTICE([$note=SSL_Application_Profile,
$msg=fmt("Possible SSL application profile: %s",
app_profile$name),
$conn=c]);
c$ssl$found_ssl_profile = T;
}
} else {
app_profile$skip = T;
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment