Reverse Engineering

Strange Battery (500 pts)​

After a station wide blackout, you found a strange battery lying in Electrical. We know there is an Imposter among us. Cyan said that this battery is somehow caused the computer to POWER OFF and a REBOOT syscall was used to do this. Investigate his claims. Make the battery POWER OFF with only the provided binaries.

Be careful not to power off your host machine. Using a VM is highly recommended.

  • The flag for this challenge is split into 5 parts.

  • This reversing challenge requires you to recreate the environment for this targeted malware.

  • This challenge is self-contained. There is no need to connect to the internet.

  • Flag format conversion may have to be done for this challenge (Refer to notifications)

StrangeBattery is a challenge that heavily borrows from challenge 10 and 11 of Flare-on 2020.
It is an overly simplified malware dropper. Like many targeted malware, StrangeBattery checks for a number of
settings on the host it is running on before executing the malicious shellcode. Seemingly benign checks like these can help malware evade sandbox techniques by only executing when the requirements are met.

For inter-process communication, the use of functions in the shm family can be observed. This is an old school technique butstill holds up well to this day. It is not as stealthy but is actually a lot easier to analyse compared to the methods used in Challenge 10 of Flare-on 2020.

The flag for this challenge is broken up into 5 parts, but there are only really 3 parts to the challenge as insinuated by the 3 .out files in the challenge folder.

Stage 0

Open up StrangeBattery on your disassembler of choice and navigate to Main.

  1. A url string "http://myfirst.dropper.simple" can be seen.

  2. A call to prctl(), where the caller is prevented from generating core dumps. [Orange]

  3. A call to basename(), where it retrieves the name of the executing binary. [Blue]


For [3], it compares the string to bAtt3ry and exits if it doesn't run.

        Steps so far:
        1. Change binary name to bAtt3ry if you renamed it.

Next, observe that we try to connect to the url in [1] at port 4444. Because the URL is bogus, we will never take the path.
However, it is useful to note how the created memfd is being utilized here.
I found the memfd + fexecve technique being explored here : https://github.com/AonCyberLabs/Cexigua, where the author introduces some other interesting techniques.


creates an anonymous file and returns a file
descriptor that refers to it. The file behaves like a regular file,
and so can be modified, truncated, memory-mapped, and so on.
However, unlike a regular file, it lives in RAM and has a volatile
backing storage. Once all references to the file are dropped, it is
automatically released.


memfd_create can be used to create a file descriptor (fd), and we can write into it like we do a usual file. Since it lives in the RAM, it never ends up on the file system.
fexecve works in tandem with the created fd, allowing us to execute a program in the same was as the functions in the exec family, but this time using a file descriptor.
In short, should the URL connect and an ELF is returned on the request, this binary will execute it. Understanding this concept will help students in the later stages when we manipulate the content dynamically. The binary moves on to try and open the "backup" files next.


This is uncharacteristic of a truly fileless malware, especially when the these ELF files themselves are not encrypted. In fact, the original plan was to encrypt all 3 files and pack them all into one. This idea was abandoned after realizing that the current challenge packs enough pain.
The runnerRoutine is called for each of the 3 stages, represented by ds1,ds2 and ds3. It opens the respective .out file, forks and have the child execute the it while the parent waits. This fork is necessary because
fexecve never returns (in fact, none of the exec associated calls -- execl, execv, execvp, etc. returns)

Stage 1

This is a small binary with some checks based largely on gethostname. So our aim in this part is to set our hostname accordingly to what is required to continue execution. By looking at the control flow back at StrangeBattery, we know our returning status must be 0x17.


Back in ds1.out, we discover there is indeed a branch that returns this value.


Backtracing the branch, we realize that it is checking for the result of the XOR of our binary name and hostname. Since our binary name is set to bAtt3ry, the hostname must be set accordingly to "bAtt3ry" XOR ")p:1gC:" , giving you K1NET1C. In the highlighted function, )p:1gC is XORed with another string ^@Mn>so to give the first part of the flag : w0w_Y0U.

        Stage 1:
        1. Change binary name to bAtt3ry if you changed it.
        2. Change hostname to K1NET1C


Stage 1 Afterthoughts

This kind of targeted malware is getting more and more common. Malware authors usually try to make seemingly innocent queries on the user to make surgical attacks. These also help to evade certain antivirus and sandboxing technologies.

Stage 2

The first thing that stands out here is a call to shmget and shmat. These are classic interprocess communication functions. You can read more about these functions at their respective manpages (https://man7.org/linux/man-pages/man2/shmget.2.html). These functions creates shared memory space using an arbitrary identifier, then attaches to it by allowing the kernel to map the memory area locally.


Later, a getenv for an environment variable is called, and if this CH3M1CAL variable is not set, the program will exit.


If it is set, then "CH3M1CAL" is set as an input argument. The contents of the environment variable is not used here. Notice that there will be a memcpy call to the shared memory. It will later be used as a parameter to a function that seems to be doing some crypto-related stuff.

Discovering the cipher algorithm

At this point, we are still not sure what the algorithm is doing, but with the large numbers of XORs, ORs, and shifting, we suspect this might be a hashing or encryption algorithm of some sort. Sometimes, if the algorithm is widely known and has some key identifying traits we can significantly reduce our reversing work. Some examples of this is trying to spot the S-box initialization for RC4, T-Box constants usage for AES.


When looking at the cipher algorithm, nothing immediately comes to mind, so we have no choice but to try and reverse the algorithm.

In ds2ctf's main(), we observe this huge number of bytes that is actually ascii-printable.


However, tracking this variable shows us that it is not used. Perhaps this is just a red herring. If you ran strings on the binaries, you might have found quite a number of these. They are not entirely useless, but for now we'll just take it that we're going in the right direction.


Observe that there are several constants being referenced from the .data section. We can try to google these values to see if they are cryptographic constants. A quick search suggests that these constants belong to the blowfish cipher. We found an implementation of the Blowfish cipher in C on Github. Although compiling it and comparing the resulting assembly did not give us a 1-1 match, there are some noticeable similarities in the code.

Reading up on the Blowfish cipher, as well as the github code, it is very possible that the function here is an initialization routine for the blowfish cipher, and the function that it calls also looks very similar to a blowfish encryption function, but with loops unrolled and perhaps with some other compiler optimizations in place.

There are multiple ways you can verify this - either by writing your own wrapper around ds2, debugging, or writing hooks. We will leave this as an exercise to the reader.

Later, it then does the shmget and shmat routine again, this time it is also copying the result into that region and then exits.


In essence, ds2 does the key initialization of the blowfish cipher with a given input.

Looking back at our main binary, we see that it reads from the shared memory to copy it to a local variable. This should be whatever we have memcpy-ed during the ds2 phase. This variable is then used as an argument for another function that is very similar to the suspected encryption function we saw in ds2. It follows up with a check using memcmp spanning across 8 bytes, and exits if it doesn't match. By what it looks like, it seems to be encrypting 0x00 bytes with CH3M1CAL as the key. If we left it as-is, it seems like it would just pass the check, so there is nothing we need to do here.


        blowfish_enc(0bytes, output, "CH3M1CAL")
        output == bb 44 61 c1 47 b3 73 7a


However, we run ds2 again, possibly to regenerate the blowfish keys. But since we didn't change the arguments, the output should be the same. Further down, we see that the CH3M1CAL environment variable is being queried again. This time, the contents of the variable are used to perform another encryption and checked. It should look something like this.


        blowfish_enc(<envvar_contents, output, "CH3M1CAL")
        output == f3 9d 68 86 a5 f0 62 ad


There is some error on Cyberchef's decryption of 8 byte long blowfish ciphers, so we can either compile our own or use another online blowfish decryptor.

https://webnet77.net/cgi-bin/helpers/blowfish.pl and http://blowfish.online-domain-tools.com/ came up after a quick search for online decryptors.

Trying to decrypt the 8 bytes of cipher text would give us L3AD@C1D. For the next part, we see that the flags will eventually give us the next 2 parts of the flag by setting an environment variable. We can set the environment variables accordingly and let it run, or we can just decrypt it ourselves. Strangely, the 16 byte decryption works on cyberchef so I'll just use that.

1b c5 f4 88 64 bb 1d 84 1b df ef 93 9a db ed 22

=> 31306f6b5f6c316b455f593075000000

=> 10ok_l1kE_Y0u

For the second part of the flag here, the contents of the variable are being used in a third run of ds2. This should create a new key. It does something like this:

        blowfish_dec(<encryptedContent>, partFlag2, "L3AD@C1D")

Trying it out again on Cyberchef gives us

52 5e 90 60 b9 d4 93 9f e9 1c 8c 77 b1 8f 2d c2

=> 5f684076335f35306d655f0000000000

=> _h@v3_50me_


And with the two parts of the flag recovered, it marks the end of this stage as we proceed to stage 3.


        Stage 2:
        Set environment variable "CH3M1CAL" to "L3AD@C1D"
        2 partial flags recovered:

Stage 2 Afterword and Thoughts

shmget and shmat has been used by several Linux malware in the early 2010s, and has recently popped up on my radar in the last few months. Still, the legitimate usage of shmget calls prevails to this day, and with that comes its own problems...

Stage 3

The third stage starts with a call to ds3, and it must be given at least 3 arguments, or else it will return immediately with 0x13.


Once again, we see another shmget and shmat call. The function appears to query for a shared memory region and testing for a 1 byte value. The child process later calls another function fcn.00001ca5. Depending on the outcomes of shmget and shmat, either a getegid() or getpid() function is called. The getgid() function is called when a variable is tested to be 0. After this operation, the child process exits.


At first, this seems a bit pointless - the child process only existed to call some informational function. But looking at the parent process, we see that the parent actually makes use of the syscall number SYS_getpid, SYS_getegid and SYS_getgid to return a different value. In essence, the child process is used to check shared memory and communicate the result back to the parent process surreptitiously.


In runnerRoutine, we are using the wait call to wait for the child process to finish executing. If you were to try this on your own test program, you will observe that the return value not the same as the exit status. This is because the return value from wait encodes both the exit status of the process as well as the reason the process exited.

The return value is encoded in the upper 8 bits, while the lower 8 bits encode the signal number that killed the process. If the process exited normally, then the lower 8 bits will be zero. Check out the <sys/wait.h> header and documentation page for additional information.

Continuing our reversing efforts, we can see that a different blob of data is passed into another cryptographic function. Either by comparing it to existing implementations of blowfish functions, debugging or hooking, we can deduce it to be a decryption function.


But this time, we are doing something funky with memfd and file operations, including what appears to be an egghunter and a subsequent write. If you have done your due diligence in the early stages of this challenge, you would find this somewhat familiar. Following the logic of this egghunt, we can see that when the bytes 0x9e3779b9 is found, we write the decrypted blob into it.

Egghunter in action:


Things are now slowly falling into place, as we once again go back to ds3 as it is called again. This time it takes a different path. It will be useful to debug this part of code as it is not easily statically analysed compared to the rest of the code.


There is a function that takes 0xffffffffffc02229 and adds 0x40000 to it, and then "returns" to that address. Hence, the "real" code flow leads to 0xffffffffffc02229 + 0x40000 = 0x2229. Right before this code blob, there is a binary blob that hinders the disassembly routine which actually turns out to be ascii-printable.


In the next function call, we see something similar. Here's how it looks like in the source.


By checking the hexdump, we once again see that the misleading disassembly actually never executes, and it is just another ascii blob.


The final function call does not appear in the decompiler at all despite appearing in the disassembler. YMMV with different decompilers.

As you follow along the function calls, you find another ascii-printable text.


Finally, you arrive at the function call where our egghunter in ds0 is searching for.

Looking at the disassembly tells you that your input will be xor-ed against a fixed string and passed into the shellcode that will eventually be written into the injection point directed by the egg.

Finally, going back to decrypt the shellcodes, we see one possible code blob that allows us to control the syscall number and several arguments.


Looking at the REBOOT syscall manpage, there are several values we need to set these values to.


With that, we have collected yet another piece of the puzzle. We should now have... w0W_y0U_10ok_l1kE_Y0u_h@v3_50me_p0T3nT1aL_eN3Rgy...? The final part of the puzzle lies right at the end of the injection area, and can even be found with strings. 5f696e5f796f7521 decodes to _in_you!

Stage 3:
Discover the last 2 parts of the flag.
Submitting the shasum as such sha256(w0W_y0U_10ok_l1kE_Y0u_h@v3_50me_p0T3nT1aL_eN3Rgy_in_you!) will get you the hashed flag for submission.


Stage 3 Afterword

This final stage took inspiration from a high profile malware family known as Gozi / Ursnif / ISFB /Saigon v3. In order to hinder reverse engineering, the functionalities of the malware was split up into a large number of different binary blobs and saved in the infected user's registry. In the real sample, the malware analyst would look forward to analyzing ~30 or so dlls, encrypted and packed to hinder static analysis.

Regarding the strange ptrace behaviours, you can look into the nanomites malware class.

With the number of well-documented writeups on this class of malware, I encourage the interested individual to take a deeper dive.


I took inspirations from my day to day work in DSO, and Fireeye's Flare-on 2020. I tried to make it more manageable by hiding among us references in places I thought would be helpful to took at. Overall, this should still present a tedious challenge. But I hope you walked away from this exercise learning something new.


Thank you for reading this writeup and for attempting my challenge! 😊