Sierra Flare
My journey with Flare-on 12.
The Flare
In September of last year, @crazyman told me about an ongoing reverse-engineering CTF. That was the first time I learned about Flare-On (and also that it used to be part of Mandiant and is now owned by Google). With the experience from last year, I decided to dedicate myself more to this year’s competition and see if I could achieve a good result. In the end, I completed all the challenges in 4 days, 21 hours, and 33 minutes.
This year’s problems seemed a bit easier, but they were still very interesting. Personally, I spent most of my time on Challenge 5 and Challenge 9. The ninth one, which is also the final challenge, wasn’t actually that complex in my opinion. However, the time required can vary greatly depending on a participant’s attention to detail and level of familiarity. Every small mistake, whether it’s a wrong decision or a script error, can appreciably increase the total time spent.
9 - 10000
An (semi-)auto-rev challenge.
As the name suggests, the 1GB challenge file contains 10,000 resource elements.
While the main logic is pretty straightforward, it only does four things without any obfuscation:
- open the
license.bin, which should be an array of 10,000[2-byte index][32-byte key]pairs. - Load No.n resource according to the index, and let the
_Z5checkPhfunction in loaded resource to check the 32-byte key. - If correct, for each loaded resource indexed x at this round, add
target[x]with round counter (resources can depend on each other). - Compare the final target array with expected array. If equal, use SHA256 of license.bin as AES key to decrypt the flag.
So we have two tasks:
- Reverse all 10,000
_Z5checkPhfunctions to get the expected 32-byte keys. - Recover the correct order of resource sequence to make the final target array equal to expected array.
At a glance, we might pick whichever task we want to do first.
However, after a closer look at some random resources, we find that each sub-check function is referencing a global variable which is actually the target[library index].
Note: The subcheck functions use a user-defined calling convention, and IDA will fail to guess the arguments, thus it will take a long time to decompile the
_Z5checkPhfunction.
We can automatically fix all subcheck function prototypes by the script:
1
2
3
4
5
6
7
8 import idautils, ida_typeinf # IDA 7.5+
idati = ida_typeinf.get_idati()
for ea, name in idautils.Names():
if name.startswith('_Z21f'):
decl = "void __usercall {}(_BYTE *ptr@<rcx>);".format(name)
tif = ida_typeinf.tinfo_t()
ida_typeinf.parse_decl(tif, idati, decl, ida_typeinf.PT_SIL)
ida_typeinf.apply_tinfo(ea, tif, ida_typeinf.TINFO_DEFINITE)
This means we have to first recover the correct order of resource sequence to make the final target array equal to expected array, and then handle those _Z5checkPh functions in the resources.
Recover the correct order of resource (calling) sequence
Here I describe the task from the reversed logic and asked ChatGPT-5 to solve this PPC-like problem.
1 | SEQ is a permutation of integers 0..9999 (length 10000). |
The SRC array can be easily recovered with dependency relation exported from pefile.
And GPT-5 immediately told us how to solve it with Möbius Inversion:
1 | def solve_seq(GOALS, CHANGES): # O(M) |
Now that we have the correct resource calling sequence, we can calculate the state of the global value (target array) before calling each _Z5checkPh function, it’s time to reverse all 10,000 of them.
It’s just 10,000 todos
Each resource is a PE file that contains a _Z5checkPh function and dozens of subcheck functions. But in general (as with a beginner-friendly challenge), there are only four types of checks:
- S_BOX transformation in subcheck functions.
- byte swapping in subcheck functions.
- A odd-mod-2^n exponent calculation in subcheck functions.
- A series of matrix operations in
_Z5checkPhfunction.
And all constants involved have the same relative offset with function start.
The only difficulty is the series of matrix operations; but again, with the help of GPT-5 we knew how to invert it:
1 | def invert_main(_const): |
Cat flag
With all the extracted constants and dependency relations, we calculated the calling sequence and reversed all 10,000 PE resources to obtain the expected 32-byte keys.
We then constructed the correct license.bin file:
And decoded the flag:
8 - FlareAuthenticator
This is a large Qt application with MBA obfuscation, but we were able to recover the correct passcode with dynamic debugging and a little observation.
(I saw some amazing writeups that use symbolic execution to solve it directly and even use symbolic composition to recreate the const generation logic, wow!)
After breaking the program, hit the press check button and after a few run-tail-return commands, we could see the check logic at 0x140021E29.
We then set a memory breakpoint to observe how the checked value was generated:
1 | keys = [unknown_value_between(0-9) for _ in range(25)] |
The algorithm seems clear enough and the unknown hash_x function has maximum 10 + 10 * 25 = 260 possible inputs. So I decided to extract all results of hash_x and see if we could solve it directly (without a deeper understanding of hash_x).
I added two breakpoints in IDA to dump the values of bucket_a and bucket_b:
1 | 0x140016772 # 0x140016B00 |
Before feeding all constraints to Z3, I asked GPT to see if it could have any suggestions and it provided a plan using heuristic DFS with pruning and can find the solution in a second:
1 | v = [[bucket_a[i] * bucket_b[i][k] for k in range(10)] for i in range(25)] |
Now we can let the program derive the key and decrypt the flag (without touching the MBA obfuscation).
7 - The Boss Needs Help
Here is where things started to get complex, we got a 4MB PE binary named hopeanddreams.exe, hmm, hopes.
Basically this is a RAT program and we also have the encrypted traffic between the RAT and its C2 server. So our task is to reverse the communication protocol (especially for key exchange) and decrypt the traffic data.
At 0x140081300 we can find unobfuscated logic of AES key = XOR(sha256(A||fmt(time())), sha256(C)), but expect there, the MBA obf (Mixed Boolean-Arithmetic Obfuscation) is everywhere:
But if we look closely, we will notice that MBA are just red herrings here, which keep reading and writing some global variables but are NEVER involved in the final calculation of enc/dec logic:
So again, I asked GPT-5 to write a taint analysis script to track the data flow starting from mov reg32, cs:{four global vars} and NOPed all tainted instructions.
This approach was based on the assumption that the final encryption/decryption logic does not depend on those four global variables or any MBA-tainted data.
After NOPing hundreds of thousands of instructions, I finally obtained clean logic without any (MBA) obfuscation, and now we are able to decode the encrypted strings like “https://www.youtube.com/watch?v=6O3MO2y30fU&…” or “cake is a lie“ (I did not expect to see my this year nickname here XD).
Of course, we were able to decrypt some initial traffic data, including the handshake:
1 | cipher = bytes.fromhex('e4b8058f06f7061e8f0f8ed15d23865ba2427b23a695d9b27bc308a26d') |
We then set the AES key to XOR(SHA256(peanut06), SHA256(TheBoss@THUNDERNODE)).
Repeating this for three more key-update rounds (XOR(SHA256([newkey]06), SHA256(TheBoss@THUNDERNODE))) allowed us to decrypt the entire traffic.
The following traffic transferred an encrypted zip file:
And at one of the last packages, we can find the password list:
1 | Email: BornToRun!75 |
We now had everything to help The Boss.
6 Chain of Demands
Again a pyinstaller packed binary, decompress and drop it to https://pylingual.io/ to get the readable source code.
This looks like a crypto challenge, which GPT is good at, and it turns out GPT can cook it without a single interaction:
1 | ... |
5 ntfsm
A maze challenge, simply export the .S file from IDA, build graph with regex, and find the longest path in the DAG.
1 | import re |
4 Unholy Dragon
Rename the file to UnholyDragon-0.exe and run it until it stops generating new files.
Apply the diff back, and the flag pops out.
3 pretty devilish file
A challenge to deal with some PDF magic, throw it to GPT and retrieve the flag.
2 project chimera & 1 DrillBabyDrill
You didn’t come here for these two, right?
End
It’s time to plan a trip to Seattle and treat myself to a Black Forest cake.