TL;DR

This post details how to use the MSFRelay library for Photon boards to write your own Metasploit compatible firmware. Specifically for an add-on called Carloop. If you have a Carloop and just want it to work with Metasploit without having to write any code (or read this) then I've also provided the full code as a library example in the Particle library and can be found here.

Photons Ready

Particle.io has a $20 hobbyist board designed to make IoT devices. They are positioned kind of like the Arduiono of IoT. Once you plug in the board for the first time you will use a phone application to setup the WiFi. From there it talks to the Particle.io cloud. It does all the initial IoT heavy lifting for you but other than that the board provides no additional functions. It is waiting on you to login to the web-based programmer and push firmware changes over the air (OTA) to the device. NOTE: it is possible to keep all your development local but since the “normal” development workflow uses Particle.io's cloud we will as well.

Here is what the board looks like:

These devices are very tiny and run the STM32 ARM Cortex M3 microcontrollers. Which is a very capable chipset. Now the idea with the Photon is to attach things to the digital and/or analog pins to make it actually do something useful. So to make our tutorial more interesting we will take advantage of the STM32's builtin CAN-bus support and use the Carloop.io attachment.

This will allow us to easily plug into the OBD-II port of our vehicle and will provide a more complicated and interesting tutorial. Note: This tutorial focuses on the Photon (WiFi) device but Particle.io also offers the Electron (Cellular) device that should also work the same.

Getting Started

We will assume you went through the Particle.io tutorial and got the photon board registered with your WiFi and connected to your network. You should also have setup your Particle.io account during this process. Verify your device is online via the Console link on the Particle.io page. It should look similar to the below image:

Obviously your ID and Name will vary. Next go to the IDE link (Note you may have to hit the back button) and click the button “Create a New App”. Go ahead and give it a name and a description. I named mine MSFCarloop.

The IDE already creates a setup and loop method that if you programmed Arduinos before you should be familiar with that structure. We will now add the Metasploit Framework relay (MSFRelay) library. Search for MSF-Relay and you should see a library called spark-msf-relay. Once you click on that library you will be prompted to add this library to your project.


This will import the library and add an “include” in the beginning of you program. Now we will add a few lines of code. First define a global MSFRelay variable as msf. The you must use msf.begin() in the setup loop and msf.loop() in the main loop. Msf.begin() sets up the webserver and msf.loop() handles the incoming request. You may notice I've also included msf.setup(). This is optional but if you include it, the device will automatically publish the LocalIP it obtained when it connected to the WiFi to your Particle.io console.


Once you've added this code go ahead and flash it to your device by pressing the little lightning bolt. Once the device is flashed it should reboot and if you view the Particle.io console log you should see the device come up and the LocalIP posted.


Your IP address will be whatever local IP the device received from your network. This is the IP you will plug into as RHOST variable for Metasploit's auxiliary/client/hardware/connect module. At this stage you are up and running and Metasploit compatible! You can connect to the device but you can't do much yet because we didn't actually wire anything up. If you want you can connect, check the uptime and do things like toggle the onboard LED via Metasploit. But since we are doing car hacking we should trudge on and build out the rest of the Carloop firmware.

First go ahead and load the carloop library the same way you added the spark-msf-relay. It will add a header. We don't really need it for such a simple project but if you get any of the carloop accessories such as GPS, it will come in handy. You will need to create a carloop global variable like below:

In the setup add a carloop.begin() and in the main loop() add carloop.update()

Overriding the base methods

To enable the automotive extensions in Metasploit you must report that you are an automotive device. You should also report that we have one CAN bus available. To do this we should extend the MSFRelay class to make our own custom class. I've called my class MSFCarloop. You will want to update the msf global and change it from MSFRelay to whatever you named your custom class.

You can look at the spark-msf-relay.h for a list of overrides and look at the HWBridge API on the return syntax. To support the Automotive extension functions we will override on_request_uri. The on_request_uri method can be used to handle any request that does not come with the base relay such as specific custom hardware commands or to support additional extensions. We will use it to handle the automotive extensions.

To support the automotive extension you need to handle three calls: supported_buses, cansend and isotpsend_and_wait. Responses from the request should be a HttpResponseStatic object that you can return via the client object.

Don't worry if you can't read the screenshot. The code in it's entirety is included at the end of this post. A convenience function is used here, parseQuery(uri). This method breaks down the query parameters into a C map or what is often called a Hash in scripting languages. Above has some examples on how to handle optional arguments such as timeout for isotopsend_and_wait.

