Finger Arithmetic
- Challenge: Finger Arithmetic
- Category: Rev
- Flag:
TBCTF{ju75u5_n_d34d_3nd5_4w417!}
My initial read / first impressions
We are given a Linux binary called chall. Running it gives a very simple prompt:
The challenge description says it expects a 32-character key, so my first thought was that this was probably just a normal flag checker with some annoying validation routine.
I started with the usual basic checks:
The binary was a 64-bit PIE executable, but luckily it was not stripped. The interesting symbols were still there, especially:
The name compare_hand_png_i32 immediately made the challenge feel weird. There were also a ton of lodepng symbols, which means the binary was doing PNG encoding/decoding internally. So instead of just comparing integers directly, it was doing some goofy PNG-based validation.
Understanding the Main Checks
Looking at main, the program reads the input, removes the newline, and then does a few checks before the real validation.
The important checks were:
- The input length must be
32. - The first 4 bytes must match
TBCT. - One specific byte must be
{. - Then the full input is passed into
validate_checksum_v2.
The first 4-byte check is little endian, so the value being checked is:
In memory, that is:
That already made it pretty clear the final flag was going to be in the normal TBCTF{...} format.
The Weird PNG Obfuscation
The annoying part was that the program was not doing something clean like:
Instead, it was doing something more like:
Each expected integer was hidden as an embedded PNG. The binary would take the candidate integer, render it as a hand/finger image, and compare that rendered PNG against the embedded PNG.
So the constants were not sitting in the binary as normal hex numbers. They were hidden as images.
Very funny. Very cursed.
The important thing is that this was still just obfuscation. The actual validation logic was normal arithmetic. Once I decoded the PNG targets back into integers, the rest of the challenge became very straightforward.
Reversing the Arithmetic
Inside validate_checksum_v2, the input is treated as 8 little-endian 32-bit integers.
So a 32-character input becomes:
The validation chain looks like this:
t0 = w0 + 0x11223344;
t1 = w1 ^ t0;
t2 = w2 - t1;
t3 = w3 ^ t2;
t4 = w4 + t3;
t5 = w5 ^ t4;
t6 = w6 - t5;
t7 = w7 ^ t6;
After each step, the program compares the result against one of those hand PNG constants.
So the actual math was not hard. The challenge was mostly hiding the constants.
After extracting/matching the embedded hand PNGs, the target values were:
targets = [
0x65657598,
0x100f0ede,
0x25662659,
0x41394806,
0xa09d7c39,
0x95f9120a,
0x9e7e2255,
0xe35f1564
]
Now we can reverse the equations.
If:
then:
If:
then:
And so on.
For subtraction steps, we add the previous value back. For XOR steps, we XOR again, since XOR reverses itself.
Solution Script
Here is the final solve script:
import struct
targets = [
0x65657598,
0x100f0ede,
0x25662659,
0x41394806,
0xa09d7c39,
0x95f9120a,
0x9e7e2255,
0xe35f1564
]
words = [0] * 8
words[0] = (targets[0] - 0x11223344) & 0xffffffff
words[1] = targets[1] ^ targets[0]
words[2] = (targets[2] + targets[1]) & 0xffffffff
words[3] = targets[3] ^ targets[2]
words[4] = (targets[4] - targets[3]) & 0xffffffff
words[5] = targets[5] ^ targets[4]
words[6] = (targets[6] + targets[5]) & 0xffffffff
words[7] = targets[7] ^ targets[6]
flag = struct.pack("<8I", *words).decode()
print(flag)
Running it gives:
Verifying the Flag
Finally, I ran the binary with the recovered input:
And that confirms the solve.