Defeating web API integrity controls in Android

Introduction

In this post, I will be showing how I could reverse engineer an Android application to find API endpoints and how to issue requests to it by defeating integrity checks.

For legal reasons, I will be referencing the application being studied as "CASE_STUDY".

CASE_STUDY is a great tool for finding services near you. It is used worldwide and a lot of people use it in a day to day basis. Finding accounts on CASE_STUDY about specific people can reveal a lot of information about a person of interest. This will be our case study today because of the density of the code and the protections put in place.

Skills

  • Bypassing SSL Pinning
  • Debugging applications using Frida and making scripts for it
  • Rooting Android emulators
  • Identifying and understanding cryptography

Skills not covered

  • Installation of the tools required
  • Installation of the Android emulator
  • Debugging/fixing own tools

Disclaimer

This document is provided solely for academic and educational purposes. All reverse engineering and analysis were conducted on a publicly available Android application binary, in accordance with applicable reverse engineering exemptions for research and interoperability.

No unauthorized access was attempted. No vulnerability was exploited. All sensitive information — including cryptographic keys, authentication tokens, endpoint identifiers, and service names — has been anonymized or redacted to prevent misuse. The purpose of this work is to demonstrate general techniques for understanding client-side integrity controls and cryptographic enforcement mechanisms.

The application studied is referred to generically as “CASE_STUDY” to respect intellectual property and contractual boundaries. This work does not claim affiliation with, endorsement by, or criticism of any vendor or service. Any resemblance to a real product is incidental to the technical nature of the research.

If any party believes this work unintentionally discloses proprietary or sensitive implementation details, the author welcomes private communication for resolution.

First steps, looking at the app

The app allows users to find their contacts in CASE_STUDY, which can be abused to check large quantities of email addresses. I already knew this, and this was the reason I decided to research this topic.

To start off, make sure to install an emulator of a well-used API level. My choice is usually API 31 because its mostly still maintained while keeping it old enough for most tools to work out of the box. You might want to update all software on the AVD (the emulator), including Play Services. This is because we will be using them for logging in.

Wanting to abuse email lookups, our usual suspect would be anything related to our contacts. We can explore until we find the options under "More" section. The app seems to vary from region to region, but the endpoint I found is located under More>Notifications>Find Friends>Find in your contacts.

Now that we have located what actions trigger a contact lookup, we will proceed to try and intercept the requests made by the app to see where its reaching and what data is passed to it.

Rooting the AVD and its reasons

Usually we would simply set up a proxy and use BurpSuite or Wireshark, but this is a bit more complicated than that because of SSL Pinning.

SSL pinning is a technique to prevent MITM attacks by binding a specific SSL/TLS certificate to a particular server or service. It is also notorious for being an excellent anti-analysis technique. Though, modern tools have advanced and we already have easy ways to bypass this.

We will be using HTTP Toolkit. But first, we need to root the Android device to allow HTTP Toolkit to take care of the SSL Pinning.

To root the Android device, its as simple as downloading the rootAVD repo and following the instructions. We might need to install additional updates for Magisk, which is automatically installed and setup by rootAVD.

This tool will take care of all the processes needed to root an Android AVD. After its done running, we would simply "Cold boot" the AVD from Android Studio device manager.

assets/images/Android Studio Device Manager.png

We now would go into the newly installed Magisk app and install/update the required software. Its an easy process, so Im going to skip writing that.

Bypassing SSL Pinning

We should now have a working rooted AVD and HTTP Toolkit installed. Our first step now is allowing all root access in a non-blocking way, which can mess up with HTTP Toolkit. To do this, simply go to the Magisk app > Settings, and turn off "User Authentication" which would display a code-blocking pop up. After that, select "Superuser Access" and select "ADB only" which will only allow our external software to run with root. This also helps to avoid detection with apps that might check for root access.

Once this is done, we should run HTTP Toolkit.

assets/images/HTTP Toolkit Menu.png

My usual choice is "Android App via Frida". We will be using this, but since its prone to fail in newer devices I will show a quick overview of its alternative.

Alternative to "Android App via Frida"

Select Android Device via ADB. The process is fairly simple and will also bypass SSL pinning. The downside of this, is that it captures all system network traffic and also installs a VPN app (used only as proxy). This might trigger detection or problems identifying our wanted requests.

Bypassing SSL pinning the easy way

Select "Android App via Frida". Select the device, and select the app. Done. Now, all web requests will be intercepted seamlessly.

assets/images/HTTP Toolkit Logged Requests.png

Lets look at what we have. First, we want to only intercept traffic coming from the apps domain. So we put on the filter hostname$=CASE_STUDY.com

assets/images/HTTP Toolkit Sample Captured Request.png

