frr-cli-fuzzer 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +28 -0
- data/Rakefile +2 -0
- data/bin/frr-cli-fuzzer +45 -0
- data/bin/setup +8 -0
- data/config.yml +215 -0
- data/frr-cli-fuzzer.gemspec +23 -0
- data/lib/frr-cli-fuzzer/libc.rb +29 -0
- data/lib/frr-cli-fuzzer/linux_namespace.rb +69 -0
- data/lib/frr-cli-fuzzer/version.rb +3 -0
- data/lib/frr-cli-fuzzer.rb +275 -0
- metadata +100 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 378f404d19c9beb961473414202aa8f24e5faad8
|
4
|
+
data.tar.gz: 5a5aff0028114fc40add71aba2a389e555c3c0e1
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 55459c77035f9d77cbe35218e20c9594bb53bb8424885329e2c89f394ca72dc105f5085c26b136255a4926120a847764dabc94e044848b30d689f75788db9c14
|
7
|
+
data.tar.gz: 513b03ec3aa21574310abd7663d5c2d9536d6254df5ef3c616febdb1b86b624a28664634ccfae9a7a45f474c3e80a02e7812ee07168d22cabfbd0a4d5a786f21
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Renato Westphal
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
# FrrCliFuzzer
|
2
|
+
|
3
|
+
## Installation
|
4
|
+
|
5
|
+
After checking out the repo, run `bin/setup` to install the dependencies (currently, only the _ffi_ gem).
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
|
9
|
+
Edit _config.yml_ as desired. Run the CLI fuzzer using the following command:
|
10
|
+
```sh
|
11
|
+
./bin/frr-cli-fuzzer config.yml
|
12
|
+
```
|
13
|
+
|
14
|
+
Once the tests complete, the results are displayed in the standard output like this:
|
15
|
+
```
|
16
|
+
Results:
|
17
|
+
- Non-filtered commands: 465
|
18
|
+
- Whitelist filtered commands: 0
|
19
|
+
- Blacklist filtered commands: 20
|
20
|
+
- Tested commands: 465
|
21
|
+
- Segfaults detected: 6
|
22
|
+
```
|
23
|
+
|
24
|
+
More details about which commands caused which segmentation faults are available in the _output.txt_ file.
|
25
|
+
|
26
|
+
## Contributing
|
27
|
+
|
28
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/rwestphal/frr-cli-fuzzer.
|
data/Rakefile
ADDED
data/bin/frr-cli-fuzzer
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
$VERBOSE = true
|
3
|
+
|
4
|
+
require 'yaml'
|
5
|
+
require_relative '../lib/frr-cli-fuzzer.rb'
|
6
|
+
|
7
|
+
# Signal handler.
|
8
|
+
trap('INT') do
|
9
|
+
FrrCliFuzzer.print_results
|
10
|
+
exit(0)
|
11
|
+
end
|
12
|
+
|
13
|
+
# Parse command-line options.
|
14
|
+
if ARGV.size != 1
|
15
|
+
puts "Usage: #{File.basename(__FILE__)} config.yml"
|
16
|
+
exit(1)
|
17
|
+
end
|
18
|
+
config_file = ARGV[0]
|
19
|
+
|
20
|
+
# Read configuration file.
|
21
|
+
begin
|
22
|
+
config = YAML.load_file(config_file)
|
23
|
+
rescue SystemCallError, Psych::SyntaxError, ArgumentError => e
|
24
|
+
warn e.message
|
25
|
+
warn e.backtrace
|
26
|
+
exit(1)
|
27
|
+
end
|
28
|
+
|
29
|
+
FrrCliFuzzer::LibC.prctl(FrrCliFuzzer::LibC::PR_SET_CHILD_SUBREAPER, 1, 0, 0, 0)
|
30
|
+
|
31
|
+
# Start fuzzer and print the results when we're done.
|
32
|
+
FrrCliFuzzer.init(iterations: config.dig('fuzzer', 'iterations'),
|
33
|
+
random_order: config.dig('fuzzer', 'random-order'),
|
34
|
+
runstatedir: config.dig('fuzzer', 'runstatedir'),
|
35
|
+
frr_build_parameters: config['frr-build-parameters'],
|
36
|
+
daemons: config['daemons'],
|
37
|
+
configs: config['configs'],
|
38
|
+
nodes: config['nodes'],
|
39
|
+
regexps: config['regexps'],
|
40
|
+
whitelist: config['whitelist'],
|
41
|
+
blacklist: config['blacklist'])
|
42
|
+
FrrCliFuzzer.gen_configs
|
43
|
+
FrrCliFuzzer.start_daemons
|
44
|
+
FrrCliFuzzer.test_fuzzing
|
45
|
+
FrrCliFuzzer.print_results
|
data/bin/setup
ADDED
data/config.yml
ADDED
@@ -0,0 +1,215 @@
|
|
1
|
+
---
|
2
|
+
|
3
|
+
fuzzer:
|
4
|
+
iterations: 0
|
5
|
+
random-order: true
|
6
|
+
runstatedir: "/tmp/frr-cli-fuzzer"
|
7
|
+
|
8
|
+
frr-build-parameters:
|
9
|
+
# FRR's sysconfdir (--sysconfdir).
|
10
|
+
sysconfdir: "/etc/frr"
|
11
|
+
|
12
|
+
# FRR's localstatedir (--localstatedir).
|
13
|
+
localstatedir: "/var/run/frr"
|
14
|
+
|
15
|
+
# FRR's user (--enable-user).
|
16
|
+
user: "frr"
|
17
|
+
|
18
|
+
# FRR's group (--enable-group).
|
19
|
+
group: "frr"
|
20
|
+
|
21
|
+
daemons:
|
22
|
+
- zebra
|
23
|
+
#- bgpd
|
24
|
+
- ospfd
|
25
|
+
- ospf6d
|
26
|
+
- isisd
|
27
|
+
- fabricd
|
28
|
+
- ripd
|
29
|
+
- ripngd
|
30
|
+
- eigrpd
|
31
|
+
- pimd
|
32
|
+
- ldpd
|
33
|
+
- nhrpd
|
34
|
+
- babeld
|
35
|
+
#- bfdd
|
36
|
+
- pbrd
|
37
|
+
- staticd
|
38
|
+
- sharpd
|
39
|
+
|
40
|
+
regexps:
|
41
|
+
A.B.C.D/M: "1.1.1.1/32"
|
42
|
+
A.B.C.D: "1.1.1.1"
|
43
|
+
X:X::X:X/M: "2001:db8::1/128"
|
44
|
+
X:X::X:X: "2001:db8::1"
|
45
|
+
INTERFACE: "eth99"
|
46
|
+
IFNAME: "eth99"
|
47
|
+
AA:BB:CC...: "1:1:1 2:2:2"
|
48
|
+
AA:BB:CC: "1:1:1"
|
49
|
+
AA:NN...: "1:1 2:2"
|
50
|
+
AA:NN: "1:1"
|
51
|
+
ASN:nn_or_IP-address:nn: "1:1"
|
52
|
+
ASN:NN_OR_IP-ADDRESS:NN: "1:1"
|
53
|
+
RTLIST...: "1:1 2:2"
|
54
|
+
YY:YY:YY:YY:YY:YY: "11:11:11:11:11:11"
|
55
|
+
MAC: "11:11:11:11:11:11"
|
56
|
+
M:A:C: "11:11:11:11:11:11"
|
57
|
+
M:A:C/M: "11:11:11:11:11:11/48"
|
58
|
+
HH:MM:SS: "10:10:10"
|
59
|
+
MONTH: "January"
|
60
|
+
BANDWIDTH: "1000"
|
61
|
+
PERCENTAGE: "50"
|
62
|
+
|
63
|
+
whitelist:
|
64
|
+
#- ^show (ip|ipv6)
|
65
|
+
#- redistribute
|
66
|
+
|
67
|
+
blacklist:
|
68
|
+
- output file
|
69
|
+
- ^write
|
70
|
+
- ^copy
|
71
|
+
- ^list
|
72
|
+
- ^find
|
73
|
+
- ^exit
|
74
|
+
- ^quit
|
75
|
+
- ^end
|
76
|
+
- ^(no )?(ip|ipv6) route
|
77
|
+
- ^show (ip|ipv6) (route|fib)
|
78
|
+
- ^no router bgp
|
79
|
+
- ^no neighbor (A.B.C.D|X:X::X:X|WORD)$
|
80
|
+
- ^no neighbor (A.B.C.D|X:X::X:X|WORD) remote-as
|
81
|
+
#- ospf
|
82
|
+
#- ^(no )?debug
|
83
|
+
|
84
|
+
nodes:
|
85
|
+
#- ""
|
86
|
+
- -c "configure terminal"
|
87
|
+
- -c "configure terminal" -c "interface eth99"
|
88
|
+
- -c "configure terminal" -c "interface eth99" -c "link-params"
|
89
|
+
- -c "configure terminal" -c "route-map RMAP permit 1"
|
90
|
+
#- -c "configure terminal" -c "router bgp 1"
|
91
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 unicast"
|
92
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 multicast"
|
93
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 vpn"
|
94
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 labeled-unicast"
|
95
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv4 flowspec"
|
96
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 unicast"
|
97
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 multicast"
|
98
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 vpn"
|
99
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 labeled-unicast"
|
100
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family ipv6 flowspec"
|
101
|
+
#- -c "configure terminal" -c "router bgp 1" -c "address-family l2vpn evpn"
|
102
|
+
#- -c "configure terminal" -c "router bgp 1" -c "vnc defaults"
|
103
|
+
#- -c "configure terminal" -c "router bgp 1" -c "vnc nve-group NAME"
|
104
|
+
#- -c "configure terminal" -c "router bgp 1" -c "vnc l2-group NAME"
|
105
|
+
#- -c "configure terminal" -c "router bgp 1" -c "vrf-policy NAME"
|
106
|
+
- -c "configure terminal" -c "key chain WORD"
|
107
|
+
- -c "configure terminal" -c "key chain WORD" -c "key 255"
|
108
|
+
- -c "configure terminal" -c "router babel"
|
109
|
+
- -c "configure terminal" -c "router ospf"
|
110
|
+
- -c "configure terminal" -c "router ospf6"
|
111
|
+
- -c "configure terminal" -c "router isis 1"
|
112
|
+
- -c "configure terminal" -c "router openfabric 1"
|
113
|
+
- -c "configure terminal" -c "router rip"
|
114
|
+
- -c "configure terminal" -c "router ripng"
|
115
|
+
- -c "configure terminal" -c "router eigrp 1"
|
116
|
+
- -c "configure terminal" -c "mpls ldp"
|
117
|
+
- -c "configure terminal" -c "mpls ldp" -c "address-family ipv4"
|
118
|
+
- -c "configure terminal" -c "mpls ldp" -c "address-family ipv4" -c "interface eth99"
|
119
|
+
- -c "configure terminal" -c "mpls ldp" -c "address-family ipv6"
|
120
|
+
- -c "configure terminal" -c "mpls ldp" -c "address-family ipv6" -c "interface eth99"
|
121
|
+
- -c "configure terminal" -c "l2vpn WORD type vpls"
|
122
|
+
- -c "configure terminal" -c "l2vpn WORD type vpls" -c "member pseudowire mpw0"
|
123
|
+
- -c "configure terminal" -c "line vty"
|
124
|
+
- -c "configure terminal" -c "logical-router 1 ns /var/run/netns/ns1"
|
125
|
+
- -c "configure terminal" -c "vrf RED"
|
126
|
+
- -c "configure terminal" -c "nexthop-group NHGROUP"
|
127
|
+
- -c "configure terminal" -c "pbr-map WORD seq 100"
|
128
|
+
#- -c "configure terminal" -c "bfd"
|
129
|
+
#- -c "configure terminal" -c "bfd" -c "peer 1.1.1.1"
|
130
|
+
|
131
|
+
configs:
|
132
|
+
all: |
|
133
|
+
hostname %(daemon)
|
134
|
+
log file %(runstatedir)/%(daemon).log
|
135
|
+
log commands
|
136
|
+
!
|
137
|
+
debug northbound
|
138
|
+
!
|
139
|
+
zebra: |
|
140
|
+
route-map WORD permit 10
|
141
|
+
vrf WORD
|
142
|
+
!
|
143
|
+
bgpd: |
|
144
|
+
route-map WORD permit 10
|
145
|
+
ip prefix-list WORD permit any
|
146
|
+
access-list WORD permit any
|
147
|
+
!
|
148
|
+
router bgp 1
|
149
|
+
neighbor 1.1.1.1 remote-as 1
|
150
|
+
neighbor 2001:db8::1 remote-as 1
|
151
|
+
neighbor WORD peer-group
|
152
|
+
neighbor WORD remote-as 1
|
153
|
+
!
|
154
|
+
address-family ipv6 unicast
|
155
|
+
neighbor 1.1.1.1 activate
|
156
|
+
neighbor 2001:db8::1 activate
|
157
|
+
neighbor WORD activate
|
158
|
+
exit-address-family
|
159
|
+
!
|
160
|
+
!
|
161
|
+
router bgp 2 view VIEWVRFNAME
|
162
|
+
!
|
163
|
+
ospfd: |
|
164
|
+
route-map WORD permit 10
|
165
|
+
!
|
166
|
+
router ospf
|
167
|
+
!
|
168
|
+
ospf6d: |
|
169
|
+
route-map WORD permit 10
|
170
|
+
!
|
171
|
+
router ospf6
|
172
|
+
!
|
173
|
+
isisd: |
|
174
|
+
route-map WORD permit 10
|
175
|
+
!
|
176
|
+
fabricd: |
|
177
|
+
route-map WORD permit 10
|
178
|
+
!
|
179
|
+
ripd: |
|
180
|
+
route-map WORD permit 10
|
181
|
+
ip prefix-list WORD permit any
|
182
|
+
access-list WORD permit any
|
183
|
+
!
|
184
|
+
!key chain WORD
|
185
|
+
! key 2147483647
|
186
|
+
!
|
187
|
+
router rip
|
188
|
+
distribute-list WORD in eth99
|
189
|
+
!
|
190
|
+
ripngd: |
|
191
|
+
route-map WORD permit 10
|
192
|
+
ipv6 prefix-list WORD permit any
|
193
|
+
ipv6 access-list WORD permit any
|
194
|
+
!
|
195
|
+
router ripng
|
196
|
+
distribute-list WORD in eth99
|
197
|
+
!
|
198
|
+
eigrpd: |
|
199
|
+
route-map WORD permit 10
|
200
|
+
!
|
201
|
+
pimd: |
|
202
|
+
route-map WORD permit 10
|
203
|
+
vrf WORD
|
204
|
+
!
|
205
|
+
ldpd: |
|
206
|
+
nhrpd: |
|
207
|
+
babeld: |
|
208
|
+
bfdd: |
|
209
|
+
pbrd: |
|
210
|
+
staticd: |
|
211
|
+
vrf WORD
|
212
|
+
!
|
213
|
+
sharpd: |
|
214
|
+
route-map WORD permit 10
|
215
|
+
!
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
$LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
|
3
|
+
require 'frr-cli-fuzzer/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "frr-cli-fuzzer"
|
7
|
+
spec.version = FrrCliFuzzer::VERSION
|
8
|
+
spec.authors = ["Renato Westphal"]
|
9
|
+
spec.email = ["renato@opensourcerouting.org"]
|
10
|
+
|
11
|
+
spec.summary = %q{FRR CLI fuzzer.}
|
12
|
+
spec.homepage = "https://github.com/rwestphal/frr-cli-fuzzer"
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
16
|
+
spec.bindir = "bin"
|
17
|
+
spec.executables = "frr-cli-fuzzer"
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
21
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
22
|
+
spec.add_dependency "ffi"
|
23
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'ffi'
|
2
|
+
|
3
|
+
module FrrCliFuzzer
|
4
|
+
# Bindings for a few libc's functions.
|
5
|
+
class LibC
|
6
|
+
extend FFI::Library
|
7
|
+
ffi_lib 'c'
|
8
|
+
|
9
|
+
attach_function :unshare, [:int], :int
|
10
|
+
attach_function :mount, [:string, :string, :string, :ulong, :pointer], :int
|
11
|
+
attach_function :prctl, [:int, :long, :long, :long, :long], :int
|
12
|
+
|
13
|
+
# include/uapi/linux/sched.h
|
14
|
+
CLONE_NEWNS = 0x00020000
|
15
|
+
CLONE_NEWPID = 0x20000000
|
16
|
+
CLONE_NEWNET = 0x40000000
|
17
|
+
|
18
|
+
# include/uapi/linux/fs.h
|
19
|
+
MS_NOSUID = (1 << 1)
|
20
|
+
MS_NODEV = (1 << 2)
|
21
|
+
MS_NOEXEC = (1 << 3)
|
22
|
+
MS_REC = (1 << 14)
|
23
|
+
MS_PRIVATE = (1 << 18)
|
24
|
+
|
25
|
+
# include/uapi/linux/prctl.h
|
26
|
+
PR_SET_PDEATHSIG = 1
|
27
|
+
PR_SET_CHILD_SUBREAPER = 36
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,69 @@
|
|
1
|
+
module FrrCliFuzzer
|
2
|
+
class LinuxNamespace
|
3
|
+
attr_accessor :pid
|
4
|
+
|
5
|
+
# Create a child process running on a separate network and mount namespace.
|
6
|
+
def fork_and_unshare
|
7
|
+
begin
|
8
|
+
io_in, io_out = IO.pipe
|
9
|
+
|
10
|
+
pid = Kernel.fork do
|
11
|
+
unshare(LibC::CLONE_NEWNS | LibC::CLONE_NEWPID | LibC::CLONE_NEWNET)
|
12
|
+
|
13
|
+
# Fork again to use the new PID namespace.
|
14
|
+
# Need to supress a warning that is irrelevant for us.
|
15
|
+
warn_level = $VERBOSE
|
16
|
+
$VERBOSE = nil
|
17
|
+
pid = Kernel.fork do
|
18
|
+
# HACK: kill when parent dies.
|
19
|
+
trap(:SIGUSR1) do
|
20
|
+
LibC.prctl(LibC::PR_SET_PDEATHSIG, 15, 0, 0, 0)
|
21
|
+
trap(:SIGUSR1, :IGNORE)
|
22
|
+
end
|
23
|
+
LibC.prctl(LibC::PR_SET_PDEATHSIG, 10, 0, 0, 0)
|
24
|
+
|
25
|
+
mount_propagation(LibC::MS_REC | LibC::MS_PRIVATE)
|
26
|
+
mount_proc
|
27
|
+
yield
|
28
|
+
end
|
29
|
+
$VERBOSE = warn_level
|
30
|
+
io_out.puts "#{pid}"
|
31
|
+
exit(0)
|
32
|
+
end
|
33
|
+
|
34
|
+
@pid = io_in.gets.to_i
|
35
|
+
Process.waitpid(pid)
|
36
|
+
rescue SystemCallError => e
|
37
|
+
$stderr.puts "System call error:: #{e.message}"
|
38
|
+
$stderr.puts e.backtrace
|
39
|
+
exit(1)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Set the mount propagation of the process.
|
44
|
+
def mount_propagation(flags)
|
45
|
+
mount('none', '/', nil, flags, nil)
|
46
|
+
end
|
47
|
+
|
48
|
+
# Mount the proc filesystem (useful after creating a new PID namespace).
|
49
|
+
def mount_proc
|
50
|
+
mount('none', '/proc', nil, LibC::MS_REC | LibC::MS_PRIVATE, nil)
|
51
|
+
mount('proc', '/proc', 'proc',
|
52
|
+
LibC::MS_NOSUID | LibC::MS_NOEXEC | LibC::MS_NODEV, nil)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Wrapper for mount(2).
|
56
|
+
def mount(source, target, fs_type, flags, data)
|
57
|
+
if LibC.mount(source, target, fs_type, flags, data) < 0
|
58
|
+
raise SystemCallError.new('mount failed', FFI::LastError.error)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# Wrapper for unshare(2).
|
63
|
+
def unshare(flags)
|
64
|
+
if LibC.unshare(flags) < 0
|
65
|
+
raise SystemCallError.new('unshare failed', FFI::LastError.error)
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
@@ -0,0 +1,275 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
require 'scanf'
|
3
|
+
require_relative 'frr-cli-fuzzer/libc'
|
4
|
+
require_relative 'frr-cli-fuzzer/linux_namespace'
|
5
|
+
require_relative 'frr-cli-fuzzer/version'
|
6
|
+
|
7
|
+
module FrrCliFuzzer
|
8
|
+
DFLT_ITERATIONS = 1
|
9
|
+
DFLT_RUNSTATEDIR = '/tmp/frr-cli-fuzzer'
|
10
|
+
DFLT_FRR_SYSCONFDIR = '/etc/frr'
|
11
|
+
DFLT_FRR_LOCALSTATE_DIR = '/var/run/frr'
|
12
|
+
DFLT_FRR_USER = 'frr'
|
13
|
+
DFLT_FRR_GROUP = 'frr'
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def init(iterations: nil,
|
17
|
+
random_order: nil,
|
18
|
+
runstatedir: nil,
|
19
|
+
frr_build_parameters: nil,
|
20
|
+
daemons: nil,
|
21
|
+
configs: nil,
|
22
|
+
nodes: nil,
|
23
|
+
regexps: nil,
|
24
|
+
whitelist: nil,
|
25
|
+
blacklist: nil)
|
26
|
+
# Load configuration and default values if necessary.
|
27
|
+
@iterations = iterations || DFLT_ITERATIONS
|
28
|
+
@random_order = random_order || false
|
29
|
+
@runstatedir = runstatedir || DFLT_RUNSTATEDIR
|
30
|
+
@frr = frr_build_parameters || []
|
31
|
+
@frr['sysconfdir'] ||= DFLT_FRR_SYSCONFDIR
|
32
|
+
@frr['localstatedir'] ||= DFLT_FRR_LOCALSTATE_DIR
|
33
|
+
@frr['user'] ||= DFLT_FRR_USER
|
34
|
+
@frr['group'] ||= DFLT_FRR_GROUP
|
35
|
+
@daemons = daemons || []
|
36
|
+
@configs = configs || []
|
37
|
+
@nodes = nodes || []
|
38
|
+
@regexps = regexps || []
|
39
|
+
@whitelist = whitelist || []
|
40
|
+
@blacklist = blacklist || []
|
41
|
+
|
42
|
+
# Initialize counters.
|
43
|
+
@counters = {}
|
44
|
+
@counters['non-filtered-cmds'] = 0
|
45
|
+
@counters['filtered-blacklist'] = 0
|
46
|
+
@counters['filtered-whitelist'] = 0
|
47
|
+
@counters['tested-cmds'] = 0
|
48
|
+
@counters['segfaults'] = 0
|
49
|
+
@segfaults = {}
|
50
|
+
|
51
|
+
# Security check to prevent accidental deletion of data.
|
52
|
+
unless @runstatedir.include?('frr-cli-fuzzer')
|
53
|
+
abort("The runstatedir configuration parameter must contain "\
|
54
|
+
"'frr-cli-fuzzer' somewhere in the path.")
|
55
|
+
end
|
56
|
+
FileUtils.rm_rf(@runstatedir)
|
57
|
+
FileUtils.mkdir_p(@runstatedir)
|
58
|
+
FileUtils.chown_R(@frr['user'], @frr['group'], @runstatedir)
|
59
|
+
|
60
|
+
# Create a new process on a new pid, mount and network namespace.
|
61
|
+
@ns = LinuxNamespace.new
|
62
|
+
@ns.fork_and_unshare do
|
63
|
+
# This is the init process of this fuzzer. We need to reap the zombies.
|
64
|
+
trap(:CHLD) { Process.wait }
|
65
|
+
trap(:INT, :IGNORE)
|
66
|
+
sleep
|
67
|
+
end
|
68
|
+
|
69
|
+
# Bind mount FRR directories.
|
70
|
+
mount(@frr['sysconfdir'], @frr['user'], @frr['group'])
|
71
|
+
mount(@frr['localstatedir'], @frr['user'], @frr['group'])
|
72
|
+
end
|
73
|
+
|
74
|
+
# nsenter(1) is a standard tool from the util-linux package. It can be used
|
75
|
+
# to run a program with namespaces of other processes.
|
76
|
+
def nsenter
|
77
|
+
"nsenter -t #{@ns.pid} --mount --pid --net"
|
78
|
+
end
|
79
|
+
|
80
|
+
# Bind mount a path under the configured runstatedir.
|
81
|
+
def mount(path, user, group)
|
82
|
+
source = "#{@runstatedir}/#{path}"
|
83
|
+
FileUtils.mkdir_p(path)
|
84
|
+
FileUtils.mkdir_p(source)
|
85
|
+
FileUtils.chown_R(user, group, source)
|
86
|
+
system("#{nsenter} mount --bind #{source} #{path}")
|
87
|
+
end
|
88
|
+
|
89
|
+
# Save configuration in the file system.
|
90
|
+
def save_config(daemon, config)
|
91
|
+
path = "#{@runstatedir}/#{@frr['sysconfdir']}/#{daemon}.conf"
|
92
|
+
File.open(path, 'w') { |file| file.write(config) }
|
93
|
+
end
|
94
|
+
|
95
|
+
# Generate FRR configuration file.
|
96
|
+
def gen_config(daemon)
|
97
|
+
config = @configs['all'] || ''
|
98
|
+
config += @configs[daemon] || ''
|
99
|
+
|
100
|
+
# Replace variables.
|
101
|
+
config.gsub!('%(daemon)', daemon)
|
102
|
+
config.gsub!('%(runstatedir)', @runstatedir)
|
103
|
+
|
104
|
+
save_config(daemon, config)
|
105
|
+
end
|
106
|
+
|
107
|
+
# Generate FRR configuration files.
|
108
|
+
def gen_configs
|
109
|
+
save_config('vtysh', '')
|
110
|
+
@daemons.each do |daemon|
|
111
|
+
gen_config(daemon)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
# Start a FRR daemon.
|
116
|
+
def start_daemon(daemon)
|
117
|
+
# Remove old pid file if it exists.
|
118
|
+
FileUtils.rm_f("#{@runstatedir}/#{@frr['localstatedir']}/#{daemon}.pid")
|
119
|
+
|
120
|
+
# Spawn new process.
|
121
|
+
pid = Process.spawn("#{nsenter} #{daemon} -d --log=stdout "\
|
122
|
+
">> #{@runstatedir}/#{daemon}.stdout "\
|
123
|
+
"2>> #{@runstatedir}/#{daemon}.stderr")
|
124
|
+
Process.detach(pid)
|
125
|
+
end
|
126
|
+
|
127
|
+
# Start all FRR daemons.
|
128
|
+
def start_daemons
|
129
|
+
@daemons.each do |daemon|
|
130
|
+
start_daemon(daemon)
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
# Check if a FRR daemon is still alive.
|
135
|
+
def daemon_alive?(daemon)
|
136
|
+
`#{nsenter} ps aux | grep #{daemon} | grep -E -v "defunct|grep"` != ''
|
137
|
+
end
|
138
|
+
|
139
|
+
# Check if a command should be white-list filtered.
|
140
|
+
def filter_whitelist(command)
|
141
|
+
return false if @whitelist.empty?
|
142
|
+
|
143
|
+
@whitelist.each do |regexp|
|
144
|
+
return false if command =~ /#{regexp}/
|
145
|
+
end
|
146
|
+
true
|
147
|
+
end
|
148
|
+
|
149
|
+
# Check if a command should be black-list filtered.
|
150
|
+
def filter_blacklist(command)
|
151
|
+
@blacklist.each do |regexp|
|
152
|
+
return true if command =~ /#{regexp}/
|
153
|
+
end
|
154
|
+
false
|
155
|
+
end
|
156
|
+
|
157
|
+
# Prepare command to be used by the CLI fuzzing tester.
|
158
|
+
def prepare_command(command)
|
159
|
+
new_command = ''
|
160
|
+
|
161
|
+
command.split.each do |word|
|
162
|
+
# Custom regexps.
|
163
|
+
@regexps.each_pair do |input, option|
|
164
|
+
word.sub!(input, option)
|
165
|
+
end
|
166
|
+
|
167
|
+
# Handle intervals.
|
168
|
+
if word =~ /(\d+\-\d+)/
|
169
|
+
interval = word.scanf('(%d-%d)')
|
170
|
+
new_command << interval[1].to_s
|
171
|
+
else
|
172
|
+
new_command << word
|
173
|
+
end
|
174
|
+
|
175
|
+
# Append whitespace after each word.
|
176
|
+
new_command << ' '
|
177
|
+
end
|
178
|
+
|
179
|
+
new_command.rstrip
|
180
|
+
end
|
181
|
+
|
182
|
+
# Obtain array of the commands we want to test.
|
183
|
+
def prepare_commmands
|
184
|
+
commands = []
|
185
|
+
|
186
|
+
@nodes.each do |hierarchy|
|
187
|
+
permutations = `#{nsenter} vtysh #{hierarchy} -c \"list permutations\"`
|
188
|
+
permutations.each_line do |command|
|
189
|
+
command = command.strip
|
190
|
+
|
191
|
+
# Check whitelist and blacklist.
|
192
|
+
if filter_whitelist(command)
|
193
|
+
puts "filtering (whitelist): #{command}"
|
194
|
+
@counters['filtered-whitelist'] += 1
|
195
|
+
next
|
196
|
+
end
|
197
|
+
if filter_blacklist(command)
|
198
|
+
puts "filtering (blacklist): #{command}"
|
199
|
+
@counters['filtered-blacklist'] += 1
|
200
|
+
next
|
201
|
+
end
|
202
|
+
|
203
|
+
@counters['non-filtered-cmds'] += 1
|
204
|
+
|
205
|
+
commands.push("vtysh #{hierarchy} -c \"#{prepare_command(command)}\"")
|
206
|
+
end
|
207
|
+
end
|
208
|
+
puts "non-filtered commands: #{@counters['non-filtered-cmds']}"
|
209
|
+
|
210
|
+
commands
|
211
|
+
end
|
212
|
+
|
213
|
+
# Send command to all running FRR daemons.
|
214
|
+
def send_command(command)
|
215
|
+
puts "testing: #{command}"
|
216
|
+
|
217
|
+
vtysh_log = "#{@runstatedir}/vtysh.txt"
|
218
|
+
File.open(vtysh_log, 'a') { |f| f.puts command }
|
219
|
+
system("#{nsenter} #{command} >> #{vtysh_log} 2>&1")
|
220
|
+
end
|
221
|
+
|
222
|
+
# Print the results of the fuzzing tests.
|
223
|
+
def print_results
|
224
|
+
puts "\nresults:"
|
225
|
+
puts "- non-filtered commands: #{@counters['non-filtered-cmds']}"
|
226
|
+
puts "- whitelist filtered commands: #{@counters['filtered-whitelist']}"
|
227
|
+
puts "- blacklist filtered commands: #{@counters['filtered-blacklist']}"
|
228
|
+
puts "- tested commands: #{@counters['tested-cmds']}"
|
229
|
+
puts "- segfaults detected: #{@counters['segfaults']}"
|
230
|
+
@segfaults.each_pair do |msg, count|
|
231
|
+
puts " (x#{count}) #{msg}"
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
# Log a segfault to both the standard output and to the fuzzer output file.
|
236
|
+
def log_segfault(daemon, command)
|
237
|
+
msg = "#{daemon} aborted: #{command}"
|
238
|
+
puts msg
|
239
|
+
File.open("#{@runstatedir}/output.txt", 'a') { |f| f.puts msg }
|
240
|
+
|
241
|
+
@counters['segfaults'] += 1
|
242
|
+
@segfaults[msg] = @segfaults[msg].to_i + 1
|
243
|
+
end
|
244
|
+
|
245
|
+
# Start fuzzing tests.
|
246
|
+
def test_fuzzing
|
247
|
+
iteration = 0
|
248
|
+
commands = prepare_commmands
|
249
|
+
return if commands.empty?
|
250
|
+
|
251
|
+
loop do
|
252
|
+
iteration += 1
|
253
|
+
puts "\nfuzz iteration: ##{iteration}"
|
254
|
+
commands.shuffle! if @random_order
|
255
|
+
|
256
|
+
# Iterate over all commands.
|
257
|
+
commands.each do |command|
|
258
|
+
@counters['tested-cmds'] += 1
|
259
|
+
send_command(command)
|
260
|
+
|
261
|
+
# Check if all daemons are still alive.
|
262
|
+
@daemons.each do |daemon|
|
263
|
+
next if daemon_alive?(daemon)
|
264
|
+
|
265
|
+
log_segfault(daemon, command)
|
266
|
+
start_daemon(daemon)
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
# Check if this is the last iteration.
|
271
|
+
break if @iterations > 0 && iteration == @iterations
|
272
|
+
end
|
273
|
+
end
|
274
|
+
end
|
275
|
+
end
|
metadata
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: frr-cli-fuzzer
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Renato Westphal
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2018-10-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.11'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.11'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: ffi
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- renato@opensourcerouting.org
|
58
|
+
executables:
|
59
|
+
- frr-cli-fuzzer
|
60
|
+
extensions: []
|
61
|
+
extra_rdoc_files: []
|
62
|
+
files:
|
63
|
+
- ".gitignore"
|
64
|
+
- Gemfile
|
65
|
+
- LICENSE.txt
|
66
|
+
- README.md
|
67
|
+
- Rakefile
|
68
|
+
- bin/frr-cli-fuzzer
|
69
|
+
- bin/setup
|
70
|
+
- config.yml
|
71
|
+
- frr-cli-fuzzer.gemspec
|
72
|
+
- lib/frr-cli-fuzzer.rb
|
73
|
+
- lib/frr-cli-fuzzer/libc.rb
|
74
|
+
- lib/frr-cli-fuzzer/linux_namespace.rb
|
75
|
+
- lib/frr-cli-fuzzer/version.rb
|
76
|
+
homepage: https://github.com/rwestphal/frr-cli-fuzzer
|
77
|
+
licenses:
|
78
|
+
- MIT
|
79
|
+
metadata: {}
|
80
|
+
post_install_message:
|
81
|
+
rdoc_options: []
|
82
|
+
require_paths:
|
83
|
+
- lib
|
84
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - ">="
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '0'
|
89
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
90
|
+
requirements:
|
91
|
+
- - ">="
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
requirements: []
|
95
|
+
rubyforge_project:
|
96
|
+
rubygems_version: 2.5.2.1
|
97
|
+
signing_key:
|
98
|
+
specification_version: 4
|
99
|
+
summary: FRR CLI fuzzer.
|
100
|
+
test_files: []
|