Aus Linux-Magazin 09/2020

Gespeicherte Geokoordinaten in Handyfotos manipulieren

© Aleksandr Ozerov, 123RF

Mike Schilli legt Wert auf Privatsphäre. Deshalb baut er ein Go-Programm, das Handyfotos vor der Veröffentlichung auf Online-Plattformen mit einem Geo-Grauschleier versieht, um Rückschlüsse auf den Standort zu verhindern.

Wer seinen Trödel online veräußert, der übersieht oft, welche brisanten privaten Informationen die verkaufsfördernden Handyfotos verraten. Lichtet der Verkäufer die Ware zu Hause mit dem Handy ab, stecken unter Umständen noch die Geodaten 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 gern mehr preis als unbedingt notwendig?

Das Handy tilgt Geodaten auf Wunsch auch direkt – aber das sieht dann so aus, als hätte der Nutzer etwas zu verbergen. Deshalb versieht ein selbstgeschriebenes Go-Programm Bilddateien mit einem Geo-Grauschleier, sodass dort zufällig verwischte Geokoordinaten stehen. Daraus lässt sich eventuell noch das Stadtviertel ermitteln, nicht aber die genaue Adresse.

Treffer im Radius

Das Ziel des Verfahrens ist es, die Geotags einer Aufnahme zufällig in einen Bereich innerhalb eines definierten Aktionskreises zu verschieben. Bei mehreren Aufnahmen liegen die Zielwerte alle innerhalb des Aktionsradius. Damit bei Hunderten von Aufnahmen niemand 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. Dazu nutzt er feste, aber geheime Werte für die geografische Breite und Länge (Abbildung 1).

Abbildung 1: Vom Originalstandort springt der Algorithmus zu einem neuen Punkt und wählt zufällig Geokoordinaten in einem Aktionsradius aus.

Abbildung 1: Vom Originalstandort springt der Algorithmus zu einem neuen Punkt und wählt zufällig Geokoordinaten in einem Aktionsradius aus.

Die Geolocation einer Bilddatei steht in den sogenannten EXIF-Tags [1] des JPEG-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.

Die Abbildungen 2 und**3 zeigen jeweils die Geotags der Aufnahme – ein Bild von einer Schachtel mit einem Google Voice Kit, das ich auf Ebay verkaufen wollte. Das Original in Abbildung 2 zeigt meine Privatadresse in San Francisco, an der die Aufnahme in meinem Arbeitszimmer entstand. Nach dem Aufruf des vorgestellten Programms Geofuzz verschiebt sich die Geolocation in der Bilddatei weiter nördlich in den Stadtteil Financial District. Abbildung 3 zeigt die nach mehreren aufeinanderfolgenden Aufrufen des Fuzzers weiter modifizierten Werte, die innerhalb des eingestellten Aktionsradius streuen.

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

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

Abbildung 3: Aufeinanderfolgende Aufrufe des Geotag-Fuzzers verschieben das Geotag auf Orte innerhalb des Aktionsradius im Financial District.

Abbildung 3: Aufeinanderfolgende Aufrufe des Geotag-Fuzzers verschieben das Geotag auf Orte innerhalb des Aktionsradius im Financial District.

Listing 1

geofuzz.go

