Revue du code: comprendre un programme au comportement anormal qui est en fait un virus ddos

 

Un client a les ressources de ses serveurs saturés. Toute la mémoire est monopolisée. L’espace disque est saturé. La production est ralentie. Que se passe-t-il ?

 

revue de code virus ddos deni de service

 

 

Une question? Posez-la ici

 

Examen d'une partie du programme

 

J'ai remarqué qu'un programme, un processus, fonctionnait sur un serveur. En reverse engineering je retrouve le code.

 

package production.requetesserveurs.siteclient.bladeserveur.archi.service.stateless.client;

import java.io.IOException;

import java.net.SocketAddress;

import java.util.Collections;

import java.util.HashMap;

import java.util.HashSet;

import java.util.LinkedHashMap;

import java.util.LinkedHashSet;

import java.util.Map;

import java.util.Random;

import java.util.Set;

import java.util.UUID;

import java.util.concurrent.ArrayBlockingQueue;

import java.util.concurrent.BlockingQueue;

import java.util.concurrent.Callable;

import java.util.concurrent.ExecutionException;

import java.util.concurrent.Executors;

import java.util.concurrent.Future;

import java.util.concurrent.RejectedExecutionException;

import java.util.concurrent.ScheduledExecutorService;

import java.util.concurrent.ThreadPoolExecutor;

import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.tuple.Pair;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.http.HttpEntity;

import org.springframework.http.HttpHeaders;

import org.springframework.http.MediaType;

import org.springframework.http.ResponseEntity;

import org.springframework.web.client.RestTemplate;

import production.requetesserveurs.siteclient.bladeserveur.archi.registry.discover.Discover;

import production.requetesserveurs.siteclient.bladeserveur.archi.registry.discover.DiscoverResultsListener;

import net.sf.ehcache.Cache;

import net.sf.ehcache.CacheManager;

import net.sf.ehcache.Element;

import net.sf.ehcache.config.CacheConfiguration;

import net.sf.ehcache.config.Configuration;

import net.sf.ehcache.config.FactoryConfiguration;

import net.sf.ehcache.config.CacheConfiguration.CacheEventListenerFactoryConfiguration;

import net.sf.ehcache.config.CacheConfiguration.TransactionalMode;

public class MainClient implements DiscoverResultsListener, Callable<Void> {

               private Discover discover;

               private static String myIP = "127.0.0.1";

               private final Map<String, String> whoAmI = new HashMap<String, String>();

               private static Logger logger = LoggerFactory.getLogger(MainClient.class);

               private Set<Pair<String, Short>> servers = Collections.synchronizedSet(new LinkedHashSet<Pair<String, Short>>());

               private final ScheduledExecutorService single = Executors.newSingleThreadScheduledExecutor();

               private final ScheduledExecutorService single_ = Executors.newSingleThreadScheduledExecutor();

               private Future<Void> _inXseconds;

               private Set<String> requests = new HashSet<>();

               BlockingQueue<Runnable> q = new ArrayBlockingQueue<>(20);

               BlockingQueue<Boolean> p = new ArrayBlockingQueue<>(1);

               private final ThreadPoolExecutor pool = new ThreadPoolExecutor(10, 10, 500, TimeUnit.MILLISECONDS, q);

               private CacheManager cacheManager ; // = CacheManager.getInstance();

               private Cache lbCache;

               private final static String LB_CACHE_NAME = "LBCache";

               private final static int port = 40000;

              

