Search code examples
iosfirebaseencryptionfirebase-analytics

How to decrypt Firebase requests to app-measurement.com


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?


Solution

  • 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/