ecutools 0.0.2

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.
Files changed (59) hide show
  1. data/.bundle/config +3 -0
  2. data/.gitignore +4 -0
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +17 -0
  5. data/bin/ecutools +4 -0
  6. data/ecutools.gemspec +29 -0
  7. data/lib/assembler.rb +22 -0
  8. data/lib/cli.rb +28 -0
  9. data/lib/disassembler.rb +219 -0
  10. data/lib/helpers.rb +84 -0
  11. data/lib/instruction.rb +53 -0
  12. data/lib/version.rb +3 -0
  13. data/tests/52690021.asm +262151 -0
  14. data/tests/52690021.hex +0 -0
  15. data/xml/code/52690021.xml +19 -0
  16. data/xml/ram/52690021.xml +45 -0
  17. data/xml/rom/52370023.xml +641 -0
  18. data/xml/rom/52370024.xml +813 -0
  19. data/xml/rom/52690019.xml +25 -0
  20. data/xml/rom/52690021.xml +1156 -0
  21. data/xml/rom/52690022.xml +25 -0
  22. data/xml/rom/52690024.xml +896 -0
  23. data/xml/rom/52690122.xml +79 -0
  24. data/xml/rom/53050006.xml +583 -0
  25. data/xml/rom/53050007.xml +25 -0
  26. data/xml/rom/53050009.xml +950 -0
  27. data/xml/rom/53050011.xml +25 -0
  28. data/xml/rom/53050012.xml +803 -0
  29. data/xml/rom/53050109.xml +80 -0
  30. data/xml/rom/53600008.xml +25 -0
  31. data/xml/rom/53600009.xml +25 -0
  32. data/xml/rom/53600010.xml +848 -0
  33. data/xml/rom/53610009.xml +25 -0
  34. data/xml/rom/53610010.xml +877 -0
  35. data/xml/rom/53610012.xml +25 -0
  36. data/xml/rom/53610013.xml +886 -0
  37. data/xml/rom/54070007.xml +778 -0
  38. data/xml/rom/54550003.xml +546 -0
  39. data/xml/rom/55580005.xml +1161 -0
  40. data/xml/rom/55580006.xml +25 -0
  41. data/xml/rom/55580106.xml +75 -0
  42. data/xml/rom/55590006.xml +1135 -0
  43. data/xml/rom/55590007.xml +25 -0
  44. data/xml/rom/55590107.xml +69 -0
  45. data/xml/rom/56190002.xml +778 -0
  46. data/xml/rom/56880009.xml +858 -0
  47. data/xml/rom/56900007.xml +566 -0
  48. data/xml/rom/56900009.xml +946 -0
  49. data/xml/rom/56900010.xml +25 -0
  50. data/xml/rom/56920006.xml +546 -0
  51. data/xml/rom/56940007.xml +674 -0
  52. data/xml/rom/56940009.xml +478 -0
  53. data/xml/rom/56940010.xml +25 -0
  54. data/xml/rom/56950006.xml +696 -0
  55. data/xml/rom/57140001.xml +906 -0
  56. data/xml/rom/58020005.xml +732 -0
  57. data/xml/rom/58030005.xml +720 -0
  58. data/xml/rom/58640002.xml +880 -0
  59. metadata +147 -0
