Updates for the SOCKS analyzer.

- Now supports SOCKSv5 in the analyzer and the DPD sigs.

- Reworked the core events.

- Tests.

- A SOCKS log!
This commit is contained in:
Seth Hall 2012-06-20 13:58:25 -04:00
parent c30c0d5ff2
commit 896f252a31
16 changed files with 411 additions and 47 deletions

View file

@ -162,33 +162,48 @@ signature dpd_teredo {
enable "teredo" enable "teredo"
} }
signature dpd_socks_client { signature dpd_socks4_client {
ip-proto == tcp ip-proto == tcp
# '32' is a rather arbitrary max length for the user name. # '32' is a rather arbitrary max length for the user name.
payload /^\x04[\x01\x02].{0,32}\x00/ payload /^\x04[\x01\x02].{0,32}\x00/
tcp-state originator tcp-state originator
} }
signature dpd_socks_server { signature dpd_socks4_server {
ip-proto == tcp ip-proto == tcp
requires-reverse-signature dpd_socks_client requires-reverse-signature dpd_socks4_client
payload /^\x00[\x5a\x5b\x5c\x5d]/ payload /^\x00[\x5a\x5b\x5c\x5d]/
tcp-state responder tcp-state responder
enable "socks" enable "socks"
} }
signature dpd_socks_reverse_client { signature dpd_socks4_reverse_client {
ip-proto == tcp ip-proto == tcp
# '32' is a rather arbitrary max length for the user name. # '32' is a rather arbitrary max length for the user name.
payload /^\x04[\x01\x02].{0,32}\x00/ payload /^\x04[\x01\x02].{0,32}\x00/
tcp-state responder tcp-state responder
} }
signature dpd_socks_reverse_server { signature dpd_socks4_reverse_server {
ip-proto == tcp ip-proto == tcp
requires-reverse-signature dpd_socks_client requires-reverse-signature dpd_socks4_reverse_client
payload /^\x00[\x5a\x5b\x5c\x5d]/ payload /^\x00[\x5a\x5b\x5c\x5d]/
tcp-state originator tcp-state originator
enable "socks" enable "socks"
} }
signature dpd_socks5_client {
ip-proto == tcp
payload /^\x05/
tcp-state originator
}
signature dpd_socks5_server {
ip-proto == tcp
requires-reverse-signature dpd_socks5_client
payload /^\x05/
tcp-state responder
enable "socks"
}

View file

@ -1 +1,2 @@
@load ./consts
@load ./main @load ./main

View file

@ -0,0 +1,41 @@
module SOCKS;
export {
type RequestType: enum {
CONNECTION = 1,
PORT = 2,
};
const v5_authentication_methods: table[count] of string = {
[0] = "No Authentication Required",
[1] = "GSSAPI",
[2] = "Username/Password",
[3] = "Challenge-Handshake Authentication Protocol",
[4] = "Unassigned",
[5] = "Challenge-Response Authentication Method",
[6] = "Secure Sockets Layer",
[7] = "NDS Authentication",
[8] = "Multi-Authentication Framework",
[255] = "No Acceptable Methods",
} &default=function(i: count):string { return fmt("unknown-%d", i); };
const v4_status: table[count] of string = {
[0x5a] = "succeeded",
[0x5b] = "general SOCKS server failure",
[0x5c] = "request failed because client is not running identd",
[0x5d] = "request failed because client's identd could not confirm the user ID string in the request",
} &default=function(i: count):string { return fmt("unknown-%d", i); };
const v5_status: table[count] of string = {
[0] = "succeeded",
[1] = "general SOCKS server failure",
[2] = "connection not allowed by ruleset",
[3] = "Network unreachable",
[4] = "Host unreachable",
[5] = "Connection refused",
[6] = "TTL expired",
[7] = "Command not supported",
[8] = "Address type not supported",
} &default=function(i: count):string { return fmt("unknown-%d", i); };
}

View file