Much better now. Now, lets repeat the contact lookup process.

assets/images/HTTP Toolkit Before Capturing Important Request.png
assets/images/HTTP Toolkit After Capturing Important Requests.png

Assuming weve added an email address we want to look up to our contacts. Im using "test@gmail.com".

Before clicking "Find in your contacts", Ive cleared the traffic history. Now, we only have 2 requests to analyse.

The first one is made to the bunsen.CASE_STUDY.com domain, containing these headers:
assets/images/Request 1.png

The next one is made to REDACTED-api.CASE_STUDY.com, and looks like this:
assets/images/Request 2.png
I think its obvious now which one of these two is doing the lookup.

Analising test results

We have now found a weird looking request to CASE_STUDY which is in fact in charge of returning user data.
Lets analyse this from top to bottom, starting from the URL.

URL and GET parameters

  • Protocol: HTTPS
  • Domain: REDACTED-api.CASE_STUDY.com
  • URI: /user/friend_finder_v2

Note: The following table contains my theories at this moment of the analysis, which by no means are yet fully correct.

Parameter Theory Observations Value
time Unix time in seconds N/A 1752058603
nonce Some kind of code to use as salt URL encoded and Base64 encoded tMTU6Q%3D%3D
ywsid None, unknown Repeats in different requests TLD_YWSID
device_type Just a device type Doesnt need much attention for now google%2Bemulator64_x86_64_arm64%2FSE1A.211212.001.B1
app_version App version N/A 25.26.0-28252600
cc Country code N/A US
lang Language N/A en
efs Account information/authentication? URL + Base64 encoded, but doesnt return a string i8m%2F6CTloWKb%2BwCGwHUL%2BjZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv%2Bwgq99BiHlGG132XjtQZ06wAME80Z4%3D
signature Integrity check for the request N/A _8QSgm7K9sRY1jMTVm82KH4JXdW0%3D

Headers

We see the payload is sent in x-www-form-urlencoded format. The cookies are often irrelevant and can be skipped, which is the case too. The user agent is handled by the app too. The X headers are the following:

Header Theory Observations Data
X-Auth-Token-2 Session token Used to authenticate requests REDACTED
X-Bunsen-MAM Telemetry JSON format {"hardware_model":"sdk_gphone64_x86_64,Google,ranchu","local_timezone_offset":"+00:00","unique_view_id":"4aa74903-fe60-REDACTED-047ecf47eb80","previous_unique_view_id":"932b92ad-00ed-4ecf-8422-388f4254632f","user_advertising_id":"00000000-0000-0000-0000-000000000000","session_id":"30877930-REDACTED-c688fa959650"}
X-Foregrounded Are we running in foreground? N/A true
X-Screen-Scale Screen size Maybe inches 2.625
  • The "X-Auth-Token" is assigned by the server, and can be gathered anytime by capturing a logged in request.
  • The "X-Bunsen-MAM" looks like telemetry or tracking information. We need to find out how its crafted.
  • The "X-Foregrounded" header seems straightforward, we just assume its always true.
  • The same goes for the "X-Screen-Scale". There is no reason to investigate further.

Test conclusions

Weve figured how the requests work, but we need to find out how some of the parameters are crafted.

Here is a list of the data we need to gather:

  • nonce
  • ywsid
  • efs
  • signature
  • X-Bunsen-MAM

Finding the code

Now that weve gathered some basic information, its time to find out how it all works from the inside. Reverse engineering experience as well as some Java and Android experience are recommended.

Now, we will be using JADX-GUI for reverse engineering the code. JADX works like Ghidra, IDA, Cutter, etc. but for Java and Android applications. Its a great choice because of its stability and ease of use. It also supports a variety of plugins which can really help with the tasks.

Before just opening the APK, we will first need to extract it or get a copy. There are a lot of ways to do it, but my preferred one is downloading it on my personal phone, sharing it through any file-sharing service and download it in my main workstation.

Once we have the APK file at hand, we can start getting our hands dirty.

Well fire up Jadx and load the APK file. It might take a while. After it finishes loading, I also recommend selecting Tools > Decompile all classes. This will also take a while, but will make things faster in the long run as well as fixing some decompilation errors. Once done, save it to a project file.

assets/images/JADX Menu.png

There is a lot of things to look at, so lets not get nervous. My usual approach is to find a string to look for in the code, then moving from there. A strong election would be the endpoint, which in this case is /user/friend_finder_v2. We will keep removing characters from the beginning until we find what we want.

assets/images/JADX Search Box.png

Which points here:
assets/images/Code sample.png