               public MainClient() {

                              try {

                                            whoAmI.put("IP", myIP);

                                            whoAmI.put("PORT", "0");

                                            discover = new Discover(whoAmI, this, 0);

                                            _inXseconds = single.schedule(this, 3, TimeUnit.SECONDS);

                                            requests.add("Serveur");

                                            requests.add("Demon");

                                            requests.add("Hote");

                                            requests.add("Processus");

                                            requests.add("Processus Leger");

                                            requests.add("Programme");

                                            requests.add("Service");

                                            requests.add("Application");

                                            FactoryConfiguration peerListenerConf = new FactoryConfiguration()

                                                                          .className("net.sf.ehcache.distribution.RMICacheManagerPeerListenerFactory")

                                                                          .properties("hostName=localhost, port=" + port + ", socketTimeoutMillis=2000");

                                            FactoryConfiguration peerProviderConf = new FactoryConfiguration()

                                                                          .className("net.sf.ehcache.distribution.RMICacheManagerPeerProviderFactory")

                                                                          .properties("peerDiscovery=automatic, multicastGroupAddress=233.168.33.10, multicastGroupPort=4446, timeToLive=32");

                                            Configuration config = new Configuration();

                                            config.addCacheManagerPeerListenerFactory(peerListenerConf);

                                            config.addCacheManagerPeerProviderFactory(peerProviderConf);

                                           

                                            cacheManager = CacheManager.create(config);

                                           

                                            CacheConfiguration cacheConfig = new CacheConfiguration(LB_CACHE_NAME, 1000);

                                            cacheConfig.eternal(false).transactionalMode(TransactionalMode.OFF);

                                            CacheEventListenerFactoryConfiguration caConfiguration = new CacheEventListenerFactoryConfiguration();

                                             caConfiguration.className("net.sf.ehcache.distribution.RMICacheReplicatorFactory")

                                                                                         .properties("replicateAsynchronously=true, replicatePuts=true, replicateUpdates=true, replicateUpdatesViaCopy=true, replicateRemovals=true ");

                                            cacheConfig.addCacheEventListenerFactory(caConfiguration);

                                            lbCache = new Cache(cacheConfig);

                                            cacheManager.addCache(lbCache);

                                           

                                            logger.debug(" C L I E N T   R E P L I C A T I O N   P O R T = " + port);

                                            single_.submit(new ShowCacheTask());

                                           

                              } catch (IOException e) {

                                            e.printStackTrace();

                              }

               }

               public static void main(String[] args) throws InterruptedException, ExecutionException {

                              final MainClient mainClient = new MainClient();

                              mainClient.inXseconds();

               }

              

               @Override

               public void newDiscover(SocketAddress s, Map<String, String> received) {

                              if ( received.get("Data") != null ) {

                                            final String host = received.get("IP");

                                            final int port = Integer.valueOf(received.get("PORT"));

                                            logger.debug("Receiving data:" + host + ":" + port);

                                            servers.add(Pair.of(host, (short) port));

                              }                                                         

               }

               private void showServers() {

                              final StringBuffer b = new StringBuffer();

                              int k = 0;

                              for(Pair<String, Short> pair : servers) {

                                            if (k>0) b.append(" ♦ ");

                                            b.append("(").append(k++).append(") ").append(pair.getKey()).append(":").append(pair.getValue());

                              }

                              logger.debug(b.toString());

               }

              

               private void startCallingServers() {

                              while (true) {

                                            for(Pair<String, Short> pair : servers) {

                                                           final TranslationRequest task = new TranslationRequest(pair.getKey(), pair.getValue());

                                                           try {

                                                                          pool.submit(task);

                                                           } catch (RejectedExecutionException x) {

                                                                          logger.error("Pool overflow but continue (Queue size = " + q.size() + ")");

                                                                          try {

                                                                                         p.poll(5, TimeUnit.SECONDS);

                                                                          } catch (InterruptedException e) {}

                                                           }

                                            }

                              }

               }

              

               public void inXseconds() throws InterruptedException, ExecutionException {

                              _inXseconds.get();

               }

              

               @Override

               public Void call() throws Exception {

                              showServers();

                              startCallingServers();

                              return null;

               }

              

