Decoding Dashcam GPS Data

Started by Neal Krawetz, May 20, 2024, 06:37:52 PM

Previous topic - Next topic

Neal Krawetz

A friend asked me if I could decode his dashcam GPS data.
It's an MP4 video with a "gps " atom.
The gps atom contains:
  Two digit version: 01 01 = 1.1
  4 byte number of records
  Then a set of 8 bytes per record: file offset for data and data size.

Each data record begins with "LIGOGPSINFO" and is followed by encoded bytes.
E.g.:
LIGOGPSINFO
####;
4FDF1/D9/E8 D=LGJL98 :L11.1=1JD8 ALEDD.GFDF=D FF.1? km/h xLD.E yL-D.D zLD.E
It looks like Phil tried to look at this encoding earlier and gave up. (https://exiftool.org/forum/index.php?topic=15387.0)

I think I've made progress on decoding it. But it's not perfect...
00068960   DA FB BA 8C  B1 66 FB B1  00 00 40 00  66 72 65 65  .....f....@.free
00068970   47 50 53 20  98 00 00 00  4C 49 47 4F  47 50 53 49  GPS ....LIGOGPSI
00068980   4E 46 4F 00  00 00 00 0D  0A 00 00 00  23 23 23 23  NFO.........####
00068990   3B 00 A0 34  46 44 46 31  2F 44 39 2F  45 38 20 44  ;..4FDF1/D9/E8 D
000689A0   3D 4C 47 4A  4C 39 38 20  3A 4C 31 31  2E 31 3D 31  =LGJL98 :L11.1=1
000689B0   4A 44 38 20  41 4C 45 44  44 2E 47 46  44 46 3D 44  JD8 ALEDD.GFDF=D
000689C0   20 46 46 2E  31 3F 20 6B  6D 2F 68 20  78 4C 44 2E   FF.1? km/h xLD.
000689D0   45 20 79 4C  2D 44 2E 44  20 7A 4C 44  2E 45 00 00  E yL-D.D zLD.E..
00004000 = block size
 "freeGPS " + 4 more bytes (unknown)
 "LIGOGPSINFO" + null
  00000d = length of header
    Header is the "####;" and ends and the "4".

Then comes the text string: "FDF1/D9/E8 D=LGJL98 :L11.1=1JD8 ALEDD.GFDF=D FF.1? km/h xLD.E yL-D.D zLD.E"
The "FDF1 is the giveaway. In the video, the dashboard time is "2024/05/19". This looks like a simple caesar cipher:
// vc is the character I want to print
        if (vc < ' ') { break; } // binary
        // substitution cipher!
        switch(vc)
          {
          case 'L': vc=':'; break;
          case '8': vc='9'; break;
          case '=': vc='8'; break;
          case 'J': vc='7'; break;
          case '?': vc='6'; break;
          case '9': vc='5'; break;
          case '1': vc='4'; break;
          case 'G': vc='3'; break;
          case 'F': vc='2'; break;
          case 'E': vc='1'; break;
          case 'D': vc='0'; break;
          case 'A': vc='-'; break;
          case ':': vc='+'; break;
          case '/': break;
          case '.': break;
          case '-': break;
          case ' ': break;
          default:  break;
          }
This decodes the string as "2024/05/19 08:37:59 +:44.484709 -:100.320280 22.46 km/h x:0.1 y:-0.0 z:0.1"
The date and time matches the video.

The weird things:
  • The GPS location appears to be off by +4,-6.5 (for my sample video, it should be around 40.48,-106.82; downtown Steamboat Springs, Colorado). The 4 degrees offset for latitude seems perfect, but the longitude is a fraction more; using -6.5 places it within 0.3 miles of the location.
  • The speed in the video says 25.8 mph. That should be 41.5 km/h, but the data encoded as 22.46 km/h. Either it's lying about encoding as km/h, or it's off by 19 km/h, or they changed the decoding.
I suspect that they are using hard-coded math offsets for obfuscation.

What I don't know:
  • Does the caesar cipher remain consistent with all videos, or is it per-video?
  • Are the math adjustments hard-coded or embedded in some of the bytes that I skipped as unknown?

Now for the really bad news: I can't share the video I'm looking at, and I don't have any others for comparison.

Does Phil have any examples he can check?

Phil Harvey

I've been collecting these videos with obfuscated coordinates and plan to have another whack at decoding these when I get a chance.  Thanks for the hint about the caesar cipher -- this will help.  Unfortunately I'm pretty busy now so it could be a while before I can work on this.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

Phil Harvey

#2
I've been going through my LIGOGPSINFO samples and I've got an algorithm that successfully decrypts the LIGOGPSINFO strings for most of them, but as you found the lat/lon/spd are still scaled somehow.

However, most of my samples are different than your example, and it isn't a simple Caesar cypher.  For example:

   c800008: 4c 49 47 4f 47 50 53 49 4e 46 4f 00 00 00 00 05 [LIGOGPSINFO.....]
   c800018: b4 00 00 00 23 23 23 23 65 00 00 00 c0 20 20 20 [....####e....   ]
   c800028: 20 f0 12 10 12 22 e9 0e 10 11 2f 99 10 11 01 f6 [ ...."..../.....]
   c800038: 12 18 10 22 b2 1a 10 07 f2 6e 18 13 21 f6 0e 10 [...".....n..!...]
   c800048: 13 21 b1 12 11 02 f3 76 18 11 22 f6 14 0c 13 28 [.!.....v.."....(]
   c800058: f7 12 14 13 28 4c 16 10 06 87 10 13 4b af 4c 09 [....(L......K.L.]
   c800068: 40 fe 58 18 01 20 f0 0e 10 10 20 4b 58 18 05 f2 [@.X.. .... KX...]
   c800078: 10 0c 10 20 78 10 5a 02 fd 0c 10 02 20 10 20 10 [... x.Z..... . .]
   c800088: 20 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 [ ...............]
   c800098: 00 00 00 00 00 00 00 00 23 23 23 23 65 00 00 00 [........####e...]
   c8000a8: c1 20 20 20 20 f0 12 10 12 22 e9 0e 10 11 2f 99 [.    ...."..../.]
   c8000b8: 10 11 01 f6 12 18 10 22 ba 1a 10 00 f2 6e 18 13 [.......".....n..]
   c8000c8: 21 f6 0e 10 13 21 b8 12 11 00 f3 76 18 11 22 f6 [!....!.....v..".]
   c8000d8: 14 0c 13 28 f9 12 14 11 27 4c 16 10 06 86 10 11 [...(....'L......]
   c8000e8: 4b af 4c 09 40 fe 58 18 01 20 f0 0e 10 10 20 4b [K.L.@.X.. .... K]
   c8000f8: 58 18 05 f2 10 0c 10 20 78 10 5a 02 fd 0c 10 02 [X...... x.Z.....]
   c800108: 20 10 20 10 20 00 00 00 00 00 00 00 00 00 00 00 [ . . ...........]
   c800118: 00 00 00 00 00 00 00 00 00 00 00 00 23 23 23 23 [............####]

Here is the algorithm I am using.  The function accepts the encrypted 0x84-byte record (starting with "####") and returns the decrypted string.

#------------------------------------------------------------------------------
# Decrypt LIGO GPS record
# Inputs: 0) GPS record incuding 8-byte header
# Returns: decrypted record including 4-byte uint32 header, or undef on error
sub DecryptLigoGPS($)
{
    my $in = shift;
    my $num = unpack('x4V',$in);
    return undef if $num < 4;
    my @in = unpack("x8C$num",$in);
    my @out;
    while (@in) {
        my $b = shift @in;
        my $steeringBits = $b & 0xe0;
        if ($steeringBits >= 0xc0) {
            return undef if @in < 4;
            push @out, (shift(@in) | $b & 0x01) ^ 0x20,
                       (shift(@in) | $b & 0x02) ^ 0x20,
                       (shift(@in) | $b & 0x0c) ^ 0x20,
                        shift(@in) ^ 0x20 | $b & 0x30;
        } elsif ($steeringBits >= 0x40) {
            return undef if @in < 3;
            if ($steeringBits == 0x40) {
                push @out, 0x20,
                           (shift(@in) | $b & 0x01) ^ 0x20,
                           (shift(@in) | $b & 0x06) ^ 0x20,
                           (shift(@in) | $b & 0x18) ^ 0x20;
            } elsif ($steeringBits == 0x60) {
                push @out, (shift(@in) | $b & 0x03) ^ 0x20,
                           0x20,
                           (shift(@in) | $b & 0x04) ^ 0x20,
                           (shift(@in) | $b & 0x18) ^ 0x20;
            } elsif ($steeringBits == 0x80) {
                push @out, (shift(@in) | $b & 0x03) ^ 0x20,
                           (shift(@in) | $b & 0x0c) ^ 0x20,
                           0x20,
                           (shift(@in) | $b & 0x10) ^ 0x20;
            } else {
                push @out, (shift(@in) | $b & 0x01) ^ 0x20,
                           (shift(@in) | $b & 0x06) ^ 0x20,
                           (shift(@in) | $b & 0x18) ^ 0x20,
                           0x20;
            }
        } elsif ($steeringBits == 0x00) {
            return undef if @in < 1;
            push @out, shift(@in) | $b & 0x13;
        } else {
            return undef;   # (shouldn't happen)
        }
    }
    return pack 'C*', @out;
}

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

Phil Harvey

#3
Ah.  Here we go.  I've figured out how to un-fuzz the lat/lon for one of my samples:

#------------------------------------------------------------------------------
# Un-do LIGOGPS fuzzing
# Inputs: 0) fuzzed latitude, 1) fuzzed longitude, 2) scale factor
# Returns: 0) latitude, 1) longitude
sub UnfuzzGPS($$$)
{
    my ($lat, $lon, $scl) = @_;
    my $lat2 = int($lat / 10) * 10;
    my $lon2 = int($lon / 10) * 10;
    return($lat2 + ($lon - $lon2) * $scl, $lon2 + ($lat - $lat2) * $scl);
}

For my sample, the scale factor was  1.5248551.  I need to test more samples to see if this is constant.

Also, the speed needed to be scaled by 1.85407333 to get km/h.

- Phil

Edit: The scaling factor for the coordinates was constant for most samples, but I've found one that is different (1.15368).
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).

Phil Harvey

#4
ExifTool 13.11 (just released) will decode LIGOGPSINFO records as in your example.

Aside: My understanding is that although this is a substitution cipher, it isn't specifically a Caesar cipher because the character mapping isn't a constant shift.  ExifTool determines the mapping for the digits by watching how the seconds advance in the date/time value, and syncing them to the first "2" in the year, accounting for the fact that they don't necessarily advance by 1 second each time because there are duplicate as well as missing timestamps.

- Phil
...where DIR is the name of a directory/folder containing the images.  On Mac/Linux/PowerShell, use single quotes (') instead of double quotes (") around arguments containing a dollar sign ($).