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.
@@ -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
@@ -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
@@ -0,0 +1,9 @@
1
+ require "c0f/version"
2
+ require "c0f/can_packet"
3
+ require "c0f/can_packet_stat"
4
+ require "c0f/can_fingerprint"
5
+ require "c0f/can_fingerprint_db"
6
+
7
+ module C0f
8
+
9
+ end
@@ -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