               private void callServer(final String host, final short port) {

                              final RestTemplate template = new RestTemplate();

                              final StringBuffer _url = new StringBuffer();

                              final StringBuffer done = new StringBuffer();

                              _url.append("http://").append(host).append(":").append(port).append("/");

                             

                              Random r = new Random();

                              int k = r.nextInt(requests.size());

                              String word = "";

                              for(String s : requests) {

                                            if (k-- == 0) {

                                                           word = s;

                                                           break;

                                            }

                              }

                              done.append(_url.toString()).append(": ").append(word).append(" â—„â–º ");

                              String requestJson = "{\"Word\":\"" + word +"\"}";

                              HttpHeaders headers = new HttpHeaders();

                              headers.setContentType(MediaType.APPLICATION_JSON);

                              HttpEntity<String> entity = new HttpEntity<String>(requestJson,headers);

                              final ResponseEntity<String> resp = template.postForEntity(_url.toString(), entity, String.class);

                              done.append(resp.getBody());

                              logger.debug(done.toString());

               }

              

               private class TranslationRequest implements Callable<Void> {

                              private String h;

                              private short p;

                              public TranslationRequest(final String h, final short p) {

                                            this.h = h;

                                            this.p = p;

                              }

                              @Override

                              public Void call() throws Exception {

                                            callServer(h, p);

                                            return null;

                              }

               }

               private class ShowCacheTask implements Callable<Void> {

                              @Override

                              public Void call() throws Exception {

                                            while (true) {

                                                           StringBuffer b = new StringBuffer();

                                                           int k = 0;

                                                           for(Object key : lbCache.getKeys()) {

                                                                          final Element el = lbCache.get(key);

                                                                          final UUID id = (UUID)el.getObjectKey();

                                                                          final int load = (Integer)el.getObjectValue();

                                                                          b.append(k++>0?",":"").append("[").append(id).append(",").append(load).append("]");

                                                           }

                                                           logger.debug(b.toString());

                                                           Thread.sleep(1000);

                                            }

                              }

               }

              

}

 

 

De manière régulière je remarque que ce programme client intérroge régulièrement un ou plusieurs serveurs.

Je regarde davantage en détail.

 

Dans la méthode main, je regarde le constructeur.

Je fais abstraction de la partie découverte multicast.

Je remarque un petit scheduleur qui execute une tache toutes les 3 secondes

Il execute la tache this grace à l’objet callable car on a implémenté la méthode « call »

On execute la méthode « call » toutes les 3 secondes.

En regardant le bout de code, je remarque que l’on prépare une requete de type REST, c’est du post.

Dans la méthode « call » il y a un show server, un start callling servers, avec une boucle infinie.

Que fait cette boucle infinie ? Elle appelle une tache de traduction : translation request.

Je remarque qu’on itère sur des serveurs : il va y avoir plusieurs serveurs. Chaque serveur sera un stateless serveur.

On parcours l’ensemble des serveurs. A chaque fois on invoque ce serveur.

 

 

Qu’est-ce qu’une translation request

 

C’est une tache callable.

La méthode « Callserveur » prend 2 arguments : c’est un appel REST fait en JSON, on fait le post et on réupère le resultat

Il y a un mot, on le traduit. C’est stateless, car quelquefois le nombre de fois que l’on interroge le service avec ce mot, il y aura toujours la meme reponse.

On va appeler 2 services différents avec le meme argument, il va toujours nous renvoyer la même réponse.

 

Une question? Posez-la ici

 

A un moment donné je remarque un dictionnaire

 

 

Il y un set<string> qui constitue l’ensemble des mots que le programme va traduire.

« private void startCallingServers »

On appelle des serveurs avec des taches avec des threads : si 5 seveurs, 5 threads vont s’executer en même temps, de manière asynchrone, sinon ca aurait été synchrone, il n’y aurait pas eu de notion de parallélisme.

Je remarque qu’au bout de 5 secondes il y a une pause : p.poll(5, TimeUnit.SECONDS);

SI je regarde bien, j’ai un while(true)

Si j’ai 5 serveurs, au bout de 15 secondes, j’ai envoyé 15 requetes.

Je remarque qu’il y a un pool de threads. Qu’est-ce qui me garantit que lorsque j’ai lancé une requete, elle est bien traitée et que j’ai le retour par le serveur ? RIEN.

Est-ce qu’un serveur peut répondre à plusieurs requêtes en même temps ? OUI.

Rien ne me garantit pour ce service que notre serveur ici peut répondre à 2 requêtes en même temps. Ici si la requête prend plus de 5 secondes, le serveur va gérer plusieurs requêtes simultanées.

 

