Nonchalant Guidance

About Me·RSS·My Projects·LinkedIn


Added on: Thursday, 28 May, 2026 | Updated on: -

Reverse Engineering the PlayStation App

I like gaming. I’ve historically played a lot of PlayStation exclusives, so I prefer sticking with the PlayStation consoles. With the PS4 onwards, Sony created an app called the PlayStation app that allows you to (among other things):

The app itself is somewhat decent, although I wished it was a bit faster. Also, the screenshots and videos are a bit of a pain to share over other mediums, and they disappear from the server in 14 days. That’s not a huge problem, except for the fact that even the stuff you’ve viewed in the app disappears. The only way to save it is to “truly” download it, ie download to the Android gallery. This is kind of annoying.

This pain point would lead me to reverse engineer enough of the app so I could build my own that would cache stuff inside the app and allow viewing, sharing and saving to gallery even after the content was deleted from Sony’s servers. Here’s how I did it.

The Setup

The root certificate is installed so that HTTPS connections can be intercepted, decrypted, stored and forwarded. However, on Android apps can also pin their preferred certificates, so they don’t necessarily have to trust the user installed certificates. While this practice is discouraged, many apps still do this to prevent reverse engineering. The PlayStation app is no different.

This consists of two steps:

  1. Running the frida server binary as root on the Android device. This will actually do the injection, and launch the app with the scripts we provide it.

  2. Launching the Frida scripts from our desktop to hook into native TLS, unpin certs, and configure our own certficate, and launching the PlayStation app under these circumstances.

Note: I initially used HTTPToolkit’s Frida scripts as highlighted in the article, however I had to use this fork for the unpinning to work, as it correctly hooked into com.android.org.conscrypt.TrustManagerImpl where the upstream repo did not. As of writing this article I first started looking into this a few months ago, so it is quite possible they have already fixed it. In case they haven’t, you can save yourself some time and try using this fork.

I used the command

frida -U -l ./config.js \
    -l ./native-tls-hook.js \
    -l ./android/android-certificate-unpinning.js \
    -l ./android/android-certificate-unpinning-fallback.js \
    -f com.scee.psxandroid

(where com.scee.psxandroid is the package name of the PlayStation app)

to launch the app under these circumstances after I had followed the instructions and edited the config.js accordingly.

First Steps

With all of these pieces in place, I ran the app, starting with logging in …. and that did not work!

The PlayStation app redirects you to what I believe is a WebView process that loads in the login page. This login page, once successful, generates a token called an “NPSSO”. This token is what will ultimately identify a user in the subsequent API calls.

However, due to the way that this WebView process is spawned, and the way the Frida scripts work, if we have Frida scripts active, the WebView is never able to load in the login page. Thus, we have to log in with no interception (ie, we can’t capture the login page URL, or what data it returns on successful login to the app through Frida).

After we’re logged in though, we’re firmly inside the PlayStation app. I was able to use the app as normal and click on all the bells and whistles I wanted. As I was using the app, PCAPDroid was dilligently capturing the network requests. I took care to hit most of all the functionalities that I was interested in: seeing recent games I played, viewing all the trophies on my account, inspecting one particular game’s trophies, inspecting a single trophy, etc. etc. and of course viewing my recent game captures and downloading those captures.

Once I was done, I used PCAPDroid’s “HTTP Requests” mode to have it get rid of the extraneous info about lower network layer info and just give me the info on the API calls, and exported it as text. Since we were doing HTTPS interception, we had all of the info in plaintext, from the NPSSO token generated at login and stored for later use by the app on subsequent runs to the data the Sony servers returned to the app.

Postprocessing

At first, I actually created a dummy user inside the PS app and captured endpoints etc that returned little to no data (seeing as how this user was just created on the app and did not actually have a PS4/5 to their name). This was useful to determine baseline behavior and to help whittle down useless telemetry calls etc.

I couldn’t capture some APIs, though, because the frontend is smart enough to not call them. For example, you have to go into your PS4/5 and enable the setting to auto-upload your captured screenshots and videos to Sony servers in order for the PlayStation App to call that API. If you don’t, a particular GraphQL API will return

[
       {
       "name": "cloudmediagalleryautoupload",
       "default": "off"
       }
      ]
  

and then the app will determine that it is pointless querying recent screenshots API.

Nevertheless, the basic profile API, basic trophy API endpoint and various other bits and pieces were still here. I was using the $20 OpenAI subscription at this point, and had Codex analyze the basic capture and generate some docs on the various endpoints, sparse as the responses were.

Then, I felt confident enough to put my own user in the app, capture the responses and analyze the bigger capture.

This capture was still huge even after just being comprised of HTTP calls. I now understood why the app was so slow. One of my sessions is 20MB big (and that is just HTTP data, not any of the lower network layers). Some of the size came from the raw image download I captured, which I concatenated. Some of it was once again telemetry (some Adobe SDK judging from the URL).

After whittling down the log file a bit, I felt I had just enough so as to get the LLM to find the “new” APIs and append them to the docs it had initially created.

Once I had some raw docs, I once again had the LLM distil those docs and clean them up, until I had something I could easily read and start creating a Rust library to implement the API calls.

Writing stationplayer

“But, you wanted an Android app? Why are you writing the library in Rust?”, you may ask. Good question. Having a Rust library is beneficial for a couple cases:

  1. The library can be more easily used in other contexts, like a desktop program, or CLI tool.

  2. Rust is more familiar to me.

  3. I wanted to experiment with native libraries in Android and building my own, as well as FFI.

  4. I figure for other people, Rust might be more easily read/parsed in case they wanted to take what I’ve learned here and use them in some other projects.

After having decided on the terribly creative name “stationplayer” for the Rust library, I laid out the general pattern:

The API is a mix of standard REST APIs and GraphQL. I don’t quite understand the rationale behind the split, but for GraphQL I opted to support building the very minimal support for the persisted queries the app made.

I created a few of these endpoints (just to set precedent) before I let Codex rip all over the codebase and generate more based on what they did (trophies, account data etc.)

I followed the user guide as best I could to generate the bindings, and Codex helped out with the remaining efforts. It was the one responsible for creating “sync” methods that just block the the Tokio runtime until the inner async method returns something. It is not the cleanest design, but it works.

This became extremely useful to close the feedback loop for the LLM. I could point Codex at the smoke-client and tell it to run and run until the client seems to return the correct output.

During development, I used the NPSSO I’d captured from the actual app’s API calls, but once that expired, I had to take a look at chiaki-ng sources, which also support logging into PlayStation to grab the account ID or some other tokens to allow using Sony servers to connect to your console at home for remote play services. The entitlements are a bit different, but the general idea is same. I also ran jadx on the PS App to decompile the code, and had Codex try its best at finding where the login URL was. A combination of these two practices, and I had enough to log into my account on the web, open up the network tab and note down the NPSSO token from there.

Slopping bettertrophies

After this, I proceeded to set up a Kotlin + Jetpack Compose based app that I could use to (as a start) track trophies and manage captures. I (once again) creatively named it “bettertrophies”, and after setting up a Nix flake with most of the required Android SDK, threw Codex at it.

Yep. The entire app is slop.

With stationplayer, the initial effort was mostly me, since I wanted to set precedent that the LLM could copy. However, I have 0 experience with Android app development, Kotlin, and Jetpack Compose. I know my way around adb, but ironically the only app development experience I have before this was using React Native. Even then I had little patience for adjusting UI elements around etc.

So, I offloaded that responsibility to Codex, partly due to me wanting the app faster, and partly because I was also curious how good it could be.

The Android feedback loop

I allowed Codex to drive my rooted phone for testing. By giving the harness permission to call adb, and having a phone connected for adb debugging, we can have the LLM build a debug APK once it is done with some changes and sideload it on the phone. A tool called andy built by my friend serves as a nice, LLM token-friendly way for the model to open and interact with the app (eg. touching, scrolling, etc). While he designed it more for the Android emulator, and had some guard rails around running on a normal phone, he added some support for dropping the guardrails to support the real device I was using for testing.

andy worked decently enough, although it did crash sometimes and forced the model to use real adb as a fallback. Either way, the LLM could interact with the app it was building, see the output, and determine how well it had done its job.

The feedback loop was complete.

Another enhancement I did was make a skill that guides the LLM into using the hut cli (SourceHut CLI) to read open tickets or pick the one the user wanted it to pick, read the ticket description, get to work fixing the issue, test it and then report back in a reply about the fixes.

Using this, I could create a ticket (and be in the mindspace of “I am reporting a bug”) with decent details, and then give the LLM the message “fix issue number X” and go off and do something else. Provided I keep my test phone plugged in and unlocked (using Caffeine), I would come back to a fixed issue. The issue tracker could thus double as a prompt library as well.

The app currently has the following feature set:

Maintaining stationplayer

Since bettertrophies was just (in blunt words) a slopped frontend that happened to cache data it got from the native stationplayer library it relied on for making API calls, the real challenge for keeping this setup working was on the library side.

As I mentioned before, Sony disallows usage of older builds of the app, so we’d need to periodically update the constants every time they push out a new build. I don’t know about you, but having my old phone plugged in constantly and relying on some dynamic analysis for this will get old real quick. I wanted a cleaner setup for this.

Hence, I decided I would use the knowledge I already had, and using those as hints, help an LLM wade through the decompiled APK (using jadx) and build some script that can grab those details out. That way, we could build a pipeline that could run every month, extract the constants we need from the latest build on the Play Store, and update stationplayer with those new constants.

A funny thing happened, though. After I’d decompiled the app, when I would search for the constants I knew were present in the app, I would never find them. Even stuff like the client ID/secret, or the version of the app were not there. This perplexed me. After checking the “codebase” for a bit I found some native libraries present. Some were standard looking, like libstdc++.so, but some were libSce or libsie prefixed, which indicated that Sony developed them.

I guess I shouldn’t have been surprised, though. Android app developers know that using Java/Kotlin etc. leaves their apps more open for reverse engineering, so some keep some critical parts of their app (eg. encryption, or key secrets in our case) in some native library and have their app call out the library for these. Static analysis would prove a lot harder as a result.

I have played with Ghidra sometimes in the past, but I was not exactly a fan of using such a big project for something like this. I didn’t really need the entire native lib decompiled, just needed some magic constants out of it. Constants that were most likely (in one way or another) just hardcoded in there.

At this point I was trying out open weight models for a change. I decided on the Deepseek V4 Pro model with the pi coding agent, both of which I was using for the first time. While Deepseek trains on the inputs fed into their API, they made the API itself ridiculously cheap for a frontier model, and seeing as how I was making all this work open source anyways, it did not seem to matter much for this purpose. Also, I’d read that the “frontier” LLMs would sometimes refuse to do reverse engineering work (or similar kinds of work) on the lines that it was against their policies.

I threw Deepseek at this conundrum, and it did not disappoint. With some steering from yours truly, and some snippets from certain Ghidra-decompiled native libs, it was able to figure out the decryption scheme for the constants I needed, and threw a nice Python script to automate this for the jadx-decompiled PlayStation App. All the Python script does is obtain the raw native lib .so, run it through roughly the same steps that the app would perform at runtime, and return the constants in a JSON schema.

For verification, the script checks if one of the fields decrypts to the package name, com.scee.psxandroid, which is also what I told the model when we embarked on this adventure together.

The API costs were roughly half a US dollar.

All of this across 4 hours while I read “A Storm of Swords” by George R.R. Martin, btw.

After that, it was relatively simple to create a SourceHut .build.yml that could download the latest build (using gplaydl) of the app, have jadx decompile it, and run the Python script to get the constants. Then, process the JSON from the script and patch up consts.rs and commit this changed version to the main branch.

Conclusion

I still don’t have everything figured out, but after I’d figured out a way to automate updating the metadata, I can rest a bit easier. Still, the one big pain point that still exists is… logging into the app, which is still TBD. This could be solved by further looking at chiaki-ng to see how it gets the credentials it needs after logging in.

I was considering at one point publishing the app on F-Droid. I would certainly like to have the app on the store so others can more easily use it. However, the slop nature of the app means I am a bit hesitant to promote the use of the app to others that I publish it. It is one thing to use an LLM to build software for an audience of one, another entirely to make for others.

For now I suppose I can be content with some software made just for me. I came across this wonderful article that puts it into works better:

“If you write something just to play, you define what it is you want out of the project. You can stop any time, and do no more or less than you’re interested in. Don’t want to write tests? Skip them. Don’t want to use an issue tracker? Ditch it. Finished learning what you wanted to? Stop the project if it’s not fun anymore!”

If you want to take a look at the code I came up with, here are the repo links:

The issue tracker, where I have “prompts” and LLM-based responses: https://todo.sr.ht/~gotlou/bettertrophies


This website was made using Markdown, Pandoc, and a custom program to automatically add headers and footers (including this one) to any document that’s published here.

Copyright © 2026 Saksham Mittal. All rights reserved. Unless otherwise stated, all content on this website is licensed under the CC BY-SA 4.0 International License