Skip to content

Instantly share code, notes, and snippets.

@parasquid
Forked from jacobrosenthal/dump_nrf51.rb
Created September 19, 2020 23:16
Show Gist options
  • Save parasquid/8613d5d9b6a614eae6f65e2497f452af to your computer and use it in GitHub Desktop.
Save parasquid/8613d5d9b6a614eae6f65e2497f452af to your computer and use it in GitHub Desktop.
#!/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