Fortunately for us, JADX renames variables and classes to a better name choosing automatically.
Lets dig into the FriendFinderRequest class.
assets/images/API code sample.png
We got redirected to ApiV1NetworkingRequest. Might be some issues with Jadx renamings, but lets stay here and read the code to see if we find something interesting.

Indeed, we see things that start making sense:
assets/images/API Code Found Parameters.png

This is worth reverse engineering. Lets work a bit on the function to have a better understanding of it.
After some superficial work on the variables, it looks like this:
assets/images/Sample code with the EFS calculation details.png

First of all, lets bookmark this class. Second, lets dig into the efs calculation.
assets/images/Exact code that calls the EFS generation.png

Were passing CASE_STUDYQuery.d to the URL encoder before adding it to the httpStrBuilder, which will be used for making the request. It is not clear what this first variable is for, even by looking at the code:
assets/images/Code sample 2.png
So lets now dig into the CASE_STUDYQuery.a function.
assets/images/Sample code 2.png

Defeating cryptography

It iterates through a map and is passed to APIUtil.a before returning. Lets see what it is.
assets/images/API encryption utils code.png
An overall explanation of what this function does is:

  1. Take a Map
  2. Create a 500 character string builder object
  3. Cast to string and append each element into the new string builder
  4. Encrypt the string.

We also have some logs that hint us what were looking to. We now know the EFS is encrypted, lets dig a bit more into this function. Remember to bookmark interesting functions.

Lets see what kind of encryption were dealing with. Lets see the line Cipher cipher = Cipher.getInstance(FileEncryptionUtil.CIPHER_TRANSFORMATION);. If we follow the CIPHER_TRANSFORMATION we will be directed into the encryption library.
assets/images/Encryption specifications.png
So we know we are dealing we AES encryption, using CBC mode and PKCS5 padding. Also, were using UTF-8 encoding for the text. The IV is 16 bytes long.

Dealing with AES encryption, both the server and the client should know the encryption key. This leaves the code with 2 options:

  1. The server gives us the means to calculate the key and then use it at runtime (we assume not, since we have looked at the web requests already)
  2. The key is somewhere in the code.

Lets try and find the key.

After the efs string is crafted, its passed to the cipher object, which first requires additional initialization. Lets see the line:

aesCipher.init(1, new SecretKeySpec(new BigInteger(CASE_STUDYWebStrings.a(CASE_STUDYWebStrings.l), 16).toByteArray(), "AES"), new IvParameterSpec(new byte[16]));

Looking at the Java docs, we can see SecretKeySpec accepts either 2 or 4 parameters. Since we are using 2, we know which overload is it.
The first parameter new BigInteger(CASE_STUDYWebStrings.a(CASE_STUDYWebStrings.l), 16).toByteArray() is the key used. The second parameter is the algorithm.
There is a lot of processing done here, so lets make a step by step breakdown

  1. CASE_STUDYWebStrings.a method loads the CASE_STUDYWebStrings.l variable.
  2. The result is then converted into a big integer
  3. The result is then converted into a byte array.

Lets see what the CASE_STUDYWebStrings class is for:
assets/images/Found obfuscated strings.png

Might look bad, but don't get scared. We hit the sweet spot!

The code brings no obfuscation whatsoever, and this looks manually obfuscated. So this must be sensitive. This obfuscation is actually really easy to defeat and was probably implemented to avoid searching for this strings.

Just by looking at the variables they look like reversed base64 encoded strings. This is confirmed by the method CASE_STUDYWebStrings.a which we will rename to "`decodeStr"
assets/images/Decode from reversed base64.png

We can only assume that Base64Coder2.a is actually what decodes base64, so lets rename that too.
assets/images/Decode from reverse base64 after variable rename.png

Lets get back to what we were looking for: CASE_STUDYWebStrings.l. Lets rename that to AES_KEY. To see what the key is, we dont need any fancy coding. Lets just open CyberChef and do our thing there.

We first clean the string:
assets/images/CyberChef 1.png
The decode it:
assets/images/CyberChef 2.png
Which leaves us with the key: AES_KEY.
The IV would now be an empty 16 byte array, so theres nothing really we should do.

So, after all the information we have on the EFS, we can craft a graph to understand it better:
assets/images/EFS encryption flowchart.png
Following this process in reverse will give us the original string. Lets try!
assets/images/Decryption demonstration.png

We got it!

Finishing the EFS

We still have to find out what those parameters are. Lets look for the string "y_device" in Jadx:
assets/images/Search for y_device.png

This one looks like a nice candidate, since we have a clear view of what it does:
assets/images/Found candidate for y_device.png

This is the class DeviceInfo3, which actually gives us plenty of information:
assets/images/Code of y_device assignment.png
It gives us nice sources for the ywsid, the device, y_device, etc. Lets bookmark it.

