Project notes
Authoring the 2026 JCC Reverse Engineering Challenges
Forward engineering, implementation, and intended reversing paths for the JCC 2026 Reverse Engineering challenge set.
Authoring a challenge means I should not only develop the challenge, but also make sure that participants can learn something from it. So, here we are.
For JCC 2026, I somehow ended up authoring the full set of Reverse Engineering challenges:
| Challenge | Difficulty | Artifact |
|---|---|---|
Shuff / rev-101 | Baby | Android APK |
Sudoku | Easy | Windows PE |
Calcluator | Medium | LuaJIT bytecode |
TinyTrace | Hard | Windows x64 PE |
This page shows both sides of each challenge:
- Author view — why I made it that way, what I wanted participants to learn, and how I built the core mechanism.
- Participants’ view — what participants receive, what they are expected to notice, and how the challenge can be solved.
The full PoC already explains how to solve the challenge, but I also wanted to show the other side: how I built the challenge and why I made some decisions in the first place.
So, the point of posting this project is to keep the page readable while still showing how each challenge works from both directions. The complete PoCs, scripts, screenshots, and distributable artifacts are kept in the repository.
Design Approach
I did not start this challenge set with some fancy framework or anything like that. Most of the time, I just kept asking myself:
What should the participant understand before the challenge becomes easy?
For Baby and Easy, I wanted the foothold to be visible enough so participants would not get stuck before they even started. For Medium and Hard, I wanted the difficulty to come from connecting more things, not just from adding more random transformations or making the code ugly for no reason.
In the simplest way, I saw it like this:
Author:
idea -> mechanism -> artifact
Participant:
artifact -> observation -> mechanism -> flag
The challenge only works if the second direction still makes sense.
Shuff / Rev-101 — Baby
The idea behind this challenge was pretty simple. Knowing that many participants were newcomers, I wanted it to work as a first Reverse Engineering challenge without requiring assembly knowledge, but still not something that could be solved by simply running strings.
The main thing I wanted participants to learn was also pretty basic: open an unfamiliar artifact, decompile it, follow the logic, and reconstruct the secret.
Forward Engineering View
To keep this challenge “baby” enough, I stored the flag characters in a shuffled array, then kept the correct reconstruction order somewhere else in the APK.
So the full chain was:
flag
-> split into characters
-> shuffle
-> store order separately
-> build APK
I chose Android because tools such as JADX can bring participants back to readable Java code quickly. I thought this was a nicer first step than directly throwing them into assembly.
The important logic lives in two classes: SecretData and IndexMap. SecretData contains the shuffled characters, while IndexMap.ORDER contains the order used to reconstruct them.
Conceptually:
for (int i : IndexMap.ORDER) {
sb.append(ASCII[i]);
}
The flag is not stored as one readable string, but once participants find those classes, the logic should be pretty clear.
What I Wanted Participants to Notice
I wanted the first useful clue to come from navigating the decompiled application, not from guessing.
The expected flow was something like:
MainActivity
-> input checker
-> SecretData
-> IndexMap
So the participant can slowly follow the program instead of getting hit by a wall at the start.
Participants’ View
The distributable artifact is shuff.apk, which can be decompiled using an online decompiler or simply opened in JADX.
The intended path was:
APK
-> decompile
-> follow the input checker
-> SecretData
-> IndexMap.ORDER
-> reorder characters
-> flag
The participant does not need to understand Dalvik bytecode, JNI, native libraries, or debugging.
The main thing I wanted participants to learn here was simply that an APK is not a black box. It can be opened, decompiled, navigated, and understood.
What I Did Not Put In
For this challenge, I intentionally avoided things like:
native libraries
heavy obfuscation
reflection tricks
dynamic code loading
anti-debugging
fake flag paths
Those can be interesting for harder Android RE, but for a Baby challenge I think they would just make the first experience more annoying.
Difficulty Justification
There is only one main mechanism, the relevant classes are intentionally discoverable, and no native code or debugging is required.
Participants still need to notice that the characters are shuffled and the order is stored somewhere else, but after that the solve is pretty short.
Proof of Concept
See the repository for the complete arrays, solver, screenshots, and original participant APK.
Sudoku — Easy
For Sudoku, I wanted a native Windows challenge that still had a clear foothold. The visible program is a normal Sudoku game, but completing the puzzle does not directly reveal the secret.
Here is the idea:
Sudoku game
-> completion
-> hidden condition
-> secret decoder
Compared with Shuff, participants need to look a bit deeper. The visible application does something normal, but there is another path hidden behind it.
Forward Engineering View
The secret path is guarded by a hidden score check:
if (secret_score == 1337)
reveal_flag();
else
show_normal_win();
The normal game never naturally reaches the secret score, since each correct answer only contributes to the normal scoring flow.
The flag itself is stored as encoded bytes and recovered through a short transformation chain:
encrypted bytes
-> reverse buffer
-> XOR using a score-derived key
-> swap nibbles
-> flag
I kept the transformation small on purpose because the point was to find the hidden path, not to turn it into a crypto chall thingy.
The decoder is there so finding the hidden branch is not immediately the end. Participants still need to understand what the reveal function actually does.
Why Sudoku?
Sudoku is already familiar, so participants do not need to spend too much time understanding what the application is supposed to do.
That leaves more room to think about things like:
What happens after completion?
Why is there a secret-related message?
Where does that message come from?
What condition leads to another branch?
The game is basically the visible surface. The reversing part starts when participants stop treating that surface as the whole program.
Participants’ View
Participants receive Sudoku.exe.
The intended solve starts from the program behavior itself:
complete / inspect the game
-> notice "Secret reward unavailable."
-> search the string in IDA
-> follow its xref
-> find secret_score == 1337
-> follow reveal_flag()
-> reproduce the decoder
-> flag
This is where strings and readable control flow become useful footholds rather than accidental weaknesses.
The full solve can be done statically with IDA Free, so no debugger is required.
Footholds
One thing I started thinking about while making this challenge was footholds.
For an Easy challenge, I do not think every readable string or obvious branch should be treated as a weakness. Sometimes that is exactly what helps participants know where to look next.
In this challenge, the completion message is one of those footholds:
interesting string
-> xref
-> nearby condition
-> hidden function
Without something like that, the same challenge could become harder, but not necessarily better.
What I Did Not Put In
I also avoided making the decoder too complicated.
No:
custom encryption
huge lookup tables
anti-disassembly
control-flow flattening
fake branches everywhere
The intended difficulty should come from discovering the hidden condition and following the reveal path.
Difficulty Justification
The participant still needs to perform native static analysis, but the path is short and discoverable:
string -> xref -> hidden condition -> decoder
Compared with Shuff, there are more things to connect, but not enough to require understanding a large part of the program.
Proof of Concept
See the repository for the complete IDA analysis, decoder stages, encrypted bytes, solver, and screenshots.
Calcluator — Medium
Calcluator was actually the first challenge I built.
Why Lua? Honestly, Lua just appeared in my search results again after quite a while for no particular reason, and I thought, why not :v
I also liked the idea that participants would not immediately jump into the usual PE or ELF disassembler flow. Instead, they first have to realize that the artifact is LuaJIT bytecode and figure out how to inspect it.
The visible program is only a simple integer calculator, but every valid operation also updates hidden internal state. So even though the arithmetic itself is simple, the state behind it is not really that simple.
The basic idea was:
operation
-> visible answer + hidden state
-> checksum requirement
-> diagnostic
-> secret
Forward Engineering View
The program keeps four important values:
ans
state
step
bad
Each valid calculation updates the visible answer and the hidden state, then generates a signature from the current operation, operand, previous result, and step.
The validation logic is roughly:
x = (value + tag * 257 + step * 911 + (before % 65521) * 13) % 65521
sig = (x * 251 + (result % 65521) * 17 + state) % 65521
Each step has its own expected checksum. If the generated checksum does not match, bad increases.
The hidden diag command only succeeds when:
step == 6
bad == 0
I also tied the final calculator state to the decryption seed, so getting the right sequence is not only about passing the validation. The resulting state is also needed to recover the secret.
From the author side, the chain was roughly:
choose intended operation sequence
-> simulate answer + hidden state
-> generate expected checksums
-> store checksum targets
-> derive the final decryption state
-> compile to LuaJIT bytecode
Why Make It Stateful?
I could have made the challenge use one long transformation or one suspicious encoded blob, but I wanted participants to understand behavior across multiple actions.
So instead of:
input -> transformation -> compare
it becomes more like:
operation 1 changes state
operation 2 uses the new state
operation 3 changes it again
...
final state is used again
The point is not really the math. The point is realizing that one operation affects what happens later.
Participants’ View
Participants receive the compiled LuaJIT bytecode instead of the original Lua source.
The intended solve is:
identify LuaJIT bytecode
-> inspect strings / bytecode
-> discover hidden diag command
-> reconstruct the state and checksum logic
-> solve the six checksum targets
-> reach the correct final state
-> decrypt the secret
The recovered operation sequence is:
+ 23
* 7
- 19
% 97
+ 58
* 2
The important part is realizing that the value printed by the calculator is only one part of the state that matters.
Where I Expected Participants to Spend Time
I expected most of the work to happen around these three parts:
1. understanding the LuaJIT artifact
2. reconstructing the state / checksum logic
3. solving the valid six-operation sequence
The final decryption is not supposed to be the hardest part. Once the state model is correct, the rest should follow.
What I Did Not Put In
I did not want the LuaJIT format itself to become the whole challenge.
The idea was not:
"good luck understanding this weird bytecode forever"
but more:
"can you recover enough logic from this bytecode to model the program?"
I also avoided making every operation use a completely different transformation, because that would mostly add more work without adding a new idea.
Difficulty Justification
I considered this Medium because participants need to connect multiple operations, understand how the hidden state changes over time, and then use that final state again for decryption.
Unlike Sudoku, there is no single xref that gives most of the answer. Participants need to build a model and check that the model is actually correct.
The operations are simple, but the relationship between them is what makes the challenge more involved.
Proof of Concept
See the repository for the complete LuaJIT analysis, checksum targets, intermediate states, solver, and flag decoder.
TinyTrace — Hard
TinyTrace came after the PO asked whether I could also make a Hard challenge.
Instead of just adding more transformations or making the validation code ugly, I tried to make the difficulty come from the program structure itself.
The same executable can run in two roles:
tinytrace.exe
|
+-- parent
| |
| +-- pipes
|
+-- tinytrace.exe --worker
|
+-- validator
The first thing participants need to notice is that the visible parent process is not where the interesting validation happens.
Forward Engineering View
The parent process handles the user-facing side, creates pipes, and launches another copy of the same executable with the --worker argument.
The worker is where the actual validation logic lives.
It reads a 32-byte candidate and validates it using four important pieces of data:
KEY
ADD
ENCODED_EXPECTED
PERM
The expected values are decoded first, then each character is checked using a small reversible relation:
EXP[idx] = (input[idx] ^ KEY[idx]) + ADD[idx] mod 256
which can be inverted as:
input[idx] = ((EXP[idx] - ADD[idx]) mod 256) ^ KEY[idx]
PERM only changes the order in which positions are checked. It does not change which KEY, ADD, or EXP value belongs to each position.
From the author side, the flow was roughly:
flag
-> choose KEY / ADD
-> compute expected bytes
-> encode the expected table
-> choose PERM
-> move validation into the worker
-> connect parent and worker with pipes
-> compile PE
The goal was not really to make the final math hard. I wanted participants to understand the program architecture first before the final validation became obvious.
Why Split It Into a Worker?
If I put all validation directly inside main, the solve could become:
find checker
-> recover arrays
-> invert equation
-> done
That is still a valid challenge, but it would feel pretty similar to many normal byte-checking binaries.
By putting the real validator inside another execution mode, there is another thing participants need to understand first:
parent
-> process creation
-> worker argument
-> pipe communication
-> actual validation
So the hard part becomes figuring out where the important logic actually lives.
Participants’ View — Static
Participants receive only tinytrace.exe.
Some useful clues are already visible in the imports and argument handling:
CreatePipe
CreateProcessA
ReadFile
WriteFile
--worker
Those clues should make participants question whether there is more than one execution path.
The intended static solve is:
inspect imports / argv handling
-> notice --worker
-> find worker validation
-> recover KEY
-> decode EXPECTED
-> identify ADD and PERM
-> realize PERM only affects check order
-> invert each position
-> flag
Once all of the data is identified correctly, the final solver itself is actually pretty small.
That part is intentional. I wanted the “Hard” part to happen before the equation, not inside the equation.
Participants’ View — Dynamic
The challenge can also be approached dynamically with WinDbg.
A possible path is:
follow the child process
-> break at the comparison
-> read idx / ADD / KEY / EXP
-> patch the failing conditional jump
-> let all 32 checks run
-> sort observations by idx
-> invert
I liked keeping both approaches possible. One participant might prefer reconstructing everything statically, while another might prefer looking at the values directly at runtime.
The PERM Thingy
One part I expected to be confusing is PERM.
At first, it can look like the permutation changes the relation between the key, addition table, expected bytes, and input position.
But it does not.
wrong idea:
PERM changes the data relationship
actual idea:
PERM only changes which index is checked first
Once that is clear, the final inversion is much more straightforward.
What I Did Not Put In
I did not add custom VM stuff, anti-debugging, packers, or intentionally broken decompilation.
Those things could make the binary harder, sure, but they would also move the challenge away from what I actually wanted participants to understand: process architecture and data flow.
The validation math also stays reversible on purpose.
Difficulty Justification
I considered TinyTrace Hard because the final byte equation is not really the hard part.
Participants first need to understand:
which process matters
how the processes communicate
where the validation data comes from
what each table means
which data changes the value
which data only changes the checking order
The challenge becomes much simpler only after that bigger mental model is correct.
For me, this was also the biggest difference between making something “Hard” and just making the code more confusing.
Proof of Concept
See the repository for the complete static analysis, validation tables, Python solver, WinDbg commands, register mapping, and screenshots.
What Changed Across the Set
Looking at the final set, each challenge asks participants to understand a slightly bigger part of the program.
| Challenge | Main Observation | What Needs to Be Understood |
|---|---|---|
Shuff | Data is shuffled and reconstructed elsewhere | One direct reconstruction path |
Sudoku | Normal completion is not the secret path | Hidden control flow |
Calcluator | Visible answer is only part of the state | State across multiple operations |
TinyTrace | Validation lives in another execution role | Process architecture + data flow |
This progression became more important to me than simply counting how many transformations each challenge had.
A Baby challenge can still hide something. An Easy challenge can still require xrefs and static analysis. A Medium challenge can use simple arithmetic if participants need to model state. A Hard challenge also does not need some fancy custom VM if participants need to understand a bigger part of the program first.
What I Would Change Next Time
Probably playtesting.
I understood the intended solve path because, well, I made the challenge :v
That also means it is very easy for me to think something is obvious when it might not be obvious at all for someone opening the artifact for the first time.
If I make another RE set, I would like to ask people from different skill levels to try it before I finalize the difficulty:
newcomer
intermediate solver
strong RE player
Then I can see things like:
Is the first foothold actually discoverable?
Is there an unintended shortcut?
Does the challenge test what I wanted it to test?
Is the difficulty coming from reasoning or just annoying friction?
I would also like to explore more kinds of reversing later. This set is still pretty close to my own comfort zone: APK, PE, LuaJIT bytecode, small transformations, and normal userland programs.
Maybe I am a bit of an old soul when it comes to RE, hehe.
But for this set, keeping the mechanisms relatively small also helped me focus on what each challenge was actually supposed to teach.
Closing
In the end, the set ended up covering different kinds of reversing:
Shuff
-> decompile and reconstruct
Sudoku
-> follow a hidden control path
Calcluator
-> model state over time
TinyTrace
-> reconstruct execution architecture and data flow
I did not build them in this difficulty order, but somehow the final set still formed a progression from a small reconstruction task to a challenge that requires understanding a bigger part of the program.
This project page only keeps the important author and participants’ views. The complete technical PoCs, original distributable artifacts, solver scripts, and screenshots are kept in the challenge repository.