Texts.blog, the blog of Texts.com
  • Texts.com
  • FAQ
  • X
  • Cracking Meta’s Messenger Certificate Pinning on macOS

    February 20th, 2024

    With Meta’s Messenger application for macOS being so close to the Texts.com model—that being a standalone desktop application—Batuhan İçöz who is leading the Meta platform project at Texts.com thought we could gain some valuable insight by analyzing it. Everyone knows that intercepting network requests is a great and low barrier-of-entry first-step.

    Meta implements certificate pinning into their applications which enhances their security model, and prevents us from being able to execute a MITM (man-in-the-middle) attack on ourselves to analyze the requests made to their servers.

    What is Certificate Pinning?

    When you set up a proxy client capable of intercepting your requests, you’re forced to configure and trust a “certificate authority,” one which you created. Certificates issued by your certificate authority will be used and will be able to intercept and decrypt information pertaining to the requests.

    If a service implements certificate pinning, they’ve effectively opted to accept certificates issued only by specific certificate authorities, preventing certificates issued by your certificate authority from being used.

    With certificate pinning enabled, our self-signed certificate is invalid, and thus our requests cannot be intercepted.

    Default Behaviour

    Without disabling certificate pinning, all requests return an “Internal Error” and our proxy software indicates that the “SSL Handshake Failed” with the request not completing its lifecycle. We thus can infer no information about the request.

    Desired Behaviour

    We want to be able to successfully make requests and read the request, response and headers from our network debugging tool by using a MITM attack on ourselves.

    Potential Approaches

    One option I’ve found to work in the past would be to alter the URL strings in the binary to insecure self-hosted endpoints that don’t implement TLS. It would forward requests and responses between the end-client and end-server. This works best for smaller applications, unlike Messenger.

    We could use a dynamic instrumentation library, such as Frida to achieve the desired outcome. I’ve found that Messenger in particular is prone to crashes when hooking into it and with all this overhead, it can be difficult to pinpoint the pain-point. There’s also the more complicated distribution process involved with Frida. Those who wanted to run it would need to configure a very specific environment and set of tools.

    Despite this, I did attempt to use a Frida script that I’ve been maintaining over the past few years that works to bypass common certificate pinning libraries and methods. It works on the vast majority of applications. Unfortunately, Meta’s subset of applications is not part of this “vast majority.”

    In this case, we’ll be looking to turn off certificate pinning entirely in a way which can be easily distributed to my fellow team members using binary patching.

    The Approach

    After downloading Messenger and moving it into my applications folder, I grabbed the compiled ARM binary from /Applications/Messenger.app/Content/MacOS/Messenger and imported it into Hopper.

    Hopper allows us to disassemble, decompile, recompile, debug, and visualize compiled binaries.

    Once the binary was loaded and references had loaded, I started by searching for certificate pinning related terminology such as “certificate,” “ssl,” “pinning,” etc.

    "SSL pinning verification failed for host:" certainly felt like a great place to start.

    Ideally we’ll modify as little as possible. When it comes to compiled binaries, modifications can easily result in gnarly crashes if we overextend ourselves. The best case scenario would be flipping a boolean value, reversing a conditional, etc., ideally modifying a single or few instructions.

    I switched to the control flow graph, allowing me to visualize the execution flow and walked myself up a series of linked references. Eventually, I found a string which said "Using custom sandbox -> turn off SSL verification". I liked it. I scanned the file for references to the function that determines this flag and found it in the top of the procedure.

    Looking at the function IsUsingSandbox(), we can see exactly where the returned value is being assigned. In the following screenshot, the w0 register is being moved from w19 and then returned. w19 is assigned through a load byte instruction.

    Instead of assigning w19 through a load byte instruction, we’re going to just set it to true no matter what. This will effectively force IsUsingSandbox to be true, which judging from that string from earlier, means certificate pinning will be disabled.

    Original

    ARM: ldrb w19, [sp, #0x40 + var_20]
    HEX: F3 83 40 39
    

    Rewritten

    ARM: mov w19, #1
    HEX: 33 00 80 52
    

    We can do this replacement with the hexadecimal mode, which allows us to directly modify the byte code in the application.

    Result

    After this, we can export our new executable using the “Produce New Executable” option under “File,” remove the signature from the executable, and we’re off to the races. We’ll replace the original Messenger binary with this new binary we’ve produced.

    After relaunching Messenger, we can see that headers, response body, and all other request information is visible in our proxy tool. By modifying just 4 of the binary’s 97,477,728 bytes we can now intercept requests!

    Want to see a similar approach for iOS? Read this 2020 post by Hassan Mostafa where he flipped a conditional branching instruction to crack certificate pinning on Instagram on a jailbroken iPhone.

    Distribution

    After compiling the binary, I sent it over to Batuhan to use. Once he had the binary in-hand he obtained and installed a signing certificate and proceeded to sign the application. Once that was done, he was able to utilize the binary on his system and view his own requests.

    codesign --force --deep -s CERTNAME_OR_ID /Applications/Messenger.app

  • Sunbird / ‘Nothing Chats’ is Not Secure.

    November 18th, 2023

    Outlining security risks in ‘Nothing Chats,’ an iMessage app for Android made in partnership with ‘Sunbird’. Observe the poor security practices that made it into the production release.

    Contributors

    These findings were discovered in a joint effort between Batuhan İçöz, 1Conan, and me (Rida F’kih). Follow them on Twitter, they are both extremely talented.

    Background

    On November 16th, 2023, alongside an announcement by MKBHD, Nothing announced ‘Nothing Chats,’ an application exclusive to their hardware which brings iMessage to Android in a partnership with Sunbird. ‘Nothing Chats’ is a reskinned version of the existing Sunbird application, currently available on the Google Play Store.

    After seeing conflicting statements related to the security of their application, members of the Texts.com reverse engineering team decided to take a look into the Sunbird application and their security practices.

    ‘Sunbird’ and consequently the ‘Nothing Chats’ application require sending your Apple ID credentials to their servers, where they authenticate on your behalf using a virtual machine running MacOS. If you’re an Apple user, this is the same Apple ID which you use to access your notes, photos, iCloud storage, email, and more. Preliminary findings were tested against the ‘Sunbird’ application, but we used the official ‘Nothing Chats’ application to confirm these vulnerabilities affected Nothing’s version as well.

    Notification

    Immediately, the Texts.com reverse engineering team noticed a few vulnerabilities and implementation issues which Kishan briefly outlined on Twitter / X. The main issue outlined being a vital request containing important credentials happening over an unencrypted channel (HTTP)

    texts team took a quick look at the tech behind nothing chats and found out it's extremely insecure

    it's not even using HTTPS, credentials are sent over plaintext HTTP

    backend is running an instance of BlueBubbles, which doesn't support end-to-end encryption yet pic.twitter.com/IcWyIbKE86

    — Kishan Bagaria (@KishanBagaria) November 17, 2023

    Sunbird’s Response

    Sunbird responded by denying any security issues, and justifying their implementation, doubling down on it being secure.

    In short, they made a few claims.

    1. Sunbird has ISO27001 certification, which testifies to their commitment towards security.
    2. The HTTP request which as subject of concern is only a one-off request to notify of an iMessage connection, which then takes place on a secure channel.
    3. The data is encrypted before being sent over HTTP with a key provided over HTTPS.

    Whether or not they are using another service behind the scenes is impossible to tell, and they may be telling the truth that this is just a naming conflict between the pre-existing BlueBubbles service and their own internal service.

    ISO27001 in this case is irrelevant. While its good to be committed to privacy and security, execution matters.

    Other points of the response simply display a misunderstanding of the functionality of the technologies they’re leveraging, the primary one being JWT (JSON Web Tokens). JWTs are signed, not encrypted. Their payloads are accessible, and they themselves act as an access token.

    In this case, the JWT is used to authenticate a user into the Firebase Realtime DB. It allows them to access storage which includes their account information, messages, accounts / connections, attachments, and more.

    Vulnerabilities

    Data in Transit Vulnerability

    While Sunbird’s claim that they generate and send the JWT over a secured channel are true, the application immediately turns around and sends the JWT back to another Sunbird service hosted on a load-balanced Express server which does not implement SSL, so requests can be easily intercepted by an attacker.

    The endpoint in question can be found at http://monarch.sunbirdapp.com:8888/register and accepts two fields in a JSON body. name which contains our Apple ID, and token which contains our JWT.

    Transmitting our JWT over an insecure channel is very dangerous, because it acts as an API token which we can use to access all our data. By nature, JWTs cannot be easily invalidated on the server side. If an attacker gets their hands on it, they have unfettered access to the resource it grants until token expiry. In this case, all our account details, messages, attachments, etc., all in realtime.

    By not implementing SSL, we’ve compromised level 7 of our OSI model. If an attacker compromises any point along our network pipeline between the application and the aforementioned Express server, our JWT can become compromised and an attacker will gain access to the information we’ve entrusted to Sunbird / Nothing Chats.

    Attacks can be user-targeted, if you’re on a non-WPA network, a WPA network with a cracked PSK, or a compromised network hosted by an attacker, they can easily steal our packets, and your JWT.

    The real danger begins as we walk down the network pipeline. Depending on the implementation of the load balancer, Express server, or encompassing network, an attacker targeting the server-end could gain access to any and all users who authenticate into iMessage once their attack begins.

    You can see a screenshot of us intercepting a JWT sent over HTTP in the next section as part of a greater attack demonstration.

    Data at Rest Vulnerability

    Sunbird does try to implement E2EE, although their implementation is overshadowed by decrypting, and then storing the unencrypted payloads in their database.

    When a message or an attachment is received by a user, they are unencrypted on the server side until the client sends a request acknowledging, and deleting them from the database. This means that an attacker subscribed to the Firebase Realtime DB will always be able to access the messages before or at the moment they are read by the user.

    In the following screenshot, we’ve intercepted the JWT.

    We then take it, and authenticate into the Firebase Realtime DB. This subscribes us to changes that occur in this database live. Messages in, out, account changes, etc.

    At this point, with nothing but the JWT, we could easily create a script which could download all information regarding the user and all their conversations with just 23 lines of code.

    const JWT = "";
    const [, payload] = JWT.split(".");
    const { user_id } = JSON.parse(Buffer.from(payload, "base64").toString());
    
    const listAccounts = (id: string) => {
      return fetch(`https://bluebubblemessaging-dev-default-rtdb.firebaseio.com/users/${id}/accounts.json?auth=${JWT}`)
        .then((response) => response.json())
        .then(Object.keys)
    }
    
    const getAccountData = (accountId: string) => {
      return fetch(`https://bluebubblemessaging-dev-default-rtdb.firebaseio.com/accounts/${accountId}.json?auth=${JWT}`)
        .then((response) => response.json())
    }
    
    const main = async () => {
      const accounts = await listAccounts(user_id);
      const accountData = await Promise.all(accounts.map(getAccountData));
      console.log(JSON.stringify(accountData, undefined, 2))
    }
    
    main();
    

    To demonstrate this, I sent myself the following message from my real iPhone to an account linked with ‘Nothing Chats’, and then requested the relevant data from Sunbird by running the script. You can see it is visible in plaintext, sans-encryption.

    Insider Threat / Data Exposure

    When you send a message using Sunbird or Nothing Chats, the data relating to your message including the contact information, message contents, and attachment URLs are sent to the Sunbird’s Sentry (a debugging platform), which can then be viewed by authorised parties within the company. This contradicts the FAQ item sourced directly from the Nothing website as of November 17th, 2023.

    Are my messages secure?

    Yes, Nothing Chats is built on Sunbird’s platform and all Chats messages are end-to-end encrypted, meaning neither we nor Sunbird can access the messages you’re sending and receiving.

    By sending unencrypted outgoing messages to Sentry, authorised individuals at Sunbird would be able to view them from their Sentry dashboard. This makes them susceptible to insider threats. Here is the text content of a request sent to Sentry from the official ‘Nothing Chats’ application. The entire payload includes more messages, and information about the sender and recipient including their contact information.

    Is It Real?

    If you’re a ‘Nothing Chats’ user and are viewing this around November 17th, 2023, you may be able to see it with your own eyes. Within a matter of minutes, Batuhan created an open-source proof of concept which allows you to observe the lack of E2EE in your own browser.

    Simply go to batuhan/sunbird-poc, and once you have a good understanding of the code, visit the URL listed on the repository sidebar. Authenticate into your ‘Nothing Chats’ account. Send yourself some texts, and observe your account details. It’s important to note this repository contains no decryption methods, so if E2EE was implemented, you wouldn’t be able to see your account and text information.


    Sending your credentials to third-party services always presents a significant risk. It’s always important to stay vigilant with your information, and consider the security implications of sharing any. By sending our Apple ID to a third-party service, we are not only trusting the third-party with our texts, but should they become compromised, our photos, videos, contacts, notes, keychain, and more.


    Update: Nothing Chats have been pulled from the Play Store.

    We've removed the Nothing Chats beta from the Play Store and will be delaying the launch until further notice to work with Sunbird to fix several bugs.

    We apologise for the delay and will do right by our users.

    — Nothing (@nothing) November 18, 2023

    Update – 2: Sunbird sent a push notification to users letting them know that their platform is temporarily on hold.

    Update – 3: My colleague Batuhan recommends the following steps to remove some of the data stored by Sunbird:

    I’ve been banned from @sunbirdapp’s Discord, probably because I made a tool that deletes some of the data they keep.

    If you are a Sunbird/Nothing Chats user, here’s my recommendation, in order:

    – Change your Apple ID password *now* and revoke their session
    – Remove the app
    -…

    — batuhan içöz (@batuhan) November 18, 2023
  • Simplifying IPC in Electron

    April 20th, 2022

    You have an Electron app with two processes, main and renderer. (For the uninitiated, the main process is responsible for launching new BrowserWindows – renderer processes. Renderer processes run JavaScript defined in the webpages. More here.)

    You likely also have some IPC code for communicating between the two processes.

    renderer/index.js:

    import { ipcRenderer } from 'electron'
    
    await ipcRenderer.invoke('addNumbers', 69)
    await ipcRenderer.invoke('subNumbers', 42)
    

    main/index.js:

    import { ipcMain } from 'electron'
    
    ipcMain.handle('addNumbers', (event, arg) => {
      return arg + 42
    })
    ipcMain.handle('subNumbers', (event, arg) => {
      return 69 - arg
    })
    
    // window creation etc.
    

    This is fine for two methods but when you have a couple dozen, the IPC logic can easily turn into spaghetti code and you’ll end up writing boilerplate glue code for each method.

    What if you could call functions in the main process directly from the renderer process as if you had written them in the renderer process?

    Instead of the above boilerplatey IPC logic, all you need to do is move all your functions that you want to access from the renderer process in a separate module, not thinking about IPC, and write a simple RPC layer using Proxy that routes all function calls through IPC under the hood:

    renderer/index.ts:

    import { ipcRenderer } from 'electron'
    
    export const mainFns = new Proxy({}, {
      get: (target, key) =>
        (...args: any[]) =>
          ipcRenderer.invoke('CALL_EXPOSED_MAIN_FN', { methodName: key, args }),
    })
    

    main/exposed-fns.ts:

    // this executes in the main process
    export default {
      getProcessType: () => process.type,
      addNumbers: (arg: number) => arg + 42,
      subNumbers: (arg: number) => 69 - arg,
    }
    

    main/index.ts:

    import { app, BrowserWindow, ipcMain } from 'electron'
    import mainFns from './exposed-fns'
    
    ipcMain.handle('CALL_EXPOSED_MAIN_FN', (event, { methodName, args }) =>
      mainFns[methodName](...args)
    )
    
    // window creation etc.
    

    That’s all. Now you can call all functions defined in main/exposed-fns.ts easily with the mainFns proxy object:

    await mainFns.getProcessType() // returns "browser"
    

    Type checking

    You can have full type checking by remapping the exported functions to return a promise.

    types.ts:

    <pre class="wp-block-syntaxhighlighter-code">type AnyFunction = (...args: any[]) => any
    type Async = ReturnType extends Promise
      ? F
      : (...args: Parameters) => Promise<ReturnType>
    
    export type Promisified = { [K in keyof T]: T[K] extends AnyFunction ? Async : never }</pre>
    

    renderer/index.ts:

    import { ipcRenderer } from 'electron'
    import type mainFnsType from '../main/exposed-fns'
    import type { Promisified } from '../types'
    
    type ExportedFunctionsType = typeof mainFnsType
    
    export const mainFns = new Proxy({}, {
      get: (target, key) =>
        (...args: any[]) =>
          ipcRenderer.invoke('CALL_EXPOSED_MAIN_FN', { methodName: key, args }),
    }) as Promisified
    
    // call main functions seamlessly
    mainFns.getProcessType()
      .then(x => console.log('getProcessType', x))
    

    Passing callbacks

    With the above, all arguments cloneable with the structured clone algorithm can be passed to the exposed functions. Notable exception is functions which cannot be cloned. Instead, you can pass a reference to them like this.

    Calling functions in renderer process from main process

    Electron doesn’t have an equivalent of ipcRenderer.invoke for the renderer processes so there’s more logic involved to handle the request and response.

    main/renderer-fns-bridge.ts:

    import { ipcMain, BrowserWindow } from 'electron'
    import type rendererFnsType from '../renderer/exposed-fns'
    import type { Promisified } from '../types'
    
    type ExportedFunctionsType = typeof rendererFnsType
    
    export default function getRendererFnsBridge(window: BrowserWindow) {
      const requestQueue = new Map()
    
      ipcMain.on('EXPOSED_RENDERER_FN_RESULT', (_, { reqID, result, error }) => {
        const promise = requestQueue.get(reqID)
        if (error) promise?.reject(new Error(error.message))
        else promise?.resolve(result)
        requestQueue.delete(reqID)
      })
    
      let reqID = 0
      const rendererFns = new Proxy({}, {
        get: (target, key) =>
          (...args: any[]) =>
            new Promise((resolve, reject) => {
              requestQueue.set(++reqID, { resolve, reject })
              window.webContents.send('CALL_EXPOSED_RENDERER_FN', {
                reqID,
                methodName: key,
                args,
              })
            }),
      })
      return rendererFns as Promisified
    }
    

    renderer/ipc.ts:

    import { ipcRenderer } from 'electron'
    import rendererFns from './exposed-fns'
    
    ipcRenderer.on('CALL_EXPOSED_RENDERER_FN', async (_, { reqID, methodName, args }) => {
      try {
        const result = await rendererFns[methodName](...args)
        ipcRenderer.send('EXPOSED_RENDERER_FN_RESULT', { reqID, result })
      } catch (err) {
        ipcRenderer.send('EXPOSED_RENDERER_FN_RESULT', { reqID, error: { message: err.message } })
      }
    })
    

    main/index.ts:

    import getRendererFnsBridge from './renderer-fns-bridge'
    
    // after window is created, call renderer functions seamlessly from main process
    const rendererFnsBridge = getRendererFnsBridge(window)
    
    rendererFnsBridge.getProcessType()
      .then(x => console.log('getProcessType', x))
    

    Here’s an example repo implementing all of the above.

    This was inspired by electron/remote, check it out if you want to handle more complex cases and proxy entire objects. This RPC implementation is of course not just limited to Electron, you can use it for client-server communication over HTTP and WebSockets too, as an alternative to REST/GraphQL.

Proudly powered by WordPress

 

Loading Comments...