@ -1,15 +1,98 @@
@load base/frameworks/tunnels @load base/frameworks/tunnels
@load ./consts
module SOCKS; module SOCKS;
export { export {
type RequestType: enum { redef enum Log::ID += { LOG };
CONNECTION = 1,
PORT = 2, type Info: record {
## Time when the proxy connection was first detected.
ts: time &log;
uid: string &log;
id: conn_id &log;
## Protocol version of SOCKS.
version: count &log;
## Username for the proxy if extracted from the network.
user: string &log &optional;
## Server status for the attempt at using the proxy.
status: string &log &optional;
## Client requested address. Mutually exclusive with req_name.
req_h: addr &log &optional;
## Client requested domain name. Mutually exclusive with req_h.
req_name: string &log &optional;
## Client requested port.
req_p: port &log &optional;
## Server bound address. Mutually exclusive with bound_name.
bound_h: addr &log &optional;
## Server bound domain name. Mutually exclusive with bound_h.
bound_name: string &log &optional;
## Server bound port.
bound_p: port &log &optional;
}; };
## Event that can be handled to access the SOCKS
## record as it is sent on to the logging framework.
global log_socks: event(rec: Info);
} }
event socks_request(c: connection, request_type: count, dstaddr: addr, dstname: string, p: port, user: string) event bro_init() &priority=5
{ {
Tunnel::register([$cid=c$id, $tunnel_type=Tunnel::SOCKS, $uid=c$uid]); Log::create_stream(SOCKS::LOG, [$columns=Info, $ev=log_socks]);
} }
redef record connection += {
socks: SOCKS::Info &optional;
};
# Configure DPD
redef capture_filters += { ["socks"] = "tcp port 1080" };
redef dpd_config += { [ANALYZER_SOCKS] = [$ports = set(1080/tcp)] };
redef likely_server_ports += { 1080/tcp };
function set_session(c: connection, version: count)
{
if ( ! c?$socks )
c$socks = [$ts=network_time(), $id=c$id, $uid=c$uid, $version=version];
}
event socks_request(c: connection, version: count, request_type: count,
dstaddr: addr, dstname: string, p: port, user: string) &priority=5
{
set_session(c, version);
if ( dstaddr != [::] )
c$socks$req_h = dstaddr;
if ( dstname != "" )
c$socks$req_name = dstname;
c$socks$req_p = p;
# Copy this conn_id and set the orig_p to zero because in the case of SOCKS proxies there will
# be potentially many source ports since a new proxy connection is established for each
# proxied connection. We treat this as a singular "tunnel".
local cid = copy(c$id);
cid$orig_p = 0/tcp;
Tunnel::register([$cid=cid, $tunnel_type=Tunnel::SOCKS, $payload_proxy=T]);
}
event socks_reply(c: connection, version: count, reply: count, dstaddr: addr, dstname: string, p: port) &priority=5
{
set_session(c, version);
if ( version == 5 )
c$socks$status = v5_status[reply];
else if ( version == 4 )
c$socks$status = v4_status[reply];
if ( dstaddr != [::] )
c$socks$bound_h = dstaddr;
if ( dstname != "" )
c$socks$bound_name = dstname;
c$socks$bound_p = p;
}
event socks_reply(c: connection, version: count, reply: count, dstaddr: addr, dstname: string, p: port) &priority=-5
{
Log::write(SOCKS::LOG, c$socks);
}

View file

