GPN - 'Honeypot'
Written: 2025-07-09
I've just bought this property in a very priviledged part of the system.
But there seem to be(e) awfully many bees around. I just hope I can find a way out of this thing the developer has constructed here before I get stung...
Solver: computerdores, sohn123
Categories: rev
Flag: GPNCTF{on_a_scale_from_1_to_10_h0w_WOULd_yOU_r4t3_yOUr_t00lIN6?}
Writeup
Run Script
The first thing we can look at is the running.md
and the run.sh
:
#!/bin/bash
&
From the running.md
we know that the run.sh
is supposed to be invoked with honeypot.jar
as its first parameter.
Looking at the run.sh
we can see that it first invokes the honeypot.jar
as a background process and then waits for another process to be killed. After the process has been killed, the script then repeatedly asks the user to select one out of four programs to be executed on the flag file (head, tail, cat, or base64).
Java Code
After decompiling the honeypot.jar
we can see that its entrypoint calls the run
method on the Honeypot
class:
public static Object throws IOException, NoSuchAlgorithmException
This function does three things:
- It kills the process that the run.sh is waiting on,
- starts a Berkley Packet Filter (BPF) program,
- and observes fields of that program during its execution and modifies others based on their values.
Futher, looking at the HoneypotImpl
class we can see the plaintext source code of the BPF program that is started there. It consists of a header file with type definitions and a source file with the implementation of the program.
BPF Program
Given that the plaintext source code is available to us, we only need to work through it and assign sensible names to get a better understanding of what it does.
After doign that, the first thing we will look at is this syscall handler:
int
Whenever a program tries to open the flag file, this handler maps the name of the program to a value from the OP_CODE enum. Each of the four programs which can be invoked by the user in run.sh
have a separate enum value and represent a different operation, all others get mapped to OPC_NONE
.
Finally, it calls exec_op
with the enum value representing one of the four possible operations.
exec_op
The function mainly operates on two arrays var3
and var4
.
Furthermore, it keeps a counter of how often it has been executed, which we called current_step
.
Those two arrays together with the current_step
counter constitute the main runtime state and are modified by the method in every execution.
Another field it interacts with is the key_src
array. In every iteration one value is written into this array at the index current_step
. This array's content will later be used to compute the key for decrypting the flag.
This is what the exec_op
function looks like:
s32
It first computes the current row, which is just the smallest positive number for which var3
and var4
have different values.
Next, it sets key_src
at the index current_step
to a value (here called character
) calculated from the values of var3
and var4
at the current row.
It then modifies var3
and var4
based on the current row and the operation it was called with.
Last but not least, it does two checks:
- If it has reached the 64th row and the
character
is 1, it sets a flag which triggersHoneypot.run
to compute the decryption by hashingkey_src
. The resulting key is written tokey
. - If the field of
var4
which was modified by the operation does not differ from the value invar3
at the same index, another flag is set which causesHoneypot.run
to abort execution and report failure.
This same flag for triggering failure is also used to abort if current_step
reaches 500.
The value of key
is used in a syscall handler for reads of the flag file to decrypt the content of the file and return the plaintext instead of the ciphertext actually contained by the file.
With this knowledge of what the program does we can now implement a solver.
Solver
To get the flag we need to find a sequence of operations which is valid in every step and ends at the 64th row with the character
value being 1.
At this point we thought the decision tree for choosing the operation sequence to be quite slim, because we believed that the specific transformations applied by exec_op
to var3
and var4
would very often cause one of the validated conditions to fail. For this reason we opted to implement a very simple depth-first search solver.
The solver traverses the decision tree and applies the appropriate transformations to the state. When it encounters a state which would cause exec_op
to trigger failure it abandons the entire subtree as exec_op
would trigger failure in that case. This is the final solve script:
= 0
= 1
= 2
= 3
= 4
=
=
=
=
= - 1
# Init stuff
= * 500
=
=
= ^
:
:
:
:
=
=
=
=
return
return 0
=
= 0
<<= 1
&=
+= 1
return 0
return
return &
=
=
=
=
return None
=
+= 1
= None
=
=
=
:
:
= - 1
=
|=
=
:
= + 1
=
|=
=
:
=
= |
:
=
= | >> 1
:
continue
:
return
continue
=
break
return
=
=
=
=
=
Running this solver will immediately output the following flag, proving our hypothesis that the tree quite slim:
> python solve.py
GPNCTF{on_a_scale_from_1_to_10_h0w_WOULd_yOU_r4t3_yOUr_t00lIN6?}