-
Notifications
You must be signed in to change notification settings - Fork 23
/
Copy pathscan.bro
621 lines (507 loc) · 17.3 KB
/
scan.bro
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
##! Scan detector ported from Bro 1.x.
##!
##! This script has evolved over many years and is quite a mess right now. We
##! have adapted it to work with Bro 2.x, but eventually Bro 2.x will
##! get its own rewritten and generalized scan detector.
@load base/frameworks/notice/main
module Scan;
export {
redef enum Notice::Type += {
## The source has scanned a number of ports.
PortScan,
## The source has scanned a number of addresses.
AddressScan,
## Apparent flooding backscatter seen from source.
BackscatterSeen,
## Summary of scanning activity.
ScanSummary,
## Summary of distinct ports per scanner.
PortScanSummary,
## Summary of distinct low ports per scanner.
LowPortScanSummary,
## Source reached :bro:id:`Scan::shut_down_thresh`
ShutdownThresh,
## Source touched privileged ports.
LowPortTrolling,
};
# Whether to consider UDP "connections" for scan detection.
# Can lead to false positives due to UDP fanout from some P2P apps.
const suppress_UDP_scan_checks = F &redef;
const activate_priv_port_check = T &redef;
const activate_landmine_check = F &redef;
const landmine_thresh_trigger = 5 &redef;
const landmine_address: set[addr] &redef;
const scan_summary_trigger = 25 &redef;
const port_summary_trigger = 20 &redef;
const lowport_summary_trigger = 10 &redef;
# Raise ShutdownThresh after this many failed attempts
const shut_down_thresh = 100 &redef;
# Which services should be analyzed when detecting scanning
# (not consulted if analyze_all_services is set).
const analyze_services: set[port] &redef;
const analyze_all_services = T &redef;
# Track address scaners only if at least these many hosts contacted.
const addr_scan_trigger = 0 &redef;
# Ignore address scanners for further scan detection after
# scanning this many hosts.
# 0 disables.
const ignore_scanners_threshold = 0 &redef;
# Report a scan of peers at each of these points.
const report_peer_scan: vector of count = {
20, 100, 1000, 10000, 50000, 100000, 250000, 500000, 1000000,
} &redef;
const report_outbound_peer_scan: vector of count = {
100, 1000, 10000,
} &redef;
# Report a scan of ports at each of these points.
const report_port_scan: vector of count = {
50, 250, 1000, 5000, 10000, 25000, 65000,
} &redef;
# Once a source has scanned this many different ports (to however many
# different remote hosts), start tracking its per-destination access.
const possible_port_scan_thresh = 20 &redef;
# Threshold for scanning privileged ports.
const priv_scan_trigger = 5 &redef;
const troll_skip_service = {
25/tcp, 21/tcp, 22/tcp, 20/tcp, 80/tcp,
} &redef;
const report_accounts_tried: vector of count = {
20, 100, 1000, 10000, 100000, 1000000,
} &redef;
const report_remote_accounts_tried: vector of count = {
100, 500,
} &redef;
# Report a successful password guessing if the source attempted
# at least this many.
const password_guessing_success_threshhold = 20 &redef;
const skip_accounts_tried: set[addr] &redef;
const addl_web = {
81/tcp, 443/tcp, 8000/tcp, 8001/tcp, 8080/tcp, }
&redef;
const skip_services = { 113/tcp, } &redef;
const skip_outbound_services = { 21/tcp, addl_web, }
&redef;
const skip_scan_sources = {
255.255.255.255, # who knows why we see these, but we do
} &redef;
const skip_scan_nets: set[subnet] = {} &redef;
# List of well known local server/ports to exclude for scanning
# purposes.
const skip_dest_server_ports: set[addr, port] = {} &redef;
# Reverse (SYN-ack) scans seen from these ports are considered
# to reflect possible SYN-flooding backscatter, and not true
# (stealth) scans.
const backscatter_ports = {
80/tcp, 8080/tcp, 53/tcp, 53/udp, 179/tcp, 6666/tcp, 6667/tcp,
} &redef;
const report_backscatter: vector of count = {
20,
} &redef;
global check_scan:
function(c: connection, established: bool, reverse: bool): bool;
# The following tables are defined here so that we can redef
# the expire timeouts.
# FIXME: should we allow redef of attributes on IDs which
# are not exported?
# How many different hosts connected to with a possible
# backscatter signature.
global distinct_backscatter_peers: table[addr] of table[addr] of count
&read_expire = 15 min;
# Expire functions that trigger summaries.
global scan_summary:
function(t: table[addr] of set[addr], orig: addr): interval;
global port_summary:
function(t: table[addr] of set[port], orig: addr): interval;
global lowport_summary:
function(t: table[addr] of set[port], orig: addr): interval;
# Indexed by scanner address, yields # distinct peers scanned.
# pre_distinct_peers tracks until addr_scan_trigger hosts first.
global pre_distinct_peers: table[addr] of set[addr]
&read_expire = 15 mins &redef;
global distinct_peers: table[addr] of set[addr]
&read_expire = 15 mins &expire_func=scan_summary &redef;
global distinct_ports: table[addr] of set[port]
&read_expire = 15 mins &expire_func=port_summary &redef;
global distinct_low_ports: table[addr] of set[port]
&read_expire = 15 mins &expire_func=lowport_summary &redef;
# Indexed by scanner address, yields a table with scanned hosts
# (and ports).
global scan_triples: table[addr] of table[addr] of set[port];
global remove_possible_source:
function(s: set[addr], idx: addr): interval;
global possible_scan_sources: set[addr]
&expire_func=remove_possible_source &read_expire = 15 mins;
# Indexed by source address, yields user name & password tried.
global accounts_tried: table[addr] of set[string, string]
&read_expire = 1 days;
global ignored_scanners: set[addr] &create_expire = 1 day &redef;
# These tables track whether a threshold has been reached.
# More precisely, the counter is the next index of threshold vector.
global shut_down_thresh_reached: table[addr] of bool &default=F;
global rb_idx: table[addr] of count
&default=1 &read_expire = 1 days &redef;
global rps_idx: table[addr] of count
&default=1 &read_expire = 1 days &redef;
global rops_idx: table[addr] of count
&default=1 &read_expire = 1 days &redef;
global rpts_idx: table[addr,addr] of count
&default=1 &read_expire = 1 days &redef;
global rat_idx: table[addr] of count
&default=1 &read_expire = 1 days &redef;
global rrat_idx: table[addr] of count
&default=1 &read_expire = 1 days &redef;
}
global thresh_check: function(v: vector of count, idx: table[addr] of count,
orig: addr, n: count): bool;
global thresh_check_2: function(v: vector of count,
idx: table[addr,addr] of count, orig: addr,
resp: addr, n: count): bool;
function scan_summary(t: table[addr] of set[addr], orig: addr): interval
{
local num_distinct_peers = orig in t ? |t[orig]| : 0;
if ( num_distinct_peers >= scan_summary_trigger )
NOTICE([$note=ScanSummary, $src=orig, $n=num_distinct_peers,
$identifier=fmt("%s", orig),
$msg=fmt("%s scanned a total of %d hosts",
orig, num_distinct_peers)]);
return 0 secs;
}
function port_summary(t: table[addr] of set[port], orig: addr): interval
{
local num_distinct_ports = orig in t ? |t[orig]| : 0;
if ( num_distinct_ports >= port_summary_trigger )
NOTICE([$note=PortScanSummary, $src=orig, $n=num_distinct_ports,
$identifier=fmt("%s", orig),
$msg=fmt("%s scanned a total of %d ports",
orig, num_distinct_ports)]);
return 0 secs;
}
function lowport_summary(t: table[addr] of set[port], orig: addr): interval
{
local num_distinct_lowports = orig in t ? |t[orig]| : 0;
if ( num_distinct_lowports >= lowport_summary_trigger )
NOTICE([$note=LowPortScanSummary, $src=orig,
$n=num_distinct_lowports,
$identifier=fmt("%s", orig),
$msg=fmt("%s scanned a total of %d low ports",
orig, num_distinct_lowports)]);
return 0 secs;
}
function clear_addr(a: addr)
{
delete distinct_peers[a];
delete distinct_ports[a];
delete distinct_low_ports[a];
delete scan_triples[a];
delete possible_scan_sources[a];
delete distinct_backscatter_peers[a];
delete pre_distinct_peers[a];
delete rb_idx[a];
delete rps_idx[a];
delete rops_idx[a];
delete rat_idx[a];
delete rrat_idx[a];
delete shut_down_thresh_reached[a];
delete ignored_scanners[a];
}
function ignore_addr(a: addr)
{
clear_addr(a);
add ignored_scanners[a];
}
function check_scan(c: connection, established: bool, reverse: bool): bool
{
local id = c$id;
local service = "ftp-data" in c$service ? 20/tcp
: (reverse ? id$orig_p : id$resp_p);
local rev_service = reverse ? id$resp_p : id$orig_p;
local orig = reverse ? id$resp_h : id$orig_h;
local resp = reverse ? id$orig_h : id$resp_h;
local outbound = Site::is_local_addr(orig);
# The following works better than using get_conn_transport_proto()
# because c might not correspond to an active connection (which
# causes the function to fail).
if ( suppress_UDP_scan_checks &&
service >= 0/udp && service <= 65535/udp )
return F;
if ( service in skip_services && ! outbound )
return F;
if ( outbound && service in skip_outbound_services )
return F;
if ( orig in skip_scan_sources )
return F;
if ( orig in skip_scan_nets )
return F;
# Don't include well known server/ports for scanning purposes.
if ( ! outbound && [resp, service] in skip_dest_server_ports )
return F;
if ( orig in ignored_scanners)
return F;
if ( ! established &&
# not established, service not expressly allowed
# not known peer set
(orig !in distinct_peers || resp !in distinct_peers[orig]) &&
# want to consider service for scan detection
(analyze_all_services || service in analyze_services) )
{
if ( reverse && rev_service in backscatter_ports &&
# reverse, non-priv backscatter port
service >= 1024/tcp )
{
if ( orig !in distinct_backscatter_peers )
{
local empty_bs_table:
table[addr] of count &default=0;
distinct_backscatter_peers[orig] =
empty_bs_table;
}
if ( ++distinct_backscatter_peers[orig][resp] <= 2 &&
# The test is <= 2 because we get two check_scan()
# calls, once on connection attempt and once on
# tear-down.
distinct_backscatter_peers[orig][resp] == 1 &&
# Looks like backscatter, and it's not scanning
# a privileged port.
thresh_check(report_backscatter, rb_idx, orig,
|distinct_backscatter_peers[orig]|)
)
{
NOTICE([$note=BackscatterSeen, $src=orig,
$p=rev_service,
$identifier=fmt("%s", orig),
$msg=fmt("backscatter seen from %s (%d hosts; %s)",
orig, |distinct_backscatter_peers[orig]|, rev_service)]);
}
if ( ignore_scanners_threshold > 0 &&
|distinct_backscatter_peers[orig]| >
ignore_scanners_threshold )
ignore_addr(orig);
}
else
{ # done with backscatter check
local ignore = F;
if ( orig !in distinct_peers && addr_scan_trigger > 0 )
{
if ( orig !in pre_distinct_peers )
pre_distinct_peers[orig] = set();
add pre_distinct_peers[orig][resp];
if ( |pre_distinct_peers[orig]| < addr_scan_trigger )
ignore = T;
}
if ( ! ignore )
{ # XXXXX
if ( orig !in distinct_peers )
distinct_peers[orig] = set() &mergeable;
if ( resp !in distinct_peers[orig] )
add distinct_peers[orig][resp];
local n = |distinct_peers[orig]|;
# Check for threshold if not outbound.
if ( ! shut_down_thresh_reached[orig] &&
n >= shut_down_thresh &&
! outbound && orig !in Site::neighbor_nets )
{
shut_down_thresh_reached[orig] = T;
local msg = fmt("shutdown threshold reached for %s", orig);
NOTICE([$note=ShutdownThresh, $src=orig,
$identifier=fmt("%s", orig),
$p=service, $msg=msg]);
}
else
{
local address_scan = F;
if ( outbound &&
# inside host scanning out?
thresh_check(report_outbound_peer_scan, rops_idx, orig, n) )
address_scan = T;
if ( ! outbound &&
thresh_check(report_peer_scan, rps_idx, orig, n) )
address_scan = T;
if ( address_scan )
NOTICE([$note=AddressScan,
$src=orig, $p=service,
$n=n,
$identifier=fmt("%s-%d", orig, n),
$msg=fmt("%s has scanned %d hosts (%s)",
orig, n, service)]);
if ( address_scan &&
ignore_scanners_threshold > 0 &&
n > ignore_scanners_threshold )
ignore_addr(orig);
}
}
} # XXXX
}
if ( established )
# Don't consider established connections for port scanning,
# it's too easy to be mislead by FTP-like applications that
# legitimately gobble their way through the port space.
return F;
# Coarse search for port-scanning candidates: those that have made
# connections (attempts) to possible_port_scan_thresh or more
# distinct ports.
if ( orig !in distinct_ports || service !in distinct_ports[orig] )
{
if ( orig !in distinct_ports )
distinct_ports[orig] = set() &mergeable;
if ( service !in distinct_ports[orig] )
add distinct_ports[orig][service];
if ( |distinct_ports[orig]| >= possible_port_scan_thresh &&
orig !in scan_triples )
{
scan_triples[orig] = table() &mergeable;
add possible_scan_sources[orig];
}
}
# Check for low ports.
if ( activate_priv_port_check && ! outbound && service < 1024/tcp &&
service !in troll_skip_service )
{
if ( orig !in distinct_low_ports ||
service !in distinct_low_ports[orig] )
{
if ( orig !in distinct_low_ports )
distinct_low_ports[orig] = set() &mergeable;
add distinct_low_ports[orig][service];
if ( |distinct_low_ports[orig]| == priv_scan_trigger &&
orig !in Site::neighbor_nets )
{
local svrc_msg = fmt("low port trolling %s %s", orig, service);
NOTICE([$note=LowPortTrolling, $src=orig,
$identifier=fmt("%s", orig),
$p=service, $msg=svrc_msg]);
}
if ( ignore_scanners_threshold > 0 &&
|distinct_low_ports[orig]| >
ignore_scanners_threshold )
ignore_addr(orig);
}
}
# For sources that have been identified as possible scan sources,
# keep track of per-host scanning.
if ( orig in possible_scan_sources )
{
if ( orig !in scan_triples )
scan_triples[orig] = table() &mergeable;
if ( resp !in scan_triples[orig] )
scan_triples[orig][resp] = set() &mergeable;
if ( service !in scan_triples[orig][resp] )
{
add scan_triples[orig][resp][service];
if ( thresh_check_2(report_port_scan, rpts_idx,
orig, resp,
|scan_triples[orig][resp]|) )
{
local m = |scan_triples[orig][resp]|;
NOTICE([$note=PortScan, $n=m, $src=orig,
$p=service,
$identifier=fmt("%s-%d", orig, n),
$msg=fmt("%s has scanned %d ports of %s",
orig, m, resp)]);
}
}
}
return T;
}
# Hook into the catch&release dropping. When an address gets restored, we reset
# the source to allow dropping it again.
event Drop::address_restored(a: addr)
{
clear_addr(a);
}
event Drop::address_cleared(a: addr)
{
clear_addr(a);
}
# When removing a possible scan source, we automatically delete its scanned
# hosts and ports. But we do not want the deletion propagated, because every
# peer calls the expire_function on its own (and thus applies the delete
# operation on its own table).
function remove_possible_source(s: set[addr], idx: addr): interval
{
suspend_state_updates();
delete scan_triples[idx];
resume_state_updates();
return 0 secs;
}
# To recognize whether a certain threshhold vector (e.g. report_peer_scans)
# has been transgressed, a global variable containing the next vector index
# (idx) must be incremented. This cumbersome mechanism is necessary because
# values naturally don't increment by one (e.g. replayed table merges).
function thresh_check(v: vector of count, idx: table[addr] of count,
orig: addr, n: count): bool
{
if ( ignore_scanners_threshold > 0 && n > ignore_scanners_threshold )
{
ignore_addr(orig);
return F;
}
if ( idx[orig] <= |v| && n >= v[idx[orig]] )
{
++idx[orig];
return T;
}
else
return F;
}
# Same as above, except the index has a different type signature.
function thresh_check_2(v: vector of count, idx: table[addr, addr] of count,
orig: addr, resp: addr, n: count): bool
{
if ( ignore_scanners_threshold > 0 && n > ignore_scanners_threshold )
{
ignore_addr(orig);
return F;
}
if ( idx[orig,resp] <= |v| && n >= v[idx[orig, resp]] )
{
++idx[orig,resp];
return T;
}
else
return F;
}
event connection_established(c: connection)
{
local is_reverse_scan = (c$orig$state == TCP_INACTIVE);
Scan::check_scan(c, T, is_reverse_scan);
}
event partial_connection(c: connection)
{
Scan::check_scan(c, T, F);
}
event connection_attempt(c: connection)
{
Scan::check_scan(c, F, c$orig$state == TCP_INACTIVE);
}
event connection_half_finished(c: connection)
{
# Half connections never were "established", so do scan-checking here.
Scan::check_scan(c, F, F);
}
event connection_rejected(c: connection)
{
local is_reverse_scan = c$orig$state == TCP_RESET;
Scan::check_scan(c, F, is_reverse_scan);
}
event connection_reset(c: connection)
{
if ( c$orig$state == TCP_INACTIVE || c$resp$state == TCP_INACTIVE )
# We never heard from one side - that looks like a scan.
Scan::check_scan(c, c$orig$size + c$resp$size > 0,
c$orig$state == TCP_INACTIVE);
}
event connection_pending(c: connection)
{
if ( c$orig$state == TCP_PARTIAL && c$resp$state == TCP_INACTIVE )
Scan::check_scan(c, F, F);
}
# Report the remaining entries in the tables.
event bro_done()
{
for ( orig in distinct_peers )
scan_summary(distinct_peers, orig);
for ( orig in distinct_ports )
port_summary(distinct_ports, orig);
for ( orig in distinct_low_ports )
lowport_summary(distinct_low_ports, orig);
}