y_device is assigned earlier in the code:
assets/images/Origin of y_device.png
Lets see what this function does:
assets/images/y_device generation function.png
Its a very simple code snippet. The y_device parameters is actually referenced as KEY_Y_DEVICE_ID in the code. This function checks if its set somewhere in the app settings (we dont really care a lot about that part), but we see how its generated. Its just a random UUID which is prepended with a "y_".

Lets find out the device parameter, which is also set earlier in the code:
assets/images/device origin shown in the code.png

This code is not very easy to understand if you dont have experience with Android:
assets/images/device checks.png
What this does is get your unique Android ID. It then checks if something has gone wrong or the ID is specifically 9774d56d682e549c, which is a magic number. This means it is not unique and there are more devices with this Android ID, such as emulators or privacy focused versions.

Knowing that its an Android ID, we know that the device variable is simply a random 16 character string.

The code that executes if we have a magic ID is a fallback mechanism to set a new trackable ID for the installation, so we dont really care about it.

We now know how to generate an EFS from scratch.
assets/images/EFS content generation flowchart.png

Finding the ywsid

We have already found a way to generate an arbitrary EFS. We now need to find the rest of the parameters used in the signature calculation before proceeding, since they will be used in the process.

Lets continue digging into the previous code since we saw something about the ywsid:
assets/images/Back to check for the ywsid.png
We can see it is assigned in the constructor:
assets/images/ywsid is assigned in the constructor.png
Following XREFs, we can see its called by an upper class:
assets/images/ywsid XREFs.png
assets/images/Only XREF for the ywsid.png
We can safely rename "str" to ywsid since its confirmed it is that. Again, lets do another XREF search for the constructor. We see there are two results:
assets/images/XREF for the constructor.png
Notice the second result calls an "invoke" method. This usually means its a synthetic class. Synthetic classes are "made up" by the Java/Kotlin compiler to make things such as lambdas, anonymous classes or other behind-the-scenes code structures work. This can result in unpredictable and confusing decompilation results, so we should avoid it unless its strictly necessary. Lets just use the first one.
assets/images/First XREF result.png
The code seems pretty straightforward. Lets rename some of the variables first for readability. Notice I renamed CASE_STUDYWebStrings.b to CASE_STUDYWebStrings.ywsid, which could be easily deduced by the codeflow.
assets/images/Codeflow sample.png
We know its in the CASE_STUDYWebStrings class, lets take a look at that again.
assets/images/WebStrings.png
The code shows us there are two possible values for the ywsid. We already know one of the options by the request weve captured, but lets see which criteria it needs to meet. After some work on the class, it looks like this:
assets/images/WebStrings reverse engineering.png
After some research on the code, we find out that the ywsid is actually static. It does try to check if we are in a subdomain, but the value is never changed so it will always use the TLD_YWSID.

We conclude the ywsid is: TLD_YWSID

Finding the time and nonce

These two we have seen them before in an earlier stage of our research. Lets get back to the ApiV1NetworkingRequest class.
assets/images/Time calculation.png
The time and nonce are very easy to make.
The time parameter is the UNIX time in seconds, since we can see it takes milliseconds and divides it by 1000.
assets/images/Pasted image 20250710151717.png

In the other hand, the nonce is just a base64 encoded random 4 byte value. Done too.

Defeating HMACs

We will now dig into calculating the signature. Staying in the same class as before, we can see where its made, right after the efs:
assets/images/Signature code sample.png
We can see the signature is actually crafted in the try-catch statement. Lets work it out from there and rename variables accordingly.
assets/images/Signature HMAC calculation.png
We can see the signature is calculated here. Its an HMAC using SHA1 algorithm. Looking at the code, we can see how it was made:
assets/images/Signature calculation flow chart.png
Now we only need to find how the URI is made. The code is very dense and contains a lot of unknown information.

Logging information with Frida

If we were on a Windows or Linux based system, we would simply debug it using a tool such as x64dbg, and set breakpoints wherever we want. When working with Android applications, this is a bit different. This is why we will be using Frida for this task.

"Frida lets you inject snippets of JavaScript or your own library into native apps on Windows, macOS, GNU/Linux, iOS, watchOS, tvOS, Android, FreeBSD, and QNX."

We dont really know what is being passed to the HMAC calculation, so lets hook and print the contents of all the steps in the code. By logging StringBuilder operations, we can see what is going on.

Before digging further, we need to keep in mind that when renaming variables inside of JADX, the names on the original APK havent been really changed. This is why we need to export the mappings from JADX to hook the specific code snippets. In this case, the class ApiV1NetworkingRequest was originally com.CASE_STUDY.android.g11.e. With this mappings, we can manually translate our reverse engineered code to the original names. To save the mappings, you can go in JADX and select File > Save mappings as... and select the one of our choosing. Usually, ProGuard is a good choice, since its widely used and human readable. To find the names, just open the mappings in a text editor and look for the renamed function. You will easily find the original name.