@ -6101,7 +6101,9 @@ event signature_match%(state: signature_state, msg: string, data: string%);
## ##
## c: The parent connection of the proxy. ## c: The parent connection of the proxy.
## ##
## t: The type of the request. ## version: The version of SOCKS this message used.
##
## request_type: The type of the request.
## ##
## dstaddr: Address that the tunneled traffic should be sent to. ## dstaddr: Address that the tunneled traffic should be sent to.
## ##
@ -6109,13 +6111,23 @@ event signature_match%(state: signature_state, msg: string, data: string%);
## ##
## p: The destination port for the proxied traffic. ## p: The destination port for the proxied traffic.
## ##
## user: Username given for the SOCKS connection. ## user: Username given for the SOCKS connection. This is not yet implemented for SOCKSv5.
event socks_request%(c: connection, request_type: count, dstaddr: addr, dstname: string, p: port, user: string%); event socks_request%(c: connection, version: count, request_type: count, dstaddr: addr, dstname: string, p: port, user: string%);
## Generated when a SOCKS reply is analyzed. ## Generated when a SOCKS reply is analyzed.
## ##
## c: The parent connection of the proxy.
## ##
event socks_reply%(c: connection, granted: bool, dst: addr, p: port%); ## version: The version of SOCKS this message used.
##
## reply: The status reply from the server.
##
## dstaddr: The address that the server sent the traffic to.
##
## dstname: The name the server sent the traffic to. Only applicable for SOCKSv5.
##
## p: The destination port for the proxied traffic.
event socks_reply%(c: connection, version: count, reply: count, dstaddr: addr, dstname: string, p: port%);
## Generated when a protocol analyzer finds an identification of a software ## Generated when a protocol analyzer finds an identification of a software
## used on a system. This is a protocol-independent event that is fed by ## used on a system. This is a protocol-independent event that is fed by

View file

@ -19,39 +19,127 @@ StringVal* array_to_string(vector<uint8> *a)
%} %}
refine connection SOCKS_Conn += { refine connection SOCKS_Conn += {
function socks_request(cmd: uint8, dstaddr: uint32, dstname: uint8[], p: uint16, user: uint8[]): bool
function socks4_request(request: SOCKS4_Request): bool
%{ %{
StringVal *dstname;
if ( ${request.v4a} )
dstname = array_to_string(${request.name});
BifEvent::generate_socks_request(bro_analyzer(), BifEvent::generate_socks_request(bro_analyzer(),
bro_analyzer()->Conn(), bro_analyzer()->Conn(),
cmd, 4,
new AddrVal(htonl(dstaddr)), ${request.command},
array_to_string(dstname), new AddrVal(htonl(${request.addr})),
new PortVal(p | TCP_PORT_MASK), dstname,
array_to_string(user)); new PortVal(${request.port} | TCP_PORT_MASK),
array_to_string(${request.user}));
static_cast<SOCKS_Analyzer*>(bro_analyzer())->EndpointDone(true); static_cast<SOCKS_Analyzer*>(bro_analyzer())->EndpointDone(true);
return true; return true;
%} %}
function socks_reply(granted: bool, dst: uint32, p: uint16): bool function socks4_reply(reply: SOCKS4_Reply): bool
%{ %{
BifEvent::generate_socks_reply(bro_analyzer(), BifEvent::generate_socks_reply(bro_analyzer(),
bro_analyzer()->Conn(), bro_analyzer()->Conn(),
granted, 4,
new AddrVal(htonl(dst)), ${reply.status},
new PortVal(p | TCP_PORT_MASK)); new AddrVal(htonl(${reply.addr})),
new StringVal(""),
new PortVal(${reply.port} | TCP_PORT_MASK));
bro_analyzer()->ProtocolConfirmation(); bro_analyzer()->ProtocolConfirmation();
static_cast<SOCKS_Analyzer*>(bro_analyzer())->EndpointDone(false); static_cast<SOCKS_Analyzer*>(bro_analyzer())->EndpointDone(false);
return true; return true;
%} %}
function socks5_request(request: SOCKS5_Request): bool
%{
AddrVal *ip_addr = 0;
StringVal *domain_name = 0;
// This is dumb and there must be a better way (checking for presence of a field)...
switch ( ${request.remote_name.addr_type} )
{
case 1:
ip_addr = new AddrVal(htonl(${request.remote_name.ipv4}));
break;
case 3:
domain_name = new StringVal(${request.remote_name.domain_name.name}.length(),
(const char*) ${request.remote_name.domain_name.name}.data());
break;
case 4:
ip_addr = new AddrVal(IPAddr(IPv6, (const uint32_t*) ${request.remote_name.ipv6}, IPAddr::Network));
break;
}
BifEvent::generate_socks_request(bro_analyzer(),
bro_analyzer()->Conn(),
5,
${request.command},
ip_addr,
domain_name,
new PortVal(${request.port} | TCP_PORT_MASK),
new StringVal(""));
static_cast<SOCKS_Analyzer*>(bro_analyzer())->EndpointDone(true);
return true;
%}
function socks5_reply(reply: SOCKS5_Reply): bool
%{
AddrVal *ip_addr = 0;
StringVal *domain_name = 0;
// This is dumb and there must be a better way (checking for presence of a field)...
switch ( ${reply.bound.addr_type} )
{
case 1:
ip_addr = new AddrVal(htonl(${reply.bound.ipv4}));
break;
case 3:
domain_name = new StringVal(${reply.bound.domain_name.name}.length(),
(const char*) ${reply.bound.domain_name.name}.data());
break;
case 4:
ip_addr = new AddrVal(IPAddr(IPv6, (const uint32_t*) ${reply.bound.ipv6}, IPAddr::Network));
break;
}
BifEvent::generate_socks_reply(bro_analyzer(),
bro_analyzer()->Conn(),
5,
${reply.reply},
ip_addr,
domain_name,
new PortVal(${reply.port} | TCP_PORT_MASK));
bro_analyzer()->ProtocolConfirmation();
static_cast<SOCKS_Analyzer*>(bro_analyzer())->EndpointDone(false);
return true;
%}
}; };
refine typeattr SOCKS_Request += &let { refine typeattr SOCKS4_Request += &let {
proc: bool = $context.connection.socks_request(command, addr, empty, port, user); proc: bool = $context.connection.socks4_request(this);
}; };
refine typeattr SOCKS_Reply += &let { refine typeattr SOCKS4_Reply += &let {
proc: bool = $context.connection.socks_reply((status == 0x5a), addr, port); proc: bool = $context.connection.socks4_reply(this);
};
refine typeattr SOCKS5_Request += &let {
proc: bool = $context.connection.socks5_request(this);
};
refine typeattr SOCKS5_Reply += &let {
proc: bool = $context.connection.socks5_reply(this);
}; };

