binproxy 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,14 @@
1
+
2
+ class ChatDemo < BinData::Record
3
+ endian :little
4
+ uint32 :timestamp
5
+ string :user, length: 8, trim_padding: true
6
+ bit1 :emote_flag
7
+ bit1 :private_flag
8
+ string :recipient, length: 8, trim_padding: true, onlyif: :private_flag
9
+ stringz :message
10
+
11
+ def summary
12
+ "#{emote_flag ? 'emote' : 'message'} from #{user}"
13
+ end
14
+ end
@@ -0,0 +1,70 @@
1
+ # DNS Parser for BinProxy
2
+ # use Dns::Packet class for UDP traffic, or Dns::TcpPacket for TCP.
3
+
4
+ require 'bindata'
5
+ require 'binproxy/bd_util'
6
+
7
+ module Dns
8
+
9
+ class Header < BinData::Record
10
+ endian :big
11
+
12
+ uint16 :id
13
+ bit1 :qr
14
+ bit4 :opcode
15
+ bit1 :aa
16
+ bit1 :tc
17
+ bit1 :rd
18
+ bit1 :ra
19
+ bit3 :z
20
+ bit4 :rcode
21
+ uint16 :qdcount
22
+ uint16 :ancount
23
+ uint16 :nscount
24
+ uint16 :arcount
25
+ end
26
+
27
+ class NameElement < BinData::Record; end
28
+
29
+ class NameSeq < BinData::Array
30
+ default_parameter read_until: lambda { element.flag != 0x0 or element.val == '' }
31
+ name_element
32
+ end
33
+ class NameElement < BinData::Record
34
+ bit2 :flag
35
+ choice :val, selection: :flag do
36
+ pascal_string 0x0, size_type: :bit6
37
+ pointer 0x3, ptr_type: :bit14, val_type: :name_seq, seek_offset: 2 #XXX
38
+ end
39
+ end
40
+
41
+ class DnsRecord < BinData::Record
42
+ endian :big
43
+ name_seq :name
44
+ uint16 :type
45
+ uint16 :rclass
46
+ end
47
+
48
+ class Question < DnsRecord; end
49
+
50
+ class ResourceRecord < DnsRecord
51
+ uint32 :ttl
52
+ uint16 :rdlength
53
+ string :rdata, read_length: :rdlength
54
+ end
55
+
56
+ #XXX can't use Packet for UDP currently b/c of hardcoded 2 byte adjustment to pointers (due to length pre-header in TCP packets)
57
+ class Packet < BinData::Record
58
+ header :header
59
+ array :questions, type: :question, initial_length: lambda { header.qdcount }
60
+ array :answers, type: :resource_record, initial_length: lambda { header.ancount }
61
+ array :authorities, type: :resource_record, initial_length: lambda { header.nscount }
62
+ array :addl_records, type: :resource_record, initial_length: lambda { header.arcount }
63
+ end
64
+
65
+
66
+ class TcpPacket < BinData::Record
67
+ uint16be :len
68
+ packet :pkt
69
+ end
70
+ end
@@ -0,0 +1,28 @@
1
+ # A stupid HTTP BinData class to demonstrate the proxy
2
+ require_relative '../bd_util'
3
+
4
+ module DumbHttp
5
+ class Headers < BinData::Array
6
+ default_parameter read_until: lambda { element == '' }
7
+ line line_end: "\r\n"
8
+ end
9
+
10
+ class BaseMessage < BinData::Record
11
+ headers :headers
12
+
13
+ def body_len
14
+ (headers.snapshot.grep(/\AContent-Length: (\d+)/) { $1.to_i }).first || 0
15
+ end
16
+
17
+ def summary; headers.first.sub(/HTTP\/\d\.\d/,'').strip; end
18
+ end
19
+
20
+ class Message < BaseMessage
21
+ string :body, display_as: 'multiline', onlyif: lambda { body_len > 0 }, length: :body_len
22
+ end
23
+
24
+ class BinMessage < BaseMessage
25
+ string :body, display_as: 'hexdump', onlyif: lambda { body_len > 0 }, length: :body_len
26
+ end
27
+
28
+ end
@@ -0,0 +1,56 @@
1
+ require 'bindata'
2
+
3
+ class MsgPack
4
+ class MpNil < BinData::Primitive
5
+ def get; nil; end
6
+ def set(v); raise unless v.nil? end;
7
+ end
8
+ class MpTrue < BinData::Primitive
9
+ def get; true; end
10
+ def set(v); raise unless v; end
11
+ end
12
+ class MpFalse < BinData::Primitive
13
+ def get; false; end
14
+ def set(v); raise unless !v; end
15
+ end
16
+ class
17
+
18
+
19
+ class TypedValue < BinData::Choice
20
+ positive_fixint :positive_fixint
21
+ fixmap :fixmap
22
+ fixarray :fixarray
23
+ fixstr :fixstr
24
+ mp_nil :nil
25
+ mp_true :true
26
+ mp_false :false
27
+ bin8 :bin8
28
+ end
29
+
30
+ uint8 :type_byte
31
+ typed_value :value, selection: lambda do
32
+ case type_byte
33
+ when 0..0x7f
34
+ :positive_fixint
35
+ when 0x80..0x8f
36
+ :fixmap
37
+ when 0x90..0x9f
38
+ :fixarray
39
+ when 0xa0..0xbf
40
+ :fixstr
41
+ when 0xc0
42
+ :nil
43
+ when 0xc1
44
+ :unused
45
+ when 0xc2
46
+ :true
47
+ when 0xc3
48
+ :false
49
+ when 0xc4
50
+ :bin8
51
+ else
52
+ raise 'todo'
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,4 @@
1
+ require 'bindata'
2
+ class PlainText < BinData::Rest
3
+ default_parameter display_as: 'multiline'
4
+ end
@@ -0,0 +1,4 @@
1
+ require 'bindata'
2
+ class RawMessage < BinData::Record
3
+ rest :data, display_as: 'hexdump'
4
+ end
@@ -0,0 +1,134 @@
1
+ require 'bindata'
2
+
3
+ module X11
4
+ end
5
+
6
+ class X11SetupReq < BinData::Record
7
+ endian :little #totally cheating here!
8
+ uint8 :byte_order
9
+ skip length: 1
10
+ uint16 :proto_major
11
+ uint16 :proto_minor
12
+ uint16 :auth_proto_name_len, value: lambda { auth_proto_name.length }
13
+ uint16 :auth_proto_data_len, value: lambda { auth_proto_data.length }
14
+ string :auth_proto_name, read_length: :auth_proto_name_len
15
+ skip length: lambda { (4 - (auth_proto_name_len % 4)) % 4 }
16
+ string :auth_proto_data, read_length: :auth_proto_data_len
17
+ skip length: lambda { (4 - (auth_proto_data_len % 4)) % 4 }
18
+ skip length: 2 #???
19
+ end
20
+
21
+ class X11StdReqBody < BinData::Record
22
+ endian :little
23
+ uint16 :req_len
24
+ string :data, read_length: lambda {(req_len - 1) * 4}
25
+ end
26
+
27
+ class X11ExtReqBody < BinData::Record
28
+ rest :data
29
+ end
30
+
31
+ class X11StdReq < BinData::Record
32
+ endian :little
33
+ uint8 :opcode
34
+ uint8 :subcode
35
+ choice :req_type, selection: lambda { opcode <= 128 } do
36
+ x11_std_req_body true
37
+ x11_ext_req_body false
38
+ end
39
+ end
40
+
41
+ class X11Req < BinData::Record
42
+ choice :msg2, display_as: 'anon', selection: lambda { current_state[:connected] } do
43
+ x11_setup_req false
44
+ x11_std_req true
45
+ end
46
+ end
47
+
48
+ class X11SetupRes < BinData::Record
49
+ endian :little #more cheating
50
+ uint8 :status
51
+ skip length: 1
52
+ uint16 :proto_major
53
+ uint16 :proto_minor
54
+ uint16 :addl_data_dwords
55
+ uint32 :release_num
56
+ uint32 :res_id_base, display_as: 'hex'
57
+ uint32 :res_id_mask, display_as: 'hex'
58
+ uint32 :motion_buffer_size
59
+ uint16 :vendor_len, value: lambda { vendor.length }
60
+ uint16 :max_req_len
61
+ uint8 :num_screens
62
+ uint8 :num_formats
63
+ uint8 :image_byte_order
64
+ uint8 :bitmap_bit_order
65
+ uint8 :bitmap_format_scanline_unit
66
+ uint8 :bitmap_format_scanlin_pad
67
+ uint8 :min_keycode
68
+ uint8 :max_keycode
69
+ skip length: 4
70
+ string :vendor, read_length: :vendor_len
71
+ string :vpad, read_length: lambda { (4 - ( vendor.length % 4 )) % 4 }
72
+ rest :listofformatandroots, display_as: 'hexdump'
73
+ end
74
+
75
+ class X11Reply < BinData::Record
76
+ endian :little
77
+ uint8 :detail
78
+ uint16 :seq_num
79
+ uint32 :extra_len
80
+ string :data, length: lambda { 24 + (4 * extra_len) }, display_as: 'hexdump'
81
+ end
82
+
83
+ class X11Event < BinData::Record
84
+ endian :little
85
+ uint8 :detail
86
+ string :data, length: 30, display_as: 'hexdump'
87
+ end
88
+
89
+ class X11Error < BinData::Record
90
+ endian :little
91
+ uint8 :detail
92
+ string :data, length: 30, display_as: 'hexdump'
93
+ end
94
+
95
+ class X11StdRes < BinData::Record
96
+ endian :little
97
+ uint8 :res_type
98
+ choice :res_detail, selection: :res_type do
99
+ x11_error 0
100
+ x11_reply 1
101
+ x11_event :default
102
+ end
103
+ end
104
+
105
+
106
+ class X11Res < BinData::Record
107
+ choice :msg2, display_as: 'anon', selection: lambda { current_state[:connected] } do
108
+ x11_setup_res false
109
+ x11_std_res true
110
+ end
111
+ end
112
+
113
+ class X11Proto < BinData::Record
114
+ def self.initial_state; { connected: false, endian: nil }; end
115
+
116
+ #XXX wrap this in something that dups the current state so we're unable to mutate it by accident
117
+ def update_state
118
+ c = current_state.dup
119
+ if eval_parameter(:src) == 'server' #hack, assume we're connected after 1st c-s-c exchange
120
+ c[:connected] = true
121
+ end
122
+ c
123
+ end
124
+
125
+ def summary
126
+ msg.msg2.summary
127
+ end
128
+
129
+ choice :msg, selection: :src, display_as: 'anon' do
130
+ x11_req 'client'
131
+ x11_res 'server'
132
+ end
133
+ end
134
+
@@ -0,0 +1,62 @@
1
+ # ZeroMQ (ZMTPv3) Parser for BinProxy
2
+
3
+ module ZMQ
4
+ # this greeting1/2/3 business is due to the fact that
5
+ # zmq sends the greeting in parts and waits for the reply
6
+ # from the other endpoint in order to do version detection.
7
+ class Greeting1 < BinData::Record
8
+ uint8 :sig_start, asserted_value: 0xFF
9
+ string :sig_padding, length: 8
10
+ uint8 :sig_end, asserted_value: 0x7F
11
+ end
12
+
13
+ class Greeting2 < BinData::Record
14
+ uint8 :version_major, asserted_value: 0x03
15
+ end
16
+
17
+ class Greeting3 < BinData::Record
18
+ uint8 :version_minor, asserted_value: 0x00
19
+
20
+ string :mechanism, length: 20
21
+
22
+ uint8 :as_server
23
+
24
+ string :filler, length: 31 #pad to 64 bytes
25
+ end
26
+
27
+ class Frame < BinData::Record
28
+ bit5 :reserved
29
+ bit1 :is_command
30
+ bit1 :is_long
31
+ bit1 :has_more
32
+ choice :body_size, selection: :is_long, display_as: 'anon' do
33
+ uint8 0
34
+ uint64be 1
35
+ end
36
+ string :body, read_length: :body_size
37
+ end
38
+
39
+ class Message < BinData::Record
40
+ # States are (G)reeting 1/2/3 (see above), (H)andshake, and (T)raffic.
41
+ def next_state(s); {'G1'=>'G2','G2'=>'G3','G3'=>'H', 'H'=>'T', 'T'=>'T'}[s]; end
42
+ def self.initial_state; {'client'=>'G1','server'=>'G1'}; end
43
+
44
+ # Protocol is symmetrical. Each endpoint has its own state.
45
+ # This is a bit clunky and maybe should be abstracted into a module?
46
+ # Or update parser.rb to differentiate between proto-shared and endpoint-separate state?
47
+ def update_state
48
+ current_state.dup.tap do |s|
49
+ src = eval_parameter :src
50
+ s[src] = next_state s[src]
51
+ end
52
+ end
53
+
54
+ choice :msg_type, selection: lambda { current_state[eval_parameter(:src) ] }, display_as: 'anon' do
55
+ greeting1 'G1'
56
+ greeting2 'G2'
57
+ greeting3 'G3'
58
+ frame 'H' #contains a command w/ arbitrary content
59
+ frame 'T' #can contain command or message
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,242 @@
1
+ require 'eventmachine'
2
+ require 'observer'
3
+ require 'bindata'
4
+ require 'active_support/core_ext/object/blank'
5
+ require_relative 'bindata'
6
+ require_relative 'connection'
7
+ require_relative 'session'
8
+ require_relative 'parser'
9
+ require_relative 'logger'
10
+
11
+ module BinProxy
12
+ class Proxy
13
+ include BinProxy::Logger
14
+ include Observable
15
+ attr_accessor :hold
16
+ attr_reader :sessions
17
+ attr_reader :buffer
18
+
19
+ def initialize(root, opts)
20
+ @class_loader = BinProxy::ClassLoader.new(root)
21
+ @options = {}
22
+ @sessions = []
23
+ @buffer = []
24
+ @filter_pids = []
25
+ configure(opts)
26
+ end
27
+
28
+ def configure(new_opts = {})
29
+ old_opts = @options.dup
30
+ new_opts = old_opts.merge new_opts
31
+
32
+ @hold = !! new_opts[:hold];
33
+ @inbound_filters = []
34
+ @upstream_filters = []
35
+
36
+ if new_opts[:socks_proxy]
37
+ @inbound_filters << BinProxy::Connection::Filters::Socks
38
+ elsif new_opts[:http_proxy]
39
+ @inbound_filters << BinProxy::Connection::Filters::HTTPConnect
40
+ else
41
+ @inbound_filters << BinProxy::Connection::Filters::StaticUpstream
42
+ end
43
+
44
+ if new_opts[:tls]
45
+ @inbound_filters << BinProxy::Connection::Filters::InboundTLS
46
+ @upstream_filters << BinProxy::Connection::Filters::UpstreamTLS
47
+ end
48
+
49
+ if new_opts[:debug_extra]
50
+ @inbound_filters << BinProxy::Connection::Filters::Logger
51
+ @upstream_filters << BinProxy::Connection::Filters::Logger
52
+ end
53
+
54
+
55
+
56
+ #XXX this reloads the class on every config change
57
+ #XXX bad, but reloading in web console relies on it
58
+ new_opts[:class] = @class_loader.load_class(new_opts[:class_name],new_opts[:class_file])
59
+ if old_opts[:class] != new_opts[:class]
60
+ @parser_class = Parser.subclass(self, new_opts[:class])
61
+ @parser_class.validate = new_opts[:validate_parser]
62
+ elsif new_opts[:class] == nil
63
+ raise "Option :class must be specified"
64
+ end
65
+
66
+ [:lhost, :lport, :dhost, :dport].each do |o|
67
+ # let people get away with it if it's already blank
68
+ if old_opts[o].present? and new_opts[o].blank?
69
+ raise "Option :#{o} must be specified"
70
+ end
71
+ end
72
+
73
+ # This will not handle an exception if the server is started asynchronously, but
74
+ # the only time that should happen is the initial configuration from command line args,
75
+ # in which case we don't have existing opts to revert to anyway.
76
+ begin
77
+ @options = new_opts
78
+ if old_opts[:lhost] != new_opts[:lhost] or old_opts[:lport] != new_opts[:lport]
79
+ restart_server!
80
+ end
81
+ rescue Exception => e
82
+ @options = old_opts
83
+ raise e
84
+ end
85
+ end
86
+
87
+ # XXX below is incorrect, we do call it on first configuration
88
+ # We'll only call this internally if the server's host or port has changed; there's still the possiblility
89
+ # that a quick a->b->a change, or manually calling this could fail because the OS hasn't released the port yet;
90
+ # we'll punt on that for now, though.
91
+ def restart_server!
92
+ if stop_server!
93
+ #server was indeed running
94
+ # TODO investigate whether this is actually necessary, or could be reduced/removed
95
+ sleep 5
96
+ end
97
+ start_server!
98
+ end
99
+
100
+ def start_server!
101
+ if EM.reactor_running?
102
+ # Prefer to do this synchronously, so we can catch errors.
103
+ start_server_now!
104
+ else
105
+ # Have to defer this until Sinatra/Thin starts EM running.
106
+ EM.next_tick { start_server_now! }
107
+ end
108
+ end
109
+
110
+ def stop_server!
111
+ if @server_id
112
+ @filter_pids.each do |pid|
113
+ log.debug "Killing #{pid}"
114
+ Process.kill "INT", pid #TODO invstigate ncat signal handling
115
+ end
116
+ @filter_pids = [];
117
+ Process.waitall
118
+ EM.stop_server @server_id
119
+ @server_id = nil
120
+ true
121
+ else
122
+ false
123
+ end
124
+ end
125
+
126
+ def status
127
+ @server_id.nil? ? 'stopped' : 'running'
128
+ end
129
+
130
+ def history_size
131
+ @buffer.length
132
+ end
133
+
134
+ def message_received(message)
135
+ log.debug "proxy handling message_received"
136
+ message.id = @buffer.length
137
+ @buffer.push(message)
138
+
139
+ if hold
140
+ message.disposition = 'Held'
141
+ else
142
+ send_message(message, :auto)
143
+ end
144
+
145
+ changed
146
+ notify_observers(:message_received, message)
147
+ end
148
+
149
+ def update_message_from_hash(hash)
150
+ log.debug "updating message from hash"
151
+ orig = @buffer[hash[:head][:message_id]]
152
+ orig.update!(hash[:body][:snapshot])
153
+ orig
154
+ rescue Exception => e
155
+ puts '', e.class, e.message, e.backtrace, ''
156
+ on_bindata_error('deserialization', e)
157
+ end
158
+
159
+ def send_message(message, reason)
160
+ log.debug "proxy.send_message #{message.inspect}"
161
+ message.forward!(reason)
162
+ rescue Exception => e
163
+ puts '', e.class, e.message, e.backtrace, ''
164
+ on_bindata_error('forwarding', e)
165
+ end
166
+
167
+ def drop_message(message, reason)
168
+ log.debug "proxy.drop_message #{message.inspect}"
169
+ message.drop!(reason)
170
+ rescue Exception => e
171
+ puts '', e.class, e.message, e.backtrace, ''
172
+ on_bindata_error('dropping', e)
173
+ end
174
+
175
+ def session_closed(session, closing_peer, reason)
176
+ pe = BinProxy::ProxyEvent.new("Connection closed (#{reason||'no error'})")
177
+ pe.id = @buffer.length
178
+ pe.src = closing_peer
179
+ pe.session = session
180
+ @buffer.push(pe)
181
+
182
+ session.delete_observer(self)
183
+ changed
184
+ notify_observers(:session_event, pe)
185
+ end
186
+
187
+ def ssl_handshake_completed(session, peer)
188
+ changed
189
+ notify_observers(:ssl_handshake_completed, session, peer)
190
+ end
191
+
192
+ def on_bindata_error(operation, err)
193
+ changed
194
+ notify_observers(:bindata_error, operation, err)
195
+ end
196
+
197
+ private
198
+ def create_session(inbound_conn, upstream_conn)
199
+ id = @sessions.length
200
+ session = Session.new(id, inbound_conn, upstream_conn, @parser_class)
201
+ session.add_observer(self, :send)
202
+ @sessions << session
203
+
204
+ pe = BinProxy::ProxyEvent.new "Connection opened"
205
+ pe.id = @buffer.length
206
+ pe.src = :client
207
+ pe.session = session
208
+ @buffer.push(pe)
209
+
210
+ changed
211
+ notify_observers(:session_event, pe)
212
+ rescue Exception => e
213
+ puts e, e.backtrace
214
+ end
215
+
216
+ def start_server_now!
217
+ lhost = @options[:lhost]
218
+ lport = @options[:lport].to_i #force port to_i, or EM may assume different method signature
219
+
220
+ #These may be nil and are ignored in socks mode
221
+ dhost = @options[:dhost]
222
+ dport = @options[:dport].to_i
223
+
224
+ log.info "(re)starting proxy on #{lhost}:#{lport}"
225
+ upstream_args = {
226
+ peer: :server,
227
+ filter_classes: @upstream_filters
228
+ }
229
+ inbound_args = {
230
+ peer: :client,
231
+ session_callback: self.method(:create_session),
232
+ tls_args: { cert_chain_file: @options[:tls_cert], private_key_file: @options[:tls_key] },
233
+ filter_classes: @inbound_filters,
234
+ upstream_host: dhost,
235
+ upstream_port: dport,
236
+ upstream_args: upstream_args
237
+ }
238
+ @server_id = EM.start_server(lhost, lport, Connection, inbound_args)
239
+ end
240
+
241
+ end
242
+ end