Spuren verwischen (Linux-Magazin, September 2020)

Wer seinen Trödel online verkloppt, ist sich vielleicht nicht im Klaren darüber, welch brisante Privatinformationen in verkaufsfördernden Handyfotos stecken. Lichtet der Verkäufer die Ware zuhause mit dem Handy ab, stecken unter Umständen noch die Geo-Daten in der Bilddatei, mit denen sich die Privatadresse auf wenige Meter genau feststellen lässt. Zwar publizieren große Verkaufsplattformen diese Metainformationen im Allgemeinen nicht, aber wer gibt Ebay, Facebook & Co schon gerne mehr preis als unbedingt notwendig? Das Handy tilgt Geo-Daten auf Wunsch auch direkt, aber das sieht so aus, als hätte der Nutzer etwas zu verbergen. Deshalb versieht heute ein selbstgeschriebenes Go-Programm Bilddateien mit einem Geo-Grauschleier, so dass dort zufällig verwischte Geo-Koordinaten stehen, aus denen sich vielleicht das Stadtviertel, aber nicht die genaue Adresse ermitteln lässt.

Treffer im Radius

Ziel des Verfahrens ist es, die Geo-Tags einer Aufnahme zufällig in einen Bereich innerhalb eines Aktionskreises zu verschieben. Bei mehreren Aufnahmen liegen die Zielwerte alle innerhalb des Aktionsradius, und damit niemand bei hunderten von Aufnahmen daraus das Zentrum und damit den Standort des Fotografen ermitteln kann, verschiebt der Geo-Fuzzer das Zentrum des Zufallskreises vorher auch noch in eine benachbarte Gegend, mit fixen aber geheimen Werten für die geografische Breite und Länge (Abbildung 1).

Abbildung 1: Vom Originalstandort der Aufnahme springt der Algorithmus zunächst zu einem neuen Punkt, von dem aus er zufällig Geo-Koordinaten in einem Aktionsradius auswählt.

Die Geo-Location einer Bilddatei steht in den sogenannten Exif-Tags ([2]) des JPG-Formats und lässt sich mit Tools wie exiftool oder online auf https://tool.geoimgr.com auslesen. Letzteres stellt den Ort der Aufnahme sogar auf einer Google-Landkarte dar.

Abbildung 2: Der tatsächliche Ort der Aufnahme liegt in der Nähe des Stadtteils "Mission" in San Francisco.

Die Abbildungen 2 und 3 zeigen jeweils die Geo-Tags der Aufnahme, ein Bild von einer Schachtel mit einem Google Voice Kit, das ich auf Ebay zu verscherbeln gedachte. Das Original in Abbildung 2 zeigt meine Privatadresse in San Francisco, an der die Aufnahme in meinem Arbeitszimmer entstand. Nach dem Aufuf des vorgestellten Programms geofuzz verschiebt sich die Geo-Location in der Bilddatei weiter nördlich in den Stadtteil "Financial District". Abbildung 3 zeigt auch noch die weiter modifizierten Werte nach mehreren aufeinanderfolgenden Aufrufen des Fuzzers, der die Werte innerhalb des eingestellten Aktionsradius streut.

Abbildung 3: Nachfolgende Aufrufe des Geotag-Fuzzers geofuzz verschieben das Geotag auf verschiedene Orte innerhalb des Aktionsradius im Viertel "Financial District".

Das aus Listing 1 generierte Programm geofuzz nimmt auf der Kommandozeile den Namen der zu manipulierenden JPG-Datei entgegen:

    $ geofuzz ebay.jpg
    Was: 37.756795,-122.426903
    Fuzz: 37.804414,-122.407682

Es gibt die im Bild gefundenen Geo-Tags für geografische Breite und Länge aus, so wie die vom Programm manipulierten Werte. Der Fuzzer modifiziert die angegebene Datei direkt, und der User kann diese nun posten, ohne allzuviel über seinen Standort bekannt zu geben.