View file

@ -1,34 +1,115 @@
type SOCKS_Message(is_orig: bool) = case is_orig of {
true -> request: SOCKS_Request; type SOCKS_Version(is_orig: bool) = record {
false -> reply: SOCKS_Reply; version: uint8;
msg: case version of {
4 -> socks4_msg: SOCKS4_Message(is_orig);
5 -> socks5_msg: SOCKS5_Message(is_orig);
default -> socks_msg_fail: empty;
};
}; };
type SOCKS_Request = record { # SOCKS5 Implementation
version: uint8; type SOCKS5_Message(is_orig: bool) = case $context.connection.v5_past_authentication() of {
true -> msg: SOCKS5_Real_Message(is_orig);
false -> auth: SOCKS5_Auth_Negotiation(is_orig);
};
type SOCKS5_Auth_Negotiation(is_orig: bool) = case is_orig of {
true -> req: SOCKS5_Auth_Negotiation_Request;
false -> rep: SOCKS5_Auth_Negotiation_Reply;
};
type SOCKS5_Auth_Negotiation_Request = record {
method_count: uint8;
methods: uint8[method_count];
};
type SOCKS5_Auth_Negotiation_Reply = record {
selected_auth_method: uint8;
} &let {
past_auth = $context.connection.set_v5_past_authentication();
};
type SOCKS5_Real_Message(is_orig: bool) = case is_orig of {
true -> request: SOCKS5_Request;
false -> reply: SOCKS5_Reply;
};
type Domain_Name = record {
len: uint8;
name: bytestring &length=len;
} &byteorder = bigendian;
type SOCKS5_Address = record {
addr_type: uint8;
addr: case addr_type of {
1 -> ipv4: uint32;
3 -> domain_name: Domain_Name;
4 -> ipv6: uint32[4];
default -> err: bytestring &restofdata &transient;
};
} &byteorder = bigendian;
type SOCKS5_Request = record {
command: uint8;
reserved: uint8;
remote_name: SOCKS5_Address;
port: uint16;
} &byteorder = bigendian;
type SOCKS5_Reply = record {
reply: uint8;
reserved: uint8;
bound: SOCKS5_Address;
port: uint16;
} &byteorder = bigendian;
# SOCKS4 Implementation
type SOCKS4_Message(is_orig: bool) = case is_orig of {
true -> request: SOCKS4_Request;
false -> reply: SOCKS4_Reply;
};
type SOCKS4_Request = record {
command: uint8; command: uint8;
port: uint16; port: uint16;
addr: uint32; addr: uint32;
user: uint8[] &until($element == 0); user: uint8[] &until($element == 0);
host: case v4a of { host: case v4a of {
true -> name: uint8[] &until($element == 0); # v4a true -> name: uint8[] &until($element == 0); # v4a
false -> empty: uint8[] &length=0; false -> empty: uint8[] &length=0;
} &requires(v4a); } &requires(v4a);
# FIXME: Can this be non-zero? If so we need to keep it for the
# next analyzer.
rest: bytestring &restofdata;
} &byteorder = bigendian &let { } &byteorder = bigendian &let {
v4a: bool = (addr <= 0x000000ff); v4a: bool = (addr <= 0x000000ff);
}; };
type SOCKS_Reply = record { type SOCKS4_Reply = record {
zero: uint8; zero: uint8;
status: uint8; status: uint8;
port: uint16; port: uint16;
addr: uint32; addr: uint32;
# FIXME: Can this be non-zero? If so we need to keep it for the
# next analyzer.
rest: bytestring &restofdata;
} &byteorder = bigendian; } &byteorder = bigendian;
refine connection SOCKS_Conn += {
%member{
bool v5_authenticated_;
%}
%init{
v5_authenticated_ = false;
%}
function v5_past_authentication(): bool
%{
return v5_authenticated_;
%}
function set_v5_past_authentication(): bool
%{
v5_authenticated_ = true;
return true;
%}
};