Here are the current project mappings:

com.CASE_STUDY.android.bp1.a -> com.CASE_STUDY.android.bp1.a:
    java.lang.Object a -> subdomain
    java.lang.String a() -> getFQDN
com.CASE_STUDY.android.et0.w -> com.CASE_STUDY.android.et0.HomeBodyComponentFactory:
com.CASE_STUDY.android.g11.e -> com.CASE_STUDY.android.g11.e:
    com.CASE_STUDY.android.td1.e j -> CASE_STUDYQuery
com.CASE_STUDY.android.td1.a -> com.CASE_STUDY.android.td1.Base64Coder:
    byte[] a(java.lang.String) -> decode
    char[] b(byte[]) -> encode
com.CASE_STUDY.android.td1.e -> com.CASE_STUDY.android.td1.e:
    java.security.SecureRandom g -> secureRandom
    java.util.HashSet i -> deviceInfo
com.CASE_STUDY.android.td1.f -> com.CASE_STUDY.android.td1.f:
    java.lang.String a -> REDACTEDapi
    java.lang.String b -> ywsid
    java.lang.String c -> hmacSecret
    com.CASE_STUDY.android.bp1.a e -> errorLogInfo
    java.lang.String f -> TLD_YWSID
    java.lang.String g -> TLD_HMAC_SECRET
    java.lang.String i -> SUBDOMAIN_YWSID
    java.lang.String l -> AES_KEY
    java.lang.String m -> BIZ_ACTIVITY_NAME
    java.lang.String o -> packageName
    java.lang.String a(java.lang.String) -> decodeStr
com.CASE_STUDY.android.x3.a -> com.CASE_STUDY.android.x3.Concatenate:
    java.lang.String a(java.lang.String,java.lang.String) -> concatenate
com.CASE_STUDY.android.xo1.a -> com.CASE_STUDY.android.xo1.a:
    java.lang.String a(java.util.Map) -> encryptEfs
com.CASE_STUDY.android.xt1.k -> com.CASE_STUDY.android.xt1.k:
    void h(java.lang.Object,java.lang.String) -> checkParamIsSet
com.CASE_STUDY.android.yt.h -> com.CASE_STUDY.android.yt.h:
    android.content.Context a -> context
    java.lang.String b -> device
    java.lang.String c -> y_device
    android.content.SharedPreferences i -> sharedPreferences
    android.content.SharedPreferences c() -> getSharedPreferences
    java.lang.String d() -> getYDevice

Using this, we can craft the Frida script. It will look something like this:

Java.perform(function () {
  const StringBuilder = Java.use("java.lang.StringBuilder");
  const TargetClass = Java.use("com.CASE_STUDY.android.g11.e");
  let isInSignRequest = false;

  TargetClass.i.overload("java.lang.String").implementation = function (uri) {
    console.log("\n=== [*] signRequest (i) called with: " + uri + " ===");
    isInSignRequest = true;
    const result = this.i(uri);
    isInSignRequest = false;
    console.log("=== [*] signRequest (i) finished ===\n");
    return result;
  };

  function safeLog(prefix, value) {
    if (!isInSignRequest) return;
    if (typeof value === "string") {
      const aesPattern = /\bAES\b|\baes-128-cbc\b|\bAES\/CBC\/PKCS5Padding\b/i;
      if (aesPattern.test(value)) {
        return;
      }
    }
    console.log(prefix + value);
  }

  StringBuilder.append.overload("java.lang.String").implementation = function (
    str
  ) {
    safeLog("[StringBuilder.append(String)] -> ", str);
    return this.append(str);
  };

  StringBuilder.append.overload("java.lang.Object").implementation = function (
    obj
  ) {
    safeLog("[StringBuilder.append(Object)] -> ", obj);
    return this.append(obj);
  };

  StringBuilder.append.overload("char").implementation = function (c) {
    safeLog("[StringBuilder.append(char)] -> ", c);
    return this.append(c);
  };

  StringBuilder.toString.implementation = function () {
    const result = this.toString();
    safeLog("[StringBuilder.toString()] => ", result);
    return result;
  };

  StringBuilder.$init.overload("int").implementation = function (capacity) {
    safeLog("[StringBuilder constructor] capacity: ", capacity);
    return this.$init(capacity);
  };
});

console.log("StringBuilder request logging script loaded successfully.");

Now, we need to install the Frida server into the Android device. Given were using an AVD, were using the x86_64 architecture. We can find the appropriate server in the Frida release page with our architecture. After downloading it, we can upload it to the AVD using ADB.

adb push frida-server /data/local/tmp/.

Then get into a shell and set up the permissions:

adb shell
$ su
# chown root:root /data/local/tmp/frida-server
# chmod 0 /data/local/tmp/frida-server
# chmod +rx /data/local/tmp/frida-server

And execute it:

# /data/local/tmp/frida-server

It will produce no output. Dont worry about this. Now, we can start playing around with Frida. Make sure to have Frida installed in your system too.

Lets find our script and run it. Make sure to open up the app first in the AVD or else it wont work.

PS> frida -U -n CASE_STUDY -l .\logStringBuilderRequest.js

assets/images/Pasted image 20250710164645.png

Now navigate normally to the found endpoint in the app, which will leave us with the following output:

=== [*] signRequest (i) called with: user/friend_finder_v2 ===
[StringBuilder constructor] capacity: 500
[StringBuilder constructor] capacity: 500
[StringBuilder.toString()] => device=c881fee26fb5f768&y_device=y_f8b6fc9c-f44f-4e50-b6b6-c5e0a5e67687
[StringBuilder constructor] capacity: 500
[StringBuilder.toString()] => device=c881fee26fb5f768&y_device=y_f8b6fc9c-f44f-4e50-b6b6-c5e0a5e67687
[StringBuilder.toString()] => ywsid=TLD_YWSID
[StringBuilder.toString()] => cc=US
[StringBuilder.toString()] => efs=i8m/6CTloWKb+wCGwHUL+jZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv+wgq99BiHlGG132XjtQZ06wAME80Z4=
[StringBuilder.toString()] => app_version=25.26.0-28252600
[StringBuilder.toString()] => device_type=google+emulator64_x86_64_arm64/SE1A.211212.001.B1
[StringBuilder.toString()] => time=1752158836
[StringBuilder.toString()] => lang=en
[StringBuilder.toString()] => nonce=3Z/0mQ==
[StringBuilder constructor] capacity: 270
[StringBuilder constructor] capacity: 21
[StringBuilder.toString()] => user/friend_finder_v2
[StringBuilder.toString()] => /user/friend_finder_v2app_version=25.26.0-28252600cc=USdevice_type=google+emulator64_x86_64_arm64/SE1A.211212.001.B1efs=i8m/6CTloWKb+wCGwHUL+jZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv+wgq99BiHlGG132XjtQZ06wAME80Z4=lang=ennonce=3Z/0mQ==time=1752158836ywsid=TLD_YWSID
[StringBuilder.toString()] => _Ld/qvLwSEblnHUxXyaTD2MDkVMQ=
[StringBuilder.toString()] => time=1752158836&nonce=3Z%2F0mQ%3D%3D&ywsid=TLD_YWSID&device_type=google%2Bemulator64_x86_64_arm64%2FSE1A.211212.001.B1&app_version=25.26.0-28252600&cc=US&lang=en&efs=i8m%2F6CTloWKb%2BwCGwHUL%2BjZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv%2Bwgq99BiHlGG132XjtQZ06wAME80Z4%3D&signature=_Ld%2FqvLwSEblnHUxXyaTD2MDkVMQ%3D
[StringBuilder.toString()] => https://REDACTED-api.CASE_STUDY.com/user/friend_finder_v2?time=1752158836&nonce=3Z%2F0mQ%3D%3D&ywsid=TLD_YWSID&device_type=google%2Bemulator64_x86_64_arm64%2FSE1A.211212.001.B1&app_version=25.26.0-28252600&cc=US&lang=en&efs=i8m%2F6CTloWKb%2BwCGwHUL%2BjZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv%2Bwgq99BiHlGG132XjtQZ06wAME80Z4%3D&signature=_Ld%2FqvLwSEblnHUxXyaTD2MDkVMQ%3D
=== [*] signRequest (i) finished ===

There is a lot of information that also verifies the other parameters.

Lets focus on these specific 3 lines:

[StringBuilder.toString()] => /user/friend_finder_v2app_version=25.26.0-28252600cc=USdevice_type=google+emulator64_x86_64_arm64/SE1A.211212.001.B1efs=i8m/6CTloWKb+wCGwHUL+jZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv+wgq99BiHlGG132XjtQZ06wAME80Z4=lang=ennonce=3Z/0mQ==time=1752158836ywsid=TLD_YWSID

[StringBuilder.toString()] => _Ld/qvLwSEblnHUxXyaTD2MDkVMQ=

[StringBuilder.toString()] => time=1752158836&nonce=3Z%2F0mQ%3D%3D&ywsid=TLD_YWSID&device_type=google%2Bemulator64_x86_64_arm64%2FSE1A.211212.001.B1&app_version=25.26.0-28252600&cc=US&lang=en&efs=i8m%2F6CTloWKb%2BwCGwHUL%2BjZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv%2Bwgq99BiHlGG132XjtQZ06wAME80Z4%3D&signature=_Ld%2FqvLwSEblnHUxXyaTD2MDkVMQ%3D

The first log is the string to be passed for the HMAC function. Notice there is no domain at the beginning or ampersands to separate the GET parameters. We now know what the content looks like before being signed.

The second parameter is the signature. It is prepended with an underscore and is Base64 encoded, which we already knew.

The last string is actually the signed URI, which contains ampersands and URL encoded parameters. This is because the first string is a copy of the last, and was processed specifically for the calculation of the signature.

Looking up, we see the parameters used:

[StringBuilder.toString()] => ywsid=TLD_YWSID
[StringBuilder.toString()] => cc=US
[StringBuilder.toString()] => efs=i8m/6CTloWKb+wCGwHUL+jZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv+wgq99BiHlGG132XjtQZ06wAME80Z4=
[StringBuilder.toString()] => app_version=25.26.0-28252600
[StringBuilder.toString()] => device_type=google+emulator64_x86_64_arm64/SE1A.211212.001.B1
[StringBuilder.toString()] => time=1752159053
[StringBuilder.toString()] => lang=en
[StringBuilder.toString()] => nonce=nK+rPA==

The previous lines were used for the EFS calculation as we saw earlier.

Matching this information with the code, we find out that the URL is not a string, but a dictionary that contains data from the URL for the signature. Before the URL is parsed to the signature function, it first sorts itself to then be casted to a string with NO ampersands to separate the GET parameters.

This is the final GET parameter to reverse engineer.

Finding the X-Bunsen-MAM headers

As usual, we will begin by searching the plain text of the header.
assets/images/Bunsen header search.png
The second one matches the capitalization of the string, so lets choose it.
assets/images/Bunsen search result.png
We see that the bunsen header is located in the state object, which is an instance of the BizWebViewContract3.c class. We infer the names of the properties and rename them:
assets/images/Reversed bunsen header code.png

Now, lets dig into the BizWebViewContract3.c class:
assets/images/More bunsen related code.png
This is the constructor, which shows that the bunsen headers are set through it. Lets now look for XREFs:
assets/images/Bunsen constructor XREFs.png
We see two results, but none of them seems to be the better option. After digging into them, I chose the first one which seemed a bit easier:
assets/images/XREF search first result.png
This code is actually very confusing as of now, so we should think of an alternative. A nice approach would be to search for the parameters of the bunsen JSON string individually, and go upwards from there. Lets search for the unique_view_id parameter:
assets/images/Search unique_view_id.png
The second one looks way nicer, lets check:
assets/images/Second result for unique_view_id.png
If we cross reference this with the JSON contents of the saved request, we can see it has the same parameters. Lets break down the Bunsen header parameters.

Parameter Description Observations Value
hardware_model Type of phone Can be reused sdk_gphone64_x86_64,Google,ranchu
local_timezone_offset Timezone Can be reused +00:00
unique_view_id 4aa74903-fe60-40aa-877f-047ecf47eb80
previous_unique_view_id 932b92ad-00ed-4ecf-8422-388f4254632f
user_advertising_id Telemetry Static 00000000-0000-0000-0000-000000000000
session_id 30877930-748a-4765-b836-c688fa959650

Lets find out how they are crafted.

Session ID

The session ID is passed in the line new Tuples("session_id", this.sessionId). Lets find what writes into this.sessionId. The XREFs are the following:
assets/images/Search for the session ID.png
Well use the last one because its the most clear one.
assets/images/Best result for the session ID search.png
The code is pretty dense, well need to work on it first. After some work it gets like this:
assets/images/After reversing.png
If we track up, this.sessionId is assigned the value of sessionId2, which is assigned session2.sessionId. If we track where session2 is made, we end up earlier in the code:
assets/images/Session generation.png
I could assign that method the name getCurrentSession because of the type and use it was being given. If the method is used to get a value of type Session and then used later in the code, the origin method is probably a getter. Lets dive in real quick:
assets/images/SessionsTracker code.png
Even though I already renamed and worked out the code, it was very easy and quick to do it because of the strings gave me the hints necessary. Notice how the code can return null if some time conditions are met. The code does not generate the session ID. Instead, it verifies it and returns it.
So we rename it and get back the caller code:
assets/images/Sesosion ID generation caller.png
Heres another variable being assigned. Lets dig into the function:
assets/images/Session ID generation.png
Remember earlier in this post where I mentioned synthetic classes? Well, this is an example.

Even when the code is very simple, it has some differences with the usual class structure. For example, it does not bring a constructor like other classes do.

This time we got lucky it was just this simple code, which tells us the sessionId is actually just a random UUID.

Unique view ID

To find the unique_view_id and the previous_unique_view_id we need to find how they are generated. From their names we can guess theyre made in the same part of the code. Lets get back to the Bunsen class where the bunsen headers were made.assets/images/unique_view_id source.png
Lets XREF the unique view ID to see where it comes from:
assets/images/View ID XREFs.png
Lets use the last one because it looks nicer:
assets/images/Best result for the XREF.png
Well, this was pretty straightforward. The bunsen.uniqueViewId was assigned string which was a assigned a random UUID.

From this we can guess that the previous_unique_view_id is also a random UUID. There is no need to work harder on this because we already know what we wanted.

We wont be digging into the advertising ID because it is set to zeroes. I really doubt that was randomly generated, so if its static there is no need to change it.

Writing down the results

Now that we have reverse engineered everything we needed, we should write down the results of the analysis:

GET parameters

Parameter Description Done Note Example
time Unix time Yes 1751368312
nonce Random 4 bytes Yes Base64 then URL encoded 53NLTA%3D%3D
ywsid Some kind of ID Yes Static TLD_YWSID
device_type Device name N/A App specific probably google%2Bemulator64_x86_64_arm64%2FSE1A.211212.001.B1
app_version Version N/A Should be latest 25.26.0-28252600
cc Country code N/A Just use whatever US
lang Language N/A en
efs Used in signature Yes URL and Base64 encoded i8m%2F6CTloWKb%2BwCGwHUL%2BjZ2hunIyXWs0pSBHhCCmKxwCX23HempE4MaITvDog4oDvSnUKw1qRntv%2Bwgq99BiHlGG132XjtQZ06wAME80Z4%3D
signature URI checksum Yes URL and Base64 encoded (prepended with a "_") _%2BGYw3yNYcgt%2FERficxDPX82c1HY%3D

Notes

The EFS contents are composed of:

  • The Android ID, which is a 16 character fingerprint of the Android device being used.
    • Pseudo code: toHex(random(16))
  • The y_device UUID value.
    • Pseudo code: "y_" + randomUUID().
  • A raw, unencrypted EFS would look like this:
    • device=c881fee26fb5f768&y_device=y_f8b6fc9c-f44f-4e50-b6b6-c5e0a5e67687

Headers

Header Description Done Mandatory Note Example
Accept-Encoding N/A N/A Yes Standard gzip
Connection N/A N/A Yes Standard Keep-Alive
Content-Length N/A N/A Yes Standard 37
Content-Type N/A N/A Yes Standard application/x-www-form-urlencoded
Cookie N/A N/A No Set in the response `datadome=6FjERKGvSDXIWZYpccgOiFCELelFkmHGFlNYqn9KLMmtLp2cKNxKFtrphtxeeOOWc9WFW8z_~Y0tK7ruN0pPq3YizT65A25ipkLXUF4jJCSVLfwqDHeCYafJ3wd1iLkn; bse=a039cb6a296e4b1f8485635ec6708206; wdi=2\ 8CBA30BA6973AB68\ 0x1.a1647e87e6877p+30\ c306da7ca2fc4c4c`
Host N/A N/A Yes Static REDACTED-api.CASE_STUDY.com
User-Agent N/A N/A Yes Custom Version/1 CASE_STUDY/v25.26.0-28252600 Carrier/T-Mobile Model/emulator64_x86_64_arm64 OSBuild/SE1A.211212.001.B1 Android/12
X-Auth-Token-2 Auth token assigned at login No Yes Dynamic REDACTED
X-Bunsen-MAM Advertising data Yes Yes {"hardware_model":"sdk_gphone64_x86_64,Google,ranchu","local_timezone_offset":"+00:00","unique_view_id":"814404d6-7055-4668-bf10-7cf68834ab4e","previous_unique_view_id":"ccedb3ff-57ad-4a7f-8964-fd38020d4899","user_advertising_id":"00000000-0000-0000-0000-000000000000","session_id":"659eb376-9a71-4650-8290-09ff4b240bc1"}
X-Foregrounded Is in foreground N/A Yes Static true
X-Screen-Scale Screen size N/A Yes Static 2.625

POST data

Key Description Done Value
emails Raw, comma separated Yes test@gmail.com
ignored Raw, static Yes false

Example POST payload: emails=test@gmail.com,test@yahoo.com&ignored=false

Wrapping up

We have reverse engineered all the code necessary to defeat integrity controls, including cryptography and telemetry checks.

By no means I encourage the misuse of this research. As previously stated in the disclaimer, this post serves only as an educational and research resource.