diff --git a/scripts/base/frameworks/dpd/dpd.sig b/scripts/base/frameworks/dpd/dpd.sig index 305383809d..245e79bfdf 100644 --- a/scripts/base/frameworks/dpd/dpd.sig +++ b/scripts/base/frameworks/dpd/dpd.sig @@ -162,33 +162,48 @@ signature dpd_teredo { enable "teredo" } -signature dpd_socks_client { +signature dpd_socks4_client { ip-proto == tcp # '32' is a rather arbitrary max length for the user name. payload /^\x04[\x01\x02].{0,32}\x00/ tcp-state originator } -signature dpd_socks_server { +signature dpd_socks4_server { ip-proto == tcp - requires-reverse-signature dpd_socks_client + requires-reverse-signature dpd_socks4_client payload /^\x00[\x5a\x5b\x5c\x5d]/ tcp-state responder enable "socks" } -signature dpd_socks_reverse_client { +signature dpd_socks4_reverse_client { ip-proto == tcp # '32' is a rather arbitrary max length for the user name. payload /^\x04[\x01\x02].{0,32}\x00/ tcp-state responder } -signature dpd_socks_reverse_server { +signature dpd_socks4_reverse_server { ip-proto == tcp - requires-reverse-signature dpd_socks_client + requires-reverse-signature dpd_socks4_reverse_client payload /^\x00[\x5a\x5b\x5c\x5d]/ tcp-state originator 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" +} + + diff --git a/scripts/base/protocols/socks/__load__.bro b/scripts/base/protocols/socks/__load__.bro index d551be57d3..0098b81a7a 100644 --- a/scripts/base/protocols/socks/__load__.bro +++ b/scripts/base/protocols/socks/__load__.bro @@ -1 +1,2 @@ +@load ./consts @load ./main \ No newline at end of file diff --git a/scripts/base/protocols/socks/consts.bro b/scripts/base/protocols/socks/consts.bro new file mode 100644 index 0000000000..6341262041 --- /dev/null +++ b/scripts/base/protocols/socks/consts.bro @@ -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); }; + +} \ No newline at end of file diff --git a/scripts/base/protocols/socks/main.bro b/scripts/base/protocols/socks/main.bro index 54d181e43e..cceea68758 100644 --- a/scripts/base/protocols/socks/main.bro +++ b/scripts/base/protocols/socks/main.bro @@ -1,15 +1,98 @@ @load base/frameworks/tunnels +@load ./consts module SOCKS; export { - type RequestType: enum { - CONNECTION = 1, - PORT = 2, + redef enum Log::ID += { LOG }; + + 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); + } \ No newline at end of file diff --git a/src/event.bif b/src/event.bif index 4d28ab7a40..d1e28a98e5 100644 --- a/src/event.bif +++ b/src/event.bif @@ -6101,7 +6101,9 @@ event signature_match%(state: signature_state, msg: string, data: string%); ## ## 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. ## @@ -6109,13 +6111,23 @@ event signature_match%(state: signature_state, msg: string, data: string%); ## ## p: The destination port for the proxied traffic. ## -## user: Username given for the SOCKS connection. -event socks_request%(c: connection, request_type: count, dstaddr: addr, dstname: string, p: port, user: string%); +## user: Username given for the SOCKS connection. This is not yet implemented for SOCKSv5. +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. ## +## 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 ## used on a system. This is a protocol-independent event that is fed by diff --git a/src/socks-analyzer.pac b/src/socks-analyzer.pac index 88a29fe383..0842303f40 100644 --- a/src/socks-analyzer.pac +++ b/src/socks-analyzer.pac @@ -19,39 +19,127 @@ StringVal* array_to_string(vector *a) %} 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(), bro_analyzer()->Conn(), - cmd, - new AddrVal(htonl(dstaddr)), - array_to_string(dstname), - new PortVal(p | TCP_PORT_MASK), - array_to_string(user)); + 4, + ${request.command}, + new AddrVal(htonl(${request.addr})), + dstname, + new PortVal(${request.port} | TCP_PORT_MASK), + array_to_string(${request.user})); static_cast(bro_analyzer())->EndpointDone(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(), bro_analyzer()->Conn(), - granted, - new AddrVal(htonl(dst)), - new PortVal(p | TCP_PORT_MASK)); + 4, + ${reply.status}, + new AddrVal(htonl(${reply.addr})), + new StringVal(""), + new PortVal(${reply.port} | TCP_PORT_MASK)); bro_analyzer()->ProtocolConfirmation(); static_cast(bro_analyzer())->EndpointDone(false); 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(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(bro_analyzer())->EndpointDone(false); + return true; + %} + }; -refine typeattr SOCKS_Request += &let { - proc: bool = $context.connection.socks_request(command, addr, empty, port, user); +refine typeattr SOCKS4_Request += &let { + proc: bool = $context.connection.socks4_request(this); }; -refine typeattr SOCKS_Reply += &let { - proc: bool = $context.connection.socks_reply((status == 0x5a), addr, port); +refine typeattr SOCKS4_Reply += &let { + 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); }; diff --git a/src/socks-protocol.pac b/src/socks-protocol.pac index a908c2da68..8ae81a6e02 100644 --- a/src/socks-protocol.pac +++ b/src/socks-protocol.pac @@ -1,34 +1,115 @@ -type SOCKS_Message(is_orig: bool) = case is_orig of { - true -> request: SOCKS_Request; - false -> reply: SOCKS_Reply; + +type SOCKS_Version(is_orig: bool) = record { + 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 { - version: uint8; +# SOCKS5 Implementation +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; port: uint16; addr: uint32; user: uint8[] &until($element == 0); - host: case v4a of { true -> name: uint8[] &until($element == 0); # v4a false -> empty: uint8[] &length=0; } &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 { v4a: bool = (addr <= 0x000000ff); }; -type SOCKS_Reply = record { - zero: uint8; - status: uint8; +type SOCKS4_Reply = record { + zero: uint8; + status: uint8; port: uint16; addr: uint32; - - # FIXME: Can this be non-zero? If so we need to keep it for the - # next analyzer. - rest: bytestring &restofdata; } &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; + %} +}; + diff --git a/src/socks.pac b/src/socks.pac index 4f16582690..15d3580674 100644 --- a/src/socks.pac +++ b/src/socks.pac @@ -18,7 +18,7 @@ connection SOCKS_Conn(bro_analyzer: BroAnalyzer) { %include socks-protocol.pac 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 \ No newline at end of file diff --git a/testing/btest/Baseline/scripts.base.protocols.socks.trace1/socks.log b/testing/btest/Baseline/scripts.base.protocols.socks.trace1/socks.log new file mode 100644 index 0000000000..4241190234 --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.socks.trace1/socks.log @@ -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 diff --git a/testing/btest/Baseline/scripts.base.protocols.socks.trace1/tunnel.log b/testing/btest/Baseline/scripts.base.protocols.socks.trace1/tunnel.log new file mode 100644 index 0000000000..a7068cd0da --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.socks.trace1/tunnel.log @@ -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 diff --git a/testing/btest/Baseline/scripts.base.protocols.socks.trace2/socks.log b/testing/btest/Baseline/scripts.base.protocols.socks.trace2/socks.log new file mode 100644 index 0000000000..556ed9263e --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.socks.trace2/socks.log @@ -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 diff --git a/testing/btest/Baseline/scripts.base.protocols.socks.trace2/tunnel.log b/testing/btest/Baseline/scripts.base.protocols.socks.trace2/tunnel.log new file mode 100644 index 0000000000..5eac3ae7ad --- /dev/null +++ b/testing/btest/Baseline/scripts.base.protocols.socks.trace2/tunnel.log @@ -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 diff --git a/testing/btest/Traces/socks-with-ssl.trace b/testing/btest/Traces/socks-with-ssl.trace new file mode 100644 index 0000000000..da27cc8873 Binary files /dev/null and b/testing/btest/Traces/socks-with-ssl.trace differ diff --git a/testing/btest/Traces/socks.trace b/testing/btest/Traces/socks.trace new file mode 100644 index 0000000000..00bf07e458 Binary files /dev/null and b/testing/btest/Traces/socks.trace differ diff --git a/testing/btest/scripts/base/protocols/socks/trace1.test b/testing/btest/scripts/base/protocols/socks/trace1.test new file mode 100644 index 0000000000..fb65b33cbc --- /dev/null +++ b/testing/btest/scripts/base/protocols/socks/trace1.test @@ -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 diff --git a/testing/btest/scripts/base/protocols/socks/trace2.test b/testing/btest/scripts/base/protocols/socks/trace2.test new file mode 100644 index 0000000000..5e3a449120 --- /dev/null +++ b/testing/btest/scripts/base/protocols/socks/trace2.test @@ -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