View file

@ -18,7 +18,7 @@ connection SOCKS_Conn(bro_analyzer: BroAnalyzer) {
%include socks-protocol.pac %include socks-protocol.pac
flow SOCKS_Flow(is_orig: bool) { flow SOCKS_Flow(is_orig: bool) {
datagram = SOCKS_Message(is_orig) withcontext(connection, this); datagram = SOCKS_Version(is_orig) withcontext(connection, this);
}; };
%include socks-analyzer.pac %include socks-analyzer.pac

View file

@ -0,0 +1,8 @@
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path socks
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p version user status req_h req_name req_p bound_h bound_name bound_p
#types time string addr port addr port count string string addr string port addr string port
1340213015.276495 UWkUyAuUGXf 10.0.0.55 53994 60.190.189.214 8124 5 - succeeded - www.osnews.com 80 192.168.0.31 - 2688

View file

@ -0,0 +1,8 @@
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path tunnel
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p tunnel_type action
#types time string addr port addr port enum enum
1340213015.276495 - 10.0.0.55 0 60.190.189.214 8124 Tunnel::SOCKS Tunnel::DISCOVER

View file

@ -0,0 +1,8 @@
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path socks
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p version user status req_h req_name req_p bound_h bound_name bound_p
#types time string addr port addr port count string string addr string port addr string port
1340113261.914619 UWkUyAuUGXf 10.0.0.50 59580 85.194.84.197 1080 5 - succeeded - www.google.com 443 0.0.0.0 - 443

View file

@ -0,0 +1,8 @@
#separator \x09
#set_separator ,
#empty_field (empty)
#unset_field -
#path tunnel
#fields ts uid id.orig_h id.orig_p id.resp_h id.resp_p tunnel_type action
#types time string addr port addr port enum enum
1340113261.914619 - 10.0.0.50 0 85.194.84.197 1080 Tunnel::SOCKS Tunnel::DISCOVER

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,6 @@
# @TEST-EXEC: bro -r $TRACES/socks.trace %INPUT
# @TEST-EXEC: btest-diff socks.log
# @TEST-EXEC: btest-diff http.log
# @TEST-EXEC: btest-diff tunnel.log
@load base/protocols/socks

View file

@ -0,0 +1,5 @@
# @TEST-EXEC: bro -r $TRACES/socks-with-ssl.trace %INPUT
# @TEST-EXEC: btest-diff socks.log
# @TEST-EXEC: btest-diff tunnel.log
@load base/protocols/socks