iPhone-Trackerdaten auslesen und aufbereiten

Ich denke viele von euch haben davon gehört, dass Apple ausversehen, in regelmäßigen Abständen, unter anderem die genaue Position, versehen mit einem Zeitparameter, gespeichert hat. Da ich selber besitzer eines solchen netten iDevices bin, habe ich mich mal etwas genauer mit dem Thema beschäftigt und will euch meine Ergebnisse hier in diesem kleinen Tutorial präsentieren.

Mein Ziel:
Die Daten so weit wie möglich automatisiert beschaffen und im besten Fall bei Googlemaps anzeigen lassen. Die technischen Aspekten kommen für manche bestimmt zu kurz, aber es soll eher ein praxisorientiertes Basteltutorial darstellen.

Schritt 1 - Rohdaten:
Wo liegen die relevanten Dateien? Auf dem Handy! Oder aber auch in den Backups, die Apple gerne macht und in denen wirklich alle wichtigen und unwichtigen Daten gesammelt werden (ein Blick lohnt sich). Speicherort hängt vom OS ab.

Windows:C:\Dokumente und Einstellungen\KONTENNAME\Anwendungsdaten\Apple Computer\MobileSync\Backup
Mac: ~/Library/Application Support/MobileSync/Backup

Dort sollte man kryptisch benannte Ordner finden, die jeweils ein Backup darstellen. Per "letztes Änderungsdatum" kann man das neuste meist schnell ausfindig machen.

