KernelSU module + Flipper Zero FAP that bridges both devices into a unified pentesting platform over USB CDC serial / BT rfcomm. Android side: bridge daemon, WebUI (:8089), bind mount namespace isolation stealth engine. Flipper side: proper FAP with 4-view GUI, GPIO/SubGHz/IR/file command handlers, async event streaming.
527 lines
17 KiB
C
527 lines
17 KiB
C
/**
|
|
* FlipperDroid Bridge — protocol handlers and USB I/O
|
|
*
|
|
* RX worker reads framed commands from USB CDC, dispatches to handlers.
|
|
* Event worker pushes async events (SubGHz RX, GPIO, buttons) to phone.
|
|
* All hardware access happens here.
|
|
*/
|
|
|
|
#include "fd_app.h"
|
|
#include <furi_hal_power.h>
|
|
#include <furi_hal_infrared.h>
|
|
#include <infrared.h>
|
|
#include <string.h>
|
|
#include <stdio.h>
|
|
|
|
#define TAG "FdBridge"
|
|
|
|
/* ---- USB CDC I/O ---- */
|
|
|
|
void fd_usb_tx(const uint8_t* data, uint16_t len) {
|
|
furi_hal_cdc_send(FD_CDC_CHANNEL, (uint8_t*)data, len);
|
|
}
|
|
|
|
uint16_t fd_usb_rx(uint8_t* buf, uint16_t max_len, uint32_t timeout_ms) {
|
|
uint32_t start = furi_get_tick();
|
|
uint16_t total = 0;
|
|
|
|
while(total < max_len) {
|
|
int32_t n = furi_hal_cdc_receive(FD_CDC_CHANNEL, buf + total, max_len - total);
|
|
if(n > 0) total += n;
|
|
if(furi_get_tick() - start > timeout_ms) break;
|
|
if(total >= max_len) break;
|
|
furi_delay_ms(1);
|
|
}
|
|
return total;
|
|
}
|
|
|
|
/* ---- Frame TX ---- */
|
|
|
|
static void fd_tx_frame(FdApp* app, uint8_t resp_cmd, const uint8_t* payload, uint16_t plen) {
|
|
uint16_t frame_len = 1 + plen; /* cmd + payload */
|
|
uint8_t hdr[5] = {
|
|
FD_MAGIC_HI, FD_MAGIC_LO,
|
|
(frame_len >> 8) & 0xFF, frame_len & 0xFF,
|
|
resp_cmd
|
|
};
|
|
|
|
/* CRC over [len_hi, len_lo, cmd, payload...] */
|
|
uint8_t crc_in[2 + 1 + FD_MAX_PAYLOAD];
|
|
crc_in[0] = hdr[2];
|
|
crc_in[1] = hdr[3];
|
|
crc_in[2] = resp_cmd;
|
|
if(payload && plen > 0) memcpy(crc_in + 3, payload, plen);
|
|
uint8_t crc = fd_crc8(crc_in, 3 + plen);
|
|
|
|
furi_mutex_acquire(app->mutex, FuriWaitForever);
|
|
fd_usb_tx(hdr, 5);
|
|
if(payload && plen > 0) fd_usb_tx(payload, plen);
|
|
fd_usb_tx(&crc, 1);
|
|
app->tx_count++;
|
|
furi_mutex_release(app->mutex);
|
|
}
|
|
|
|
void fd_send_ok(FdApp* app, const uint8_t* data, uint16_t len) {
|
|
fd_tx_frame(app, FD_RESP_OK, data, len);
|
|
}
|
|
|
|
void fd_send_err(FdApp* app, FdError err, const char* msg) {
|
|
uint16_t mlen = msg ? strlen(msg) : 0;
|
|
uint8_t buf[1 + 256];
|
|
buf[0] = (uint8_t)err;
|
|
if(msg && mlen > 0) memcpy(buf + 1, msg, mlen > 255 ? 255 : mlen);
|
|
fd_tx_frame(app, FD_RESP_ERR, buf, 1 + (mlen > 255 ? 255 : mlen));
|
|
app->err_count++;
|
|
}
|
|
|
|
void fd_send_event(FdApp* app, FdEvent ev, const uint8_t* data, uint16_t len) {
|
|
fd_tx_frame(app, (uint8_t)ev, data, len);
|
|
}
|
|
|
|
/* ---- GPIO helpers ---- */
|
|
|
|
static FdGpioEntry* fd_find_gpio(FdApp* app, uint8_t pin_id) {
|
|
for(uint8_t i = 0; i < app->gpio_count; i++) {
|
|
if(app->gpio[i].id == pin_id) return &app->gpio[i];
|
|
}
|
|
return NULL;
|
|
}
|
|
|
|
/* ---- Command handlers ---- */
|
|
|
|
static void fd_cmd_system(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
|
|
UNUSED(p);
|
|
UNUSED(len);
|
|
|
|
switch(cmd) {
|
|
case FdCmdPing:
|
|
fd_send_ok(app, (const uint8_t*)"PONG", 4);
|
|
fd_log(app, "PING -> PONG");
|
|
if(!app->connected) {
|
|
app->connected = true;
|
|
app->connect_tick = furi_get_tick();
|
|
notification_message(app->notifications, &sequence_blink_start_green);
|
|
fd_log(app, "Android connected!");
|
|
}
|
|
break;
|
|
|
|
case FdCmdVersion: {
|
|
char buf[64];
|
|
int n = snprintf(buf, sizeof(buf), "%s v%s (%s)",
|
|
FD_DEVICE_NAME, FD_VERSION,
|
|
furi_hal_version_get_name_ptr() ? furi_hal_version_get_name_ptr() : "Flipper");
|
|
fd_send_ok(app, (const uint8_t*)buf, n);
|
|
break;
|
|
}
|
|
|
|
case FdCmdCapabilities: {
|
|
uint8_t caps = FD_CAP_GPIO | FD_CAP_SUBGHZ | FD_CAP_IR | FD_CAP_CPU;
|
|
fd_send_ok(app, &caps, 1);
|
|
break;
|
|
}
|
|
|
|
case FdCmdStatus: {
|
|
uint8_t st[9];
|
|
st[0] = furi_hal_power_get_pct();
|
|
int16_t temp = (int16_t)(furi_hal_power_get_battery_temperature(FuriHalPowerICFuelGauge) * 10);
|
|
st[1] = (temp >> 8) & 0xFF;
|
|
st[2] = temp & 0xFF;
|
|
uint32_t up = (furi_get_tick() - app->connect_tick) / 1000;
|
|
st[3] = (up >> 24) & 0xFF;
|
|
st[4] = (up >> 16) & 0xFF;
|
|
st[5] = (up >> 8) & 0xFF;
|
|
st[6] = up & 0xFF;
|
|
st[7] = app->subghz_rx_active ? 1 : 0;
|
|
st[8] = app->ir_rx_active ? 1 : 0;
|
|
fd_send_ok(app, st, 9);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
fd_send_err(app, FdErrUnknownCmd, "bad sys cmd");
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void fd_cmd_gpio(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
|
|
switch(cmd) {
|
|
case FdCmdGpioInit: {
|
|
if(len < 2) { fd_send_err(app, FdErrInvalidParams, "need pin,mode"); return; }
|
|
FdGpioEntry* g = fd_find_gpio(app, p[0]);
|
|
if(!g) { fd_send_err(app, FdErrInvalidParams, "bad pin"); return; }
|
|
|
|
switch(p[1]) {
|
|
case 0:
|
|
furi_hal_gpio_init(g->pin, GpioModeInput, GpioPullNo, GpioSpeedLow);
|
|
g->output = false;
|
|
break;
|
|
case 1:
|
|
furi_hal_gpio_init(g->pin, GpioModeOutputPushPull, GpioPullNo, GpioSpeedVeryHigh);
|
|
g->output = true;
|
|
break;
|
|
case 2:
|
|
furi_hal_gpio_init(g->pin, GpioModeOutputOpenDrain, GpioPullNo, GpioSpeedVeryHigh);
|
|
g->output = true;
|
|
break;
|
|
case 3:
|
|
furi_hal_gpio_init(g->pin, GpioModeAnalog, GpioPullNo, GpioSpeedLow);
|
|
g->output = false;
|
|
break;
|
|
default:
|
|
fd_send_err(app, FdErrInvalidParams, "bad mode");
|
|
return;
|
|
}
|
|
g->initialized = true;
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "GPIO %s init mode %u", g->name, p[1]);
|
|
break;
|
|
}
|
|
|
|
case FdCmdGpioWrite: {
|
|
if(len < 2) { fd_send_err(app, FdErrInvalidParams, "need pin,val"); return; }
|
|
FdGpioEntry* g = fd_find_gpio(app, p[0]);
|
|
if(!g) { fd_send_err(app, FdErrInvalidParams, "bad pin"); return; }
|
|
if(!g->initialized) { fd_send_err(app, FdErrDisabled, "pin not init"); return; }
|
|
furi_hal_gpio_write(g->pin, p[1] ? true : false);
|
|
g->value = p[1] ? true : false;
|
|
fd_send_ok(app, NULL, 0);
|
|
break;
|
|
}
|
|
|
|
case FdCmdGpioRead: {
|
|
if(len < 1) { fd_send_err(app, FdErrInvalidParams, "need pin"); return; }
|
|
FdGpioEntry* g = fd_find_gpio(app, p[0]);
|
|
if(!g) { fd_send_err(app, FdErrInvalidParams, "bad pin"); return; }
|
|
if(!g->initialized) {
|
|
/* Auto-init as input for convenience */
|
|
furi_hal_gpio_init(g->pin, GpioModeInput, GpioPullNo, GpioSpeedLow);
|
|
g->initialized = true;
|
|
g->output = false;
|
|
}
|
|
uint8_t val = furi_hal_gpio_read(g->pin) ? 1 : 0;
|
|
g->value = val;
|
|
fd_send_ok(app, &val, 1);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
fd_send_err(app, FdErrNotSupported, "gpio cmd n/a");
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void fd_cmd_subghz(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
|
|
switch(cmd) {
|
|
case FdCmdSubghzSetFreq: {
|
|
if(len < 4) { fd_send_err(app, FdErrInvalidParams, "need 4B freq"); return; }
|
|
uint32_t freq = ((uint32_t)p[0] << 24) | ((uint32_t)p[1] << 16) |
|
|
((uint32_t)p[2] << 8) | p[3];
|
|
|
|
bool valid = (freq >= 300000000 && freq <= 348000000) ||
|
|
(freq >= 387000000 && freq <= 464000000) ||
|
|
(freq >= 779000000 && freq <= 928000000);
|
|
if(!valid) { fd_send_err(app, FdErrInvalidParams, "bad freq range"); return; }
|
|
|
|
app->subghz_freq = freq;
|
|
furi_hal_subghz_idle();
|
|
furi_hal_subghz_set_frequency(freq);
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "SubGHz freq=%lu", freq);
|
|
break;
|
|
}
|
|
|
|
case FdCmdSubghzTx: {
|
|
if(len < 1) { fd_send_err(app, FdErrInvalidParams, "need data"); return; }
|
|
if(app->subghz_freq == 0) { fd_send_err(app, FdErrInvalidParams, "set freq first"); return; }
|
|
furi_hal_subghz_idle();
|
|
furi_hal_subghz_set_frequency(app->subghz_freq);
|
|
furi_hal_subghz_write_packet(p, len);
|
|
furi_hal_subghz_tx();
|
|
furi_delay_ms(100);
|
|
furi_hal_subghz_idle();
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "SubGHz TX %u bytes", len);
|
|
break;
|
|
}
|
|
|
|
case FdCmdSubghzRxStart: {
|
|
if(app->subghz_freq == 0) { fd_send_err(app, FdErrInvalidParams, "set freq first"); return; }
|
|
furi_hal_subghz_idle();
|
|
furi_hal_subghz_set_frequency(app->subghz_freq);
|
|
furi_hal_subghz_rx();
|
|
app->subghz_rx_active = true;
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "SubGHz RX start");
|
|
break;
|
|
}
|
|
|
|
case FdCmdSubghzRxStop:
|
|
furi_hal_subghz_idle();
|
|
app->subghz_rx_active = false;
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "SubGHz RX stop");
|
|
break;
|
|
|
|
case FdCmdSubghzGetRssi: {
|
|
float rssi = furi_hal_subghz_get_rssi();
|
|
app->subghz_last_rssi = rssi;
|
|
int16_t ri = (int16_t)(rssi * 10);
|
|
uint8_t rb[2] = {(ri >> 8) & 0xFF, ri & 0xFF};
|
|
fd_send_ok(app, rb, 2);
|
|
break;
|
|
}
|
|
|
|
default:
|
|
fd_send_err(app, FdErrNotSupported, "subghz cmd n/a");
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void fd_cmd_ir(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
|
|
switch(cmd) {
|
|
case FdCmdIrTx: {
|
|
if(len < 9) { fd_send_err(app, FdErrInvalidParams, "need 9B"); return; }
|
|
uint32_t addr = ((uint32_t)p[1] << 24) | ((uint32_t)p[2] << 16) |
|
|
((uint32_t)p[3] << 8) | p[4];
|
|
uint32_t command = ((uint32_t)p[5] << 24) | ((uint32_t)p[6] << 16) |
|
|
((uint32_t)p[7] << 8) | p[8];
|
|
|
|
InfraredMessage msg = {
|
|
.protocol = p[0],
|
|
.address = addr,
|
|
.command = command,
|
|
.repeat = false,
|
|
};
|
|
infrared_send(&msg, 1);
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "IR TX proto=%u", p[0]);
|
|
break;
|
|
}
|
|
|
|
case FdCmdIrRxStart:
|
|
app->ir_rx_active = true;
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "IR RX start");
|
|
break;
|
|
|
|
case FdCmdIrRxStop:
|
|
app->ir_rx_active = false;
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "IR RX stop");
|
|
break;
|
|
|
|
default:
|
|
fd_send_err(app, FdErrNotSupported, "ir cmd n/a");
|
|
break;
|
|
}
|
|
}
|
|
|
|
static void fd_cmd_file(FdApp* app, uint8_t cmd, const uint8_t* p, uint16_t len) {
|
|
Storage* storage = furi_record_open(RECORD_STORAGE);
|
|
|
|
switch(cmd) {
|
|
case FdCmdFileList: {
|
|
char path[256] = "/ext";
|
|
if(len > 0 && len < sizeof(path)) {
|
|
memcpy(path, p, len);
|
|
path[len] = '\0';
|
|
}
|
|
|
|
File* dir = storage_file_alloc(storage);
|
|
if(!storage_dir_open(dir, path)) {
|
|
fd_send_err(app, FdErrHardware, "open dir fail");
|
|
storage_file_free(dir);
|
|
break;
|
|
}
|
|
|
|
char name[128];
|
|
FileInfo info;
|
|
uint8_t resp[FD_MAX_PAYLOAD];
|
|
uint16_t pos = 0;
|
|
|
|
while(storage_dir_read(dir, &info, name, sizeof(name))) {
|
|
uint16_t nlen = strlen(name);
|
|
if(pos + nlen + 3 > sizeof(resp)) break;
|
|
resp[pos++] = (info.flags & FSF_DIRECTORY) ? 'd' : 'f';
|
|
resp[pos++] = ',';
|
|
memcpy(resp + pos, name, nlen);
|
|
pos += nlen;
|
|
resp[pos++] = '\n';
|
|
}
|
|
storage_dir_close(dir);
|
|
storage_file_free(dir);
|
|
fd_send_ok(app, resp, pos);
|
|
fd_log(app, "ls %s -> %u entries", path, pos);
|
|
break;
|
|
}
|
|
|
|
case FdCmdFileRead: {
|
|
if(len < 1) { fd_send_err(app, FdErrInvalidParams, "need path"); break; }
|
|
char path[256];
|
|
uint16_t pl = (len < 255) ? len : 255;
|
|
memcpy(path, p, pl);
|
|
path[pl] = '\0';
|
|
|
|
File* file = storage_file_alloc(storage);
|
|
if(!storage_file_open(file, path, FSAM_READ, FSOM_OPEN_EXISTING)) {
|
|
fd_send_err(app, FdErrHardware, "open fail");
|
|
storage_file_free(file);
|
|
break;
|
|
}
|
|
uint8_t buf[FD_MAX_PAYLOAD];
|
|
uint16_t rd = storage_file_read(file, buf, sizeof(buf));
|
|
storage_file_close(file);
|
|
storage_file_free(file);
|
|
fd_send_ok(app, buf, rd);
|
|
fd_log(app, "read %s %uB", path, rd);
|
|
break;
|
|
}
|
|
|
|
case FdCmdFileWrite: {
|
|
if(len < 3) { fd_send_err(app, FdErrInvalidParams, "need plen+path+data"); break; }
|
|
uint16_t plen_f = ((uint16_t)p[0] << 8) | p[1];
|
|
if(2 + plen_f > len) { fd_send_err(app, FdErrInvalidParams, "bad plen"); break; }
|
|
|
|
char path[256];
|
|
uint16_t cpy = (plen_f < 255) ? plen_f : 255;
|
|
memcpy(path, p + 2, cpy);
|
|
path[cpy] = '\0';
|
|
|
|
File* file = storage_file_alloc(storage);
|
|
if(!storage_file_open(file, path, FSAM_WRITE, FSOM_CREATE_ALWAYS)) {
|
|
fd_send_err(app, FdErrHardware, "create fail");
|
|
storage_file_free(file);
|
|
break;
|
|
}
|
|
uint16_t dlen = len - 2 - plen_f;
|
|
storage_file_write(file, p + 2 + plen_f, dlen);
|
|
storage_file_close(file);
|
|
storage_file_free(file);
|
|
fd_send_ok(app, NULL, 0);
|
|
fd_log(app, "write %s %uB", path, dlen);
|
|
break;
|
|
}
|
|
|
|
case FdCmdFileDelete: {
|
|
if(len < 1) { fd_send_err(app, FdErrInvalidParams, "need path"); break; }
|
|
char path[256];
|
|
uint16_t pl = (len < 255) ? len : 255;
|
|
memcpy(path, p, pl);
|
|
path[pl] = '\0';
|
|
if(storage_simply_remove(storage, path))
|
|
fd_send_ok(app, NULL, 0);
|
|
else
|
|
fd_send_err(app, FdErrHardware, "delete fail");
|
|
break;
|
|
}
|
|
|
|
default:
|
|
fd_send_err(app, FdErrUnknownCmd, "bad file cmd");
|
|
break;
|
|
}
|
|
|
|
furi_record_close(RECORD_STORAGE);
|
|
}
|
|
|
|
/* ---- Command dispatch ---- */
|
|
|
|
void fd_handle_cmd(FdApp* app, uint8_t cmd, const uint8_t* payload, uint16_t len) {
|
|
FURI_LOG_D(TAG, "CMD 0x%02X len=%u", cmd, len);
|
|
app->rx_count++;
|
|
|
|
if(cmd <= 0x04) fd_cmd_system(app, cmd, payload, len);
|
|
else if(cmd <= 0x15) fd_cmd_gpio(app, cmd, payload, len);
|
|
else if(cmd <= 0x26) fd_cmd_subghz(app, cmd, payload, len);
|
|
else if(cmd >= 0x50 && cmd <= 0x54) fd_cmd_ir(app, cmd, payload, len);
|
|
else if(cmd >= 0x90 && cmd <= 0x93) fd_cmd_file(app, cmd, payload, len);
|
|
else fd_send_err(app, FdErrUnknownCmd, "unknown");
|
|
}
|
|
|
|
/* ---- RX worker ---- */
|
|
|
|
int32_t fd_rx_worker(void* ctx) {
|
|
FdApp* app = (FdApp*)ctx;
|
|
FURI_LOG_I(TAG, "RX worker started");
|
|
|
|
while(app->running) {
|
|
/* Scan for magic bytes */
|
|
uint8_t magic[2];
|
|
if(fd_usb_rx(magic, 2, 100) < 2) continue;
|
|
if(magic[0] != FD_MAGIC_HI || magic[1] != FD_MAGIC_LO) continue;
|
|
|
|
/* Read frame length */
|
|
uint8_t lb[2];
|
|
if(fd_usb_rx(lb, 2, 500) < 2) continue;
|
|
uint16_t flen = ((uint16_t)lb[0] << 8) | lb[1];
|
|
if(flen > FD_MAX_PAYLOAD + 1) {
|
|
FURI_LOG_W(TAG, "Frame too big: %u", flen);
|
|
continue;
|
|
}
|
|
|
|
/* Read cmd + payload + crc */
|
|
uint8_t frame[FD_MAX_PAYLOAD + 2];
|
|
uint16_t got = fd_usb_rx(frame, flen + 1, 1000);
|
|
if(got < flen + 1) {
|
|
FURI_LOG_W(TAG, "Short frame: %u/%u", got, flen + 1);
|
|
continue;
|
|
}
|
|
|
|
/* Verify CRC */
|
|
uint8_t crc_in[2 + FD_MAX_PAYLOAD + 1];
|
|
crc_in[0] = lb[0];
|
|
crc_in[1] = lb[1];
|
|
memcpy(crc_in + 2, frame, flen);
|
|
uint8_t expected = fd_crc8(crc_in, 2 + flen);
|
|
if(expected != frame[flen]) {
|
|
FURI_LOG_W(TAG, "CRC fail: %02X vs %02X", frame[flen], expected);
|
|
continue;
|
|
}
|
|
|
|
/* Dispatch */
|
|
fd_handle_cmd(app, frame[0], frame + 1, flen - 1);
|
|
notification_message(app->notifications, &sequence_blink_cyan_10);
|
|
}
|
|
|
|
FURI_LOG_I(TAG, "RX worker stopped");
|
|
return 0;
|
|
}
|
|
|
|
/* ---- Event worker ---- */
|
|
|
|
int32_t fd_event_worker(void* ctx) {
|
|
FdApp* app = (FdApp*)ctx;
|
|
FURI_LOG_I(TAG, "Event worker started");
|
|
|
|
while(app->running) {
|
|
/* SubGHz RX check */
|
|
if(app->subghz_rx_active) {
|
|
if(furi_hal_subghz_is_rx_data_crc_valid()) {
|
|
uint8_t pkt[64];
|
|
furi_hal_subghz_read_packet(pkt, sizeof(pkt));
|
|
float rssi = furi_hal_subghz_get_rssi();
|
|
app->subghz_last_rssi = rssi;
|
|
|
|
/* Pack: data(64) + rssi_x10(2) + freq(4) */
|
|
uint8_t ev[70];
|
|
memcpy(ev, pkt, 64);
|
|
int16_t ri = (int16_t)(rssi * 10);
|
|
ev[64] = (ri >> 8) & 0xFF;
|
|
ev[65] = ri & 0xFF;
|
|
ev[66] = (app->subghz_freq >> 24) & 0xFF;
|
|
ev[67] = (app->subghz_freq >> 16) & 0xFF;
|
|
ev[68] = (app->subghz_freq >> 8) & 0xFF;
|
|
ev[69] = app->subghz_freq & 0xFF;
|
|
|
|
fd_send_event(app, FdEventSubghzRx, ev, 70);
|
|
notification_message(app->notifications, &sequence_blink_green_10);
|
|
fd_log(app, "SubGHz RX rssi=%.1f", (double)rssi);
|
|
}
|
|
}
|
|
|
|
furi_delay_ms(10);
|
|
}
|
|
|
|
FURI_LOG_I(TAG, "Event worker stopped");
|
|
return 0;
|
|
}
|