aoandon 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitattributes +10 -0
- data/.gitignore +19 -0
- data/.rbenv-version +1 -0
- data/Gemfile +5 -0
- data/LICENSE +22 -0
- data/README.md +296 -0
- data/Rakefile +1 -0
- data/aoandon.gemspec +22 -0
- data/bin/aoandon +10 -0
- data/config/rules.yml +54 -0
- data/lib/aoandon.rb +64 -0
- data/lib/aoandon/analysis.rb +11 -0
- data/lib/aoandon/analysis/semantic.rb +26 -0
- data/lib/aoandon/analysis/syntax.rb +155 -0
- data/lib/aoandon/dynamic_rule/less1024.rb +37 -0
- data/lib/aoandon/error/not_implemented_error.rb +4 -0
- data/lib/aoandon/log.rb +16 -0
- data/lib/aoandon/static_rule.rb +16 -0
- data/lib/aoandon/version.rb +3 -0
- metadata +67 -0
data/.gitattributes
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
# Set default behaviour, in case users don't have core.autocrlf set.
|
2
|
+
* text=auto
|
3
|
+
|
4
|
+
# Explicitly declare text files we want to always be normalized and converted
|
5
|
+
# to native line endings on checkout.
|
6
|
+
*.rb text
|
7
|
+
|
8
|
+
# Denote all files that are truly binary and should not be modified.
|
9
|
+
*.png binary
|
10
|
+
*.jpg binary
|
data/.gitignore
ADDED
data/.rbenv-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
1.9.3-p194
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Cyril Wack
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,296 @@
|
|
1
|
+
# Aoandon
|
2
|
+
|
3
|
+
Aoandon (青行燈) is a minimalist network intrusion detection system (NIDS).
|
4
|
+
|
5
|
+
![Blue andon creature](https://raw.github.com/cyril/aoandon/master/blue-andon-creature.jpg)
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'aoandon'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install aoandon
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
Aoandon NIDS is the selective ignoring or alerting of data packets as they pass through its network interface. The criteria that it uses when inspecting packets are based on the Layer 3 (IPv4 and IPv6) and Layer 4 (TCP, UDP) headers. The most often used criteria are source and destination address, source and destination port, and protocol.
|
24
|
+
|
25
|
+
Rules specify the criteria that a packet must match and the resulting action, either pass or alert, that is taken when a match is found. Rules are evaluated in sequential order, first to last. Unless the packet matches a rule containing the `quick` keyword, the packet will be evaluated against all rules before the final action is taken. The last rule to match is the *winner* and will dictate what action to take on the packet. There is an implicit pass all at the beginning of a ruleset meaning that if a packet does not match any rule the resulting action will be pass.
|
26
|
+
|
27
|
+
Both static and dynamic ruleset can be applied to packets.
|
28
|
+
|
29
|
+
### Static ruleset
|
30
|
+
|
31
|
+
Aoandon NIDS reads its configuration rules from `config/rules.yml` at boot time. In order to be able to load rules, this JSON/YAML file must have at least a `rules` key.
|
32
|
+
|
33
|
+
#### Rule syntax
|
34
|
+
|
35
|
+
The general syntax for static rules is:
|
36
|
+
|
37
|
+
1. action
|
38
|
+
2. context
|
39
|
+
3. options
|
40
|
+
|
41
|
+
Where *action* can use as a logger level such as INFO or ERROR that indicate alerts' importance. Note: the `pass` action will ignore the packet back to the kernel for further processing while any other action will react.
|
42
|
+
|
43
|
+
Every *context* params are evaluated for analysis to determine whether a given package matches.
|
44
|
+
|
45
|
+
The last part, *options*, can be:
|
46
|
+
|
47
|
+
* `log`: specifies that the packet should be logged.
|
48
|
+
* `quick`: if a packet matches a rule specifying `quick`, then that rule is considered the last matching rule and the specified action is taken.
|
49
|
+
* `msg`: tells the alerting engine the message to print to an alert.
|
50
|
+
|
51
|
+
#### Default alert
|
52
|
+
|
53
|
+
The recommended practice when setting up a NIDS is to take a "default alert" approach. That is, to alert everything and then selectively allow certain traffic through the interface. This approach is recommended because it errs on the side of caution and also makes writing a ruleset easier.
|
54
|
+
|
55
|
+
To create a default alert sniffer policy, the first rules should be:
|
56
|
+
|
57
|
+
```yaml
|
58
|
+
[ info, {}, {log: true, msg: "Suspected packet!"} ]
|
59
|
+
```
|
60
|
+
|
61
|
+
This will alert all traffic on the given interface in either direction from anywhere to anywhere.
|
62
|
+
|
63
|
+
#### The `quick` keyword
|
64
|
+
|
65
|
+
As indicated earlier, each packet is evaluated against the sniffer ruleset from top to bottom. By default, the packet is marked for passage, which can be changed by any rule, and could be changed back and forth several times before the end of the sniffer rules. The last matching rule *wins*. There is an exception to this: the `quick` option on a sniffing rule has the effect of canceling any further rule processing and causes the specified action to be taken. Let's look at a couple examples:
|
66
|
+
|
67
|
+
Wrong:
|
68
|
+
|
69
|
+
```yaml
|
70
|
+
- [ crit, {proto: tcp, to: {port: 22}}, {msg: "...SSH?", log: true} ]
|
71
|
+
- [ pass, {} ]
|
72
|
+
```
|
73
|
+
|
74
|
+
In this case, the alert line may be evaluated, but will never have any effect, as it is then followed by a line which will ignore everything.
|
75
|
+
|
76
|
+
Better:
|
77
|
+
|
78
|
+
```yaml
|
79
|
+
- [ crit, {proto: tcp, to: {port: 22}}, {msg: "...SSH?", log: true, quick: true} ]
|
80
|
+
- [ pass, {} ]
|
81
|
+
```
|
82
|
+
|
83
|
+
These rules are evaluated a little differently. If the alert line is matched, due to the `quick` option, the packet will be reported, and the rest of the ruleset will be ignored.
|
84
|
+
|
85
|
+
#### Ruleset example
|
86
|
+
|
87
|
+
```yaml
|
88
|
+
hosts:
|
89
|
+
- &honeypots [ 192.168.1.4, 192.168.1.9 ]
|
90
|
+
- &my_station 192.168.1.38
|
91
|
+
|
92
|
+
rules:
|
93
|
+
# "default alert" approach
|
94
|
+
- [ info, {}, {log: true, msg: "Suspected packet!"} ]
|
95
|
+
|
96
|
+
# then, selectively ignore certain traffic
|
97
|
+
- [ warn, {to: {addr: *honeypots}}, {msg: "Touché.", quick: true, log: true} ]
|
98
|
+
- [ pass, {from: {addr: *my_station}} ]
|
99
|
+
- [ pass, {to: {addr: *my_station}} ]
|
100
|
+
- [ pass, {to: {addr: '224.0.0.1'}} ]
|
101
|
+
```
|
102
|
+
|
103
|
+
#### A more complete ruleset example
|
104
|
+
|
105
|
+
```yaml
|
106
|
+
macros:
|
107
|
+
web_server: &web_server
|
108
|
+
114.21.70.71
|
109
|
+
gateway: &gw
|
110
|
+
192.168.0.1
|
111
|
+
|
112
|
+
tables:
|
113
|
+
redzone: &redzone
|
114
|
+
- "81.15.142.23"
|
115
|
+
hacker: &id001
|
116
|
+
- 81.15.142.23
|
117
|
+
- 42.154.25.213
|
118
|
+
blacklist: &blacklist
|
119
|
+
- *id001
|
120
|
+
- *gw
|
121
|
+
- 81.15.142.23
|
122
|
+
- "64.81.240.57"
|
123
|
+
unknown:
|
124
|
+
- any
|
125
|
+
mz: &mz
|
126
|
+
192.168.0.201
|
127
|
+
dmz: &dmz
|
128
|
+
sql_server: &sql_server
|
129
|
+
10.0.0.2
|
130
|
+
|
131
|
+
ports:
|
132
|
+
web: &www
|
133
|
+
- 80
|
134
|
+
- 443
|
135
|
+
p2p:
|
136
|
+
- 63192
|
137
|
+
|
138
|
+
messages:
|
139
|
+
- &msg001 "ICMP packet from Google to MZ"
|
140
|
+
- &msg002 "MZ intrusion detected!"
|
141
|
+
|
142
|
+
rules:
|
143
|
+
# "default alert" approach
|
144
|
+
- [ info, {}, {quick: true, log: true, msg: "Suspected packet!"} ]
|
145
|
+
|
146
|
+
# then, selectively ignore certain traffic
|
147
|
+
- [ pass, {af: inet, from: {addr: any}, to: {addr: any}} ]
|
148
|
+
- [ warn, {proto: tcp, from: {addr: *blacklist}, to: {addr: any, port: *www}, flags: syn} ]
|
149
|
+
- [ warn, {proto: tcp, from: {addr: any, port: 123}, to: {addr: *dmz}} ]
|
150
|
+
- [ crit, {af: inet6, from: {addr: any}, to: {addr: any}}, {log: true} ]
|
151
|
+
- [ pass, {af: inet, proto: tcp, from: {addr: *mz}, to: {addr: *web_server, port: *www}, {quick: true}} ]
|
152
|
+
- [ warn, {proto: udp, from: {addr: *redzone}, to: {addr: 10.1.0.32, port: 21}} ]
|
153
|
+
- [ info, {proto: tcp, from: {addr: 172.16.0.6}, to: {addr: 192.168.0.14, port: 22}} ]
|
154
|
+
- [ crit, {proto: tcp, from: {addr: *blacklist}, to: {addr: *mz}}, {log: true, msg: *msg002} ]
|
155
|
+
- [ info, {proto: tcp, to: {addr: 192.168.0.14, port: 22}} ]
|
156
|
+
- [ pass, {proto: tcp, from: {addr: *id001}, to: {addr: *sql_server, port: 3306}} ]
|
157
|
+
- [ info, {af: inet, proto: icmp, from: {addr: google.com}, to: {addr: *mz}}, {log: true, msg: *msg001} ]
|
158
|
+
```
|
159
|
+
|
160
|
+
### Dynamic ruleset
|
161
|
+
|
162
|
+
Some semantic analysis can also be done through Aoandon NIDS extensions, using modules such as:
|
163
|
+
|
164
|
+
```ruby
|
165
|
+
# lib/aoandon/dynamic_rule/less1024.rb
|
166
|
+
module Aoandon
|
167
|
+
module DynamicRule
|
168
|
+
module Less1024
|
169
|
+
MESSAGE = 'Port numbers < 1024'
|
170
|
+
PROTO_TCP = 6
|
171
|
+
PROTO_UDP = 17
|
172
|
+
WELL_KNOWN_PORTS = (0..1023)
|
173
|
+
|
174
|
+
def self.control?(packet)
|
175
|
+
(tcp?(packet) || (udp?(packet) && different_ports?(packet.sport, packet.dport))) &&
|
176
|
+
less_1024?(packet.sport) && less_1024?(packet.dport)
|
177
|
+
end
|
178
|
+
|
179
|
+
def self.logging?(packet)
|
180
|
+
false
|
181
|
+
end
|
182
|
+
|
183
|
+
private
|
184
|
+
|
185
|
+
def self.different_ports?(src_port, dst_port)
|
186
|
+
src_port != dst_port
|
187
|
+
end
|
188
|
+
|
189
|
+
def self.less_1024?(port)
|
190
|
+
WELL_KNOWN_PORTS.include?(port)
|
191
|
+
end
|
192
|
+
|
193
|
+
def self.tcp?(packet)
|
194
|
+
packet.ip_proto == PROTO_TCP
|
195
|
+
end
|
196
|
+
|
197
|
+
def self.udp?(packet)
|
198
|
+
packet.ip_proto == PROTO_UDP
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
202
|
+
end
|
203
|
+
```
|
204
|
+
|
205
|
+
```ruby
|
206
|
+
# lib/aoandon/dynamic_rule/more_fragments.rb
|
207
|
+
module Aoandon
|
208
|
+
module DynamicRule
|
209
|
+
module MoreFragments
|
210
|
+
MESSAGE = 'More Fragment bit is set'
|
211
|
+
|
212
|
+
def self.control?(packet)
|
213
|
+
packet.ip_mf?
|
214
|
+
end
|
215
|
+
|
216
|
+
def self.logging?(packet)
|
217
|
+
false
|
218
|
+
end
|
219
|
+
end
|
220
|
+
end
|
221
|
+
end
|
222
|
+
```
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
# lib/aoandon/dynamic_rule/same_ip.rb
|
226
|
+
module Aoandon
|
227
|
+
module DynamicRule
|
228
|
+
module SameIp
|
229
|
+
LOCALHOST = '127.0.0.1'
|
230
|
+
MESSAGE = 'Same IP'
|
231
|
+
|
232
|
+
def self.control?(packet)
|
233
|
+
packet.ip_src == packet.ip_dst && !loopback?(packet.ip_src)
|
234
|
+
end
|
235
|
+
|
236
|
+
def self.logging?(packet)
|
237
|
+
false
|
238
|
+
end
|
239
|
+
|
240
|
+
private
|
241
|
+
|
242
|
+
def self.loopback?(ip_addr)
|
243
|
+
ip_addr.to_num_s == LOCALHOST
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
```
|
249
|
+
|
250
|
+
```ruby
|
251
|
+
# lib/aoandon/dynamic_rule/syn_flood.rb
|
252
|
+
module Aoandon
|
253
|
+
module DynamicRule
|
254
|
+
module SynFlood
|
255
|
+
BUFFER = 20
|
256
|
+
MESSAGE = 'SYN flood attack'
|
257
|
+
PROTO_TCP = 6
|
258
|
+
|
259
|
+
def self.control?(packet)
|
260
|
+
tcp?(packet) && fifo!(packet.tcp_syn?) && packet.tcp_syn? && overflow?
|
261
|
+
end
|
262
|
+
|
263
|
+
def self.logging?(packet)
|
264
|
+
false
|
265
|
+
end
|
266
|
+
|
267
|
+
private
|
268
|
+
|
269
|
+
def self.fifo!(input)
|
270
|
+
stack << input
|
271
|
+
stack.shift
|
272
|
+
end
|
273
|
+
|
274
|
+
def self.overflow?
|
275
|
+
stack == [true] * BUFFER
|
276
|
+
end
|
277
|
+
|
278
|
+
def self.stack
|
279
|
+
@syn_flood_stack ||= [false] * BUFFER
|
280
|
+
end
|
281
|
+
|
282
|
+
def self.tcp?(packet)
|
283
|
+
packet.ip_proto == PROTO_TCP
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
```
|
289
|
+
|
290
|
+
## Contributing
|
291
|
+
|
292
|
+
1. Fork it
|
293
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
294
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
295
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
296
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'bundler/gem_tasks'
|
data/aoandon.gemspec
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'aoandon/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.name = 'aoandon'
|
8
|
+
gem.version = Aoandon::VERSION
|
9
|
+
gem.authors = ['Cyril Wack']
|
10
|
+
gem.email = ['contact@cyril.io']
|
11
|
+
gem.description = %q{Aoandon (青行燈) is a minimalist network intrusion detection system (NIDS).}
|
12
|
+
gem.summary = %q{Minimalist network intrusion detection system (NIDS).}
|
13
|
+
gem.homepage = 'http://cyril.io'
|
14
|
+
gem.license = 'MIT'
|
15
|
+
|
16
|
+
gem.bindir = 'bin'
|
17
|
+
|
18
|
+
gem.files = `git ls-files`.split($/).reject {|f| f == 'blue-andon-creature.jpg' }
|
19
|
+
gem.executables = gem.files.grep(%r{^bin/}).map {|f| File.basename(f) }
|
20
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
21
|
+
gem.require_paths = ['lib', 'config']
|
22
|
+
end
|
data/bin/aoandon
ADDED
data/config/rules.yml
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
# Aoandon NIDS configuration file
|
2
|
+
---
|
3
|
+
#macros:
|
4
|
+
# web_server: &web_server
|
5
|
+
# 114.21.70.71
|
6
|
+
# gateway: &gw
|
7
|
+
# 192.168.0.1
|
8
|
+
|
9
|
+
#tables:
|
10
|
+
# redzone: &redzone
|
11
|
+
# - "81.15.142.23"
|
12
|
+
# hacker: &id001
|
13
|
+
# - 81.15.142.23
|
14
|
+
# - 42.154.25.213
|
15
|
+
# blacklist: &blacklist
|
16
|
+
# - *id001
|
17
|
+
# - *gw
|
18
|
+
# - 81.15.142.23
|
19
|
+
# - "64.81.240.57"
|
20
|
+
# unknown:
|
21
|
+
# - any
|
22
|
+
# mz: &mz
|
23
|
+
# 192.168.0.201
|
24
|
+
# dmz: &dmz
|
25
|
+
# sql_server: &sql_server
|
26
|
+
# 10.0.0.2
|
27
|
+
|
28
|
+
#ports:
|
29
|
+
# web: &www
|
30
|
+
# - 80
|
31
|
+
# - 443
|
32
|
+
# p2p:
|
33
|
+
# - 63192
|
34
|
+
|
35
|
+
#messages:
|
36
|
+
# - &msg001 "ICMP packet from Google to MZ"
|
37
|
+
# - &msg002 "MZ intrusion detected!"
|
38
|
+
|
39
|
+
rules:
|
40
|
+
# # "default alert" approach
|
41
|
+
# - [ info, {}, {quick: true, log: true, msg: "Suspected packet!"} ]
|
42
|
+
#
|
43
|
+
# # then, selectively ignore certain traffic
|
44
|
+
# - [ pass, {af: inet, from: {addr: any}, to: {addr: any}} ]
|
45
|
+
# - [ warn, {proto: tcp, from: {addr: *blacklist}, to: {addr: any, port: *www}, flags: syn} ]
|
46
|
+
# - [ warn, {proto: tcp, from: {addr: any, port: 123}, to: {addr: *dmz}} ]
|
47
|
+
# - [ crit, {af: inet6, from: {addr: any}, to: {addr: any}}, {log: true} ]
|
48
|
+
# - [ pass, {af: inet, proto: tcp, from: {addr: *mz}, to: {addr: *web_server, port: *www}, {quick: true}} ]
|
49
|
+
# - [ warn, {proto: udp, from: {addr: *redzone}, to: {addr: 10.1.0.32, port: 21}} ]
|
50
|
+
# - [ info, {proto: tcp, from: {addr: 172.16.0.6}, to: {addr: 192.168.0.14, port: 22}} ]
|
51
|
+
# - [ crit, {proto: tcp, from: {addr: *blacklist}, to: {addr: *mz}}, {log: true, msg: *msg002} ]
|
52
|
+
# - [ info, {proto: tcp, to: {addr: 192.168.0.14, port: 22}} ]
|
53
|
+
# - [ pass, {proto: tcp, from: {addr: *id001}, to: {addr: *sql_server, port: 3306}} ]
|
54
|
+
# - [ info, {af: inet, proto: icmp, from: {addr: google.com}, to: {addr: *mz}}, {log: true, msg: *msg001} ]
|
data/lib/aoandon.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
require_relative 'aoandon/analysis'
|
2
|
+
require_relative 'aoandon/analysis/semantic'
|
3
|
+
require_relative 'aoandon/analysis/syntax'
|
4
|
+
require_relative 'aoandon/error/not_implemented_error'
|
5
|
+
require_relative 'aoandon/log'
|
6
|
+
require_relative 'aoandon/static_rule'
|
7
|
+
require_relative 'aoandon/version'
|
8
|
+
|
9
|
+
Dir['lib/aoandon/dynamic_rule/*.rb'].each do |src|
|
10
|
+
load src
|
11
|
+
end
|
12
|
+
|
13
|
+
module Aoandon
|
14
|
+
class Nids
|
15
|
+
CONF_PATH = 'config/rules.yml'
|
16
|
+
|
17
|
+
def initialize
|
18
|
+
options = Nids.parse
|
19
|
+
options[:file] = CONF_PATH unless options[:file]
|
20
|
+
options[:interface] = Pcap.lookupdev unless options[:interface]
|
21
|
+
puts "Starting Aoandon NIDS on interface #{options[:interface]}..."
|
22
|
+
log = Log.new(options[:verbose])
|
23
|
+
@syntax = Syntax.new(log, {file: options[:file]})
|
24
|
+
@semantic = Semantic.new(log)
|
25
|
+
@network_interface = Pcap::Capture.open_live(options[:interface])
|
26
|
+
end
|
27
|
+
|
28
|
+
def run
|
29
|
+
puts 'You can stop Aoandon NIDS by pressing Ctrl-C.'
|
30
|
+
|
31
|
+
@network_interface.each_packet do |packet|
|
32
|
+
if packet.ip?
|
33
|
+
@semantic.test(packet)
|
34
|
+
@syntax.test(packet)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@network_interface.close
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.parse
|
42
|
+
options = {}
|
43
|
+
|
44
|
+
OptionParser.new do |opts|
|
45
|
+
opts.banner = "Usage: #$0 [options]"
|
46
|
+
opts.on('-f', '--file <path>', 'Load the rules contained in file <path>.') {|f| options[:file] = f }
|
47
|
+
opts.on('-h', '--help', 'Help.') { puts opts; exit }
|
48
|
+
opts.on('-i', '--interface <if>', 'Sniff on network interface <if>.') {|i| options[:interface] = i }
|
49
|
+
opts.on('-v', '--verbose', 'Produce more verbose output.') { options[:verbose] = true }
|
50
|
+
opts.on('-V', '--version', 'Show the version number and exit.') { version; exit }
|
51
|
+
end.parse!
|
52
|
+
|
53
|
+
options
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.version
|
57
|
+
puts "Aoandon #{VERSION}"
|
58
|
+
end
|
59
|
+
|
60
|
+
trap('INT') { exit }
|
61
|
+
at_exit { print 'Stopping Aoandon NIDS... ' }
|
62
|
+
ObjectSpace.define_finalizer('string', proc { puts 'done.' })
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Aoandon
|
2
|
+
class Semantic < Analysis
|
3
|
+
def initialize(logger, options = {})
|
4
|
+
super(logger, options)
|
5
|
+
|
6
|
+
puts "Modules: #{DynamicRule.constants.join(', ')}"
|
7
|
+
end
|
8
|
+
|
9
|
+
def test(packet)
|
10
|
+
if defined? DynamicRule
|
11
|
+
DynamicRule.constants.each do |rule|
|
12
|
+
if DynamicRule.const_get(rule).control?(packet)
|
13
|
+
dump = DynamicRule.const_get(rule).logging?(packet) ? packet : nil
|
14
|
+
message = if DynamicRule.const_get(rule).constants.include?(:MESSAGE)
|
15
|
+
DynamicRule.const_get(rule)::MESSAGE
|
16
|
+
else
|
17
|
+
nil
|
18
|
+
end
|
19
|
+
|
20
|
+
@logger.message(packet.time.iso8601, 'SEMANT', rule.downcase, message, dump)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Aoandon
|
2
|
+
class Syntax < Analysis
|
3
|
+
def initialize(logger, options = {})
|
4
|
+
super(logger, options)
|
5
|
+
|
6
|
+
abort("Configuration file not found: #{options[:file]}") unless File.exist?(options[:file])
|
7
|
+
@rules = Array(YAML::load_file(options[:file])['rules']).map {|rule| StaticRule.new(*rule) }
|
8
|
+
|
9
|
+
puts "Ruleset: #{File.expand_path(options[:file])}"
|
10
|
+
end
|
11
|
+
|
12
|
+
def test(packet)
|
13
|
+
@rules.each do |rule|
|
14
|
+
if match?(packet, rule.context)
|
15
|
+
break if (@last_rule = rule).options['quick']
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
if @last_rule && @last_rule.action != 'pass'
|
20
|
+
message = @last_rule.options['msg'] || 'Bad packet detected!'
|
21
|
+
dump = @last_rule.options['log'] ? packet : nil
|
22
|
+
@logger.message(packet.time.iso8601, 'SYNTAX', @last_rule.action, message, dump)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
protected
|
27
|
+
|
28
|
+
def match?(packet, network_context)
|
29
|
+
network_context.update({'af' => af2id(packet.ip_ver)}) unless network_context.has_key?('af')
|
30
|
+
match_proto?(packet, network_context) if packet.ip_ver == af(network_context.fetch('af'))
|
31
|
+
end
|
32
|
+
|
33
|
+
def af2id(af)
|
34
|
+
if af == 4
|
35
|
+
'inet'
|
36
|
+
elsif af == 6
|
37
|
+
'inet6'
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def af(name)
|
42
|
+
if name.to_sym == :inet
|
43
|
+
4
|
44
|
+
elsif name.to_sym == :inet6
|
45
|
+
6
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def match_proto?(packet, network_context)
|
50
|
+
if network_context['proto']
|
51
|
+
if packet.ip_proto == proto(network_context['proto'])
|
52
|
+
if packet.ip_proto == 1
|
53
|
+
match_proto_icmp?(packet, network_context)
|
54
|
+
elsif packet.ip_proto == 6
|
55
|
+
match_proto_tcp?(packet, network_context)
|
56
|
+
elsif packet.ip_proto == 17
|
57
|
+
match_proto_udp?(packet, network_context)
|
58
|
+
elsif packet.ip_proto == 58
|
59
|
+
match_proto_icmp6?(packet, network_context)
|
60
|
+
else
|
61
|
+
match_addr?(packet, network_context)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
else
|
65
|
+
match_addr?(packet, network_context)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def proto(name)
|
70
|
+
if name.to_sym == :icmp
|
71
|
+
1
|
72
|
+
elsif name.to_sym == :icmp6
|
73
|
+
58
|
74
|
+
elsif name.to_sym == :tcp
|
75
|
+
6
|
76
|
+
elsif name.to_sym == :udp
|
77
|
+
17
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def match_proto_icmp?(packet, network_context)
|
82
|
+
match_addr?(packet, network_context)
|
83
|
+
end
|
84
|
+
|
85
|
+
def match_proto_icmp6?(packet, network_context)
|
86
|
+
match_proto_icmp?(packet, network_context)
|
87
|
+
end
|
88
|
+
|
89
|
+
def match_addr?(packet, network_context)
|
90
|
+
result = true
|
91
|
+
|
92
|
+
[['from', 'src'], ['to', 'dst']].each do |way, obj|
|
93
|
+
unless network_context[way].fetch('addr') == 'any'
|
94
|
+
result = result && refer2addr?((packet.send(obj)), network_context[way].fetch('addr'))
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
result
|
99
|
+
end
|
100
|
+
|
101
|
+
def match_port?(packet, network_context)
|
102
|
+
result = true
|
103
|
+
|
104
|
+
[['from', 'sport'], ['to', 'dport']].each do |way, obj|
|
105
|
+
if network_context[way].has_key?('port')
|
106
|
+
result = result && refer2port?((packet.send(obj)).to_i, network_context[way].fetch('port'))
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
result
|
111
|
+
end
|
112
|
+
|
113
|
+
def match_flag?(packet, network_context)
|
114
|
+
return true unless network_context['flags']
|
115
|
+
|
116
|
+
network_context['flags'].each do |flag|
|
117
|
+
return true if packet.send("tcp_#{flag}?")
|
118
|
+
end
|
119
|
+
|
120
|
+
false
|
121
|
+
end
|
122
|
+
|
123
|
+
def match_proto_tcp?(packet, network_context)
|
124
|
+
match_proto_udp?(packet, network_context) && match_flag?(packet, network_context)
|
125
|
+
end
|
126
|
+
|
127
|
+
def match_proto_udp?(packet, network_context)
|
128
|
+
match_addr?(packet, network_context) && match_port?(packet, network_context)
|
129
|
+
end
|
130
|
+
|
131
|
+
def refer2addr?(addr, pattern)
|
132
|
+
if pattern.is_a? Array
|
133
|
+
pattern.include?(addr.to_num_s) || pattern.include?(addr.hostname)
|
134
|
+
elsif pattern.is_a? Hash
|
135
|
+
pattern.has_key?(addr.to_num_s) || pattern.has_key?(addr.hostname)
|
136
|
+
elsif pattern.is_a? String
|
137
|
+
addr.to_num_s == pattern || addr.hostname == pattern
|
138
|
+
else
|
139
|
+
false
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def refer2port?(number, pattern)
|
144
|
+
if pattern.is_a? Array
|
145
|
+
pattern.include?(number)
|
146
|
+
elsif pattern.is_a? Hash
|
147
|
+
pattern.has_key?(number)
|
148
|
+
elsif pattern.is_a? Fixnum
|
149
|
+
number == pattern
|
150
|
+
else
|
151
|
+
false
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Aoandon
|
2
|
+
module DynamicRule
|
3
|
+
module Less1024
|
4
|
+
MESSAGE = 'Port numbers < 1024'
|
5
|
+
PROTO_TCP = 6
|
6
|
+
PROTO_UDP = 17
|
7
|
+
WELL_KNOWN_PORTS = (0..1023)
|
8
|
+
|
9
|
+
def self.control?(packet)
|
10
|
+
(tcp?(packet) || (udp?(packet) && different_ports?(packet.sport, packet.dport))) &&
|
11
|
+
less_1024?(packet.sport) && less_1024?(packet.dport)
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.logging?(packet)
|
15
|
+
false
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def self.different_ports?(src_port, dst_port)
|
21
|
+
src_port != dst_port
|
22
|
+
end
|
23
|
+
|
24
|
+
def self.less_1024?(port)
|
25
|
+
WELL_KNOWN_PORTS.include?(port)
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.tcp?(packet)
|
29
|
+
packet.ip_proto == PROTO_TCP
|
30
|
+
end
|
31
|
+
|
32
|
+
def self.udp?(packet)
|
33
|
+
packet.ip_proto == PROTO_UDP
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/aoandon/log.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
module Aoandon
|
2
|
+
class Log
|
3
|
+
def initialize(verbose = false)
|
4
|
+
@file = File.open('log/aoandon.yml', 'a')
|
5
|
+
@verbose = verbose
|
6
|
+
|
7
|
+
puts "Log file: #{File.expand_path(@file.path)}"
|
8
|
+
end
|
9
|
+
|
10
|
+
def message(*args)
|
11
|
+
puts args.compact.map(&:to_s).join(' | ') if @verbose
|
12
|
+
@file.puts "- #{args.compact.map(&:to_s)}"
|
13
|
+
@file.flush
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module Aoandon
|
2
|
+
class StaticRule < Struct.new(:action, :context, :options)
|
3
|
+
def initialize(*args)
|
4
|
+
super(*args)
|
5
|
+
|
6
|
+
self.context['from'] ||= {'addr' => 'any'}
|
7
|
+
self.context['to' ] ||= {'addr' => 'any'}
|
8
|
+
|
9
|
+
self.context['from'].update('addr' => 'any') unless self.context['from']['addr']
|
10
|
+
self.context['to' ].update('addr' => 'any') unless self.context['to' ]['addr']
|
11
|
+
|
12
|
+
self.options ||= {}
|
13
|
+
self.options.update('log' => false) unless self.options.has_key?('log')
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
metadata
ADDED
@@ -0,0 +1,67 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: aoandon
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Cyril Wack
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-09-16 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Aoandon (青行燈) is a minimalist network intrusion detection system (NIDS).
|
15
|
+
email:
|
16
|
+
- contact@cyril.io
|
17
|
+
executables:
|
18
|
+
- aoandon
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitattributes
|
23
|
+
- .gitignore
|
24
|
+
- .rbenv-version
|
25
|
+
- Gemfile
|
26
|
+
- LICENSE
|
27
|
+
- README.md
|
28
|
+
- Rakefile
|
29
|
+
- aoandon.gemspec
|
30
|
+
- bin/aoandon
|
31
|
+
- config/rules.yml
|
32
|
+
- lib/aoandon.rb
|
33
|
+
- lib/aoandon/analysis.rb
|
34
|
+
- lib/aoandon/analysis/semantic.rb
|
35
|
+
- lib/aoandon/analysis/syntax.rb
|
36
|
+
- lib/aoandon/dynamic_rule/less1024.rb
|
37
|
+
- lib/aoandon/error/not_implemented_error.rb
|
38
|
+
- lib/aoandon/log.rb
|
39
|
+
- lib/aoandon/static_rule.rb
|
40
|
+
- lib/aoandon/version.rb
|
41
|
+
homepage: http://cyril.io
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
post_install_message:
|
45
|
+
rdoc_options: []
|
46
|
+
require_paths:
|
47
|
+
- lib
|
48
|
+
- config
|
49
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
50
|
+
none: false
|
51
|
+
requirements:
|
52
|
+
- - ! '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
none: false
|
57
|
+
requirements:
|
58
|
+
- - ! '>='
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: '0'
|
61
|
+
requirements: []
|
62
|
+
rubyforge_project:
|
63
|
+
rubygems_version: 1.8.23
|
64
|
+
signing_key:
|
65
|
+
specification_version: 3
|
66
|
+
summary: Minimalist network intrusion detection system (NIDS).
|
67
|
+
test_files: []
|