12

We noticed that our iOS app is sending requests to http://app-measurement.com. The body seems to be encrypted or compressed though

:method: POST
:scheme: https
:path: /a
:authority: app-measurement.com
accept: */*
content-type: application/x-www-form-urlencoded
content-encoding: gzip
accept-language: en-gb
content-length: 371
accept-encoding: br, gzip, deflate


 ;

_uwa

_pfoq


_oauto

_r

_c_fݶ- 
...

I already checked if it's gzip but that doesn't seem to be it.

Does anyone know how to decrypt this to be able to see the request content?

Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
hennes
  • 9,147
  • 4
  • 43
  • 63
  • This call is part of Google Analytics for Firebase to report the analytics events from the client. It is heavily compressed to minimize the bandwidth usage. I'm actually not sure if there's a way to see its raw contents, so hope someone else can answer. – Frank van Puffelen Jan 31 '19 at 14:24
  • Note that these requests ARE gzipped, but whatever you're using to intercept HTTP requests is already decompressing the body before showing it to you. What you're seeing is a raw protobuf, albeit, probably with character encoding idiosyncrasies that prevent you from actually decoding it as is. – Chris Sep 04 '21 at 20:17

2 Answers2

20

firebaser here

This request is part of Google Analytics for Firebase to report the analytics events from the client. It is heavily compressed to minimize the bandwidth usage.

There is no public API to see the raw contents of the request, but here is where you can find more on the data collected:

  1. The documentation on auto-collected events and auto collected user properties.
  2. The BigQuery schema is a pretty good representation of what we collect (although this also includes some fields which are added on the server, like location information and traffic source, in some cases).
  3. Device logs (e.g. logcat) expose the events/parameters we log.
Frank van Puffelen
  • 565,676
  • 79
  • 828
  • 807
  • 1
    Thanks, that sheds a little light on it. I upvoted but gonna hold off accepting a bit since this doesn't answer the decompression part. – hennes Jan 31 '19 at 17:08
  • I doubt anyone has a way to decompress. I checked with our product team, and even they have tools for that. It seems the data in the request is (close to a) raw protobuf. If you have more specific questions that are hard to discuss here, feel free to [reach out to Firebase support](https://firebase.google.com/support/contact/troubleshooting/) and mention my name. – Frank van Puffelen Feb 01 '19 at 20:45
  • You can certainly decompress and view the values in a raw protobuf! Of course the tough part is that most of the values are meaningless without context. But some things like strings have obvious meaning. See my answer below for how to actually decompress and decode these requests. :-D – Chris Sep 04 '21 at 20:15
6

It's a gzip compressed protocol buffer (protobuf). With decoding tools you can see all the values and types which can be useful. Though without the .proto definition it's a struggle to figure out what it all means.

To decode the request, first get it in the raw form. I do this by exporting a HTTP .trace file and extracting just the body. I've had better luck doing the gzip decompression myself.

Once you have the raw request body, decode it like this:

$ gunzip - < request_body > request_uncompressed.bin
$ protoc --decode_raw < request_uncompressed.bin

Here's a simple CyberChef formula that also decodes it for you: https://gchq.github.io/CyberChef/#recipe=Gunzip()Protobuf_Decode('',false,false)

When it works you'll see the raw protobuf values. They'll look something like this (actual values randomized):

1 {
  1: 1
  2 {
    1 {
      1: "_si"
      3: 161212808641
    }
    1 {
      1: "_et"
      3: 57
    }
    1 {
      1: "_sc"
      2: "SomeControllerName"
    }
    1 {
      1: "_o"
      2: "auto"
    }
    2: "_e"
    3: 161236824
    4: 163120534
  }
  2 {
    1 {
      1: "_si"
      3: 1358166110
    }
    1 {
      1: "_sc"
      2: "SomeControllerName"
    }
    1 {
      1: "_o"
      2: "auto"
    }
    2: "_ab"
    3: 161336826
    4: 163123680
  }
  3 {
    1: 163129524107
    2: "_fi"
    4: 1
  }
  3 {
    1: 15514295
    2: "_fot"
    4: 15514241
  }
  3 {
    1: 1530783276
    2: "_sid"
    4: 1530783376
  }
...
  8: "ios"
  9: "13.5"
  10: "iPhone12,3"
...

Update from @lari: Creating a custom protobuf definition to decode the requests

On Android you can enable verbose logging and see in device logs what Firebase Analytics sends to the servers in its original format. Here's an example:

FA-SVC com.google.android.gms V Uploading data. app, uncompressed size, data: com.my.app, 9332, 
batch {
  bundle {
    protocol_version: 1
    platform: android
    gmp_version: 46000
    config_version: 1679644809123456
    gmp_app_id: 1:123456789:android:aaaaaaaaaa
    app_id: com.my.app
    app_version: 1.0.0
    app_version_major: 100
    firebase_instance_id: xx_xxxx_xx
    upload_timestamp_millis: 1681470819289
    start_timestamp_millis: 1681468977430
    app_instance_id: f8s9fa09vsa4a4lk2983fsdf
    os_version: 9
    user_property {
      set_timestamp_millis: 1631520687985
      name: first_open_time(_fot)
      string_value: 
      int_value: 1631523600000
    }
    user_property {
      set_timestamp_millis: 1681468712345
      name: ga_session_id(_sid)
      string_value: 
      int_value: 1681468788
    }
    event {
      name: user_engagement(_e)
      timestamp_millis: 1681468977430
      previous_timestamp_millis: 1681468884057
      param {
        name: ga_event_origin(_o)
        string_value: auto
      }
      param {
        name: engagement_time_msec(_et)
        string_value: 
        int_value: 90654
      }
      param {
        name: ga_screen_class(_sc)
        string_value: MyViewController
      }
      param {
        name: ga_screen_id(_si)
        string_value: 
        int_value: -13918239812398123
      }
    }
  }
}

You can quite easily recreate parts of the .proto definition by comparing this to the encoded version (the one with numbers as keys).

For example:

  • the root message is called "batch"
  • "1" in batch is "bundle"
  • "2" in bundle is "event"
  • "2" in event is "name"

and so on...

Based on this, you can create a custom definition, for example:

// app-measurement.proto
syntax = "proto3";

package app_measurement;

message Bundle {
    message Event {
        string name = 2;
    }
    repeated Event event = 2;    
}

message Batch {
    repeated Bundle bundle = 1;
}

And use this to decode the message:

$ protoc --decode=app_measurement.Batch app-measurement.proto < request_uncompressed.bin

You will now see the identified parts replaced with key names from the .proto file while the rest stay as numbers, for example:

bundle {
  event {
    name: "_e"
    1 {
      1: "_et"
      3: 10856
    }
    1 {
      1: "_o"
      2: "auto"
    }
    3: 1680595820225
    4: 1680595807912
  }
...

If you need a compiled "descriptor" for some other tool, you can create it with the --descriptor_set_out= flag:

$ protoc --descriptor_set_out=app-measurement.desc app-measurement.proto

As you'll probably notice, Firebase Analytics also shortens the default event, parameter and user property names. E.g. _e = user_engagement and _o = ga_event_origin. The original names can be seen in the device logs on both Android and iOS.

I have created and published an open-source version of a protocol buffers definition for the app-measurement.com requests and shared it in GitHub: https://github.com/lari/firebase-ga4-app-measurement-protobuf

There's also a blog post with more details: https://larihaataja.com/firebase-ga4-app-measurement-com-calls/

Chris
  • 3,184
  • 4
  • 26
  • 24