package main
import (
  "bytes"
  "fmt"
  exif "github.com/xor-gate/goexif2/exif"
  "math"
  "math/rand"
  "os"
  "os/exec"
  "path/filepath"
  "time"
)
func usage(msg string) {
  fmt.Printf("%s\n", msg)
  fmt.Printf("usage: %s image.jpg\n",
             filepath.Base(os.Args[0]))
  os.Exit(1)
}
func main() {
  if len(os.Args) != 2 {
    usage("Missing argument")
  }
  img := os.Args[1]
  lat, lon, err := geopos(img)
  if err != nil {
    panic(err)
  }
  latFuzz, lonFuzz := fuzz(lat, lon)
  fmt.Printf("Was: %f,%f\n", lat, lon)
  fmt.Printf("Fuzz: %f,%f\n",
             latFuzz, lonFuzz)
  patch(img, latFuzz, lonFuzz)
}
func patch(path string,
           lat, lon float64) {
  var out bytes.Buffer
  cmd := exec.Command(
    "exiftool", path,
    fmt.Sprintf("-gpslatitude=%f", lat),
    fmt.Sprintf("-gpslongitude=%f", lon))
  cmd.Stdout = &out
  cmd.Stderr = &out
  err := cmd.Run()
  if err != nil {
    panic(out.String())
  }
}
func geopos(path string) (
  float64, float64, error) {
  f, err := os.Open(path)
  if err != nil {
    return 0, 0, err
  }
  x, err := exif.Decode(f)
  if err != nil {
    return 0, 0, err
  }
  lat, lon, err := x.LatLong()
  if err != nil {
    return 0, 0, err
  }
  return lat, lon, nil
}
func fuzz(lat, lon float64) (
  float64, float64) {
  r := 1000.0 / 111300 // 1km radius
  // secret center
  lat += .045
  lon += .021
  s1 := rand.NewSource( // random seed
    time.Now().UnixNano())
  r1 := rand.New(s1)
  u := r1.Float64()
  v := r1.Float64()
  w := r * math.Sqrt(u)
  t := 2.0 * math.Pi * v
  x := w * math.Cos(t)
  y := w * math.Sin(t)
  x = x / math.Cos(lat*math.Pi/180.0)
  return lat + y, lon + x
}

Das aus Listing 1 generierte Programm Geofuzz nimmt in Listing 2 auf der Kommandozeile den Namen der zu manipulierenden JPEG-Datei entgegen. Es gibt neben den im Bild gefundenen Geotags für geografische Breite und Länge auch die vom Programm manipulierten Werte aus. Der Fuzzer modifiziert die angegebene Datei direkt, und der User kann diese nun posten, ohne allzu viel über seinen Standort zu verraten.

Listing 2

Namensübergabe

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

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 Geokoordinaten 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 liegt also weiter nördlich als San Francisco und nicht westlich des nullten Längengrads, sondern östlich. Deshalb ist der Wert für Münchens elften Längengrad positiv.

Abbildung 4: Die Geokoordinaten des Münchner Marienplatzes.

Abbildung 4: Die Geokoordinaten 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 demonstriert und das Programm mit einem Exit-Code von 1 abbricht.

Die Geodaten liegen als Breite und Länge (Latitude, Longitude) in den EXIF-Tags des JPEG-Formats der Fotodatei vor. Die Library »go-exif2« auf Github macht es erstaunlich leicht, diese relativ komplexe Struktur [1] zu lesen. Sie kam in dieser Reihe schon einmal in einer Applikation bei der Geosuche in einer Fotosammlung zum Einsatz [2].

Die Funktion »geopos()« ab Zeile 58 öffnet die ihr per Namen übergebene Bilddatei, dekodiert das JPEG-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 Hauptprogramm zurück. Das ruft mit ihnen in Zeile 34 die Funktion »fuzz()« auf, die den Grauschleier auflegt (ab Zeile 78).

Mathematik im Raum

Wer auf der Erdoberfläche Strecken zurücklegt, bewegt sich streng genommen 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 zu einem anderen ergibt sich deshalb nicht aus der simplen zweidimensionalen euklidischen Geometrie, sondern muss die dritte Dimension auf dem sogenannten Großkreis der Kugel berücksichtigen.

