FlareOn 2025 writeup

Challenge 1

Challenge1 was a python script; with a XOR of encoded bytes that were using bear coordinates; I just bruteforced it with the below script:

def GenerateFlagText(sum):
    key = sum >> 8
    encoded = b"\xd0\xc7\xdf\xdb\xd4\xd0\xd4\xdc\xe3\xdb\xd1\xcd\x9f\xb5\xa7\xa7\xa0\xac\xa3\xb4\x88\xaf\xa6\xaa\xbe\xa8\xe3\xa0\xbe\xff\xb1\xbc\xb9"
    plaintext = []
    for i in range(len(encoded)):
        char_value = encoded[i] ^ (key + i)
        # Check if the character is ASCII (0-127)
        if char_value > 127:
            return None
        plaintext.append(chr(char_value))
    return ''.join(plaintext)

# brute force bear_sum
for test_sum in range(1, 200000):
    flag = GenerateFlagText(test_sum)
    if flag is not None and ("flare" in flag.lower() or "flag" in flag.lower()):
        print(test_sum, flag)

Flag was drilling_for_teddies@flare-on.com ; Interesting to note is that multiple coords sum work.

Challenge2

This one was fun! We have a marshalled + zlib +baseN encoded bytestring that runs some authentication;

I used dis.dis to disassemble the code object in an interpreter as well as the nested one to figure out what was going on. Here is a paste of the bytecode for the second nested marshalled object:

We see we have some kind of xor key+42 on the researcher signature for the username to get authenticated. I wrote a bruteforce ascii script:

And we get:

G0ld3n_Tr4nsmut4t10n as a password

Nice! Next step is to mock our os.getlogin() with that username (I actually ran the bf script again): I used an llm to put the pieces together into a final script:

And I got the flag!

Challenge 3

So this one was a real pain. I must say I really did not enjoy it...

We get a broken pdf ; after a lot of back and forth, I manage to fix it (mostly comparing to a valid pdf, fixing missing endobj etc. With a more or less fixed pdf, I ran pdf-parser from Didier Stevens

And we get a hex suite; we throw that in cybercheff and notice the JIFF header. I extracted that to a jpeg that was basically just pixels on a grey scale and that's where I really didn't like the challenge. I basically assumed this was junk and kept trying to fix the pdf for hours. Then it hit me (and mostly because I vaguely remember doing something like that in a previous ctf). This could be steganography; something of the sort each pixel is a byte/bit that we need to assemble back to ASCII... I made a script to do that; tried a bunch of different things and go figure! Each grayscale pixel corresponds to an ascii char and we get the flag!

=== grayscale_ascii === Puzzl1ng-D3vilish-F0rmat@flare-on.com

Challenge 4

Ok for this one; we get a broken PE file; Opened it in 010 editor and turns out the MZ header is missing; after patching it in a hex editor, we get a valid pe32 file.

Running it, we'll see the file basically copy itself a bunch of times in the same directory and incrementing it's file name. It werfaults at UnholyDragon-154.exe ; we quickly see in the file Manifest that it's compiled with TwinBasic which is basically similar to VB6 except it doesn't interpret but rather compiles to native assembly. At first I thought I was supposed to open a TwinBasic IDE and debug this somehow but nope... So I compiled a helloworld twinbasic, generated a flirt lib and diffd on functions we don't see in both binaries. That was a bit of a waste of time.

I then Xrefd CreateProcessW and GetModuleFileName and found a big subroutine that has the main logic for the binary; we see it check it's name and basically increment over and over. When debugging this in x32dbg I was hitting an exception and crashing the program. At first I thought this was anti-debug so I used Frida to instrument interesting Windows API calls. One thing then stood out: The parent process was reading exactly one byte from it's child and then writing 1 byte to it. At first I thought I was supposed to read all these bytes and make some Ascii string/flag out of these so I wrote the below Frida to log the reads and writes:

Running the script, we get:

That was super slow and the written bytes weren't encodable to text. That's when I got another idea; what if the binary was patching itself and slowly fixing itself/unpacking into something else? I took the original binary we get with the challenge (with the fixed MZ header), renamed it to it's original file name from the VersionInfo: UnholyDragon_win32.exe and ran it; When it hits UnholyDragon-140.exe we see some forms/window pop ups showing up but nothing in there. Seems like the theory I had was correct. At UnholyDragon-150.exe we get another broken exe; Again, we patch it's MZ header, run it and...

Nice! We get the flag! dr4g0n_d3n1al_of_s3rv1ce@flare-on.com

Challenge 5

Ok so this one was rough (and fun)! We get a x64 binary; ntfsm.exe ; try to input a password, it spawns itself as a subprocess a bunch of time and outputs "wrong!" to the console ( it also tells us the password needs to be 16 characters long). So the binary has a ton of jmp instructions which make it hard to analyze in ida. You can disable "agressively make thunks of jmp" or whatever the instruction name is in IDA to speed up analysis but it will probably still hang and you won't be able to graph or decompile functions with so many thuncks. I read on discord someone had a lot of success with cutter + ghidra so I thought I'd try that. We open the binary in cutter; look-up the string "wrong!" and xref it; That drops us in a huge function with a bunch of outcomes and a "correct!" string;

We also see a couple of interesting strings: "A strange game. The only winning move is not to play" "state" "input" "position" "transitions"

We also note that there's a bunch of large switch case/jmp tables and a large number of blocks checking if a variable equals an ascii character. Below is an example of such a case block:

Ok so these blocks do basically the below in pseudocode

Another thing I noticed was the state blocks almost all have the same structure highlighted below:

This is important for later because it gives us some nice byte patterns to search to find all the states.

From the above, we know we're dealing with a finite state machine and each character in the password is a state transition. Running this while logging it in procmon shows the binary call CreateFile,WriteFile and ReadFile on NTFS Alternate Data streams. I worked on this challenge with carbon_xx whom I met on the OALabs discord and he wrote a usefull python script to monitor these data streams which was way better than what I had (Apimonitorx64) Below is the script:

And it's output on an example run:

A couple of things that are interesting: position is incremented every time we check a password character

input doesn't really matter

state is changed if we give the right character to move from StateX to StateY

So my theory was that we'd need to find 16 transitions from State0 to StateN in order to find the password. Going back to our main function with the string "wrong!" which has the switch case table: I had some interesting findings using the jsdec decompiler and ghidra decompiler embedded into cutter. Both had slightly different info usefull to our analysis: JS dec: JSdec gave a nice overview of what was basically going on. It's not necessarily mandatory but helps.

Ghidra:

So Ghidra actually helped me quite a bit. At first, I was just trusting jsdec. I drew a mermaid diagram of states and transitions and got the following: I noted the following states (they have numbers but are not necessarily sorted in order):

This was a good start to get an idea of what the FSM looked like. However, we see we're missing most of the States as we've established before there's thousands of states and transitions. I initially wrote some python to find all states but then I noticed I was missing the most important thing: I knew StateN had x transititons that allowed it to move to some other state but in my final txt, I didn't have the mapping between the states I had parsed and the state they were transitioning to. In other words, my output looked something like this:

That's when I noticed in both the ghidra output and cutter disassembly, we had a comment telling us where the switch case jump table was:

Going to this address in a hexdump view, we see the following by:

That 0x140860241 address is the address of our first state and so on. That gave me the last piece of the puzzle. Putting these things together, I wrote the below script to dump out the full FSM (the script dumps other heuristics as I initially had other theories around how to find the path):

Ok so this script does the following:

  1. Find all states by looking for the bytes for cmp rax, 0x12ad1659

  2. Find the first rtdsc by going two rtdsc over the address we found in step 1.

  3. Make that the address of UnknownStateN

  4. Disassemble untill we hit the bottom rtdsc

  5. find all cmp byte [arg_3bb84h], <someascii> and write these down as transition characters for this state.

  6. je to transitionN and write down all of these for that state

  7. Count how many states we have

  8. Go to the address of our jump table 0x140c687b8

  9. Parse out all the 4 byte addresses and write these down as StateN (renaming all the UnknownStates we got in Step3)

Now we dump a nice json dict to fsm_out.txt that looks something like:

Now I thought I could try to find the longest path I can reach from State0 and that would likely be my password! I wrote another script that takes fsm_out.txt as an input to find the 100 longest paths:

The above outputs:

Going to 0x14018c9e0 we see the fsm dumper failed to find the last transition/state for some reason. We can see in the disassembler that there's only one ascii cmp instruction for Q

One thing this challenge taught me is that having multiple tools in your arsenal is pretty important. Ida was failing and jsdec and ghidra both had some pieces of the puzzle. Not relying on one approach is key!

Challenge 6

This time, we get a 64 bit ELF binary.

We pop it in IDA and see a bunch of pyinstaller related strings. I just threw pyinstextractor ( https://github.com/extremecoders-re/pyinstxtractor ) and got some extracted pyc files and an interesting chat_logs.json . It seems like Part of the chat is using LCG-XOR encryption (and we have the plaintext) and the rest is encrytped with RSA and we don't have the plaintext which we probably need to recover!

I wanted to decompyle the file challenge_to_compile.pyc ;

uncompyle6 and decompyle3 didn't work for me and the same went for python -m dis but after compiling pycdc, I got:

Note the two byte blobs which are EVM bytecode.

We run a script on chat_log.json to recover the keystream:

When run, we get:

Couple of interesting things we can gather from the python we got earlier:

  1. The RSA key is weak because it's composed of 8 × 256-bit primes (total ~2048 bits)

  2. The primes come from a predictable LCG (Linear Congruential Generator) from which we just recovered the key stream

  3. We probably need to factor the RSA modulus to get the private key and decrypt the messages

I also disassembled both contracts at https://ethervm.io/decompile but didn't find the output super helpfull;

One thing I thought of doing before going down the factorization approach was try to find the hostname as that's used to init the LCG. After some extensive grep + strings, I couldn't find anything. For the RSA factorization, I used [factordb](https://factordb.com/index.php?query=966937097264573110291784941768218419842912477944108020986104301819288091060794069566383434848927824136504758249488793818136949609024508201274193993592647664605167873625565993538947116786672017490835007254958179800254950175363547964901595712823487867396044588955498965634987478506533221719372965647518750091013794771623552680465087840964283333991984752785689973571490428494964532158115459786807928334870321963119069917206505787030170514779392407953156221948773236670005656855810322260623193397479565769347040107022055166737425082196480805591909580137453890567586730244300524109754079060045173072482324926779581706647) to factorize RSA's n.

After a bit of drafting python, experimenting and some llm back and forth, I got:

Running the above, we get:

Challenge 7 - NOT SOLVED!

Disclaimer I never finished challenge 7 from lack of time; below are a couple of my notes I'll leave here just to illustratre the thought process. I went a bit further with Windbg TTD and got a good handle of where the decryption was happening for the first packet but never got around to writing a decryptor for the packets and solving the challenge.

We get a cpp x64 binary and a pcap http capture. Following the http stream we get:

First interesting thing when opening the binary in ida was being greeted with:

Looking at strings we see rustc-hyper which is an http rust library; huh so maybe this isn't a Cpp binary even though detect it easy was pointing to msvc as the compiler. I had no idea but it seems like you can compile rust with msvc? In retrsopect, this was a red herring and we were dealing with Cpp

When we run this in x64dbg; we notice we hit a TLS Callback; in IDA:

The file in VT is interesting https://www.virustotal.com/gui/file/14e60fb48803c06762b4fffdd4e0a2bd2bcac7ae81c92d7393f1198951dbfbbb/behavior we see it drops a .cab with a broken bmp called doublesuns.bmp ( https://www.virustotal.com/gui/file/ea71829a0c7072e4bdda5df1bd1ee044b916cf9dfaf04469a962af7027d339f8/content )

We can trace from that main TLS callback like so:

mainTLS callback -> TLSCallback1 -> Safe Exception Handler -> Main obfuscated function

I ended up using a TTD trace in IDA. One tip that helped was using the new IDA9 shortcuts; I set a shortcut for backwards step into and backwards step over. Then traced from ws2_32.dll send function's buffer to see how the Bearer token was made. With no deobfuscation, this took a while... Finally, after A LONG time tracing this buffer, I found a call that modified it into the bearer token. Decrypted, it looked like:

After going through the function, we get:

That's probably why we got a different bearer from the one in the pcap. We have a different path?

Last updated