Now you just need to create your cansend and isotpsend_and_wait methods. Feel free to just copy them from the Appendix. In these methods we use a structure called CANMessage. This is provided by Particle. MSFRelay has some other convenience features that are used: asc2nibble(char) and incPacketCount(). The method asc2bibble(char) takes a character representing a hex value and converts it to decimal. If the value passed in isn't hex then a value > 0x0F is returned. This is primarily used to validate user input. The second function incPacketCount() is used to increment the sent packets as well as time stamp when the last packet left.

Test it out!

Push the firmware to your device if you haven't already. Once it is done updating, fire up Metasploit and connect to it.

msf > use auxiliary/client/hwbridge/connect

msf auxiliary(connect) > set RHOST 10.1.10.130

RHOST => 10.1.10.130

msf auxiliary(connect) > run

[*] Attempting to connect to 10.1.10.130...

[*] Hardware bridge interface session 1 opened (127.0.0.1 -> 127.0.0.1) at 2017-01-30 22:23:47 -0800

[ ] HWBridge session established

[*] HW Specialty: {"automotive"=>true} Capabilities: {"automotive"=>true}

[!] NOTICE: You are about to leave the matrix. All actions performed on this hardware bridge

[!] could have real world consequences. Use this module in a controlled testing

[!] environment and with equipment you are authorized to perform testing on.

[*] Auxiliary module execution completed

msf auxiliary(connect) > sessions

Active sessions

===============

  Id Type Information Connection

  -- ---- ----------- ----------

  1 hwbridge cmd/hardware automotive 127.0.0.1 -> 127.0.0.1 (10.1.10.130)

Appendix


