/* Android IMSI-Catcher Detector | (c) AIMSICD Privacy Project * ----------------------------------------------------------- * LICENSE: http://git.io/vki47 | TERMS: http://git.io/vki4o * ----------------------------------------------------------- */ package com.secupwn.aimsicd.smsdetection; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.support.annotation.StringRes; import android.support.v7.app.AlertDialog; import android.view.WindowManager; import com.secupwn.aimsicd.R; import com.secupwn.aimsicd.data.model.GpsLocation; import com.secupwn.aimsicd.utils.RealmHelper; import com.secupwn.aimsicd.data.model.SmsData; import com.secupwn.aimsicd.data.model.SmsDetectionString; import com.secupwn.aimsicd.service.AimsicdService; import com.secupwn.aimsicd.utils.MiscUtils; import java.io.BufferedReader; import java.io.DataOutputStream; import java.io.IOException; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Date; import java.util.List; import io.freefair.android.util.logging.AndroidLogger; import io.freefair.android.util.logging.Logger; import io.realm.Realm; import lombok.Cleanup; import lombok.Getter; /** * Description: Detects mysterious SMS by scraping Logcat entries. * <p/> * <p/> * NOTES: For this to work better Samsung users might have to set their Debug Level to High * in SysDump menu *#9900# or *#*#9900#*#* * <p/> * This is by no means a complete detection method but gives us something to work off. * <p/> * For latest list of working phones/models, please see: * https://github.com/CellularPrivacy/Android-IMSI-Catcher-Detector/issues/532 * <p/> * PHONE:Samsung S5 MODEL:SM-G900F ANDROID_VER:4.4.2 TYPE0:YES MWI:YES * PHONE:Samsung S4-min MODEL:GT-I9195 ANDROID_VER:4.2.2 TYPE0:YES MWI:YES * PHONE:Sony Xperia J MODEL:ST260i ANDROID_VER:4.1.2 TYPE0:NO MWI:YES * <p/> * To Use: * <p/> * SmsDetector smsDetector = new SmsDetector(context); * <p/> * smsDetector.startSmsDetection(); * smsDetector.stopSmsDetection(); * <p/> * <p/> * TODO: * [ ] Add more mTAG to the detection Log items * * @author Paul Kinsella @banjaxbanjo */ public final class SmsDetector extends Thread { private final Logger log = AndroidLogger.forClass(SmsDetector.class); private AimsicdService mAIMSICDService; private boolean mBound; private RealmHelper mDbAdapter; private Context mContext; private static final int TYPE0 = 1, MWI = 2, WAP = 3; // TODO: replace this with retrieval from AIMSICDDbAdapter private static final int LOGCAT_BUFFER_MAX_SIZE = 100; /** * To correctly detect sms data and phone numbers on wap, we need at least * 10 lines after line which indicates wap communication */ private static final int LOGCAT_WAP_EXTRA_LINES = 10; private static boolean isRunning = false; public SmsDetector(Context context) { mContext = context; mDbAdapter = new RealmHelper(context); } public static boolean getSmsDetectionState() { return isRunning; } public static void setSmsDetectionState(boolean isRunning) { SmsDetector.isRunning = isRunning; } public void startPopUpInfo(SmsType smsType) { MiscUtils.showNotification( mContext, mContext.getString(smsType.getAlert()), mContext.getString(R.string.app_name_short) + " - " + mContext.getString(smsType.getTitle()), R.drawable.sense_danger, true); AlertDialog alertDialog = new AlertDialog.Builder(mContext) .setTitle(smsType.getTitle()) .setMessage(smsType.getMessage()) .setIcon(R.drawable.sense_danger) .create(); alertDialog.getWindow().setType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT); alertDialog.show(); } public void startSmsDetection() { Intent intent = new Intent(mContext, AimsicdService.class); mContext.bindService(intent, mConnection, Context.BIND_AUTO_CREATE); start(); log.info("SMS detection started"); } public void stopSmsDetection() { setSmsDetectionState(false); // Unbind from the service if (mBound) { mContext.unbindService(mConnection); mBound = false; } log.info("SMS detection stopped"); } @Override public void run() { setSmsDetectionState(true); BufferedReader mLogcatReader; try { Thread.sleep(500); String MODE = "logcat -v time -b radio -b main\n"; Runtime r = Runtime.getRuntime(); Process process = r.exec("su"); @Cleanup DataOutputStream dos = new DataOutputStream(process.getOutputStream()); dos.writeBytes(MODE); dos.flush(); mLogcatReader = new BufferedReader(new InputStreamReader(process.getInputStream())); } catch (InterruptedException | IOException e) { log.error("Exception while initializing LogCat (time, radio, main) reader", e); return; } String logcatLine; List<String> logcatLines = new ArrayList<>(); while (getSmsDetectionState()) { try { logcatLine = mLogcatReader.readLine(); if (logcatLines.size() <= LOGCAT_BUFFER_MAX_SIZE || logcatLine != null) { logcatLines.add(logcatLine); } else if (logcatLines.size() == 0) { /** * Sleep only when there is no more input, not after going through buffer * to not unnecessary slow down the process * */ Thread.sleep(1000); } else { /** * In moment, where there are no data * we check the current buffer and clear it * */ String[] outLines = new String[logcatLines.size()]; logcatLines.toArray(outLines); for (int counter = 0; counter < logcatLines.size(); counter++) { String bufferedLine = logcatLines.get(counter); switch (checkForSms(bufferedLine)) { case TYPE0: parseTypeZeroSms(outLines, MiscUtils.parseLogcatTimeStamp(bufferedLine)); break; case MWI: parseMwiSms(outLines, MiscUtils.parseLogcatTimeStamp(bufferedLine)); break; case WAP: int remainingLinesInBuffer = logcatLines.size() - counter - LOGCAT_WAP_EXTRA_LINES; if (remainingLinesInBuffer < 0) { /** * we need to go forward a few more lines to get data * and store it in post buffer array * */ String[] wapPostLines = new String[Math.abs(remainingLinesInBuffer)]; String extraLine; for (int x = 0; x < Math.abs(remainingLinesInBuffer); x++) { extraLine = mLogcatReader.readLine(); if (extraLine != null) { wapPostLines[x] = extraLine; } } /** * We'll add the extra lines to logcat buffer, so we don't miss anything * on detection cycle continue * */ int insertCounter = logcatLines.size(); for (String postLine : wapPostLines) { logcatLines.add(counter + insertCounter, postLine); insertCounter++; } } /** * Will readout from LogcatBuffer remaining lines, or next LOGCAT_WAP_EXTRA_LINES lines * depending on how many are available * */ int availableLines = Math.min(logcatLines.size() - counter - LOGCAT_WAP_EXTRA_LINES, LOGCAT_WAP_EXTRA_LINES); String[] nextAvailableLines = new String[availableLines]; for (int nextLine = 0; nextLine < availableLines; nextLine++) { nextAvailableLines[nextLine] = logcatLines.get(counter + nextLine); } parseWapPushSms(outLines, nextAvailableLines, MiscUtils.parseLogcatTimeStamp(bufferedLine)); break; } counter++; } logcatLines.clear(); } } catch (IOException e) { log.error("IO Exception", e); } catch (InterruptedException e) { log.error("Interrupted Exception", e); } } try { mLogcatReader.close(); } catch (IOException ee) { log.error("IOE Error closing BufferedReader", ee); } } private int checkForSms(String line) { Realm realm = Realm.getDefaultInstance(); //0 - null 1 = TYPE0, 2 = MWI, 3 = WAPPUSH for (SmsDetectionString detectionString : realm.allObjects(SmsDetectionString.class)) { //looping through detection strings to see does logcat line match if (line.contains(detectionString.getDetectionString())) { if ("TYPE0".equalsIgnoreCase(detectionString.getSmsType())) { log.info("TYPE0 detected"); return TYPE0; } else if ("MWI".equalsIgnoreCase(detectionString.getSmsType())) { log.info("MWI detected"); return MWI; } else if ("WAPPUSH".equalsIgnoreCase(detectionString.getSmsType())) { log.info("WAPPUSH detected"); return WAP; } } // This is currently unused, but keeping as an example of possible data contents // else if (line.contains("BroadcastReceiver action: android.provider.Telephony.SMS_RECEIVED")) { // log.info("SMS found"); // return 0; // } } realm.close(); return 0; } private void parseTypeZeroSms(String[] bufferLines, Date logcat_timestamp) { @Cleanup Realm realm = Realm.getDefaultInstance(); long count = realm.where(SmsData.class).equalTo("timestamp", logcat_timestamp).count(); // Only alert if the timestamp is not in the data base if (count == 0) { realm.beginTransaction(); SmsData capturedSms = realm.createObject(SmsData.class); String smsText = findSmsData(bufferLines, null); String num = findSmsNumber(bufferLines, null); capturedSms.setSenderNumber(num); capturedSms.setMessage(smsText); capturedSms.setTimestamp(logcat_timestamp); capturedSms.setType("TYPE0"); setCurrentLocationData(realm, capturedSms); realm.commitTransaction(); mDbAdapter.toEventLog(realm, 3, "Detected Type-0 SMS"); startPopUpInfo(SmsType.SILENT); } else { log.debug("Detected Sms already logged"); } } private void parseMwiSms(String[] logcatLines, Date logcat_timestamp) { @Cleanup Realm realm = Realm.getDefaultInstance(); long count = realm.where(SmsData.class).equalTo("timestamp", logcat_timestamp).count(); // Only alert if the timestamp is not in the data base if (count == 0) { realm.beginTransaction(); SmsData capturedSms = realm.createObject(SmsData.class); String smsText = findSmsData(logcatLines, null); String num = findSmsNumber(logcatLines, null); capturedSms.setSenderNumber(num); capturedSms.setMessage(smsText); capturedSms.setTimestamp(logcat_timestamp); capturedSms.setType("MWI"); setCurrentLocationData(null, capturedSms); realm.commitTransaction(); mDbAdapter.toEventLog(realm, 4, "Detected MWI SMS"); startPopUpInfo(SmsType.MWI); } else { log.debug("Detected Sms already logged"); } } private void parseWapPushSms(String[] logcatLines, String[] postWapMessageLines, Date logcat_timestamp) { @Cleanup Realm realm = Realm.getDefaultInstance(); long count = realm.where(SmsData.class).equalTo("timestamp", logcat_timestamp).count(); // Only alert if the timestamp is not in the data base if (count == 0) { realm.beginTransaction(); SmsData capturedSms = realm.createObject(SmsData.class); String smsText = findSmsData(logcatLines, postWapMessageLines); String num = findSmsNumber(logcatLines, postWapMessageLines); capturedSms.setSenderNumber(num); capturedSms.setMessage(smsText); capturedSms.setTimestamp(logcat_timestamp); capturedSms.setType("WAPPUSH"); setCurrentLocationData(realm, capturedSms); realm.commitTransaction(); mDbAdapter.toEventLog(realm, 6, "Detected WAPPUSH SMS"); startPopUpInfo(SmsType.WAP_PUSH); } else { log.debug("Detected SMS already logged"); } } private void setCurrentLocationData(Realm realm, SmsData capturedSms) { capturedSms.setLocationAreaCode(mAIMSICDService.getCellTracker().getMonitorCell().getLocationAreaCode()); capturedSms.setCellId(mAIMSICDService.getCellTracker().getMonitorCell().getCellId()); capturedSms.setRadioAccessTechnology(mAIMSICDService.getCell().getRat()); boolean isRoaming = false; if (mAIMSICDService.getCellTracker().getDevice().isRoaming()) { isRoaming = true; } capturedSms.setRoaming(isRoaming); GpsLocation gpsLocation = realm.createObject(GpsLocation.class); gpsLocation.setLatitude(mAIMSICDService.lastKnownLocation().getLatitudeInDegrees()); gpsLocation.setLongitude(mAIMSICDService.lastKnownLocation().getLongitudeInDegrees()); capturedSms.setGpsLocation(gpsLocation); } private String findSmsData(String[] preBuffer, String[] postBuffer) { //check pre buffer for number and sms msg if (preBuffer != null) { for (String preBufferLine : preBuffer) { if (preBufferLine != null) { if (preBufferLine.contains("SMS message body (raw):") && preBufferLine.contains("'")) { preBufferLine = preBufferLine.substring(preBufferLine.indexOf("'") + 1, preBufferLine.length() - 1); return preBufferLine; } } } //check post buffer for number and sms msg if (postBuffer != null) { for (int x = 0; x < postBuffer.length; x++) { if (postBuffer[x] != null) { String testLine = preBuffer[x]; if (testLine.contains("SMS message body (raw):") && testLine.contains("'")) { testLine = testLine.substring(testLine.indexOf("'") + 1, testLine.length() - 1); return testLine; } } } } } return null; } private String findSmsNumber(String[] preBuffer, String[] postBuffer) { //check pre buffer for number and sms msg if (preBuffer != null) { for (String preBufferLine : preBuffer) { if (preBufferLine != null) { if (preBufferLine.contains("SMS originating address:") && preBufferLine.contains("+")) { return preBufferLine.substring(preBufferLine.indexOf("+")); } else if (preBufferLine.contains("OrigAddr")) { preBufferLine = preBufferLine.substring(preBufferLine.indexOf("OrigAddr")).replace("OrigAddr", "").trim(); return preBufferLine; } } } } //check post buffer for number and sms msg if (postBuffer != null) { for (String postBufferLine : postBuffer) { if (postBufferLine != null) { if (postBufferLine.contains("SMS originating address:") && postBufferLine.contains("+")) { return postBufferLine.substring(postBufferLine.indexOf("+")); } else if (postBufferLine.contains("OrigAddr")) { postBufferLine = postBufferLine.substring(postBufferLine.indexOf("OrigAddr")).replace("OrigAddr", "").trim(); return postBufferLine; } } } } return null; } private final ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName name, IBinder service) { mAIMSICDService = ((AimsicdService.AimscidBinder) service).getService(); mBound = true; } @Override public void onServiceDisconnected(ComponentName arg0) { log.info("Disconnected SMS Detection Service"); mBound = false; } }; @Getter public enum SmsType { SILENT( R.string.alert_silent_sms_detected, R.string.typezero_header, R.string.typezero_data ), MWI( R.string.alert_mwi_detected, R.string.typemwi_header, R.string.typemwi_data ), WAP_PUSH( R.string.alert_silent_wap_sms_detected, R.string.typewap_header, R.string.typewap_data ); @StringRes private int alert; @StringRes private int title; @StringRes private int message; SmsType(@StringRes int alert, @StringRes int title, @StringRes int message) { this.alert = alert; this.title = title; this.message = message; } } }