c0f 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|