Hacking The Hacker’s Tool — Patching Flipper Zero’s Levels For Fun
TL;DR
I researched how the levels functionality in Flipper Zero works and tampered with it by changing the backup files so I can rank up [very] fast.
There’s a demo video at the end.
Why
So recently I started learning C and at a perfect timing I also got my hands on a Flipper Zero.
It has 3 levels, and since I’m familiar with game hacking — I really wanted to reach the next levels by walking the “unintended” way — AKA cheating ;)
Current stats:
DISCLAIMER: I know that Flipper Zero’s firmware was meant to be patched, rewritten and flashed, but this time I challenged myself to dig into the C source code of a [real] firmware for the first time and to tamper with something that is kinda like a game (Tamagochi), so I think that’s a cool experience that worth some flex and was really interesting!
What
Basically, there’s a desktop application called qFlipper that enables you to do a lot of stuff w/ your flipper (flash firmware, backup settings, factory reset) and more.
Specifically I wanted to test the backup/restore functionality, since I saw that you can backup your “Dolphin”.
“Dolphin” is your avatar in flipper and it has levels and moods. Its levels are increased every time that you use your flipper, e.g read RF signals, scan NFC cards and more.
You can backup your flipper settings and restore them. After I did a backup, factory reset and restored from backup I returned to my original level — which means that the level is also saved in the backup files.
Let’s see how they look like.
We can get the backup files by pressing the “Backup” button in qflipper:
The backup files are saved in your PC as a gzip compressed data file like the following:
Specifically, the .dolphin.state
is the one that is most likely to store your level/XP:
Now, let’s dig into the source code of the firmware.
dolphin_state_filename.h
defines the name of the saved file
define DOLPHIN_STATE_FILE_NAME ".dolphin.state"
And in dolphin_state.c
we can see that the actual path is defined, alongside other parameters such as DOLPHIN_STATE_HEADER_MAGIC
,DOLPHIN_STATE_HEADER_VERSION
and LEVEL2_THRESHOLD
:
#define DOLPHIN_STATE_PATH INT_PATH(DOLPHIN_STATE_FILE_NAME)
#define DOLPHIN_STATE_HEADER_MAGIC 0xD0
#define DOLPHIN_STATE_HEADER_VERSION 0x01
#define LEVEL2_THRESHOLD 300
#define LEVEL3_THRESHOLD 1800
#define BUTTHURT_MAX 14
#define BUTTHURT_MIN 0
Following the references to DOLPHIN_STATE_PATH
we see a function that might be responsible to save the struct that we saw previously - dolphin_state_save
which calls saved_struct_save
and seem to use the size of a struct, the path, the data and all other good stuff:
bool dolphin_state_save(DolphinState* dolphin_state) {
if(!dolphin_state->dirty) {
return true;
}
bool result = saved_struct_save(
DOLPHIN_STATE_PATH,
&dolphin_state->data,
sizeof(DolphinStoreData),
DOLPHIN_STATE_HEADER_MAGIC,
DOLPHIN_STATE_HEADER_VERSION);
if(result) {
FURI_LOG_I(TAG, "State saved");
dolphin_state->dirty = false;
} else {
FURI_LOG_E(TAG, "Failed to save state");
}
return result;
}
Jumping to the declaration of the saved_struct_save function to see what the types are:
bool saved_struct_save(const char* path, void* data, size_t size, uint8_t magic, uint8_t version) {
So, it suits the parameters that we saw before.
Jumping into the definition of the saved_struct_save
function - we see an initialization of a boolean result:
bool result = true;
And while trying to load the state in saved_struct_load
- the result
is set to false
in case the header (savedStructHeader
) isn’t sized correctly:
if(bytes_count != (sizeof(SavedStructHeader) + size)) {
FURI_LOG_E(TAG, "Size mismatch of file \\"%s\\"", path);
result = false;
}
And there are more tests here to verify the magic
& version
, for example:
if(result && (header.magic != magic || header.version != version)) {
FURI_LOG_E(
TAG,
"Magic(%d != %d) or Version(%d != %d) mismatch of file \\"%s\\"",
header.magic,
magic,
header.version,
version,
path);
result = false;
}
And a calculation of a checksum. size
= size_t
argument in function (see definition above), and eventually is sizeof(DolphinStoreData).
Here’s how the checksum
is calculated:
if(result) {
uint8_t checksum = 0;
const uint8_t* source = (const uint8_t*)data_read;
for(size_t i = 0; i < size; i++) {
checksum += source[i];
}
The checksum goes byte after byte of the data_read
and adds each bytes to itself (initial value is 0).
So the final checksum
should be the sum of all the data of the state.
In a case of a checksum mismatch — we are doomed:
if(header.checksum != checksum) {
FURI_LOG_E(
TAG, "Checksum(%d != %d) mismatch of file \\"%s\\"", header.checksum, checksum, path);
result = false;
}
And the restore from backup won’t work:
if(result) {
memcpy(data, data_read, size);
}
Now, we know the following:
- How the
checksum
is calculated - How the state is verified, saved and loaded
We need to understand:
- Where the
checksum
is stored in the binary.dolphin.state
file - Where the level/XP are in this whole state, how they behave and where they are saved in the binary
.dolphin.state
file, so we could tamper with them.
Let’s go.
In saved_struct.c
we have the state’s header - see the checksum’s location in the following struct:
typedef struct {
uint8_t magic;
uint8_t version;
uint8_t checksum;
uint8_t flags;
uint32_t timestamp;
} SavedStructHeader;
The checksum is 2 bytes (16 bits) after the 1st member:
So when we’ll change the values of the state itself — we need to keep in mind to re-calculate a valid checksum
— otherwise our patch won’t work.
So the header struct can be represented like this:
+----------+ +------------+ +-------------+ +-----------+ +-------------+
| magic | | version | | checksum | | flags | | timestamp |
| (1 byte) | | (1 byte) | | (1 byte) | | (1 byte) | | (4 bytes) |
+----------+ +------------+ +-------------+ +-----------+ +-------------+
(8 bytes in total)
Now, let’s see how the levels/XP are calculated & stored.
By searching for already-known value DOLPHIN_STATE_FILE_NAME
from before - we return to the file dolphin_state.c
and notice the following:
#define LEVEL2_THRESHOLD 300
#define LEVEL3_THRESHOLD 1800
Ok, so there are 3 levels and there’s some threshold for each of them. Maybe that’s the XP threshold? Let’s see.
At the bottom of the page we notice the following:
bool dolphin_state_is_levelup(uint32_t icounter) {
return (icounter == LEVEL2_THRESHOLD) || (icounter == LEVEL3_THRESHOLD);
}
uint8_t dolphin_get_level(uint32_t icounter) {
if(icounter <= LEVEL2_THRESHOLD) {
return 1;
} else if(icounter <= LEVEL3_THRESHOLD) {
return 2;
} else {
return 3;
}
}
Ok, so there’s an icounter
variable that decides the actual level of the Dolphin.
In another function in the same file we see this:
uint32_t dolphin_state_xp_to_levelup(uint32_t icounter) {
uint32_t threshold = 0;
if(icounter <= LEVEL2_THRESHOLD) {
threshold = LEVEL2_THRESHOLD;
} else if(icounter <= LEVEL3_THRESHOLD) {
threshold = LEVEL3_THRESHOLD;
} else {
threshold = (uint32_t)-1;
}
return threshold - icounter;
}
Great. The icounter
is surely the XP that is used.
Now that we know how the XP (icounter
) behaves, let’s see where it’s saved in the struct and change it (and the checksum
) accordingly!
Let’s go to the header file of the same file, called dolphin_state.h
typedef struct DolphinState DolphinState;
typedef struct {
uint8_t icounter_daily_limit[DolphinAppMAX];
uint8_t butthurt_daily_limit;
uint32_t flags;
uint32_t icounter;
int32_t butthurt;
uint64_t timestamp;
} DolphinStoreData;
Ok, here’s the struct that we need. It has 8-bit array of DolphinAppMAX
(we don’t know what it is yet), a few other members and icounter
is right there - uint32_t
.
Now, here’s the binary presentation of our state data struct in memory:
+---------------------------------------+
| DolphinStoreData |
+---------------------------------------+
| icounter_daily_limit[DolphinAppMAX] |
| (DolphinAppMAX bytes) |
+---------------------------------------+
| butthurt_daily_limit |
| (1 byte) |
+---------------------------------------+
| flags |
| (4 bytes) |
+---------------------------------------+
| icounter |
| (4 bytes) |
+---------------------------------------+
| butthurt |
| (4 bytes) |
+---------------------------------------+
| timestamp |
| (8 bytes) |
+---------------------------------------+
Let’s understand what’s the size of the icounter_daily_limit
array so we could know how to find the icounter
bytes in the .dolphin.state
binary!
We see thatDolphinAppMAX
is an enum in dolphin_deed.c
:
typedef enum {
DolphinAppSubGhz,
DolphinAppRfid,
DolphinAppNfc,
DolphinAppIr,
DolphinAppIbutton,
DolphinAppBadusb,
DolphinAppPlugin,
DolphinAppMAX,
} DolphinApp;
Since the value of the DolphinAppMax
isn’t declared - it gets the default one according to the index, which is 7 (the indexes of the values in the enum are starting from 0), like that:
typedef enum {
DolphinAppSubGhz, (val is 0)
DolphinAppRfid, ( val is 1)
DolphinAppNfc, ( val is 2)
DolphinAppIr, (val is 3)
DolphinAppIbutton, (val is 4)
DolphinAppBadusb, (val is 5)
DolphinAppPlugin, (val is 6)
DolphinAppMAX, (val is 7) <-------- this!
} DolphinApp;
So icounter_daily_limit
is basically an array sized 7.
Here’s how the body(data) of the state looks like now:
+---------------------------------------+
| DolphinStoreData |
+---------------------------------------+
| icounter_daily_limit[7] |
| (7 bytes) |
+---------------------------------------+
| butthurt_daily_limit |
| (1 byte) |
+---------------------------------------+
| flags |
| (4 bytes) |
+---------------------------------------+
| icounter |
| (4 bytes) |
+---------------------------------------+
| butthurt |
| (4 bytes) |
+---------------------------------------+
| timestamp |
| (8 bytes) |
+---------------------------------------+
(28 bytes in total)
This DolphinStoreData
struct is actually a member of another struct called DolphinState
in dolphin_state.h
:
struct DolphinState {
DolphinStoreData data;
bool dirty;
};
Which is actually the struct that is used when allocating, freeing and writing to memory for the save/load functionality of the binary .dolphin.state
void dolphin_state_free(DolphinState* dolphin_state);
bool dolphin_state_save(DolphinState* dolphin_state);
bool dolphin_state_load(DolphinState* dolphin_state);
DolphinState* dolphin_state_alloc();
Therefore, our new DolphinState struct is saved in memory like this:
+---------------------------------------+
| DolphinState |
| |
+---------------------------------------+
| DolphinStoreData data |
| (28 bytes) |
+---------------------------------------+
| bool dirty |
| (1 byte) |
+---------------------------------------+
So the size of the body of the dolphin is actually 29 bytes.
Now, SavedStructHeader
is the header (with the magic and checksum) and DolphinState
is the body (with the icounter and butthurt members):
+---------------------------------------+
| SavedStructHeader |
| (8 bytes) |
+---------------------------------------+
+---------------------------------------+
| DolphinState |
| (29 bytes) |
+---------------------------------------+
Calculating the offset of our bytes in file:
checksum
offset: magic
(1) + version
(1) = 1+1 = 2
icounter
offset: magic
(1) + version
(1) + checksum
(1) + flags
(1) + timestamp
(4) + icounter_daily_limit
(7) + butthurt_daily_limit
(1) + flags
(4) = 1+1+1+1+4+7+1+4= 20
Let’s return to the actual .dolphin.state
file:
00000000: d001 cf00 0000 0000 000f 1414 0100 022e ................
00000010: 0000 0000 3a00 0000 0000 0000 0000 0000 ....:...........
00000020: 113e 7a64 0000 0000 0a .>zd.....
Now, we know how to calculate the offsets of each one of the members in the two structs (header and body):
So the values are:
checksum
: 0xcf (207 in decimal)
icounter
: 0x3a (58 in decimal)
Now, we want to increase the icounter
, let’s increase both icounter
and checksum
by 200.
tempchecksum
: 0x197 (407 in decimal)
new icounter
: 0x102 (258 in decimal)
Notice I called the new value of checksum
temp checksum
. Here’s why:
The chucksum
is 1 byte long (8-bit) -
uint8_t checksum;
And the largest value that 1 byte can hold is 0xff (255 in decimal).
BUT — have here 0x197- which is 407 in decimal that should be represented by 1 byte.
The new value 0x197 (407 in decimal) is bigger than the possible largest 1-byte value, which is 255 in decimal, 0xff in hex.
Therefore, we should use modulo on the new value of 256 and add the remainder from the devision by 256 (if there’s any) to the total checksum
value.
In other words, the new value should be 407 % 256 + remainder (remainder from dividing 407 by 256).
calculating the new value as the following:
temp checksum
= 407 in decimal
remainder = tempchecksum
/ 256 = 407 / 256 = 1
result = temp checksum
% 256 + remainder = 407 % 256 + 1 = 151
new checksum
: 0x98 (151 in decimal)
new icounter
: 0x102 (258 in decimal)
We don’t have the same thing with icounter
since it’s uint32_t
, therefore the largest value that icounter
can hold is 0xffffffff - 2^32, (4294967295 in decimal)
Now, we can edit the .dolphin.state
file manually.
Since I’m lazy I’m using a trick to automatically edit the bytes:
- Open the file in vim
- run
:%!xxd
to convert to hexdump - Edit the hexdump
- run
:%!xxd -r
to convent the hexdump to binary - Save
So our new .dolphin.state
file looks like this:
*Notice the new values for the checksum
and icounter
*To easily convert hex or decimal 4-byte values I wrote this little script, you might find it useful:
import argparse
import struct
import os
parser = argparse.ArgumentParser(description="Packer for bytes, enter byte and get the little endian representation for binary relace")
parser.add_argument("byte", type=str, help="unter byte value, such as 300, 0x50")
args = parser.parse_args()
byte = args.byte
print(f"Packing {byte}")
packed = struct.pack("I", int(byte, 0))
print(f"here's your binary stuff: \\n{packed}")
Running it to get the bytes representation:
We can upload the new .dolphin.state
file in the flash in Flipper using qflipper program:
And viola! 258 XP out of 300!
HURRAY!
Let’s show off and set the icounter
to 858, to benefit from all the hard work we’ve done.
Using the new values:
icounter
- 0x35a (858 in decimal)
checksum
-0xf2 (242 in decimal)
The hexdump of .dolphin.state
looks like this:
Level UP!
Here’s a script that does that automatically in python:
https://github.com/noypearl/FlipFlop/blob/main/levelup.py
Aaaand — see it in action:
Let me know if you have any comments!