Introduction

Recently I needed to reverse engineer an application as I needed to figure out its login mechanisms. The diagram below is a high level diagram of its layout and it shall henceforth be collectively referred to as The Application.

The primary question I wanted to answer was: Could I clone a logged in user’s session stored by The Service? Spoiler: Yes.

With the spoiler out of the way, the following section will provide some background on The Application and its constituent parts.

Background

The Service is a system service that runs on all major operating systems. For user interactivity, a frontend GUI is provided. The GUI interacts with The Service via a locally bound HTTPS (TLSv1!) port.

The Service, in its normal operation, takes requests from users and makes backend calls to a remote server. To make it more user friendly, The Service needs to persist the user’s session so that the user doesn’t need to go through the annoying login process (user ID, password, second factor etc) every time he wants to use it.

Low effort hacking attempt #1

I am a lazy person, so I tried the simple solution first. I copied the files in ~/Library/Application Support/<The Service>, and copied it over to a fresh VM. These were the files created by The GUI, which included a leveldb database.

However, when I started The GUI, I was greeted with the login screen instead of my already signed in session on my original machine.

Sadness.

Okay well, the simple way did not work. Time to roll up the sleeves and dig deeper.

Hacking attempt #2 - time to bring in everything and the kitchen sink

After the expected but sad failure of Hacking Attempt #1, it was time for a deeper look.

I went back to the Application Folder and looked at the files created. I spent quite a while stymied by this, because:

  1. Meddling with the leveldb database affected The Application’s login status.
  2. Leveldb was something new to me.
  3. I didn’t really want to look into binary and was hoping all the logic was in The GUI.

However, after unpacking the asar file and going through the JavaScript files, hooking it up to a Chrome debugger and stepping through all the code, I had to accept the wise words of Sherlock Holmes in The Sign of Four:

When you have eliminated the impossible, whatever remains, no matter how improbable, must be the truth.

Through the Chrome debugger, I realised that the leveldb wasn’t anything very special. The initialisation and operation of it seemed to be in system library code, deep in the bowels of Electron and not application code. In fact the application code did not import any leveldb libraries at all.

While plugging through The GUI seemed to be a wash, it helped refresh my knowledge of Electron app debugging (probably been more than 2 years since I looked at one), and I also found out that it was communicating both to The Service and to The Server.

When it communicated to The Server, The Server responded with an authenticated response. This meant that somehow The GUI managed to acquire a session from somewhere and was reusing it when communicating to The Server.

It was time to look at the binary.

To start off, I had to get the lay of the land, so I looked at the binary’s installation folder and looked at the configuration files in that folder. All of them were Base64 files such as the image below.

However, these files were encrypted, so decoding them didn’t do anything useful. Transferring them to another machine also did not clone the login session of the target user. This meant that somehow there was a machine specific encryption or session being applied to each installation.

Since debugging and tracing files is annoying on MacOS, it was time to switch over to Windows, a much friendlier operating system for debugging.

Windows VM setup

With a Windows VM created, it was time to get the tradecraft in. For this operation I set up the following:

  • My trusty debugger, x64dbg, because I’m too primitive and noob to remember how to use WinDbg.
  • ProcMon, because I needed to see what files The Service touched. This could help me narrow down likely candidates rather than wading through a debugger.
  • IDA. Of course, one can go nowhere without The Lady herself. I ran this on my Host MacOS though, because it’s way nicer to run on Macs rather than Windows.

The Service

Process Monitor

ProcMon is a useful tool to run it before attempting to reverse engineer an application, and even in conjunction with a debugger to quickly see what resources it touches as it runs.

Over here in this screenshot I could see that The Service created a bunch of registry keys at a certain path, and also creates a .config.json file at a certain folder. The “.” in front would hide the file in *nix systems, which would explain why I didn’t see it on my Mac initially.

Looking into the .config.json showed yet another Base64 file that decodes into binary. Most likely an encrypted file.

IDA

Now it was time to look at The Service in IDA.

Luckily the binary had most of the important functions with symbols, so it was relatively easy to do some simple reconnaissance to narrow down interesting functions to breakpoint on. Since I knew that The Service was receiving a cookie from The Server, I looked for some cookie related functions and happily found one called SetCookies.