Attention, ici, il faufrait contrôler la charge. C'est bizarre. Je lance une alerte.

 

On soumet une tache de traduction à un pool de thread. Il permet de limiter les requetes pour éviter de surcharger les serveurs. Lorsque ce pool.submit est surchargée, l’exécution es rejetée, car on dépasse la charge que l’on a autorisé.

 

Si mon pool est surchargé, mes serveurs sont surchargés. Pas grave, je vais attendre un peu et je recommencerai 5 secondes après.

 

Ce système va lisser la charge.

 

Traduite un mot prend toujours le même temps, donc les charges vont s’équilibrer. Par contre si l’on traduisait une phrase complète, les requêtes ne prendraient plus le même temps : la charge ne serait plus bien répartie, et il n’y aurait plus de lissage.

 

Donc le round-robin fonctionne, et pour cause


 

L’objectif de ce code est donc de surcharger le serveur, nous avons affaire ici à un code qui fait du DDOS, du deni de service, de la surcharge serveur infinie. Un virus!

 

Allons voir le pool.

 

Explication du « ThreadPoolExecutor » qui fait partie de la stack java classique

Principe d pool de threads : lancer des threads avant l’initialisation du programme, car la creation de threads est une fonction système couteuse.

 

Une question? Posez-la ici

 

Paramètres du ThreadPoolExecutor 

 

CorePoolsize : 10 : on aura maximum 10 connections.

Si le pool est plein, vu que l’on utilise une BlockingQueue de Runnable

Chaque thread est un runnable

Lorsqu’on soumet une tache au pool, la tache rentre dans le pool si le nombre de theads actif est inférieur à 10.

Si mon pool est plein au moment du submit

10 :

Heureusement que je peux consulter la javadoc sur internet pour m’aider.

Pourquoi 500 ms ? Parce que le programme tourne en local , c’est une requete REST qui va chercher une donnée. Ca prend ½ milliseconde.

Le « offer » c’est l’action de produire des éléments dans la queue.

Il vau mieux utiliser p.poll que sleep pour faire des pauses

Dans le MainClient.Java

Je remarque la méthode inXseconds()

On va executer dans 3 secondes showServeur et startCallingServer

On a un discover : on lance la boucle infinie sur une liste de serveur établie.

Au début du cycle de vie de ce programme, on ne connait pas les serveurs. On dans l’idée que dans 3 secondes, on aura au moins 1 ou 2 serveurs. On aura donc quelques serveurs qui vont répondre.

This c’est la classe en cours, la classe MainClient.Java . Je regarde la signature : la classe implémenta callable : le client a donc une méthode call.

Quand le programme fait un new discover, implementation discover, il y a un callback this.listener.newDiscover . Quand on appelle la méthode newDiscover on rappelle la classe en cours au travers de la méthode newDiscover.

Dans newDiscover, on ajoute les serveurs découverts. Mais qu’est-ce qu’un objet serveur ? Une adresse IP et un port. On assimile un serveur à une paire adresse IP et port, avec un Set<Pair<String,Short>> servers.

 

       private Set<Pair<String, Short>> servers = Collections.synchronizedSet(new LinkedHashSet<Pair<String, Short>>());

 

SendHello, routine send de l’API Java sur le datagram chanel

 

 

Et ce n’est pas tout. Outre un client, il y a aussi une partie serveur.

 

Une question? Posez-la ici

 

Maintenant je regarde le MainServer.java

 

package production.requetesserveurs.siteclient.bladeserveur.archi.service.stateless.client;

import java.io.IOException;

import java.net.SocketAddress;

import java.util.HashMap;

import java.util.Map;

import java.util.UUID;

import org.codehaus.jackson.JsonGenerationException;

import org.codehaus.jackson.map.JsonMappingException;

import org.eclipse.jetty.server.Server;

import org.eclipse.jetty.servlet.ServletContextHandler;

import org.eclipse.jetty.servlet.ServletHolder;

import org.slf4j.Logger;

import org.slf4j.LoggerFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.web.context.ContextLoaderListener;

import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;

import org.springframework.web.servlet.DispatcherServlet;

import productionrequetesserveurs.siteclient.bladeserveur.archi.registry.discover.Discover;

import productionrequetesserveurs.siteclient.bladeserveur.archi.registry.discover.DiscoverResultsListener;

import productionrequetesserveurs.siteclient.bladeserveur.archi.service.stateless.controller.CoreController;

public class MainServer implements DiscoverResultsListener {

              

               private Server server = null;                                                                                                                                                             // The Jetty Embbeded Server

               private final AnnotationConfigWebApplicationContext webSpringContext = new AnnotationConfigWebApplicationContext();

               private short port;

               private ServletContextHandler handler = new ServletContextHandler();

               private static String myIP = "127.0.0.1";

               private final Map<String, String> whoAmI = new HashMap<String, String>();

               private Discover discover;

               private static Logger logger = LoggerFactory.getLogger(MainServer.class);

               private UUID id = UUID.randomUUID();

              

              

               public MainServer() throws Exception {

                              final String _port = System.getProperty("server.http.port", "9092");

                              port = Short.parseShort(_port);

                              final String initialDeposit = System.getProperty("server.initial.deposit", "0");

                              final int initialDeposit_ = Integer.parseInt(initialDeposit);

                              webSpringContext.setConfigLocation("productionrequetesserveurs

.siteclient.bladeserveur.archi.service.statefull.config");

                             

                              handler.setContextPath("/");

                              handler.addServlet(new ServletHolder(new DispatcherServlet(webSpringContext)), "/*");

                              handler.addEventListener(new ContextLoaderListener(webSpringContext));

                              handler.setAttribute("server.uuid", id);

                              handler.setAttribute("server.replication.cache.rmi.port", port+1);

                              handler.setAttribute("server.initial.deposit", initialDeposit_);

                             

                              server = new Server(port);

                              server.setHandler(handler);

                              server.start();

                             

                              whoAmI.put("IP", myIP);

                              whoAmI.put("PORT", String.valueOf(port));

                              whoAmI.put("ID", id.toString());

                             

                              discover = new Discover(whoAmI, this, 0);

                              server.join();

              

               }

              

              

               public static void main(String[] args) throws Exception {

                              //new AnnotationConfigApplicationContext(StatelessServiceConfiguration.class);

                              new MainServer();

               }

               @Override

               public void newDiscover(SocketAddress s, Map<String, String> received) {

                              if ( received.get("Hello") != null ) {

                                            try {

                                                           final String host = received.get("IP");

                                                           final int port = Integer.valueOf(received.get("PORT"));

                                                           logger.debug("Receiving data:" + host + ":" + port);

                                                           discover.send(whoAmI);

                                            } catch (JsonGenerationException e) {

                                                           // TODO Auto-generated catch block

                                                           e.printStackTrace();

                                            } catch (JsonMappingException e) {

                                                           // TODO Auto-generated catch block

                                                           e.printStackTrace();

                                            } catch (IOException e) {

                                                           // TODO Auto-generated catch block

                                                           e.printStackTrace();

                                            }

                              }                           

               }

}

 

Ce programme contient un serveur web Jetty en standalone.

 

                              server = new Server(port);

                              server.setHandler(handler);

                              server.start();

 

Une question? Posez-la ici

 

Il y a du spring : l’initialisation du serveur se fait en ligne 30

Plusieurs types d’application context.

Initialisation d’un context spring sur une application web.

Les classes de configuration de spring sont des classes annontées avec des annotations de Spring. On doit voir le package équivalent.

J’ai donc un process java qui fait tourner un serveur web Jetty, avec un framework Spring.

Spring accueille la servlet. Ce sera une servlet déployée sous forme d’un service REST.

 

Fin de revue de code.

Je crée un rapport pour le client

 

Vos serveurs sont aussi saturés? Vous avez besoin d'aide? D'une revue de code poru étudier plus en détail ce qu'il se passe?

Pour un retour rapide, remplissez ce formulaire: