Noam's Blog

Noam's Blog

Navigate back to the homepage

Whack-A-Mole: Reverse Engineering the 'Wie is de Mol?' App

Noam Drong
January 2nd, 2021 · 5 min read

Wie is de Mol?

Wie is de Mol? (‘Who is the Mole?’) is a popular Dutch television game show, currently airing its 21st season. Contestants compete in challenges to win money that goes into a shared prize pool. One of the contestants acts as a mole, attempting to sabotage the other contestants in their efforts to win challenges. The identity of the mole is unknown and it is up to the contestants to determine who it might be. Every episode a contestant leaves the show, until the final contestant leaves with the money collected during the season.

For the last couple of years, my colleagues have been competing in a group in the Wie is de Mol? app, available for iOS and Android. The app allows users to place a bet (using free digital tokens, there are no payments involved as far as I know) on the persons who they deem most likely to be the mole.

When I received an invitation from a colleague for this year’s group, I couldn’t resist to take a look inside the Android app and its accompying API to see whether I could dig up any interesting vulnerabilities. I should note that I first made sure that the broadcasting network, AVRO, has a vulnerability disclosure policy in place that allows such research. They appear to welcome security research, so off we go!

Preface

My goal for this post is to share my process in tackling challenges that may occur while reverse engineering an Android application in a black box setting, therefore I won’t delve into too much detail on the usage of any of the tools listed below. Having a basic understanding of Android and web applications is recommended.

Tools

Application       Usage
ApktoolUsed to disassemble the Android application and to inject Frida Gadget as a native library
FridaUsed for dynamic instrumentation at runtime
Burp SuiteUsed to intercept traffic between the Android app and its backend
CyberChefUsed for quick encoding/decoding/hashing/etc.

For information on how to inject Frida Gadget into the apk, please take a look at one of the many tutorials out there, such as this one or this one.

Intercepting HTTP Traffic

Frida enables us to override common certificate pinning implementations, so we can intercept traffic using a proxy of our choosing (in this case, Burp Suite). After setting up the environment, launch the Android application and interact with it to generate some traffic. Let’s start out by pressing the ‘I forgot my password’ button, resulting in the following request and response:

Request

1POST /profile/forgot_password/ HTTP/1.1
2Content-Type: application/json; charset=UTF-8
3Content-Length: 26
4Host: api.wieisdemol.nl
5Connection: close
6Accept-Encoding: gzip, deflate
7User-Agent: okhttp/4.7.2
8X-Signature: HMAC widm-api:UaXaUB6YCKY2AeeuFk00K/Cj5O5cB77NivxtWFT03iU=
9Date: 2021-01-01T23:38:50.529
10
11{"email":"***REDACTED***"}

Response

1HTTP/1.1 400 Bad Request
2Server: nginx/1.19.5
3Date: Fri, 01 Jan 2021 22:38:50 GMT
4Content-Type: application/json
5Content-Length: 131
6x-config-version: 9
7Via: 1.1 google
8Alt-Svc: clear
9Connection: close
10
11{"detail":{"email":["Geen e-mailaccount gevonden.\nMisschien was je ingelogd met een social account (bijv. Facebook of Google)?"]}}

Notice the X-Signature header in the request: this header provides a HMAC signature which is verified by the backend, so as to ensure the integrity of the message. Sending a request with an invalid signature, or with no signature at all, results in a 403 response code:

Image showing a HTTP 403 response as a result of a request with an invalid signature.
Response based on a request with an invalid signature.

This makes modifying requests a bit harder, because we will need to resign the message before sending it to the API. Fortunately, we have the application’s decompiled bytecode at hand to take a look at what’s going on.

Inside the HMAC

In the decompiled bytecode (a.k.a. smali), we can simply search for the string HMAC and see what pops up. Results turn out to be limited, and we can determine that the signature is composed using the SHA256 hashing function in the following method:

1.method public static final calculateHmac(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)[B
2
3.locals 9
4
5const-string v0, "secret"
6
7invoke-static {p0, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V
8
9const-string v0, "method"
10
11invoke-static {p1, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V
12
13const-string v0, "body"
14
15invoke-static {p2, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V
16
17const-string v0, "contentType"
18
19invoke-static {p3, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V
20
21const-string v0, "date"
22
23invoke-static {p4, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V
24
25const-string v0, "path"
26
27invoke-static {p5, v0}, Lv/x/c/i;->e(Ljava/lang/Object;Ljava/lang/String;)V
28
29[... snip ...]

Using Frida, we can override the calculateHmac method to log its arguments before executing, resulting in the following data:

1{
2 "0": "█████",
3 "1": "POST",
4 "2": "{\"email\":\"***REDACTED***\"}",
5 "3": "application/json; charset=UTF-8",
6 "4": "2021-01-01T22:13:30.166",
7 "5": "/profile/forgot_password/"
8}

Notice how these arguments line up perfectly with the method signature as seen in the decompiled bytecode above. As an added bonus, we now have the UTF-8 encoded ‘secret’ key used to sign messages: █████.

  • Update — January 5, 2021
    Upon request from the broadcasting network, AVRO, I have removed all references to the secret signing key from this blog post (as well as from the repository linked down below).

Remember that a HMAC function takes two arguments: a key and a message. Therefore, the aforementioned arguments need to be transformed into a message (i.e. a byte sequence) before being passed onto the hashing function. Usually developers simply compose the message by concatenating the strings. However, on occasion, a custom implementation is used. To be on the safe side, as well as to avoid making false assumptions, let’s hook the call to the class method that makes the actual call into the crypto library. This method is aptly named hmac and, as expected, accepts two strings: the message to be signed and a key. Logging provides the following arguments:

1{
2 "0": "POST\n078bdf5f7296200ae9a89901305dec37852dbf84e21496550ad2c477149aa6ac\napplication/json; charset=UTF-8\n2021-01-01T22:13:30.166\n/profile/forgot_password/",
3 "1": "█████"
4}

It appears that the strings, as seen in the first argument, are separated by newline characters. Somewhat surprisingly, a sequence that we haven’t seen before surfaces: 078bdf5f7296200ae9a89901305dec37852dbf84e21496550ad2c477149aa6ac. Looking at our findings so far, notice that this sequence is right where the body of the request was placed before. Perhaps this represents a hash of the body? Since I’m not sure what this seemingly random sequence denotes, we’ll return to the decompiled methods to take a closer look.

Following some of the cross-references, it doesn’t take long to find a call to another class implementing hashing, this time using a SHA-256 message digest (i.e. good-old hashing without any secrets). Internally, a call is made into Java’s built-in security library:

1invoke-static {p1}, Ljava/security/MessageDigest;->getInstance(Ljava/lang/String;)Ljava/security/MessageDigest;
2
3move-result-object p1
4
5invoke-virtual {p1, p0}, Ljava/security/MessageDigest;->digest([B)[B

Let’s verify whether we can compute the exact same hash for the request shown above, by applying the SHA-256 hashing algorithm to the request body. Start out by hashing the body of the request using CyberChef, like so:

Image showing a hash of the body using CyberChef
SHA-256 message digest generated using CyberChef.

Great, that’s the exact same hash as seen in the arguments above. Notice that I excluded the backslashes in the JSON, as these were added by JSON.stringify in our Frida script (due to escaping) and are therefore not part of the original body (as confirmed by the original request at the top of this post).

Now that we have all the arguments we need, all that’s left is to compute the final HMAC used to verify the integrity of the HTTP request. Again, we could use CyberChef to do the heavy lifting for us, but for some reason I did not manage to get CyberChef to produce the same results as the built-in Java library, most likely due to encoding differences. Therefore, I wrote a small Python script based on a ready-to-use snippet I found online. Computing the signature using this script indeed results in the expected base64 encoded string as present in the original request:

Image showing the output of the request signing script.
Output from the first version of the request signer, now available on GitHub.
Image showing the original request and response, highlighting the signature in the request.
Original request (and response) highlighting the matching signature.

Automated Request Signing

In order to effectively interact with the API, some automation might come in useful. Specifically, we want to be able to compose arbitrary requests, which we can then copy-paste into a tool to generate a valid HMAC signature, requiring as little manual labor as needed. Since we’ve already done most of the work, creating a Python script is not that much effort. You can find the resulting script in this repository.

What’s Next?

Now that we have successfully reverse engineered the signing algorithm, we can continue using the app as one normally would, while intercepting requests in the background. We can analyze these requests for interesting behavior, modifying and signing them as we please, thanks to our automated request signer. At the moment, I haven’t found any vulnerabilities yet, but if I find anything interesting, I’ll be sure to update this post once the issue has been resolved by the broadcasting network. If you find a vulnerability using any of the methods described here, please let me know! You can contact me on Twitter or via any of the links down in the footer of the page. Thanks for stopping by! :)

Join the email list and get notified about new content

Be the first to receive the latest content with the ability to opt-out at anytime. Your email address is stored by Mailchimp and will never be shared with anyone else.

More articles from Noam Drong

Hello, World!

Welcome to my personal blog, thanks for stopping by! I aim to publish here every now and then (whenever I have something I deem interesting…

August 31st, 2020 · 1 min read
© 2020–2021 Noam Drong
Link to $https://twitter.com/noamdrongLink to $https://github.com/ndrongLink to $https://linkedin.com/in/noamdrongLink to $mailto:[email protected]