Der Fuzzer muss deshalb ausrechnen, wie groß der Abstand (»x«, »y«) zum Breiten- und Längengrad eines Ausgangspunkts (»x0«, »y0«) ausfällt, von dem sich jemand auf dem Großkreis innerhalb des Radius »r« in eine zufällige Richtung wegbewegt. Zum Glück hat ein Fachmann auf Stack Overflow die Frage schon einmal beantwortet [3].

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 (er entspricht einem Streukreis mit 1 Kilometer 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.

Was die Streuung von Zufallspunkten anbelangt, hilft es, zunächst einmal vereinfachend anzunehmen, dass der Algorithmus die Zielpunkte in einen zweidimensionalen Kreis mit dem Radius »r« einpflanzt. Mit zwei zufällig generierten Werten »u« und »v« im Bereich von »[0,« »1[« ergeben sich aus Listing 3 die Polarkoordinaten, die sich mit Listing 4 wiederum in kartesische Koordinaten »x« und »y« umrechnen lassen.

Listing 3

Polarkoordinaten

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

Listing 4

Kartesische Koordinaten

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

Aufmerksame Leser werden sich in Listing 3 vielleicht über die Wurzel »sqrt(u)« im ersten Term wundern – warum ergibt sich »w« nicht einfach aus »r« »*« »u«? Das 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 ein Viertel der gesamten Kreisfläche ausmacht. Die Wurzelfunktion korrigiert das und verteilt die Punkte gleichmäßig über die gesamte Kreisfläche.

Allerdings muss der Algorithmus nun noch berücksichtigten, dass die Kreisfläche nicht in einer zweidimensionalen Ebene liegt, sondern auf der Erdkugel. Auf ihr fallen Kreise am Äquator schön ebenmäßig aus, verkürzen sich aber Richtung Pol in West-Ost-Richtung. Eine Korrekturformel verlängert das Ergebnis in x-Richtung (also die ermittelte Differenz im Längengrad) für Regionen, die weiter vom Äquator entfernt liegen:

x' = x / cos(y0)

Nun liegt der Breitengrad »y0« allerdings in Grad vor und nicht als Radiant, wie es die Implementierung der Kosinus-Funktion in vielen Programmiersprachen erwartet. Deshalb rechnet »fuzz()« vor der Korrektur die Gradzahl in den Radianten um:

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

Das Verfahren liefert nur eine Näherung, aber bei kleinen Kreisradien und weitab der Polregionen funktioniert es 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

Für Go gibt es zwar mit »go-exit2« eine Bibliothek, die EXIF-Tags liest. Es existiert aber keine einfach zu nutzende Library, die neue schreibt oder bestehende in einer JPEG-Datei auffrischt. Daher behilft sich Listing 1 mit dem Unix-Tool Exiftool. Unter Ubuntu lässt sich der Tausendsassa über die Paketverwaltung einrichten (Listing 5, erste Zeile). Das Werkzeug nimmt Koordinaten im Fließkommaformat entgegen und legt die entsprechenden EXIF-Tags in einer JPEG-Bildatei entweder an oder frischt sie auf (zweite Zeile).

Listing 5

EXIF-Tags bearbeiten

$ sudo apt-get install exiftool
$ exiftool -gpslatitude=37.xx -gpslongitude=-122.yy file.jpg

Die Funktion »patch()« ab Zeile 42 von Listing 1 ruft das Tool aus Go über die Schnittstelle »os/exec« auf. Sie nimmt den Namen eines Programms samt Parametern sowie Byte-Puffern fürs Auffangen der Ausgabe aus den Kanälen Stdout und Stderr entgegen. »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. In diesem Fall ist der Fehler »err« in Zeile 53 gleich »nil«, und »patch()« kehrt nach getaner Arbeit zum Aufrufer (dem Hauptprogramm) zurück. Exiftool modifiziert die ihm übergebene JPEG-Datei direkt und hinterlegt in »_original/« ein Backup des Originals, das Geofuzz jedoch nicht interessiert und das es deshalb einfach links liegen lässt.

Aus dem Quellcode in Listing 1 erzeugt der Aufruf aus Listing 6 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 über die Geotags der Datei. Neugierige Online-Schnüffler raufen sich anschließend die Haare, weil sie im falschen Stadtteil suchen.

Listing 6

Binary erzeugen

$ go mod init geofuzz
$ go build geofuzz.go

Online PLUS

Im Screencast unter http://www.linux-magazin.de/videos/ demonstriert Michael Schilli das vorgestellte Programmierbeispiel.

Der Autor

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 mailto:mschilli@perlmeister.com beantwortet er gern Fragen.

Infos

  1. EXIF-Format: https://en.wikipedia.org/wiki/EXIF
  2. Snapshot: Mike Schilli, “Alles im Umkreis”, LM 06/2019, S. 88, https://www.lm-online.de/42685
  3. “Generating random locations nearby”: https://gis.stackexchange.com/questions/25877/generating-random-locations-nearby
DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 4 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben