Trending Tags
Tio's Bell
Tio’s Bell
Author: Sagnik
This challenge provides a Linux x86-64 ELF binary which appears to be a laboratory monitoring program.
The binary contains several chemistry-themed functions, a custom decoder, a state machine, and a final flag-generation routine. The intended solve is to reverse engineer the important execution path rather than simply searching the binary for the flag.
The overall solve path is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
ELF
↓
main
↓
dispatcher
↓
crystallize()
↓
custom decoder
↓
hidden clue
↓
bell/state-machine sequence
↓
target resolution
↓
FNV-1a
↓
LCG + XOR
↓
FLAG
Firstly, identify the binary
Run:
1
file lab_monitor
The binary is a Linux x86-64 ELF.
It is useful to inspect its strings as well:
1
strings -a lab_monitor
There are several laboratory-themed messages and diagnostic strings.
The binary also contains the flag format:
1
CYS{%s}
but the actual flag is generated at runtime.
Find the main function
Open the binary in Ghidra and follow the ELF entry point until the call to __libc_start_main.
This leads to main.
The main function initializes the program state and then passes it through a dispatcher which calls several themed stages.
The interesting stages include functions associated with:
1
2
3
4
5
purity
temperature
yield
crystallize
sensor
Most of the early logic is useful mainly for reconstructing the intended execution path.
Follow the crystallize stage
The interesting function is the crystallize() stage.
It processes a 50-byte encoded blob using a 16-byte substitution table.
The binary contains the following table:
1
2
13 5a 27 41 0d 6e 32 19
55 08 3c 72 24 4f 11 38
and a 50-byte encoded blob.
The function also maintains a rolling 32-bit state.
Identify the LCG
The state update is:
1
state = state * 0x19660D + 0x3C6EF35F;
with 32-bit wrapping.
The initial state is:
1
737
This is a Linear Congruential Generator.
For every byte, the program advances the state and uses the high byte of the result as part of the decoder.
Reverse the byte transformation
The Ghidra output contains a position-dependent rotation:
1
(i % 5) + 3
The operation:
1
x >> rotation | x << (8 - rotation)
is an 8-bit right rotation.
The decoder can therefore be reproduced conceptually as:
1
2
3
4
5
6
7
8
9
10
11
12
13
state = 737
for i in range(50):
state = (state * 0x19660D + 0x3C6EF35F) & 0xffffffff
x = SUB_TABLE[i & 0xf] + ENC[i]
x &= 0xff
x ^= state >> 24
x = ror8(x, (i % 5) + 3)
plaintext[i] = x
Decoding the blob gives:
1
IMMOBILE ELDER / PRIVATE RESIDENCE / SIGNAL DEVICE
This is the first major clue.
Interpret the clue
The decoded text describes:
1
2
3
IMMOBILE ELDER
PRIVATE RESIDENCE
SIGNAL DEVICE
This points toward Hector Salamanca from Breaking Bad, who is an elderly, largely immobile character associated with a bell used as his communication device.
The bell reference is important because it leads into the next stage of the binary.
Reverse the sensor state machine
The next interesting function implements a state machine.
The expected sequence is embedded as:
1
02 02 05 02 02
Therefore the required signal sequence is:
1
2 2 5 2 2
Following the state transitions shows that successfully processing this sequence reaches:
1
2
trigger_state = 6
trigger_ok = 1
This confirms that the bell clue is connected to the intended execution path.
Resolve the target
The next stage checks that the clue and sensor state are valid.
It then evaluates the candidate records stored in the binary.
The matching candidate produces:
1
target_id = 0x6d
or:
1
109
in decimal.
This value is later mixed into the final flag-generation seed.
Identify FNV-1a
The final flag-generation routine hashes the recovered clue.
The hash function starts with:
1
0x811C9DC5
and multiplies by:
1
0x01000193
after XORing each input byte.
These are the standard constants for FNV-1a.
The equivalent Python function is:
1
2
3
4
5
6
7
8
def fnv1a(data):
h = 0x811C9DC5
for byte in data:
h ^= byte
h = (h * 0x01000193) & 0xffffffff
return h
For:
1
IMMOBILE ELDER / PRIVATE RESIDENCE / SIGNAL DEVICE
the resulting hash is:
1
0x95516be4
Construct the final seed
The program mixes the clue hash with the recovered target ID and trigger state:
1
2
3
4
5
seed =
clue_hash
XOR target_id
XOR trigger_state * 0x45D9F3B
XOR 0x224BB268
This can be reproduced with:
1
2
3
4
5
6
seed = (
clue_hash
^ 0x6D
^ ((6 * 0x45D9F3B) & 0xffffffff)
^ 0x224BB268
) & 0xffffffff
This produces the initial state used by the final flag generator.
Generate the flag
The program uses the same LCG:
1
state = state * 0x19660D + 0x3C6EF35F
For each of 20 characters it:
- advances the LCG;
- takes the high byte;
- rotates it;
- XORs it with the 16-byte substitution table;
- reduces the result modulo 26;
- converts it into a lowercase letter.
The rotation is:
1
(i % 5) + 3
The equivalent generation logic is:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
result = []
for i in range(20):
seed = (
seed * 0x19660D
+ 0x3C6EF35F
) & 0xffffffff
byte = (seed >> 24) & 0xff
rotation = (i % 5) + 3
byte = rol8(byte, rotation)
byte ^= SUB_TABLE[i & 0xf]
result.append(
chr(ord('a') + (byte % 26))
)
flag = "CYS{" + "".join(result) + "}"
This produces:
1
CYS{ekrfcozsmqktfpbmtcyq}
Complete solve chain
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
lab_monitor
|
v
main
|
v
dispatcher
|
v
crystallize
|
+-----------+-----------+
| |
16-byte table 50-byte blob
| |
+-----------+-----------+
|
v
LCG decoder
|
v
IMMOBILE ELDER / PRIVATE RESIDENCE /
SIGNAL DEVICE
|
v
sensor state machine
|
v
2 2 5 2 2
|
v
state = 6
|
v
target = 0x6d
|
v
FNV-1a
|
v
seed construction
|
v
LCG
|
v
ROL + XOR
|
v
% 26
|
v
FLAG
Final Flag
1
CYS{ekrfcozsmqktfpbmtcyq}
Flag
CYS{ekrfcozsmqktfpbmtcyq}