San Francisco liegt auf dem 37sten nördlichen Breitengrad und dem 122sten westlichen Längengrad, also ist die 37 positiv und die 122 negativ. Zum Vergleich: Der Münchner Marienplatz liegt auf den Geo-Koordinaten 48.137365 und 11.575127, was sich ganz leicht in Google Maps mit einem Rechtsklick der Maus auf die entsprechende Stelle unter "What's here?" ablesen lässt (Abbildung 4). München ist also weiter nördlich als San Francisco, und liegt nicht westlich sondern östlich des nullten Längengrades durch Greenwich, also ist der Wert für Münchens 11ten Längengrad positiv.

Abbildung 4: Geo-Koordinaten des Münchner Marienplatzes

Lesen einfacher als Schreiben

Falls die Anzahl der übergebenen Argumente auf der Kommandozeile nicht stimmt, verzweigt Listing 1 in Zeile 24 zur Funktion usage() ab Zeile 15, die den Fehler anzeigt, den richtigen Gebrauch zeigt und das Programm mit einem Exit-Code von 1 abbricht.

Die Geo-Daten liegen als geographische Breite und Länge (Latitude/Longitude) in den Exif-Tags des JPG-Formats der Fotodatei vor. Diese relativ komplexe Struktur [2] zu lesen ist dank der Library go-exif2 auf Github erstaunlich simpel. In [3] kam sie schon einmal in dieser Reihe in einer Applikation zur Geo-Suche in einer Fotosammlung zum Einsatz.

Die Funktion geopos() ab Zeile 58 öffnet die ihr per Namen übergebene Bilddatei, dekodiert das JPG-Format mit dem Aufruf der Library-Funktion Decode() und findet mit LatLong() die in den Exif-Tags gespeicherten Informationen zur geografischen Breite und Länge des Standorts. Beide Werte gibt die Funktion als Fließkommazahlen an das aufrufende Hauptprogramm zurück. Dieses ruft mit ihnen in Zeile 34 die Funktion fuzz() auf, die den Grauschleier auflegt, sie ist in Listing 1 ab Zeile 78 definiert.

Listing 1: geofuzz.go

    001 package main
    002 
    003 import (
    004   "bytes"
    005   "fmt"
    006   exif "github.com/xor-gate/goexif2/exif"
    007   "math"
    008   "math/rand"
    009   "os"
    010   "os/exec"
    011   "path/filepath"
    012   "time"
    013 )
    014 
    015 func usage(msg string) {
    016   fmt.Printf("%s\n", msg)
    017   fmt.Printf("usage: %s image.jpg\n",
    018              filepath.Base(os.Args[0]))
    019   os.Exit(1)
    020 }
    021 
    022 func main() {
    023   if len(os.Args) != 2 {
    024     usage("Missing argument")
    025   }
    026 
    027   img := os.Args[1]
    028 
    029   lat, lon, err := geopos(img)
    030   if err != nil {
    031     panic(err)
    032   }
    033 
    034   latFuzz, lonFuzz := fuzz(lat, lon)
    035 
    036   fmt.Printf("Was: %f,%f\n", lat, lon)
    037   fmt.Printf("Fuzz: %f,%f\n",
    038              latFuzz, lonFuzz)
    039   patch(img, latFuzz, lonFuzz)
    040 }
    041 
    042 func patch(path string,
    043            lat, lon float64) {
    044   var out bytes.Buffer
    045   cmd := exec.Command(
    046     "exiftool", path,
    047     fmt.Sprintf("-gpslatitude=%f", lat),
    048     fmt.Sprintf("-gpslongitude=%f", lon))
    049   cmd.Stdout = &out
    050   cmd.Stderr = &out
    051 
    052   err := cmd.Run()
    053   if err != nil {
    054     panic(out.String())
    055   }
    056 }
    057 
    058 func geopos(path string) (float64,
    059   float64, error) {
    060   f, err := os.Open(path)
    061   if err != nil {
    062     return 0, 0, err
    063   }
    064 
    065   x, err := exif.Decode(f)
    066   if err != nil {
    067     return 0, 0, err
    068   }
    069 
    070   lat, lon, err := x.LatLong()
    071   if err != nil {
    072     return 0, 0, err
    073   }
    074 
    075   return lat, lon, nil
    076 }
    077 
    078 func fuzz(lat, lon float64) (
    079     float64, float64) {
    080   r := 1000.0 / 111300 // 1km radius
    081 
    082     // secret center
    083   lat += .045
    084   lon += .021
    085 
    086   s1 := rand.NewSource( // random seed
    087     time.Now().UnixNano())
    088   r1 := rand.New(s1)
    089 
    090   u := r1.Float64()
    091   v := r1.Float64()
    092 
    093   w := r * math.Sqrt(u)
    094   t := 2.0 * math.Pi * v
    095   x := w * math.Cos(t)
    096   y := w * math.Sin(t)
    097 
    098   x = x / math.Cos(lat*math.Pi/180.0)
    099   return lat + y, lon + x
    100 }