@@ -0,0 +1,3 @@
1
+ ---
2
+ BUNDLE_PATH: ecutools.gemspec
3
+ BUNDLE_DISABLE_SHARED_GEMS: '1'
@@ -0,0 +1,4 @@
1
+ /*.asm
2
+ /*.gem
3
+ /*.zip
4
+ /.DS_Store
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "http://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in gem_template.gemspec
4
+ gemspec
@@ -0,0 +1,17 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ ecutools (0.0.1)
5
+ thor (>= 0.14)
6
+
7
+ GEM
8
+ remote: http://rubygems.org/
9
+ specs:
10
+ thor (0.14.6)
11
+
12
+ PLATFORMS
13
+ ruby
14
+
15
+ DEPENDENCIES
16
+ ecutools!
17
+ thor (>= 0.14)
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env ruby
2
+ require 'cli'
3
+
4
+ command = ECUTools::CLI.start
@@ -0,0 +1,29 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+
4
+ require 'version'
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "ecutools"
8
+ s.version = ECUTools::VERSION
9
+ s.platform = Gem::Platform::RUBY
10
+ s.authors = ["Javier Muniz"]
11
+ s.email = "javier@granicus.com"
12
+ s.summary = "Toolkit for disassembling and reverse engineering ECU ROMs"
13
+ s.homepage = "http://github.com/javiermuniz/ecutools"
14
+ s.description = "Toolkit for ECU disassembly and analysis"
15
+
16
+ s.rubyforge_project = "ecutools"
17
+
18
+ s.files = `git ls-files`.split("\n")
19
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
20
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
21
+ s.require_paths = ["lib"]
22
+
23
+
24
+ s.add_development_dependency('thor', '>= 0.14')
25
+ s.add_development_dependency('nokogiri', '>= 1.5')
26
+
27
+ s.add_dependency('thor', '>= 0.14')
28
+ s.add_dependency('nokogiri', '>= 1.5')
29
+ end
@@ -0,0 +1,22 @@
1
+ module ECUTools
2
+ class Assembler
3
+ @@instructions = [
4
+ :invalid, :add, :add3, :and, :and3, :or, :or3, :xor,
5
+ :xor3, :addi, :addv, :addv3, :addx, :bc8, :bc24, :beq,
6
+ :beqz, :bgez, :bgtz, :blez, :bltz, :bnez, :bl8, :bl24, :bcl8, :bcl24, :bnc8, :bnc24,
7
+ :bne, :bra8, :bra24, :bncl8, :bncl24, :cmp, :cmpi, :cmpu, :cmpui, :cmpeq, :cmpz, :div, :divu, :rem,
8
+ :remu, :divh, :jc, :jnc, :jl, :jmp, :ld, :ld_d, :ldb, :ldb_d, :ldh, :ldh_d, :ldub,
9
+ :ldub_d, :lduh, :lduh_d, :ld_plus, :ld24, :ldi8, :ldi16, :lock, :machi, :machi_a, :maclo, :maclo_a,
10
+ :macwhi, :macwhi_a, :macwlo, :macwlo_a, :mul, :mulhi, :mulhi_a, :mullo, :mullo_a, :mulwhi,
11
+ :mulwhi_a, :mulwlo, :mulwlo_a, :mv, :mvfachi, :mvfachi_a, :mvfaclo, :mvfaclo_a, :mvfacmi,
12
+ :mvfacmi_a, :mvfc, :mvtachi, :mvtachi_a, :mvtaclo, :mvtaclo_a, :mvtc,
13
+ :neg, :nop, :not, :rac, :rac_dsi, :rach, :rach_dsi, :rte, :seth, :sll, :sll3, :slli, :sra, :sra3,
14
+ :srai, :srl, :srl3, :srli, :st, :st_d, :stb, :stb_d, :sth, :sth_d, :st_plus, :st_minus,
15
+ :sub, :subv, :subx, :trap, :unlock, :satb, :sath, :sat, :pcmpbz, :sadd, :macwu1, :msblo,
16
+ :mulwu1, :maclh1, :sc, :snc, :max ]
17
+
18
+ def initialize
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,28 @@
1
+ require 'optparse'
2
+ require 'thor'
3
+ require 'disassembler'
4
+
5
+ module ECUTools
6
+ class CLI < Thor
7
+ include Thor::Actions
8
+
9
+ check_unknown_options!
10
+
11
+ default_task :disassemble
12
+
13
+ desc "disassemble <ROM>", "Disassembles a ROM into the current working directory"
14
+ long_desc <<-D
15
+ Dissassemble will take a hex or binary ROM and disassemble it into the proper M32R assembly code.
16
+ Tables and data will be disassembled as code, which will need to be sorted out by the user.
17
+ D
18
+ method_option "out", :type => :string, :aliases => "-o", :banner =>
19
+ "The output filename. If none is provided the rom name will be used with a .asm extension"
20
+ def disassemble(input)
21
+ outfile = "#{File.basename(input, File.extname(input))}.asm"
22
+ dasm = ECUTools::Disassembler.new(input, {:verbose => true})
23
+ dasm.analyze
24
+ dasm.write(outfile)
25
+ end
26
+
27
+ end
28
+ end
@@ -0,0 +1,219 @@
1
+ require 'version'
2
+ require 'instruction'
3
+ require 'helpers'
4
+ require 'nokogiri'
5
+
6
+ module ECUTools
7
+ class Disassembler
8
+ include ECUTools::Helpers
9
+
10
+ def initialize(input = nil, options = {})
11
+ @options = options
12
+ @reference_addresses = {}
13
+ if(!input.nil?)
14
+ open input
15
+ end
16
+ end
17
+
18
+ def verbose
19
+ @options[:verbose]
20
+ end
21
+
22
+ def open(file)
23
+ $stderr.puts "Disassembling binary..." if verbose
24
+ h = '[\d|[a-f]|[A-F]]'
25
+ @assembly = []
26
+ io = IO.popen("gobjdump -b binary --architecture=m32r --disassemble-all --disassemble-zeroes -EB #{file}")
27
+ while line = io.gets
28
+ match = /\s+(#{h}+):\s+(#{h}{2}) (#{h}{2}) (#{h}{2}) (#{h}{2})\s+(.+)/.match(line)
29
+ if match
30
+ @assembly << Instruction.new(match[1], [ match[2], match[3], match[4], match[5] ], match[6])
31
+ end
32
+ end
33
+
34
+ $stderr.puts "Disassembly complete." if verbose
35
+ end
36
+
37
+ def write(file)
38
+ f = File.new(file,"w")
39
+ header = "; ecutools v#{ECUTools::VERSION}\n"
40
+ header << "; Generated assembly from ROM ID #{rom_id}\n"
41
+ header << "; Base Address: #{base_address}\n"
42
+ header << "; Disassembly:\n"
43
+ f.write(header)
44
+ $stderr.puts "Writing assembly..." if verbose
45
+ @assembly.each do |instruction|
46
+ f.write("#{instruction}\n")
47
+ end
48
+ $stderr.puts "Done." if verbose
49
+ end
50
+
51
+ def analyze
52
+ $stderr.puts "Analyzing assembly..." if verbose
53
+ annotate_tables
54
+ annotate_subroutines
55
+ annotate_code
56
+ $stderr.puts "Analyzation complete." if verbose
57
+ end
58
+
59
+ def annotate_tables
60
+ $stderr.puts "Annotating tables..." if verbose
61
+ tables = rom_xml.xpath('/rom/table')
62
+ count = 0
63
+ tables.each do |table|
64
+ elements = 1 # all tables start with one element
65
+ element_size = rom_xml.xpath("/rom/scaling[@name='#{table.attr('scaling')}']").attr('storagetype').value().gsub(/[^\d]+/,'').to_i / 8
66
+ address = from_hex table.attr('address')
67
+ is_data = false
68
+
69
+ table.xpath('table').each do |subtable|
70
+ elements = elements * subtable.attr('elements').to_i
71
+ is_data = true
72
+ end
73
+
74
+ possible_offsets = []
75
+
76
+ case elements
77
+ when 1,4
78
+ possible_offsets << 0
79
+ when 3..20
80
+ possible_offsets << 4
81
+ possible_offsets << 6
82
+ when 44
83
+ possible_offsets << 28 # wtf? maf scaling
84
+ else
85
+ possible_offsets << 7
86
+ possible_offsets << 10
87
+ possible_offsets << 16
88
+ end
89
+
90
+ possible_offsets.each do |offset|
91
+ offset_hex = (address - offset).to_s(16)
92
+ if verbose and @reference_addresses.include? offset_hex
93
+ $stderr.puts "WARNING: Reference hunt collision at 0x#{offset_hex} (#{@reference_addresses[offset_hex]} vs #{table.attr('name')})! Check code carefully!"
94
+ end
95
+ @reference_addresses[offset_hex] = table.attr('name')
96
+ end
97
+
98
+ storage_size = element_size * elements
99
+
100
+ storage_size.times do |n|
101
+ instruction = instruction_at(address + n)
102
+ instruction.comments[0] = table.attr('name') + "(0x#{table.attr('address')} -> 0x#{(address + storage_size - 1).to_s(16)}, #{storage_size} bytes)"
103
+ instruction.data = is_data
104
+ end
105
+
106
+ $stderr.puts "Annotated: #{table.attr('name')}" if verbose
107
+ count = count + 1
108
+ end
109
+ $stderr.puts "#{count} tables annotated." if verbose
110
+ end
111
+
112
+ def annotate_subroutines
113
+
114
+ end
115
+
116
+ def annotate_code
117
+ $stderr.puts "Annotating code..." if verbose
118
+ count = 0
119
+ found_rom_addresses = {}
120
+ found_ram_addresses = {}
121
+ @assembly.each do |instruction|
122
+ # annotate subroute prologue/epilogue
123
+ if instruction.assembly =~ /push lr/
124
+ c = 'begin subroutine'
125
+ c << " #{subroutine_descriptions[instruction.address]}" if subroutine_descriptions.include? instruction.address
126
+ instruction.comments << c
127
+ end
128
+ if instruction.assembly =~ /jmp lr/
129
+ instruction.comments << 'return'
130
+ end
131
+
132
+ # annotate subroutine calls
133
+ match = /bl 0x(\w+)/.match(instruction.assembly)
134
+ if match
135
+ address = match[1]
136
+ if subroutine_descriptions.include? address
137
+ instruction.comments << "Call #{subroutine_descriptions[address]}"
138
+ end
139
+ end
140
+
141
+ # annotate table address references
142
+ address = /0x([\d|[a-f]|[A-F]]+)/.match(instruction.assembly)
143
+ if address and @reference_addresses.include? address[1]
144
+ instruction.comments << "contains possible reference to '#{@reference_addresses[address[1]]}'"
145
+ $stderr.puts "Annotated reference to: #{@reference_addresses[address[1]]} at #{instruction.address}" if verbose
146
+ count = count + 1
147
+ found_rom_addresses[@reference_addresses[address[1]]] = true
148
+ end
149
+
150
+ # annotate absolute RAM addressing ld24 r4,0x800700
151
+ match = /(\w+)\s+\w\w,0x(8\w\w\w\w\w)/.match(instruction.assembly)
152
+ if match
153
+ address = match[2]
154
+ display = address_descriptions[address]
155
+
156
+ if match[1] == "ld24"
157
+ op = "Assign pointer to"
158
+ else
159
+ op = "Unknown op on"
160
+ end
161
+
162
+ if !display.nil? and verbose
163
+ $stderr.puts "Found reference to absolute RAM address #{address} (#{display})"
164
+ end
165
+ instruction.comments << "#{op} RAM address 0x#{address}" + (display.nil? ? '' : " (#{display})")
166
+ found_ram_addresses[display] = true if !display.nil?
167
+ count = count + 1
168
+ end
169
+
170
+ # annotate relative RAM addressing
171
+ match = /(\w+)\s+.+?@\((-?\d+),fp\)/.match(instruction.assembly)
172
+ if match
173
+ address = absolute_address match[2].to_i
174
+ display = address_descriptions[address]
175
+
176
+ case match[1]
177
+ when "lduh"
178
+ op = "Load unsigned halfword from"
179
+ when "ldub"
180
+ op = "Load unsigned byte from"
181
+ when "ld"
182
+ op = "Load from"
183
+ when "ldb"
184
+ op = "Load byte from"
185
+ when "st"
186
+ op = "Store at"
187
+ when "stb"
188
+ op = "Store byte at"
189
+ when "sth"
190
+ op = "Store half word at"
191
+ when "bclr"
192
+ op = "Clear bit in"
193
+ else
194
+ op = "Unknown op on"
195
+ end
196
+ if !display.nil? and verbose
197
+ $stderr.puts "Found reference to relative RAM address #{address} (#{display})"
198
+ end
199
+ instruction.comments << "#{op} RAM address 0x#{address}" + (display.nil? ? '' : " (#{display})")
200
+ found_ram_addresses[display] = true if !display.nil?
201
+ count = count + 1
202
+ end
203
+
204
+ end
205
+
206
+ $stderr.puts "#{count} lines of code annotated." if verbose
207
+ if verbose
208
+ @reference_addresses.each_key do |key|
209
+ $stderr.puts "Unable to find any reference to table #{@reference_addresses[key]}" if !found_rom_addresses.include? @reference_addresses[key]
210
+ found_rom_addresses[@reference_addresses[key]] = true # stop multiple reports
211
+ end
212
+ address_descriptions.each_key do |key|
213
+ $stderr.puts "Unable to find any reference to RAM address #{address_descriptions[key]} (#{key})" if !found_ram_addresses.include? address_descriptions[key]
214
+ end
215
+ end
216
+ end
217
+
218
+ end
219
+ end
@@ -0,0 +1,84 @@
1
+ module ECUTools
2
+ module Helpers
3
+ def rom_id
4
+ return @rom_id if @rom_id
5
+ $stderr.puts "Getting ROM ID..." if verbose
6
+ @rom_id = read_bytes("5002a", 4).join
7
+ end
8
+
9
+ def read_bytes(address, number)
10
+ bytes = []
11
+ start = from_hex address
12
+ number.times do |n|
13
+ inst = instruction_at(start + n)
14
+ bytes << inst.bytes[(start+n) % 4]
15
+ end
16
+ bytes
17
+ end
18
+
19
+ def instruction_at(address, strict = false)
20
+ address = from_hex address
21
+ if strict and (address % 4) > 0
22
+ raise "Address #{address} does not fall on an instruction boundary and strict is enabled."
23
+ end
24
+ @assembly[(address - (address % 4)) / 4]
25
+ end
26
+
27
+ def from_hex(address)
28
+ if address.is_a? String
29
+ address.to_i(16)
30
+ else
31
+ address.to_i
32
+ end
33
+ end
34
+
35
+ def base_address
36
+ return @base_address if @base_address
37
+ $stderr.puts "Getting base address..." if verbose
38
+ @assembly.each_with_index do |instruction,i|
39
+ if instruction.assembly == "st r3,@r0 \|\| nop"
40
+ match = /ld24 fp,(0x\w+)/.match(@assembly[i+1].assembly)
41
+ if match
42
+ @base_address = match[1]
43
+ @assembly[i+1].comments[0] = "Assign base address to #{@base_address}"
44
+ return @base_address
45
+ end
46
+ end
47
+ end
48
+
49
+ $stderr.puts "WARNING: Base address unknown! Setting to 0 (THIS IS WRONG!)" if verbose
50
+ @base_address = 0
51
+ end
52
+
53
+ def absolute_address(relative_address)
54
+ (base_address.to_i(16) + relative_address).to_s(16)
55
+ end
56
+
57
+ def address_descriptions
58
+ return @address_descriptions if @address_descriptions
59
+ @address_descriptions = {}
60
+ xml = Nokogiri::XML(File.open(File.dirname(__FILE__) + "/../xml/ram/#{rom_id}.xml"))
61
+ xml.xpath('/EvoScanDataLogger/vehicle/ecu/Mode2/DataListItem').each do |node|
62
+ @address_descriptions[node.attr('RequestID')[2..-1]] = node.attr('Display')
63
+ end
64
+
65
+ @address_descriptions
66
+ end
67
+
68
+ def subroutine_descriptions
69
+ return @subroutine_descriptions if @subroutine_descriptions
70
+ @subroutine_descriptions = {}
71
+ xml = Nokogiri::XML(File.open(File.dirname(__FILE__) + "/../xml/code/#{rom_id}.xml"))
72
+ xml.xpath('/rom/routine').each do |node|
73
+ @subroutine_descriptions[node.attr('address')] = node.attr('name')
74
+ end
75
+
76
+ @subroutine_descriptions
77
+ end
78
+
79
+ def rom_xml
80
+ return @rom_xml if @rom_xml
81
+ @rom_xml = Nokogiri::XML(File.open(File.dirname(__FILE__) + "/../xml/rom/#{rom_id}.xml"))
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,53 @@
1
+ module ECUTools
2
+ class Instruction
3
+
4
+ def initialize(address, bytes, assembly)
5
+ @address = address
6
+ @bytes = bytes
7
+ @assembly = assembly
8
+ @comments = []
9
+ @data = !(/unknown/.match(assembly).nil?)
10
+ end
11
+
12
+ def to_s
13
+ s = "0x#{@address}:\t#{@bytes.join(' ')}"
14
+ s << "\t#{@assembly}" if (!data)
15
+ s << "\t; #{@comments.join(', ')}" if @comments.length > 0
16
+ s
17
+ end
18
+
19
+ def address
20
+ @address
21
+ end
22
+
23
+ def data
24
+ @data
25
+ end
26
+
27
+ def data=(val)
28
+ @data=val
29
+ end
30
+
31
+ def assembly
32
+ @assembly
33
+ end
34
+
35
+ def assembly=(value)
36
+ @assembly = value
37
+ # TODO: Update bytes based on assembly instructions
38
+ end
39
+
40
+ def binary
41
+ @bytes.join.to_a.pack("H*")
42
+ end
43
+
44
+ def bytes
45
+ @bytes
46
+ end
47
+
48
+ def comments
49
+ @comments
50
+ end
51
+
52
+ end
53
+ end