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?
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
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
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).
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