/* Android IMSI-Catcher Detector | (c) AIMSICD Privacy Project * ----------------------------------------------------------- * LICENSE: http://git.io/vki47 | TERMS: http://git.io/vki4o * ----------------------------------------------------------- */ package com.secupwn.aimsicd.ui.fragments; import android.os.Bundle; import android.os.Handler; import android.os.Looper; import android.os.Message; import android.text.Editable; import android.view.View; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.Button; import android.widget.EditText; import android.widget.RelativeLayout; import android.widget.Spinner; import android.widget.TextView; import com.secupwn.aimsicd.R; import com.secupwn.aimsicd.utils.Helpers; import com.secupwn.aimsicd.utils.atcmd.AtCommandTerminal; import com.secupwn.aimsicd.utils.atcmd.TtyPrivFile; import com.stericson.RootShell.RootShell; import com.stericson.RootShell.execution.Command; import com.stericson.RootShell.execution.Shell; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.List; import io.freefair.android.injection.annotation.Inject; import io.freefair.android.injection.annotation.InjectView; import io.freefair.android.injection.annotation.XmlLayout; import io.freefair.android.injection.app.InjectionFragment; import io.freefair.android.util.logging.Logger; /** * Description: This is the AT Command Interface or AT Command Processor (ATCoP) that * allow the user to communicate directly to the baseband processor (BP), * via old-school AT commands. This can be very useful for debugging radio * related network problems on those devices that are using this interface. * The most common baseband hardware that allow for this are those made by * Qualcomm (MSM) or Mediatek (MTK). Intel XMM based devices have been found * very difficult or impossible to use this, especailly on Samsung devices. * * Requirements: 1) You need to have supported hardware that already has an AT serial device * enumerated in the Android /dev tree. Some common ones are: * Qualcomm: /dev/smd[0,7] * MTK: /dev/radio/atci[0-9] * XMM: TBA * * 2) You need to be rooted as this interface is using a persistent root shell. * * Issues: * [ ] Need to increase time for long AT commands like "AT+COPS=?" (~30 sec) * [ ] Need a "no" timeout to watch output for while, or let's make it 10 minutes. * Perhaps with a manual stop? */ @XmlLayout(R.layout.activity_at_command) public class AtCommandFragment extends InjectionFragment { @Inject private Logger log; //Return value constants private static final int SERIAL_INIT_OK = 100; private static final int SERIAL_INIT_ERROR = 101; private static final int ROOT_UNAVAILABLE = 102; private static final int BUSYBOX_UNAVAILABLE = 103; private static final List<String> mSerialDevices = new ArrayList<>(); private String mSerialDevice; private int mTimeout; @InjectView(R.id.atcommandView) private RelativeLayout mAtCommandLayout; @InjectView(R.id.at_command_error) private TextView mAtCommandError; @InjectView(R.id.serial_device) private TextView mSerialDeviceDisplay; @InjectView(R.id.response) private TextView mAtResponse; @InjectView(R.id.at_command) private EditText mAtCommand; @InjectView(R.id.serial_device_spinner) private Spinner mSerialDeviceSpinner; @InjectView(R.id.serial_device_spinner_title) private TextView mSerialDeviceSpinnerLabel; private AtCommandTerminal mCommandTerminal; @InjectView(R.id.timeout_spinner) private Spinner timeoutSpinner; @InjectView(R.id.execute) private Button atCommandExecute; @Override public void onViewCreated(View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); atCommandExecute.setOnClickListener(new btnClick()); mSerialDeviceSpinner.setOnItemSelectedListener(new spinnerListener()); timeoutSpinner.setOnItemSelectedListener(new timeoutSpinnerListener()); timeoutSpinner.setSelection(1); mTimeout = 5000; } private class timeoutSpinnerListener implements AdapterView.OnItemSelectedListener { @Override public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) { switch (position) { // Don't forget to also change the arrays.xml case 0: //2 seconds mTimeout = 2000; break; case 1: //5 seconds mTimeout = 5000; break; case 2: //10 seconds mTimeout = 10000; break; case 3: //20 seconds mTimeout = 20000; break; case 4: //30 seconds mTimeout = 30000; break; case 5: // No timeout, when watching output... mTimeout = 600000; // Well, ok, 10 min break; default: mTimeout = 5000; } } @Override public void onNothingSelected(AdapterView<?> parentView) { } } private class spinnerListener implements AdapterView.OnItemSelectedListener { @Override public void onItemSelected(AdapterView<?> parentView, View selectedItemView, int position, long id) { mSerialDevice = String.valueOf(mSerialDeviceSpinner.getSelectedItem()); mSerialDeviceDisplay.setText(mSerialDevice); setSerialDevice(); } @Override public void onNothingSelected(AdapterView<?> parentView) { } } @Override public void onDestroy() { super.onDestroy(); if (mCommandTerminal != null) { mCommandTerminal.dispose(); } } @Override public void onResume() { super.onResume(); int serialDevice = initSerialDevice(); switch (serialDevice) { case SERIAL_INIT_OK: mAtCommandLayout.setVisibility(View.VISIBLE); break; case ROOT_UNAVAILABLE: mAtCommandError.setText(R.string.unable_to_acquire_root_access); break; case BUSYBOX_UNAVAILABLE: mAtCommandError.setText(R.string.unable_to_detect_busybox); break; case SERIAL_INIT_ERROR: mAtCommandError.setText(R.string.unknown_error_trying_to_acquire_serial_device); break; default: mAtCommandError.setText(R.string.unknown_error_initialising_at_command_injector); break; } } private class btnClick implements View.OnClickListener { @Override public void onClick(View v) { if (mAtCommand.getText() != null) { String command = mAtCommand.getText().toString(); log.info("AT Command Detected: " + command); executeAT(); } } } /** * Description: This is looking for possible serial devices that may be used for ATCoP. * * Issues: This is generally not working since it is very HW and SW dependent. * * * @return */ private int initSerialDevice() { /** * NOTE: * * Because of how RootShell is being used the handler has to be disabled. * * With the handler disabled absolutely NO UI work can be done in the callback methods * since they will be called on a separate thread. * * To work around this, either: * * a) Execute all Shell commands in a thread, such as AsyncTask * OR * b) Stop using commandWait (which is a no no...you should never sleep on the * main UI thread) and implement the callback commandFinished/commandTerminated * to determine when to continue on. * */ RootShell.handlerEnabled = false; // Check for root access boolean root = RootShell.isAccessGiven(); if (!root) { return ROOT_UNAVAILABLE; } // Check if Busybox is installed boolean busybox = RootShell.isBusyboxAvailable(); if (!busybox) { return BUSYBOX_UNAVAILABLE; } try { mAtResponse.setText(R.string.at_command_response_looking); mSerialDevices.clear(); // THIS IS A BAD IDEA TODO: Consider removing // Use RIL Serial Device details from the System Property try { String rilDevice = Helpers.getSystemProp(getActivity(), "rild.libargs", "UNKNOWN"); mSerialDevice = ("UNKNOWN".equals(rilDevice) ? rilDevice : rilDevice.substring(3)); if (!"UNKNOWN".equals(mSerialDevice)) { mSerialDevices.add(mSerialDevice); } } catch (StringIndexOutOfBoundsException e) { log.warn(e.getMessage()); // ignore, move on } //================================================================== // WARNING: Scraping commands can be masked by aliases in: mkshrc // and even hardcoded in the sh binary or elsewhere. // To get unaliased versions, use: "\\<command>" //================================================================== for (File file : new File("/dev").listFiles()) { String name = file.getName(); boolean add = false; // QC: /dev/smd[0-7] if (name.matches("^smd.$")) { add = true; } else if ("radio".equals(name)) { // MTK: /dev/radio/*atci* for (File subfile : file.listFiles()) { String subname = subfile.getName(); if (subname.contains("atci")) { add = true; file = subfile; } } } if (add) { mSerialDevices.add(file.getAbsolutePath()); mAtResponse.append(getString(R.string.at_command_response_found) + file.getAbsolutePath() + "\n"); } } // Now try XMM/XGOLD modem config File xgold = new File("/system/etc/ril_xgold_radio.cfg"); if (xgold.exists() && xgold.isFile()) { Command cmd = new Command(1, "\\cat /system/etc/ril_xgold_radio.cfg | " + "\\grep -E \"atport*|dataport*\"") { @Override public void commandOutput(int id, String line) { if (id == 0) { if (!line.trim().isEmpty() && line.contains("/dev/")) { int place = line.indexOf("=") + 1; mSerialDevices.add(line.substring(place, line.length() - 1)); mAtResponse.append(getString(R.string.at_command_response_found) + line.substring(place, line.length() - 1) + "\n"); } } super.commandOutput(id, line); } }; Shell shell = RootShell.getShell(true); shell.add(cmd); commandWait(shell, cmd); } } catch (Exception e) { log.error("InitSerialDevice ", e); } if (!mSerialDevices.isEmpty()) { String[] entries = new String[mSerialDevices.size()]; entries = mSerialDevices.toArray(entries); ArrayAdapter<String> spinnerAdapter = new ArrayAdapter<>(getActivity(), android.R.layout.simple_spinner_item, entries); mSerialDeviceSpinner.setAdapter(spinnerAdapter); mSerialDeviceSpinner.setVisibility(View.VISIBLE); mSerialDeviceSpinnerLabel.setVisibility(View.VISIBLE); } mAtResponse.append(getString(R.string.at_command_response_setup_complete)); mAtResponse.setVisibility(View.VISIBLE); return SERIAL_INIT_OK; } private void setSerialDevice() { if (mCommandTerminal != null) { mCommandTerminal.dispose(); mCommandTerminal = null; } } private AtCommandTerminal getSerialDevice() { if (mCommandTerminal == null) { try { mCommandTerminal = new TtyPrivFile(mSerialDevice); return mCommandTerminal; } catch (IOException e) { mAtResponse.append(e.toString()); } } else { return mCommandTerminal; } return null; } private void executeAT() { // It seem that MTK devices doesn't need "\r" but QC devices do. // We need a device-type check here, perhaps: gsm.version.ril-impl. Editable cmd = mAtCommand.getText(); if (cmd != null && cmd.length() != 0) { log.debug("ExecuteAT: attempting to send: " + cmd.toString()); if (getSerialDevice() != null) { mCommandTerminal.send(cmd.toString(), new Handler(Looper.getMainLooper()) { @Override public void handleMessage(Message message) { if (message.obj instanceof List) { List<String> lines = ((List<String>) message.obj); StringBuffer response = new StringBuffer(); for (String line : lines) { response.append(line); response.append('\n'); } if (response.length() != 0) { mAtResponse.append(response); } } else if (message.obj instanceof IOException) { mAtResponse.append("IOException: " + ((IOException) message.obj).getMessage() + "\n"); } } }.obtainMessage()); } } } /** * This below method is part of the RootTools Project: https://github.com/Stericson/RootTools * Copyright (c) 2012 Stephen Erickson, Chris Ravenscroft, Dominik Schuermann, Adam Shanks * * Slightly modified commandWait method as found in RootToolsInternalMethods.java to utilise * the user selected timeout value. * */ private void commandWait(Shell shell, Command cmd) throws Exception { while (!cmd.isFinished()) { synchronized (cmd) { try { if (!cmd.isFinished()) { cmd.wait(mTimeout); } } catch (InterruptedException e) { log.error(e.getMessage()); } } if (!cmd.isExecuting() && !cmd.isFinished()) { Exception e = new Exception(); if (!shell.isExecuting && !shell.isReading) { log.warn("Waiting for a command to be executed in a shell that is not executing and not reading! \n\n Command: " + cmd.getCommand()); e.setStackTrace(Thread.currentThread().getStackTrace()); log.error(e.getMessage(), e); } else if (shell.isExecuting && !shell.isReading) { log.error("Waiting for a command to be executed in a shell that is executing but not reading! \n\n Command: " + cmd.getCommand()); e.setStackTrace(Thread.currentThread().getStackTrace()); log.error(e.getMessage(), e); } else { log.error("Waiting for a command to be executed in a shell that is not reading! \n\n Command: " + cmd.getCommand()); e.setStackTrace(Thread.currentThread().getStackTrace()); log.error(e.getMessage(), e); } } } } }