c0f 0.0.1
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/Gemfile +4 -0
- data/LICENSE +340 -0
- data/README.rdoc +106 -0
- data/Rakefile +61 -0
- data/bin/c0f +144 -0
- data/c0f.gemspec +30 -0
- data/features/c0f.feature +45 -0
- data/features/step_definitions/c0f_steps.rb +51 -0
- data/features/support/env.rb +16 -0
- data/lib/c0f.rb +9 -0
- data/lib/c0f/can_fingerprint.rb +97 -0
- data/lib/c0f/can_fingerprint_db.rb +100 -0
- data/lib/c0f/can_packet.rb +57 -0
- data/lib/c0f/can_packet_stat.rb +32 -0
- data/lib/c0f/version.rb +3 -0
- data/test/sample-can.log +6158 -0
- data/test/sample-fp.json +2 -0
- data/test/sample.db +0 -0
- data/test/tc_can_packet.rb +32 -0
- metadata +198 -0
data/Rakefile
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
def dump_load_path
|
2
|
+
puts $LOAD_PATH.join("\n")
|
3
|
+
found = nil
|
4
|
+
$LOAD_PATH.each do |path|
|
5
|
+
if File.exists?(File.join(path,"rspec"))
|
6
|
+
puts "Found rspec in #{path}"
|
7
|
+
if File.exists?(File.join(path,"rspec","core"))
|
8
|
+
puts "Found core"
|
9
|
+
if File.exists?(File.join(path,"rspec","core","rake_task"))
|
10
|
+
puts "Found rake_task"
|
11
|
+
found = path
|
12
|
+
else
|
13
|
+
puts "!! no rake_task"
|
14
|
+
end
|
15
|
+
else
|
16
|
+
puts "!!! no core"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
if found.nil?
|
21
|
+
puts "Didn't find rspec/core/rake_task anywhere"
|
22
|
+
else
|
23
|
+
puts "Found in #{path}"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
require 'bundler'
|
27
|
+
require 'rake/clean'
|
28
|
+
|
29
|
+
require 'rake/testtask'
|
30
|
+
|
31
|
+
require 'cucumber'
|
32
|
+
require 'cucumber/rake/task'
|
33
|
+
gem 'rdoc' # we need the installed RDoc gem, not the system one
|
34
|
+
require 'rdoc/task'
|
35
|
+
|
36
|
+
include Rake::DSL
|
37
|
+
|
38
|
+
Bundler::GemHelper.install_tasks
|
39
|
+
|
40
|
+
|
41
|
+
Rake::TestTask.new do |t|
|
42
|
+
t.pattern = 'test/tc_*.rb'
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
CUKE_RESULTS = 'results.html'
|
47
|
+
CLEAN << CUKE_RESULTS
|
48
|
+
Cucumber::Rake::Task.new(:features) do |t|
|
49
|
+
t.cucumber_opts = "features --format html -o #{CUKE_RESULTS} --format pretty --no-source -x"
|
50
|
+
t.fork = false
|
51
|
+
end
|
52
|
+
|
53
|
+
Rake::RDocTask.new do |rd|
|
54
|
+
|
55
|
+
rd.main = "README.rdoc"
|
56
|
+
|
57
|
+
rd.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")
|
58
|
+
end
|
59
|
+
|
60
|
+
task :default => [:test,:features]
|
61
|
+
|
data/bin/c0f
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# c0f - CAN of Fingers
|
3
|
+
#
|
4
|
+
# Analyzes CAN bus packets to create a fingerprint or identify a BUS line
|
5
|
+
#
|
6
|
+
# (c) 2015 Craig Smith <craig@theialabs.com> GPLv3
|
7
|
+
|
8
|
+
require 'optparse'
|
9
|
+
require 'methadone'
|
10
|
+
require 'formatador'
|
11
|
+
require 'json'
|
12
|
+
require 'c0f.rb'
|
13
|
+
|
14
|
+
class App
|
15
|
+
include Methadone::Main
|
16
|
+
include Methadone::CLILogging
|
17
|
+
|
18
|
+
# Main c0f routine
|
19
|
+
main do |candevice|
|
20
|
+
options[:progress] = false if options[:quiet] == true
|
21
|
+
@stat = C0f::CANPacketStat.new
|
22
|
+
candb = nil
|
23
|
+
candump = nil
|
24
|
+
if candevice then
|
25
|
+
if options[:logfile] then
|
26
|
+
puts "You can specify logfile OR a candevice but not both"
|
27
|
+
exit 1
|
28
|
+
end
|
29
|
+
end
|
30
|
+
if not candevice and not options[:logfile] and not options["add-fp"] then
|
31
|
+
puts "Current use does not perform a valid action. See --help"
|
32
|
+
exit -1
|
33
|
+
end
|
34
|
+
if options[:fpdb] then
|
35
|
+
candb = C0f::CANFingerprintDB.new(options[:fpdb])
|
36
|
+
puts "Loaded #{candb.count} fingerprints from DB" if not options[:quiet]
|
37
|
+
end
|
38
|
+
if not options["add-fp"].nil? then
|
39
|
+
if candb.nil? then
|
40
|
+
puts "Can not add a fingerprint unless a DB is specified"
|
41
|
+
exit 5
|
42
|
+
else
|
43
|
+
if not File.exists? options["add-fp"] then
|
44
|
+
puts "Fingerprint JSON file not found"
|
45
|
+
exit 6
|
46
|
+
else
|
47
|
+
fp = JSON.parse(File.read(options["add-fp"]))
|
48
|
+
if fp["Make"] == "Unknown" then
|
49
|
+
puts "You must specify at *least* the make before adding the fingerprint to the DB"
|
50
|
+
exit 20
|
51
|
+
end
|
52
|
+
r = candb.match_json(fp)
|
53
|
+
row = r.next
|
54
|
+
if row then
|
55
|
+
puts "Base Fingerprint already exists. TODO: Added common ID matching/inserting"
|
56
|
+
# TODO: add a fuzzy match and an exact match function. If not exact and we don't have the
|
57
|
+
# same make, model, year and trim then insert the slightly different one
|
58
|
+
exit 10
|
59
|
+
else
|
60
|
+
id = candb.insert_json(fp)
|
61
|
+
puts "Successfully inserted fingprint (#{id})" if id
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
if not candevice.nil? then
|
67
|
+
progress = Formatador::ProgressBar.new(options["sample-size"]) { |b|
|
68
|
+
b.opts[:color] = "green"
|
69
|
+
b.opts[:started_at] = Time.now
|
70
|
+
b.opts[:label] = "Reading Packets..."
|
71
|
+
} if options[:progress]
|
72
|
+
IO.popen "candump -t a -n #{options["sample-size"]} #{candevice}" do |pipe|
|
73
|
+
pipe.each do |line|
|
74
|
+
progress.increment if options[:progress]
|
75
|
+
pkt = C0f::CANPacket.new
|
76
|
+
@stat.add pkt if pkt.parse line
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
if options[:logfile] then
|
81
|
+
log = options[:logfile]
|
82
|
+
if File.read log then
|
83
|
+
lines = File.readlines(log)
|
84
|
+
progress = Formatador::ProgressBar.new(lines.count) { |b|
|
85
|
+
b.opts[:color] = "green"
|
86
|
+
b.opts[:started_at] = Time.now
|
87
|
+
b.opts[:label] = "Loading Packets..."
|
88
|
+
} if options[:progress]
|
89
|
+
lines.each do |line|
|
90
|
+
progress.increment if options[:progress]
|
91
|
+
pkt = C0f::CANPacket.new
|
92
|
+
@stat.add pkt if pkt.parse line
|
93
|
+
end
|
94
|
+
else
|
95
|
+
puts "Could not read logfile #{log}"
|
96
|
+
exit 3
|
97
|
+
end
|
98
|
+
end
|
99
|
+
puts @stat if @stat.pkt_count > 0 and options["print-stats"]
|
100
|
+
if (options["print-fp"] or options["save-fp"]) and (options[:logfile] or candevice) then
|
101
|
+
if @stat.pkt_count < options["sample-size"] then
|
102
|
+
puts "Fingerprinting requires at least 2000 packets"
|
103
|
+
exit 4
|
104
|
+
end
|
105
|
+
fp = C0f::CANFingerprint.new(@stat)
|
106
|
+
if candb then
|
107
|
+
r = candb.match_json(fp.to_json)
|
108
|
+
row = r.next
|
109
|
+
if row then
|
110
|
+
fp.make = row["make"]
|
111
|
+
fp.model = row["model"]
|
112
|
+
fp.year = row["year"]
|
113
|
+
fp.trim = row["trim"]
|
114
|
+
end
|
115
|
+
end
|
116
|
+
puts fp.to_json if options["print-fp"]
|
117
|
+
if options["save-fp"] then
|
118
|
+
File.open(options["save-fp"], "w") { |f| f.write fp.to_json }
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
description "CAN Bus Passive Make/Model analyzer"
|
124
|
+
|
125
|
+
options["print-fp"] = true
|
126
|
+
options[:progress] = true
|
127
|
+
options["sample-size"] = 2000
|
128
|
+
on("--logfile FILENAME", "CANDump logfile")
|
129
|
+
on("--print-stats", "Prints logfile stats (Default: No)")
|
130
|
+
on("--[no-]print-fp", "Print fingerprint info (Default: Yes)")
|
131
|
+
on("--[no-]progress", "Print a progress bar")
|
132
|
+
on("--add-fp FINGERPRINT", "Add a JSON fingerprint from a file to the DB")
|
133
|
+
on("--fpdb DB", "Location of saved fingerprint DB")
|
134
|
+
on("--quiet", "No output other than what is specified")
|
135
|
+
on("--sample-size PACKETCOUNT", "The number of packets to process")
|
136
|
+
on("--save-fp FILE", "Saves the fingerprint to a file")
|
137
|
+
arg :candevice, "Launch candump on a given CAN device", :optional
|
138
|
+
|
139
|
+
version C0f::VERSION
|
140
|
+
|
141
|
+
use_log_level_option :toggle_debug_on_signal => 'USR1'
|
142
|
+
|
143
|
+
go!
|
144
|
+
end
|
data/c0f.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'c0f/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "c0f"
|
8
|
+
spec.version = C0f::VERSION
|
9
|
+
spec.authors = ["Craig Smith"]
|
10
|
+
spec.email = ["craig@theialabs.com"]
|
11
|
+
spec.summary = %q{c0f - CAN Bus Fingerprinting}
|
12
|
+
spec.description = %q{c0f - CAN of Fingers. CAN Bus fingerprinting to passively determine make/model of vehicle}
|
13
|
+
spec.homepage = "http://opengarages.orb"
|
14
|
+
spec.license = "GPLv3"
|
15
|
+
|
16
|
+
spec.files = `git ls-files -z`.split("\x0")
|
17
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
18
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
19
|
+
spec.require_paths = ["lib"]
|
20
|
+
|
21
|
+
spec.add_development_dependency "minitest", "~> 4.7"
|
22
|
+
spec.add_development_dependency "bundler", "~> 1.7"
|
23
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
24
|
+
spec.add_development_dependency('rdoc')
|
25
|
+
spec.add_development_dependency('aruba')
|
26
|
+
spec.add_dependency('methadone', '~> 1.8')
|
27
|
+
spec.add_dependency('formatador')
|
28
|
+
spec.add_dependency('sqlite3')
|
29
|
+
spec.add_dependency('json')
|
30
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
Feature: Analyze CAN Bus fingerprints
|
2
|
+
In order to determine fingerprints for CAN Bus traffic
|
3
|
+
I want to have a tool that can analyze, save and report CAN bus fingerprints
|
4
|
+
So I can eventually make fingerprints for shellcode
|
5
|
+
|
6
|
+
Scenario: Basic UI
|
7
|
+
When I get help for "c0f"
|
8
|
+
Then the exit status should be 0
|
9
|
+
And the banner should be present
|
10
|
+
And the banner should document that this app takes options
|
11
|
+
And the following options should be documented:
|
12
|
+
|--version|
|
13
|
+
|--logfile|
|
14
|
+
|--[no-]progress|
|
15
|
+
|--[no-]print-fp|
|
16
|
+
|--print-stats|
|
17
|
+
|--add-fp|
|
18
|
+
|--fpdb|
|
19
|
+
|--quiet|
|
20
|
+
|--sample-size|
|
21
|
+
|--save-fp|
|
22
|
+
And the banner should document that this app's arguments are:
|
23
|
+
| candevice |
|
24
|
+
|
25
|
+
Scenario: Parse a valid CANDump Log with enough sample packets
|
26
|
+
Given a valid candump logfile at "/tmp/sample-can.log"
|
27
|
+
When I successfully run `c0f --logfile /tmp/sample-can.log --quiet`
|
28
|
+
Then the stdout should contain valid JSON
|
29
|
+
And the Make is "Unknown" and the Model is "Unknown" and the Year is "Unknown" and the Trim is "Unknown"
|
30
|
+
And the common IDs should be "166 158 161 191 18E 133 136 13A 13F 164 17C 183 143 095"
|
31
|
+
And the main ID should be "143"
|
32
|
+
And the main interval should be "0.009998683195847732"
|
33
|
+
|
34
|
+
Scenario: Take a valid signature and initialze a fingerprint database with it
|
35
|
+
Given a completed fingerprint at "/tmp/sample-fp.json" and a fingerprint db at "/tmp/can.db"
|
36
|
+
When I successfully run `c0f --add-fp /tmp/sample-fp.json --fpdb /tmp/can.db`
|
37
|
+
Then the output should contain "Created Tables"
|
38
|
+
And the output should contain "Successfully inserted fingprint"
|
39
|
+
|
40
|
+
Scenario: Match a canbus fingerprint to that in the fingerprint DB
|
41
|
+
Given a valid fingerprint db at "/tmp/sample-fp.db" valid logfile "/tmp/sample-can.log"
|
42
|
+
When I successfully run `c0f --fpdb /tmp/sample-fp.db --logfile /tmp/sample-can.log --quiet`
|
43
|
+
Then the stdout should contain valid JSON
|
44
|
+
And the Make is "Honda" and the Model is "Civic" and the Year is "2009" and the Trim is "Hybrid"
|
45
|
+
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Put your step definitions here
|
2
|
+
require "fileutils"
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
Given(/^a valid candump logfile at "(.*?)"$/) do |logfile|
|
6
|
+
FileUtils.copy "test/sample-can.log", logfile
|
7
|
+
end
|
8
|
+
|
9
|
+
Then(/^the stdout should contain valid JSON$/) do
|
10
|
+
expect { JSON.parse(all_output) }.not_to raise_error
|
11
|
+
end
|
12
|
+
|
13
|
+
Then(/^the common IDs should be "(.*?)"$/) do |idstr|
|
14
|
+
ids = idstr.split
|
15
|
+
fp = JSON.parse(all_output)
|
16
|
+
missing = false
|
17
|
+
ids.each do |id|
|
18
|
+
found = false
|
19
|
+
fp["Common"].each do |cid|
|
20
|
+
found = true if cid["ID"] == id
|
21
|
+
end
|
22
|
+
missing = true if not found
|
23
|
+
end
|
24
|
+
expect(missing).to be_falsey
|
25
|
+
end
|
26
|
+
|
27
|
+
Then(/^the main ID should be "(.*?)"$/) do |id|
|
28
|
+
fp = JSON.parse(all_output)
|
29
|
+
expect(fp["MainID"] == id).to be_truthy
|
30
|
+
end
|
31
|
+
|
32
|
+
Then(/^the main interval should be "(.*?)"$/) do |i|
|
33
|
+
fp = JSON.parse(all_output)
|
34
|
+
expect(fp["MainInterval"] == i).to be_truthy
|
35
|
+
end
|
36
|
+
|
37
|
+
Given(/^a completed fingerprint at "(.*?)" and a fingerprint db at "(.*?)"$/) do |fp_dest, candb_dest|
|
38
|
+
File.unlink candb_dest if File.exists? candb_dest
|
39
|
+
FileUtils.cp "test/sample-fp.json", fp_dest
|
40
|
+
end
|
41
|
+
|
42
|
+
Given(/^a valid fingerprint db at "(.*?)" valid logfile "(.*?)"$/) do |db_dest, log_dest|
|
43
|
+
FileUtils.cp "test/sample.db", db_dest
|
44
|
+
FileUtils.cp "test/sample-can.log", log_dest
|
45
|
+
end
|
46
|
+
|
47
|
+
Then(/^the Make is "(.*?)" and the Model is "(.*?)" and the Year is "(.*?)" and the Trim is "(.*?)"$/) do |make, model, year, trim|
|
48
|
+
fp = JSON.parse(all_output)
|
49
|
+
expect((fp["Make"] == make and fp["Model"] == model and fp["Year"] == year and fp["Trim"] == trim)).to be_truthy
|
50
|
+
end
|
51
|
+
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'aruba/cucumber'
|
2
|
+
require 'methadone/cucumber'
|
3
|
+
|
4
|
+
ENV['PATH'] = "#{File.expand_path(File.dirname(__FILE__) + '/../../bin')}#{File::PATH_SEPARATOR}#{ENV['PATH']}"
|
5
|
+
LIB_DIR = File.join(File.expand_path(File.dirname(__FILE__)),'..','..','lib')
|
6
|
+
|
7
|
+
Before do
|
8
|
+
# Using "announce" causes massive warnings on 1.9.2
|
9
|
+
@puts = true
|
10
|
+
@original_rubylib = ENV['RUBYLIB']
|
11
|
+
ENV['RUBYLIB'] = LIB_DIR + File::PATH_SEPARATOR + ENV['RUBYLIB'].to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
After do
|
15
|
+
ENV['RUBYLIB'] = @original_rubylib
|
16
|
+
end
|
data/lib/c0f.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
module C0f
|
2
|
+
class CANFingerprint
|
3
|
+
attr_reader :common_ids, :most_common, :padding
|
4
|
+
attr_accessor :make, :model, :year, :trim
|
5
|
+
# Requires CANPacketStat
|
6
|
+
def initialize(stats, db=nil)
|
7
|
+
@stats = stats
|
8
|
+
@db = db
|
9
|
+
@most_common = nil
|
10
|
+
@common_ids = Array.new
|
11
|
+
@padding = nil
|
12
|
+
self.analyze
|
13
|
+
@make = "Unknown"
|
14
|
+
@model = "Unknown"
|
15
|
+
@year = "Unknown"
|
16
|
+
@trim = "Unknown"
|
17
|
+
end
|
18
|
+
|
19
|
+
# intended for direct access to the most common IDs interval
|
20
|
+
def main_interval
|
21
|
+
@most_common.avg_delta
|
22
|
+
end
|
23
|
+
|
24
|
+
def main_id
|
25
|
+
@most_common.id
|
26
|
+
end
|
27
|
+
|
28
|
+
# helper function
|
29
|
+
def dynamic
|
30
|
+
@stats.dynamic
|
31
|
+
end
|
32
|
+
|
33
|
+
def to_s
|
34
|
+
s = "Make: #{@make} Model: #{@model} Year: #{@year} Trim: #{@trim}\n"
|
35
|
+
s += "Dyanmic: #{@stats.dynamic}\n"
|
36
|
+
s += "Padding: 0x#{@padding}\n" if @padding.to_s(16).upcase
|
37
|
+
s += "Common IDs: #{@common_ids}\n"
|
38
|
+
s += "Most Common: #{@most_common.id} Expected interval: #{@most_common.avg_delta}ms\n"
|
39
|
+
s
|
40
|
+
end
|
41
|
+
|
42
|
+
def to_json
|
43
|
+
s = '{"Make": "' + @make + '", "Model": "' + @model + '", "Year": "' + @year + '", "Trim": "' + @trim + '"'
|
44
|
+
s += ', "Dynamic": "' + (@stats.dynamic ? "true" : "false") + '", '
|
45
|
+
s += '"Padding": "' + @padding.to_s(16).upcase + '", ' if @padding
|
46
|
+
s += '"Common": [ '
|
47
|
+
json_ids = Array.new
|
48
|
+
@common_ids.each do |id|
|
49
|
+
json_ids << '{ "ID": "' + id + '" }'
|
50
|
+
end
|
51
|
+
s += json_ids.join(',')
|
52
|
+
s += ' ], "MainID": "' + @most_common.id + '", "MainInterval": "' + @most_common.avg_delta.to_s + '"'
|
53
|
+
s += "}"
|
54
|
+
s
|
55
|
+
end
|
56
|
+
|
57
|
+
# Analyzes the CANPacketStats
|
58
|
+
# @stat must be set
|
59
|
+
def analyze
|
60
|
+
high_count = 0
|
61
|
+
padd_stat = Hash.new
|
62
|
+
# First pass
|
63
|
+
# * calculate the IDs we saw the most
|
64
|
+
@stats.pkts.each_value do | pkt|
|
65
|
+
high_count = pkt.count if pkt.count > high_count
|
66
|
+
if not @stats.dynamic then
|
67
|
+
if padd_stat.has_key? pkt.data[-1] then
|
68
|
+
padd_stat[pkt.data[-1]] += 1
|
69
|
+
else
|
70
|
+
padd_stat[pkt.data[-1]] = 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
# Second pass
|
75
|
+
fastest_interval = 5.0
|
76
|
+
@stats.pkts.each_value do |pkt|
|
77
|
+
# Common IDs are ones that the highest report - 1% of the total reported
|
78
|
+
if pkt.count >= high_count - (@stats.pkt_count * 0.01) then
|
79
|
+
@common_ids << pkt.id
|
80
|
+
if pkt.avg_delta < fastest_interval then
|
81
|
+
fastest_interval = pkt.avg_delta
|
82
|
+
@most_common = pkt
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
if @stats.dynamic then
|
87
|
+
highest_pad = 0
|
88
|
+
padd_stat.each do |val, count|
|
89
|
+
if count > highest_pad
|
90
|
+
hightest_pad = count
|
91
|
+
@padding = val
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end # CANFingerprint
|
97
|
+
end
|