/ This #include statement was automatically added by the Particle IDE.  
#include <carloop.h>  
  
    
// This #include statement was automatically added by the Particle IDE.  
#include <spark-msf-relay.h>  
  
    
Carloop<CarloopRevision2> carloop;  
  
    
class MSFCarloop : public MSFRelay {  
public:  
  String get_hw_specialty() { return "{ \"automotive\": true }"; }  
  String get_hw_capabilities() { return "{ \"can\": true }"; }  
  bool on_request_uri(String uri) {  
  if(uri.startsWith("/automotive/supported_buses")) {  
  HttpResponseStatic resp("[ { \"bus_name\": \"can0\" } ]", strlen("[ { \"bus_name\": \"can0\" } ]"));  
  client << resp.status(200);  
  return true;  
  } else if(uri.startsWith("/automotive/can0/cansend")) {  
  std::map <String, String>params = parseQuery(uri);  
  if(params.find("id") == params.end() || params.find("data") == params.end()) {  
  HttpResponseStatic resp(not_supported(), strlen(not_supported()));  
  client << resp.status(404);   
  } else {  
  String msg = cansend(params["id"], params["data"]);  
  HttpResponseStatic resp(msg, strlen(msg));  
  client << resp.status(200);  
  }  
  return true;  
  } else if(uri.startsWith("/automotive/can0/isotpsend_and_wait")) {  
  std::map<String, String>params = parseQuery(uri);  
  if(params.find("srcid") == params.end() || params.find("dstid") == params.end() || params.find("data") == params.end()) {  
  HttpResponseStatic resp(not_supported(), strlen(not_supported()));  
  client << resp.status(404);  
  } else {  
  int timeout = 2000;  
  int maxpkts = 3;  
  if (params.find("timeout") != params.end()) timeout = params["timeout"].toInt();  
  if (params.find("maxpkts") != params.end()) maxpkts = params["maxpkts"].toInt();  
  String msg = isotp_send_and_wait(params["srcid"], params["dstid"], params["data"], timeout, maxpkts);  
  HttpResponseStatic resp(msg, strlen(msg));  
  client << resp.status(200);  
  }  
  return true;  
  }  
  return false;  
  }  
  String cansend(String id, String data) {  
  bool success;  
  unsigned char tmp;  
  int i;  
  String resp;  
  success = false;  
  CANMessage message;  
  if (strlen(data) > 16 || strlen(data) == 0 || strlen(id) > 7)  
  return String("{ \"status\": \"not supported\" }");  
  if (strlen(id) < 9) {  
  for(i = 0; i < strlen(id); i++) {  
  if((tmp = asc2nibble(id[i])) > 0x0F) {  
  return String("{ \"status\": \"not supported\" }");  
  }  
  message.id |= (tmp << (2-i)*4);  
  }  
  }  
  message.len = strlen(data) / 2;  
  for(i=0; i< message.len; i++) {  
  tmp = asc2nibble(data[i * 2]);  
  if(tmp > 0x0F)  
  return String("{ \"success\": false }");  
  message.data[i] = (tmp << 4);  
  tmp = asc2nibble(data[i * 2 +1]);  
  if(tmp > 0x0F)  
  return String("{ \"success\": false }");  
  message.data[i] |= tmp;  
  }  
  success = carloop.can().transmit(message);  
  resp ="{ \"success\": ";  
  if(success) {  
  incPacketCount();  
  resp +="true";  
  } else {  
  resp += "false";  
  }  
  resp += " }";  
  return resp;  
  }  
  String isotp_send_and_wait(String src, String dst, String data, int timeout, int maxpkts) {  
  bool success = false;  
  bool first = false;  
  bool data_first = false;  
  unsigned char tmp;  
  unsigned int i, dst_id, current_pkts;  
  unsigned long current_time;  
  String resp;  
  success = false;  
  CANMessage message, answer;  
  if (strlen(data) > 14 || strlen(data) == 0 || strlen(src) > 7 || strlen(dst) > 7)  
  return String("{ \"status\": \"not supported\" }");  
  if (strlen(src) < 9) {  
  for(i = 0; i < strlen(src); i++) {  
  if((tmp = asc2nibble(src[i])) > 0x0F) {  
  return String("{ \"status\": \"not supported\", \"reason\": \"srcid is invalid\" }");  
  }  
  message.id |= (tmp << (2-i)*4);  
  }  
  }  
  tmp = 0;  
  dst_id = 0;  
  if (strlen(dst) < 9) {  
  for(i = 0; i < strlen(dst); i++) {  
  if((tmp = asc2nibble(dst[i])) > 0x0F) {  
  return String("{ \"status\": \"not supported\", \"reason\": \"dstid is invalid\" }");  
  }  
  dst_id |= (tmp << (2-i)*4);  
  }  
  }  
  message.len = (strlen(data) / 2) + 1;  
  message.data[0] = strlen(data) / 2;  
  for(i=0; i< message.len - 1; i++) {  
  tmp = asc2nibble(data[i * 2]);  
  if(tmp > 0x0F)  
  return String("{ \"success\": false, \"reason\": \"Byte " + String(data[i*2]) + " is invalid (" + String(i * 2)+ ")\" }");  
  message.data[i+1] = (tmp << 4);  
  tmp = asc2nibble(data[i * 2 +1]);  
  if(tmp > 0x0F)  
  return String("{ \"success\": false, \"reason\": \"Byte " + String(data[i*2]) + " is invalid (" + String(i * 2 + 1) + ")\" }");  
  message.data[i+1] |= tmp;  
  }  
  
  carloop.can().clearFilters();  
  carloop.can().addFilter(dst_id, 0x7FF);  
  success = carloop.can().transmit(message);  
  if(success) {  
  incPacketCount();  
  resp = "{ \"Packets\": [";  
  first = true;  
  current_pkts = 0;  
  current_time = millis();  
  while(current_pkts < maxpkts && millis() - current_time < timeout) {  
  while(carloop.can().receive(answer)) {  
  if (answer.id == dst_id) {  
  if(first) {  
  resp += "{ ";  
  } else {  
  resp +=", {";  
  }  
  resp += " \"ID\": \"";  
  resp += String::format("%03x", answer.id);  
  resp += "\", ";  
  data_first = true;  
  resp += "\"DATA\": [";  
  for(i = 0; i < answer.len; i++) {  
  if(data_first) {  
  resp += "\"";  
  } else {  
  resp += ", \"";  
  }  
  resp += String::format("%02x", answer.data[i]);  
  resp += "\"";  
  data_first = false;  
  }  
  resp += " ] }";  
  first = false;  
  current_pkts++;  
  }  
  }  
  }  
  resp += " ] }";  
  } else {  
  return String("{ \"success\": false, \"reaons\": \"Couldn't transmit packet, bus busy?\" }");  
  }  
  return resp;  
  }  
};  
  
    
MSFCarloop msf;  
  
    
  
    
void setup() {  
  msf.setup();  
  msf.begin();  
  carloop.begin();  
}  
  
    
void loop() {  
  msf.loop();  
  carloop.update();  
}