Schritt 2 - Ins Eingemachte:
Nun wird etwas interessater. In dem ganzen Wust von Daten müssen wir die Datenbank ausfindig machen, die unsere Trackingdaten enthält. natürlich hat schon ein anderer schlauer Kopf all diese Sachen gefunden [1] und zwar besteht der Prozess aus 2 Schritten. Apple verschleiert seine Backupdaten. Eine ausgiebe Diskussion inklusive Lösung in Form eines Python Skripts findet sich bereits bei Stack-Overflow [2]. Benötigt werden, die dem Backup beiliegenden Dateien, "Manifest.mbdb" und "Manifest.mbdx". Da das Tutorial sich damit beschäftigen soll die relevanten Daten aufzubereiten, war die Basis meines Zielprogramms dieses Pythonskript. Nach [1] ist genau die datei die richige Datenbank, die die Information "consolidated" enthällt. Daher habe ich das eigentliche Pythonskript so angepasst, dass er nicht alle Dateien angibt, sondern die relevante Datenbank findet. Sprich ab diesem Moment beschäftigt mich das Finden der Daten nicht mehr besonders (siehe gabnz unten im Code: "#ab hier setzte ich an). Im weiteren Verlauf benutze ich auch weiterhin Python (Achtung: Bin selber ganz neu bei Python, deswegen ist der Code vielleicht etwas unsauber).

Code:
#!/usr/bin/env python
import sys
import sqlite3
import webbrowser
import time

def getint(data, offset, intsize):
    """Retrieve an integer (big-endian) and new offset from the current offset"""
    value = 0
    while intsize > 0:
        value = (value<<8) + ord(data[offset])
        offset = offset + 1
        intsize = intsize - 1
    return value, offset

def getstring(data, offset):
    """Retrieve a string and new offset from the current offset into the data"""
    if data[offset] == chr(0xFF) and data[offset+1] == chr(0xFF):
        return '', offset+2 # Blank string
    length, offset = getint(data, offset, 2) # 2-byte length
    value = data[offset:offset+length]
    return value, (offset + length)

def process_mbdb_file(filename):
    mbdb = {} # Map offset of info in this file => file info
    data = open(filename).read()
    if data[0:4] != "mbdb": raise Exception("This does not look like an MBDB file")
    offset = 4
    offset = offset + 2 # value x05 x00, not sure what this is
    while offset < len(data):
        fileinfo = {}
        fileinfo['start_offset'] = offset
        fileinfo['domain'], offset = getstring(data, offset)
        fileinfo['filename'], offset = getstring(data, offset)
        fileinfo['linktarget'], offset = getstring(data, offset)
        fileinfo['datahash'], offset = getstring(data, offset)
        fileinfo['unknown1'], offset = getstring(data, offset)
        fileinfo['mode'], offset = getint(data, offset, 2)
        fileinfo['unknown2'], offset = getint(data, offset, 4)
        fileinfo['unknown3'], offset = getint(data, offset, 4)
        fileinfo['userid'], offset = getint(data, offset, 4)
        fileinfo['groupid'], offset = getint(data, offset, 4)
        fileinfo['mtime'], offset = getint(data, offset, 4)
        fileinfo['atime'], offset = getint(data, offset, 4)
        fileinfo['ctime'], offset = getint(data, offset, 4)
        fileinfo['filelen'], offset = getint(data, offset, 8)
        fileinfo['flag'], offset = getint(data, offset, 1)
        fileinfo['numprops'], offset = getint(data, offset, 1)
        fileinfo['properties'] = {}
        for ii in range(fileinfo['numprops']):
            propname, offset = getstring(data, offset)
            propval, offset = getstring(data, offset)
            fileinfo['properties'][propname] = propval
        mbdb[fileinfo['start_offset']] = fileinfo
    return mbdb

def process_mbdx_file(filename):
    mbdx = {} # Map offset of info in the MBDB file => fileID string
    data = open(filename).read()
    if data[0:4] != "mbdx": raise Exception("This does not look like an MBDX file")
    offset = 4
    offset = offset + 2 # value 0x02 0x00, not sure what this is
    filecount, offset = getint(data, offset, 4) # 4-byte count of records
    while offset < len(data):
        # 26 byte record, made up of ...
        fileID = data[offset:offset+20] # 20 bytes of fileID
        fileID_string = ''.join(['%02x' % ord(b) for b in fileID])
        offset = offset + 20
        mbdb_offset, offset = getint(data, offset, 4) # 4-byte offset field
        mbdb_offset = mbdb_offset + 6 # Add 6 to get past prolog
        mode, offset = getint(data, offset, 2) # 2-byte mode field
        mbdx[mbdb_offset] = fileID_string
    return mbdx

def modestr(val):
    def mode(val):
        if (val & 0x4): r = 'r'
        else: r = '-'
        if (val & 0x2): w = 'w'
        else: w = '-'
        if (val & 0x1): x = 'x'
        else: x = '-'
        return r+w+x
    return mode(val>>6) + mode((val>>3)) + mode(val)

def fileinfo_str(f, verbose=False):
    if not verbose: return "(%s)%s::%s" % (f['fileID'], f['domain'], f['filename'])
    if (f['mode'] & 0xE000) == 0xA000: type = 'l' # symlink
    elif (f['mode'] & 0xE000) == 0x8000: type = '-' # file
    elif (f['mode'] & 0xE000) == 0x4000: type = 'd' # dir
    else:
        print >> sys.stderr, "Unknown file type %04x for %s" % (f['mode'], fileinfo_str(f, False))
        type = '?' # unknown
    info = ("%s%s %08x %08x %7d %10d %10d %10d (%s)%s::%s" %
            (type, modestr(f['mode']&0x0FFF) , f['userid'], f['groupid'], f['filelen'],
             f['mtime'], f['atime'], f['ctime'], f['fileID'], f['domain'], f['filename']))
    if type == 'l': info = info + ' -> ' + f['linktarget'] # symlink destination
    for name, value in f['properties'].items(): # extra properties
        info = info + ' ' + name + '=' + repr(value)
    return info

verbose = True
if __name__ == '__main__':
    mbdb = process_mbdb_file("Manifest.mbdb")
    mbdx = process_mbdx_file("Manifest.mbdx")
    for offset, fileinfo in mbdb.items():
        if offset in mbdx:
            fileinfo['fileID'] = mbdx[offset]
        else:
            fileinfo['fileID'] = "<nofileID>"
            print >> sys.stderr, "No fileID found for %s" % fileinfo_str(fileinfo)


    if 'consolidated' in fileinfo_str(fileinfo, verbose):
        #ab hier setzte ich an

Schritt 3 - Die Datenbank:
Nun bin ich soweit: In der Variable "fileinfo_str(fileinfo, verbose)" beinhaltet den richtigen Datenbanknamen. Nun müssen "nur noch "die eigentlich "Trackindaten" lokalisiert und verarbeitet werden. Die DB ist vom Typ SqLite und ich habe mit unter Ubuntu einen "Explorer" dafür besorgt um ein wenig zu stöbern. Python bietet weiterhin im Modul "sqlite3" eine simple Schnittstelle. Um diese nun zu öffnen habe ich den Namen der Datenbank, den ich vorher noch dur mehrfaches Zerschneiden aus dem Skript von Stack-Overflow extrahiert habe, sodass dieser so aussieht: "fileinfo_str(fileinfo, verbose).partition(')')[0].partition('(')[2]", an "sqlite3.connect" weitergegeben. Außerdem muss noch ein Zeigerobjekt erstellt werden und die relevanten Tabellen und Spalten müssen ausgewählt werden. Der neue Code sieht dann folgendermaßen aus:

Code:
    if 'consolidated' in fileinfo_str(fileinfo, verbose):
        connection = sqlite3.connect(fileinfo_str(fileinfo, verbose).partition(')')[0].partition('(')[2])

        cursor = connection.cursor()

        cursor.execute("SELECT Timestamp, Latitude, Longitude FROM CellLocation")

Schritt 4 - Die Zeit:
Apple hat zu den Koordinaten jeweils einen Timestamp gespeichert. Dieser umfasst die Sekunden ab dem 1.1.2001. Da Python mit der C-ähnlichen Funktion arbeitet, die ab dem 1.1.1970 anfängt zu zählen, habe ich die Differenz auf jedes Datum von Apple aufaddiert, sodass ich schlussendlich mit den Daten im "Python-Format" arbeiten konnte. Außerdem habe ich noch "60*60" Sekunden aufaddiert, da diese die Zeitverschiebung darstellen.

Code:
time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) + row[0] + 60*60)