SetCookies

The SetCookies function is rather simple, and my attention was quickly drawn to the Save function.

Digging deeper into the Save function led me to a SaveConfig function, which had a function that was helpfully named Encrypt.

This gave more credence to the theory that the .config.json and other files were Base64 encrypted data. Also, since the files are encrypted, this would mean that there would be a Decrypt function, and The Service would definitely be calling this function whenever it started.

Revisiting the Problem Statement #1

To recap, the question I wanted to answer by reverse engineering The Service was this: Could I clone a logged in user’s session stored by The Service?

Now that I had taken a deeper look into The Application’s setup, the previous singular question could now be expressed into 4 new questions.

  1. What file(s) does The Service use to persist the user’s session?
    • Answer: This would possibly be .config.json and some registry values. However, these are all encrypted.
  2. How does it encrypt and decrypt the file(s)?
  3. What is the encryption key?
  4. How do I derive or load the encryption key?

Time to debug!

The Windows binary was compiled as a 32 bit binary (oddly), so I fired up x32dbg rather than x64dbg.

At this point, it was ready to fire up the debugger to take a look at how The Service behaved during run time. To make life easier when debugging, I would always rebase my IDA’s base address to be the same as the loaded module in the debugger.

This ensures that both have the same frame of reference and I can easily copy an address from IDA to the debugger and vice versa.

In this case, the base address of The Service was 00CF0000. Once I set up IDA to rebase the module image to be of the same base address, I looked up the address of Decrypt and set a breakpoint at the same address in the debugger and executed it.

As expected, the breakpoint hit, as The Service would need to access these encrypted files on startup.

As a sidenote, I don’t know if this is a Golang mechanism, but it is interesting to notice that the function preamble is very different from the usual push ebp; mov ebp, esp; types that I normally see.

The function call to aes_newCipher comes across immediately as an interesting function, because typical encryption APIs would construct some kind of Cipher or Encryption object using the encryption key as the parameter. A quick check with Golang documentation showed that the function NewCipher expected an input argument of the cipher key, given in the form of a byte array, and returned two objects, a cipher.Block and an Error.

Based on the screenshot above, the multitude of mov instructions above the call to aes_newCipher would undoubtedly be the ones setting up the arguments to be passed to that function.

Again, as another sidenote, it was interesting to observe the way Golang does this. In “normal” binaries that I’ve seen, arguments are usually pushed onto the stack via a push instruction such as in the screenshot below.

Example 32 bit ASM compiled by gcc

In this one, we can see the argument 10 that is fed to square on L16 is pushed into the stack at L33 before the call. However, in the Golang binary, the required arguments were moved into a preallocated section on the stack before the call like in the screenshot below.

Golang arguments

In any case, it is clear that the key is fed as the first argument to aes_newCipher so I quickly executed to just before the call and checked it out.

Stack of aes_newCipher

The screenshot above shows the stack of The Service, with the first value at 12C75BBC as esp. This value was definitely the key used for a decryption operation, but where did this key come from? I was confused for a bit but I decided to try the simple theory first. As The Service would need to “bootstrap” itself, it would definitely need some easily derivable key, so… could this key be contained somewhere?

A quick grep on the MacOS version of The Service turned it up.

Yes! Easy peasy, don’t need to dig around for the key! But of course, life is tough and the adventure does not end here. Now I needed to find out what it was encrypting in this instance, and what the IV was.

More questions

The first order of business is to find out what it was encrypting. To do that, I needed to look deeper into the function in IDA.

From the screenshot below, I could see that the happy path was the block to the left. The block to the right was the error state in case there was any issue in creating the Cipher object.

The call to Base64 DecodeString stood out as I knew that the data was encrypted and then saved as Base64.

Looking at the arguments pushed into the stack prior to the function call, I came across a new Base64 string. This was not the content from .config.json. At this point I did not know where this string came from and I was lazy to trace up the callstack and debug. So I decided to use ProcMon to see if I could find out where it was retrieving this string from.

To do that, I set up ProcMon to include only events coming from The Service, and to filter out events that were uninteresting, such as opening exe or dll files. This would reduce the gunk in the logs and make it easier to parse through.

With ProcMon started, I reran The Service and breaked at the DecodeString function called in Decrypt.

ProcMon delivers! From the trace, I could see that it most likely retrieved the string from the registry, stored as DeviceKey. Checking out the registry proved that it was correct. Yes! More information found without tracing through IDA or the debugger!

Now I had the (fixed) encryption key, the cipher text, the last thing was to look for the IV. But wait! The encryption method is CFB which is a block cipher method. Did I really need to know the IV?

A quick look at Wiki mentions that it is similar to CBC, and in the article, Wiki had this to say about the IV for CBC:

Decrypting with the incorrect IV causes the first block of plaintext to be corrupt but subsequent plaintext blocks will be correct. This is because each block is XORed with the ciphertext of the previous block, not the plaintext, so one does not need to decrypt the previous block before using it as the IV for the decryption of the current one. This means that a plaintext block can be recovered from two adjacent blocks of ciphertext.

With this information, I could possibly try to decrypt the data without finding out what IV was used. I took the Base64 data that was being used and tested it out in CyberChef.

As expected, the first block was rubbish as I did not have the correct IV, but the encrypted data could be decrypted properly! Yes! Less debugging to do!

Needless to say, I attempted to decrypt .config.json with the fixed key I found and also with the decrypted data from the DeviceKey registry value. Sadly it did not work.

Revisiting the Problem Statement #2

At this point, it was time to go back to the original problem statement.

Could I clone a logged in user’s session stored by The Service?

  • What file(s) does The Service use to persist the user’s session?
    • Answer: This would possibly be .config.json and some registry values. However, these are all encrypted.
  • How does it encrypt and decrypt the file(s)?
    • Partial Answer: A key has been found embedded as a string in The Service. It decrypts a registry value into something.
  • What is the encryption key?
    • Partial Answer: A hardcoded key has been found in The Service. It decrypts something but I’m not sure what it is used for.
  • (New question!): What is the first decrypted value used for?

What is the first decrypted value used for?

At this point, I was guessing that the first decrypted value would be used in some fashion to decrypt the data stored in .config.json. But using it directly did not give me the answer that I sought for.

This meant more debugging.

I let the debugger pop out of several functions, and came across this interesting block. After the first value is decrypted, it is finally returned by the loadKey function. An interesting function called getHash is subsequently called. There was also an interesting hardcoded string in 0102152E.

Time to bring the debugger up to speed. I stopped the execution just before the call to getHash like in the screenshot below.

The parameters on the stack was my decrypted value(!) but it was also concatenated with something, which turned out to be the hardcoded string in 0102152E.

I looked into the getHash function in IDA and realised that it was doing some sort of MD5.

I tried taking the my decrypted value, concatenated it with the string and MD5 hashed it. This looked like the final key that was used to decrypt .config.json!

A quick check with the debugger on decrypting .config.json showed that it was what I expected.

Time to use CyberChef!

Success!

Revisiting the Problem Statement #3

Now that I could decrypt .config.json, it was time to revisit my original intent.

At this point, it was time to go back to the original problem statement.

Could I clone a logged in user’s session stored by The Service?

Answer: Yes. We can decrypt the data contained in .config.json and use its values to clone another session. The exact mechanism to do so is out of scope for this article.

And also, since I could decrypt it, this also meant I could encrypt it for The Application to consume.

For the other questions…

  • What file(s) does The Service use to persist the user’s session?
    • Answer: This is .config.json and the registry value is the encryption key. The encryption key, once generated, is fixed.
  • How does it encrypt and decrypt the file(s)?
    • Answer: It uses a hardcoded string as the initial way to encrypt a randomly generated key. This randomly generated key is used to encrypt data stored in .config.json.

Conclusion

As I am really rusty to reverse engineering (haven’t done it in more than a year!) and I wasn’t too familiar with Golang binaries, this took me about 2 days to do. Luckily this program is reasonably easy for my noob skill levels to tackle.

Phew!