Dumping Pebble’s Firmware

Pebble (http://www.getpebble.com) is a smart-watch pretty popular for rasing more than $10 million from a Kickstarter campaign.

Features a STM32 microprocessor as can be seen in wikipedia (http://en.wikipedia.org/wiki/Pebble_(watch)).

Pebble’s update package contains a bootloader image but I wanted to dump what really is on the internal flash.

In this post I dump the memory content using a logging feature provided by pebble’s sdk, which allows a developer to send text from the watch to a monitoring computer.

The next image shows the memory map of the microprocessor and how the flash is mapped onto the memory space, which will be used later to dump the stored firmware.


Pebble-Mem-Map

The version I used to dump the firmware from the Pebble in versions prior to 1.14.1 was much simpler than the current one. It was just a loop reading all the data in the address range and sending it using the Logging feature.

#include "pebble_os.h"
#include "pebble_app.h"
#include "pebble_fonts.h"
 
#define MY_UUID { 0x0A, 0x0A, 0x49, 0x5E, 0x16, 0x60, 0x45, 0x33, 0xBB, 0x7A, 0x00, 0xE3, 0xF6, 0xF1, 0xF6, 0xCB }
PBL_APP_INFO(MY_UUID,
             "Dump FW", "Your Company",
             1, 0, /* App version */
             DEFAULT_MENU_ICON,
             APP_INFO_STANDARD_APP);
 
Window 		window;
TextLayer 	hello_layer;
 
void select_single_click_handler(ClickRecognizerRef recognizer, Window *window) {
  APP_LOG(APP_LOG_LEVEL_DEBUG, "Sending Firmware");
  {
	char		tmp[16] = { 0 }, *p = (char *)0x8000000;
	unsigned int	i = 0;
 
	for (i = 0; i < 0x80FFFFF; i += 4) { 		
            snprintf(tmp, 15, "%02x %02x %02x %02x", *(p + i), *(p + i + 1), *(p + i + 2), *(p + i + 3));   		
            APP_LOG(APP_LOG_LEVEL_DEBUG, tmp); 	
        }
   }
 } 
 
void config_provider(ClickConfig **config, Window *window) {   
   // single click / repeat-on-hold config:   
  config[BUTTON_ID_SELECT]->click.handler = (ClickHandler) select_single_click_handler;
  config[BUTTON_ID_SELECT]->;
  click.repeat_interval_ms = 1000;
}
 
void handle_init(AppContextRef ctx) {
  window_init(&window, "Dump FW");
 
  text_layer_init(&hello_layer, GRect(10, 10, 144, 120));
  text_layer_set_text_alignment(&hello_layer, GTextAlignmentLeft);
  text_layer_set_text(&hello_layer, "Dumping Memory:\nFrom: 0x8000000\nTo: 0x80FFFFF\nClick to send ...");
  text_layer_set_font(&hello_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
 
  layer_add_child(&amp;window.layer, hello_layer.layer);
 
  window_set_click_config_provider(&window, (ClickConfigProvider) config_provider);
  window_stack_push(&window, true /* Animated */);
 
}
 
void pbl_main(void *params) {
  PebbleAppHandlers handlers = {
    .init_handler = &handle_init
  };
  app_event_loop(params, &handlers);
}

This is no longer valid since the current versions seem to have a different watchdog mechanism which resets the device sooner and a problem in the Logging feature makes the dumping much slower.

The newer version makes use of the “ticking” mechanism used by the Pebble to update applications at different timings to make it easier to implement watchfaces.

The version found below outputs 0x1F Bytes each second and allows to select the address from where the app will start sending data so we can resume the firmware extraction if the communication halts.

#include "pebble_os.h"
#include "pebble_app.h"
#include "pebble_fonts.h"
 
#define MY_UUID { 0x0A, 0x0A, 0x49, 0x5E, 0x16, 0x60, 0x45, 0x33, 0xBB, 0x7A, 0x00, 0xE3, 0xF6, 0xF1, 0xF6, 0xCB }
PBL_APP_INFO(MY_UUID,
             "Dump FW", "Your Company",
             1, 0, /* App version */
             DEFAULT_MENU_ICON,
             APP_INFO_STANDARD_APP);
 
Window 		window;
TextLayer 	hello_layer;
static int	i = 0, last_addr = 0;
static char	txttmp[256] = { 0 }, *p = (char *)0x8000000;
static int	dump = 0;
 
void handle_up_button(ClickRecognizerRef recognizer, Window *window) {
	if (!dump) {
		i += 0x100;
		if (i > 0xFFFFC)
			i = 0xFFFC;
	}
        snprintf(txttmp, 255, "Start at:\n\n%p", p + i);
  	text_layer_set_text(&hello_layer, txttmp);
}
 
void handle_middle_button(ClickRecognizerRef recognizer, Window *window) {
	if (!dump)
		dump ++;
	else {
		dump = 0;
        	snprintf(txttmp, 255, "Start at:\n\n%p", p + i);
  		text_layer_set_text(&hello_layer, txttmp);
	}
}
 
void handle_down_button(ClickRecognizerRef recognizer, Window *window) {
	if (!dump) {
		i -= 0x10;
		if (i < 0)
			i = 0x0;
	}
        snprintf(txttmp, 255, "Start at:\n\n%p", p + i);
  	text_layer_set_text(&amp;hello_layer, txttmp);
}
 
void handle_seconds_tick(AppContextRef ctx, PebbleTickEvent *t) {
  char			tmp[256] = { 0 };
 
  if (dump) {
 
        snprintf(txttmp, 255, "Dumping Memory:\n\n%p", p + i);
	for (; i < (0x1F + last_addr); i += 4) { 		if ((unsigned char*)p > (unsigned char *)0x80FFFFC)
			break;
		snprintf(tmp, 255, "%p: %02x %02x %02x %02x", p + i, *(p + i), *(p + i + 1), *(p + i + 2), *(p + i + 3));
  		APP_LOG(APP_LOG_LEVEL_DEBUG, tmp);
	}
	last_addr = i;
  } else
        snprintf(txttmp, 255, "Start at:\n\n%p", p + i);
  text_layer_set_text(&hello_layer, txttmp);
}
 
void config_provider(ClickConfig **config, Window *window) {
  // single click / repeat-on-hold config:
  config[BUTTON_ID_SELECT]->click.handler = (ClickHandler) handle_middle_button;
  config[BUTTON_ID_SELECT]->click.repeat_interval_ms = 50;
  config[BUTTON_ID_UP]->click.handler = (ClickHandler) handle_up_button;
  config[BUTTON_ID_UP]->click.repeat_interval_ms = 50;
  config[BUTTON_ID_DOWN]->click.handler = (ClickHandler) handle_down_button;
  config[BUTTON_ID_DOWN]->click.repeat_interval_ms = 50; 
}
 
void handle_init(AppContextRef ctx) {
  window_init(&amp;window, "Dump FW");
 
  text_layer_init(&hello_layer, GRect(10, 10, 144, 120));
  text_layer_set_text_alignment(&hello_layer, GTextAlignmentLeft);
  text_layer_set_text(&hello_layer, "Dumping Memory:");
  text_layer_set_font(&hello_layer, fonts_get_system_font(FONT_KEY_GOTHIC_18));
 
  layer_add_child(&window.layer, &hello_layer.layer);
 
  window_set_click_config_provider(&window, (ClickConfigProvider) config_provider);
  window_stack_push(&window, true /* Animated */);
 
}
 
void pbl_main(void *params) {
  PebbleAppHandlers handlers = {
    .init_handler = &handle_init,
    .tick_info = {
	.tick_handler = &handle_seconds_tick,
	.tick_units = SECOND_UNIT
    }
  };
  app_event_loop(params, &handlers);
}

 

Botch chunks of codes are based on examples provided in the PebbleSDK; more information about the Pebble’s specific framework can be found online.

For this post, the dumping is done in the in the seconds handler, where the memory is accessed directly from 0x8000000 to 0x80FFFFF; this will print all the data stored in the internal Flash.

void handle_seconds_tick(AppContextRef ctx, PebbleTickEvent *t) {
  char			tmp[256] = { 0 };
 
  if (dump) {
 
        snprintf(txttmp, 255, "Dumping Memory:\n\n%p", p + i);
 
	for (; i < (0x1F + last_addr); i += 4) { 		if ((unsigned char*)p > (unsigned char *)0x80FFFFC)
			break;
		snprintf(tmp, 255, "%p: %02x %02x %02x %02x", p + i, *(p + i), *(p + i + 1), *(p + i + 2), *(p + i + 3));
  		APP_LOG(APP_LOG_LEVEL_DEBUG, tmp);
	}
	last_addr = i;
  } else
        snprintf(txttmp, 255, "Start at:\n\n%p", p + i);
 
  text_layer_set_text(&amp;hello_layer, txttmp);
}

There are several methods to install apps into Pebble, one is opening an app URL  in the phone’s browser and the Pebble’s Phone App will do the rest for us. But the command-line method, included in the libpebble, is much more suitable for our purposes.

$ ./p.py --lightblue reinstall test01.pbw
[DEBUG   ] LightBlue process has started on pid 5440
[DEBUG   ] Connection established to AA:BB:CC:DD:EE:FF
[DEBUG   ] Initializing reader thread
[DEBUG   ] Reader thread loaded on tid Thread-1
app removed
[DEBUG   ] Attempting to add app to bank 1 of 8
[DEBUG   ] Sent 4000 of 4292 bytes
[DEBUG   ] Sent 4292 of 4292 bytes

The next step is to press the button, listen to the debugging events coming from the watch and collect the data for later post-processing. For this the Debugging option must be enabled in the device.

En example of the data received from the Pebble is shown below:

$ ./p.py --lightblue logcat > pebble.fw 
 
[DEBUG ] LightBlue process has started on pid 6207
[DEBUG ] Connection established to AA:BB:CC:DD:EE:FF
[DEBUG ] Initializing reader thread
[DEBUG ] Reader thread loaded on tid Thread-1
1389125231 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x8000000: 90 11 00 20
1389125231 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x8000004: 5d 10 00 08
1389125231 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x8000008: 25 1f 00 08
1389125231 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x800000c: 55 30 00 08
1389125231 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x8000010: 27 1f 00 08
1389125231 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x8000014: 29 1f 00 08
1389125231 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x8000018: 2b 1f 00 08
1389125231 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x800001c: 00 00 00 00
1389125232 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x8000020: 00 00 00 00
1389125232 D aabbccdd-1234-4455-aabb-aabbccddeeff test01.c 59 0x8000024: 00 00 00 00

Now is time to reformat the output, below you can find scripts and a simple C program to generate the binary firmware image:

 

$ cat log2hex.sh 
#!/bin/sh
 
FILE=$1
 
cat ${FILE} | cut -d: -f2- | tr -d '
'
$ cat hex2bin.c 
#include 
 
#define C(a,b,c) (a >=b && a <= c)
#define AB(c) (C(c,'0','9') ? (c - '0') : (C(c,'a','f') ? (c - 'a' + 0xA) : 0xF))
int
main(int argc, char *argv[])
{
	while (!feof(stdin)) { 
		char	a0, b0;
 
		fscanf(stdin, "%c%c ", &amp;a0, &amp;b0);
		printf("%c", (AB(a0) << 4) + AB(b0));
	}
 
	return 0;
}

And combined together to generate the firmware image:

./log2hex.sh pebble.fw | ./hex2bin > pebble_boot.bin

Now the firmware analysis begins, this section contains the bootloader which loads the rest of the Operating System. Below a few lines from a simple string command:

 _   _       _   _
| |_(_)_ __ | |_(_)_ __
| __| | '_ \| __| | '_ \
| |_| | | | | |_| | | | |
 \__|_|_| |_|\__|_|_| |_|
Last firmware boot was stable; clear strikes
Force-booting recovery mode...
Watchdog caused a reset