Schritt 5 - Datenaufbereitung:
Die relevanten Daten sind nun gefunden. Mein Ziel war es diese dann bei Googlemaps darzustellen und zwar in Form einer Route über mehrere Punkte. Wenn man sich mal so eine bei Googlemaps von Hand und mit Koordinaten bastelt fällt folgendes auf:

Die allgemeine Struktur des Links ist:

"http://maps.google.de/maps?saddr=[1.Koordinate (Kommentar)]"+"to:[2.Koordinate (Kommentar)]"...+"to:[n.Koordinate (Kommentar)]"+"&t=h&om=0

Bsp:
Sieboldstraße nach Unbekannte Straße - Google Maps
(seht euch einfach den link oben im Browserfenster an)

Sprich ich bringe die Koordinaten in die richtige Form eines Googlemaplinks und öffne diesen in einem Browser. Eigentlich wollte ich als Kommentar das jeweilige Datum hinzufügen, jedoch verarbeitet Google keine beliebig langen URL's, sodass ich die jeweiligen Zeilen auskommentiert habe. Können bei Bedarf aber hinzugefügt werden. Außerdem kann es vorkommen, dass die Menge der gesammelten Koordinaten auch zu einer zu langen URL führt, sodass ich immer 20 Datensätze in einem Tab öffne.

Code:
def makeCoordinatesToGMURL(row):
    #print row
    return "+to:"+str(row[1])+","+str(row[2])
    #+"("+time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) + row[0] + 60*60)+")"


		row = cursor.fetchone()
		url = "http://maps.google.de/maps?saddr="+str(row[1])+","+str(row[2])
		#+"("+time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) + row[0] + 60*60)+")"

		row = cursor.fetchone() 
		counter = 0
		lulz = 0
		for row in cursor:
			if lulz != row[0] and counter < 20:
				lulz = row[0]
 				url = url + makeCoordinatesToGMURL(row)
				counter = counter +1
			if lulz != row[0] and counter == 20:
				lulz = row[0]		
				counter = 0
				webbrowser.open(url + "&t=h&om=0", 0)
				url = "http://maps.google.de/maps?saddr="+str(row[1])+","+str(row[2])
				#+"("+time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) +

        		#print "+to:"+str(row[1])+","+str(row[2])+"("+time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) + row[0] + 60*60)+")"
 		#row.partition('(')[2].partition(', ')[0]
		webbrowser.open(url + "&t=h&om=0", 0)