Mathematik im Raum

Wer auf der Erdoberfläche Strecken zurücklegt, bewegt sich strenggenommen nicht in einem zweidimensionalen Raum, sondern auf der Oberfläche einer mehr oder weniger ebenmäßigen Kugel. Die von einem als geografische Breite und Länge vorliegenden Ort zurückgelegte Entfernung zum anderen ergibt sich deshalb nicht aus simpler zweidimensional euklidischer Geometrie, sondern muss die dritte Dimension auf dem sogenannten Großkreis der Kugel berücksichtigen.

Der Fuzzer muss nun ausrechnen, wie hoch die Differenz (x, y) auf den Breiten- und Längengrad eines Ausgangspunktes (x0,y0) ausfällt, wenn sich jemand eine Strecke kleiner als der Radius r davon in eine zufällige Richtung wegbewegt. Zum Glück hat ein Fachmann auf Stackoverflow die Frage schon einmal beantwortet ([4]).

Der Radius r des Kreises, innerhalb dessen der Algorithmus die Koordinaten streut, liegt üblicherweise in Metern und nicht in Grad vor. Zur Umrechnung dividiert Zeile 80 den Wert von 1000 Metern (entspricht einem Streukreis mit 1km Radius) durch 111.300. Woher kommt diese Konstante? Sie entspricht der Wegstrecke in Metern, die jemand auf dem Äquator zurücklegt, der genau ein Grad weit wandert. Da die Erde dort einen Umfang von etwa 40.075km hat, entspricht ein Grad dem 360sten Teil davon, also etwa 111.300 Metern.

Abbildung 5: Zufällig verteilte Punkte innerhalb eines Kreises mit Radius r als Polarkoordinaten w (Radius) und t (Winkel), sowie karthesische Koordinaten x und y (Uli: kannst du vielleicht auch Teile von https://de.wikipedia.org/wiki/Polarkoordinaten#/media/Datei:Ebene_polarkoordinaten.svg kopieren)

Was die Streuung von Zufallspunkten anlangt, hilft es, zunächst einmal vereinfachend anzunehmen, dass der Algorithmus die Zielpunkte in einen zweidimensionalen Kreis mit Radius r einpflanzt (Abbildung 5). Mit zwei zufällig generierten Werten u und v im Bereich von [0, 1[ ergeben sich die Polarkoordiaten

    w = r * sqrt(u)
    t = 2 * Pi * v

die sich wiederum in karthesische Koordinaten x und y umrechnen lassen:

    x = w * cos(t) 
    y = w * sin(t)

Aufmerksame Leser werden sich vielleicht über die Wurzel sqrt(u) im ersten Term wundern, warum ergbit sich w nicht einfach aus r * u? Dies liegt daran, dass bei einer linearen Distribution der Radien w zwischen 0 und r die Zufallspunkte sich nicht gleichmäßig auf der Kreisfläche verteilen würden. Läge die Hälfte der Punkte unterhalb von r/2, würde sich die Hälfte der Ergebnisse auf den inneren Kreisbereich konzentrieren, der nur 1/4 der gesamten Kreisfläche ausmacht. Die Wurzelfunktion korrigiert dies, und verteilt die Punkte gleichmäßig über die gesamte Kreisfläche.

Allerdings muss der Algorithmus nun schließlich noch berücksichtigten, dass die Kreisfläche sich nicht in einer zweidimensionalen Ebene befindet, sondern auf der Erdkugel, auf der Kreise am Äquator schön ebenmäßig, sich aber Richtung Pol in West-Ost-Richtung verkürzen. Als Korrektur verlängert

    x' = x / cos(y0)

das Ergebnis in x-Richtung (also die ermittelte Differenz im Längengrad), für Regionen, die weiter vom Äquator entfernt liegen. Und da der Breitengrad y0 in Grad vorliegt und nicht als Radiant, was die Implementierung der Cosinusfunktion in vielen Programmiersprachen erwartet, errechnet fuzz() vor der Korrektur die Gradzahl in den Radianten um:

    x = x / math.Cos(y*math.Pi/180.0)

Das Verfahren ist natürlich nur eine Näherung, aber bei kleinen Kreisradien und weitab der Polregionen gut genug. Realistisch gesehen hätte es beim vorliegenden Fuzzing-Problem auch ein einfacherer Ansatz getan, aber interessant war dieser Ausflug hoffentlich trotzdem.

Exiftool eilt zu Hilfe

Leider existiert für Go mit go-exit2 nur eine Bibliothek, die Exif-Tags liest, aber keine einfach zu nutzende, die neue schreibt oder bestehende in einer JPG-Datei auffrischt. So hilft sich Listing 1 mit dem Unix-Tool exiftool, das mit dem Aufruf

    $ exiftool -gpslatitude=37.xx \
      -gpslongitude=-122.xx file.jpg

Koordinaten im Fließkommaformat entgegennimmt und die entsprechenden Exif-Tags in einer Bildatei im JPG-Format entweder anlegt oder auffrischt. Unter Ubuntu installiert sich der Tausendsassa mit

    $ sudo apt-get install exiftool

Die Funktion patch() ab Zeile 42 in Listing 1 ruft das Tool aus Go über die Schnittstelle os/exec auf. Sie nimmt den Namen eines Programm-Binaries mit Parametern entgegen, sowie Byte-Puffer fürs Auffangen der Ausgabe aus den Kanälen Stdout und Stderr. Run() ab Zeile 52 startet den externen Prozess, schnappt sich dessen Ausgabe, und prüft den Exit-Code. Im Normalfall kehrt exiftool mit einem Exit-Code von 0 zurück, dann ist der Fehler err in Zeile 53 gleich nil und patch() kehrt nach getaner Arbeit zum Aufrufer, dem Hauptprogramm zurück. Das Tool exiftool modifiziert die ihm übergebene JPG-Datei direkt und lässt ein Backup des Originals in _original zurück, die geofuzz jedoch nicht interessiert, und einfach herumliegen lässt.

Aus dem Source-Code in Listing 1 erzeugt der Aufruf

    $ go mod init geofuzz
    $ go build geofuzz.go

das Binary geofuzz, das der User in einen mit PATH abgedeckten Pfad installiert. Der Aufruf geofuzz file.jpg legt dann blitzschnell den privatsphärenfreundlichen Grauschleier auf die Geo-Tags der Datei. Neugierige Online-Schnüffler raufen sich anschließend die Haare, weil sie im falschen Stadtteil suchen.

Infos

[1]

Listings zu diesem Artikel: http://www.linux-magazin.de/static/listings/magazin/2020/09/snapshot/

[2]

Exif-Format, Wikipedia: https://en.wikipedia.org/wiki/Exif

[3]

Michael Schilli, "Alles im Umkreis": Linux-Magazin 06/19, S.xxx, <U>https://www.linux-magazin.de/ausgaben/2019/06/snapshot-15/<U>

[4]

"Generating random locations nearby?", https://gis.stackexchange.com/questions/25877/generating-random-locations-nearby

Michael Schilli

arbeitet als Software-Engineer in der San Francisco Bay Area in Kalifornien. In seiner seit 1997 laufenden Kolumne forscht er jeden Monat nach praktischen Anwendungen verschiedener Programmiersprachen. Unter mschilli@perlmeister.com beantwortet er gerne Ihre Fragen.

POD ERRORS

Hey! The above document had some coding errors, which are explained below:

Around line 5:

Unknown directive: =desc