Schritt 6 - Staunen:
Es ist wirklich eine nette Geste von Apple diese Daten zu sammeln, da dies ein teures GPS-Tracker-App überflüssig macht. Mich hat überrascht wie genau die Koordinaten sind, wobei ich aber sagen muss, dass es lustig ist bei Googlemaps seine Bewegungsdaten von einem längeren Zeitraum zu sehen...

So das wars dann auch schon. Bin zum Ende hin glaube ich etwas oberflächlicher geworden, aber ich hänge den gesamten Code, inklusive unnötiger Kommentare, einfach mal an, sodass jeder noch etwas dran basteln kann. Außerdem sagen die 30 Zeilen Code, die ich hinzugefügt mehr als 1000 Wörter :wink:. Fragen sind erwünscht...

#!/usr/bin/env python
import sys
import sqlite3
import webbrowser
import time

def getint(data, offset, intsize):
"""Retrieve an integer (big-endian) and new offset from the current offset"""
value = 0
while intsize > 0:
value = (value<<8) + ord(data[offset])
offset = offset + 1
intsize = intsize - 1
return value, offset

def getstring(data, offset):
"""Retrieve a string and new offset from the current offset into the data"""
if data[offset] == chr(0xFF) and data[offset+1] == chr(0xFF):
return '', offset+2 # Blank string
length, offset = getint(data, offset, 2) # 2-byte length
value = data[offset:eek:ffset+length]
return value, (offset + length)

def process_mbdb_file(filename):
mbdb = {} # Map offset of info in this file => file info
data = open(filename).read()
if data[0:4] != "mbdb": raise Exception("This does not look like an MBDB file")
offset = 4
offset = offset + 2 # value x05 x00, not sure what this is
while offset < len(data):
fileinfo = {}
fileinfo['start_offset'] = offset
fileinfo['domain'], offset = getstring(data, offset)
fileinfo['filename'], offset = getstring(data, offset)
fileinfo['linktarget'], offset = getstring(data, offset)
fileinfo['datahash'], offset = getstring(data, offset)
fileinfo['unknown1'], offset = getstring(data, offset)
fileinfo['mode'], offset = getint(data, offset, 2)
fileinfo['unknown2'], offset = getint(data, offset, 4)
fileinfo['unknown3'], offset = getint(data, offset, 4)
fileinfo['userid'], offset = getint(data, offset, 4)
fileinfo['groupid'], offset = getint(data, offset, 4)
fileinfo['mtime'], offset = getint(data, offset, 4)
fileinfo['atime'], offset = getint(data, offset, 4)
fileinfo['ctime'], offset = getint(data, offset, 4)
fileinfo['filelen'], offset = getint(data, offset, 8)
fileinfo['flag'], offset = getint(data, offset, 1)
fileinfo['numprops'], offset = getint(data, offset, 1)
fileinfo['properties'] = {}
for ii in range(fileinfo['numprops']):
propname, offset = getstring(data, offset)
propval, offset = getstring(data, offset)
fileinfo['properties'][propname] = propval
mbdb[fileinfo['start_offset']] = fileinfo
return mbdb

def process_mbdx_file(filename):
mbdx = {} # Map offset of info in the MBDB file => fileID string
data = open(filename).read()
if data[0:4] != "mbdx": raise Exception("This does not look like an MBDX file")
offset = 4
offset = offset + 2 # value 0x02 0x00, not sure what this is
filecount, offset = getint(data, offset, 4) # 4-byte count of records
while offset < len(data):
# 26 byte record, made up of ...
fileID = data[offset:eek:ffset+20] # 20 bytes of fileID
fileID_string = ''.join(['%02x' % ord(b) for b in fileID])
offset = offset + 20
mbdb_offset, offset = getint(data, offset, 4) # 4-byte offset field
mbdb_offset = mbdb_offset + 6 # Add 6 to get past prolog
mode, offset = getint(data, offset, 2) # 2-byte mode field
mbdx[mbdb_offset] = fileID_string
return mbdx

def modestr(val):
def mode(val):
if (val & 0x4): r = 'r'
else: r = '-'
if (val & 0x2): w = 'w'
else: w = '-'
if (val & 0x1): x = 'x'
else: x = '-'
return r+w+x
return mode(val>>6) + mode((val>>3)) + mode(val)

def fileinfo_str(f, verbose=False):
if not verbose: return "(%s)%s::%s" % (f['fileID'], f['domain'], f['filename'])
if (f['mode'] & 0xE000) == 0xA000: type = 'l' # symlink
elif (f['mode'] & 0xE000) == 0x8000: type = '-' # file
elif (f['mode'] & 0xE000) == 0x4000: type = 'd' # dir
else:
print >> sys.stderr, "Unknown file type %04x for %s" % (f['mode'], fileinfo_str(f, False))
type = '?' # unknown
info = ("%s%s %08x %08x %7d %10d %10d %10d (%s)%s::%s" %
(type, modestr(f['mode']&0x0FFF) , f['userid'], f['groupid'], f['filelen'],
f['mtime'], f['atime'], f['ctime'], f['fileID'], f['domain'], f['filename']))
if type == 'l': info = info + ' -> ' + f['linktarget'] # symlink destination
for name, value in f['properties'].items(): # extra properties
info = info + ' ' + name + '=' + repr(value)
return info

def makeCoordinatesToGMURL(row):
#print row
return "+to:"+str(row[1])+","+str(row[2])
#+"("+time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) + row[0] + 60*60)+")"

verbose = True
if __name__ == '__main__':
mbdb = process_mbdb_file("Manifest.mbdb")
mbdx = process_mbdx_file("Manifest.mbdx")
for offset, fileinfo in mbdb.items():
if offset in mbdx:
fileinfo['fileID'] = mbdx[offset]
else:
fileinfo['fileID'] = "<nofileID>"
print >> sys.stderr, "No fileID found for %s" % fileinfo_str(fileinfo)


if 'consolidated' in fileinfo_str(fileinfo, verbose):
print fileinfo_str(fileinfo, verbose).partition(')')[0].partition('(')[2]
connection = sqlite3.connect(fileinfo_str(fileinfo, verbose).partition(')')[0].partition('(')[2])

cursor = connection.cursor()

cursor.execute("SELECT Timestamp, Latitude, Longitude FROM CellLocation")

"""""
row = cursor.fetchone()
URLstring = "http://maps.google.de/maps?saddr=50.017,8.80552(ET+Dtzb)"
"""""
row = cursor.fetchone()
url = "http://maps.google.de/maps?saddr="+str(row[1])+","+str(row[2])
#+"("+time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) + row[0] + 60*60)+")"

row = cursor.fetchone()
counter = 0
lulz = 0
for row in cursor:
if lulz != row[0] and counter < 20:
lulz = row[0]
url = url + makeCoordinatesToGMURL(row)
counter = counter +1
if lulz != row[0] and counter == 20:
lulz = row[0]
counter = 0
webbrowser.open(url + "&t=h&om=0", 0)
url = "http://maps.google.de/maps?saddr="+str(row[1])+","+str(row[2])
#+"("+time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) +

#print "+to:"+str(row[1])+","+str(row[2])+"("+time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) + row[0] + 60*60)+")"
#row.partition('(')[2].partition(', ')[0]
webbrowser.open(url + "&t=h&om=0", 0)
#print url + "&t=h&om=0"


#t = time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))
#print t.tm_year
#print time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0)))

#print time.ctime(time.mktime(time.struct_time((2001, 1, 1, 0, 0, 0, 0, 0, 0))) + row[0])


#webbrowser.open("http://www.galileo-press.de", 2)

Quellen:
[1] petewarden/iPhoneTracker @ GitHub
[2] iphone - How to parse the Manifest.mbdb file in an iOS 4.0 iTunes Backup - Stack Overflow
PythonAPI
mehre Pythontutorials
 
Funktioniert seit 4.3.3 nicht mehr da das Backup gelöscht wird.
 
Zurück
Oben