Barbhack 2026: Refactor incomplet
Recon
The website loads a WebAssembly module, like the previous challenge “Contrôle d’intégrité”.
The webpage consists of a button that reveals the full scrambled flag bytes by calling some data() function. We get this:
0xde 0x99 0xcb 0x95 0xcb 0x84 0x18 0x52 0x67 0x62 0x48 0xc8 0xaa 0xc1 0x06 0x8e 0x38 0xde 0x6e 0x74 0xd5 0x4f 0xb4 0x0c 0x5a 0x83
Of course, the ASCII of these bytes does not yield anything potent; we can see this because some of these bytes are over 0x7F (which is the last byte character in standard ASCII), like the byte 0x99.
We need to find a way to unscramble these bytes. Fortunately, we can identify a static string section at the end of the .wat text equivalent of the WASM module. Its contents is as follows:
=== REFACTOR NOTES / DO NOT SHIP ===
The unscramble() function was lost. Its inverse was defined as:
scramble(b, i) = rotl8( b XOR ((i * 7 + 13) mod 256), (i mod 5) + 1 )
All arithmetic wraps on 8 bits.
Test vectors : unscramble(0xde, 0) == 0x62 unscramble(0x99, 1) == 0x72 unscramble(0x52, 7) == 0x74
While the stub returns 0, reveal() only produces noise.
====================================
The unscramble() function body has been stripped from the WebAssembly module and only returns 0, which means it’s now our job to implement it. The developer gave us the structure of the scramble() function.
In the formula, we can identify two blobs that are easy to understand and to compute: we will name them x and y, such as:
x = (i * 7 + 13) % 256
y = (i % 5) + 1
Now the formula is easier to read. With b as the input byte, and i the number of bits to shift in the byte, we have:
scramble(b, i) = rotl8(b XOR x, y)
The XOR function is really basic and has the following truth table:
| A | B | A XOR B |
|---|---|---|
| 0 | 0 | 0 |
| 0 | 1 | 1 |
| 1 | 0 | 1 |
| 1 | 1 | 0 |
One cool thing about this function is that it is its own inverse, meaning if we have C = A XOR B we can find A = C XOR B or B = C XOR A.
The remaining mystery is the rotl8 function. The hint tells us that arithmetic is wrapping on 8 bits, which probably means this function is a circular left rotation (shift) on 8 bits (a byte).
The standard operation for left shift (often written b << i in C, or shl in assembly) is shifting the bits in the byte by i positions. For example, considering the byte 0xFE, which is binary 1111 1110. Shifting the byte to the left yields these results:
0xFE << 1 = 1111 1100
0xFE << 2 = 1111 1000
0xFE << 3 = 1111 0000
And so on. We notice that we’re losing the information from the most significant bit (the leftmost bit) and it disappears in an eternal void.
Wrapping the operation means we won’t lose the most significant bit anymore; it will wrap around the byte and come back as least significant bit (the rightmost bit), like so:
rotl8(0xFE, 1) = 1111 1101
rotl8(0xFE, 2) = 1111 1011
rotl8(0xF3, 3) = 1111 0111
To implement this function, we have to do a regular left shift, and also take the bits that will disappear and put them at the least significant end of our byte (right end).
For that, we can use the OR logical operation, like so:
rotl8(b, i) = (b << i) | (b >> 8-i)
As an example, let’s take the 0xFE (1111 1110) byte from before: we apply our left shift operator on it with i=1, giving 1111 1100, then we compute the right shift of our original byte by 8-n positions (here, 7) which should only give us the information we’re losing in the left shift: 0000 0001. Now, we OR these two bytes and we have the rotl8() value of our original byte: 1111 1101.
This is great, but we have to remember that the circular left shift was an operation on the scramble() function. So if we want to reverse that process, we have to find the opposite function: rotr8().
rotr8(b,i) will compute the circular bit shift on byte b by i positions. We can take our previous function and simply reverse the orientation of the bitshift operations, giving us:
rotr8(b,i) = (b >> i) | (b << 8-i)
The process is the same: we apply a right shift on our byte, then OR it with the information we would’ve lost with a regular bitshift, by shifting the bits left by 8-i positions.
We must pay attention to the order we do the operations in. In the scramble() function, the rotl8 operation was applied last, right after the XOR. This means that in our unscramble() function we have to apply the rotr8 bitshift first, and only then apply the XOR operation.
Now that we have all the idea behind the unscramble() function we can implement it using a scripting language like Python. We have to pay special attention to one thing: in Python, integers aren’t always represented on a single byte. We will have to add a bitmask to our << operation in rotr8() to keep only the lower byte in the operation.
That said, a Python implementation for our solution may look like this:
data = [0xde,0x99,0xcb,0x95,0xcb,0x84,0x18,0x52,0x67,0x62,0x48,0xc8,0xaa,0xc1,0x06,0x8e,0x38,0xde,0x6e,0x74,0xd5,0x4f,0xb4,0x0c,0x5a,0x83]
def rotr8(byte, n):
return ((byte >> n) | (byte << (8-n))) & 0xFF
def unscramble(byte, i):
x = (i*7+13) % 256
y = (i % 5) + 1
s1 = rotr8(byte, y)
return s1 ^ x
print(f"unscramble(0xde, 0)={hex(unscramble(0xde, 0))}")
print(f"unscramble(0x99, 1)={hex(unscramble(0x99, 1))}")
print(f"unscramble(0x52, 7)={hex(unscramble(0x52, 7))}")
The print statements at the end of our script are here to test our solution against the known test vectors we were given on the webpage. When we execute that we see that the test vector results are identical to the examples given:
$ python test.py
unscramble(0xde, 0)=0x62
unscramble(0x99, 1)=0x72
unscramble(0x52, 7)=0x74
Now we can try to decipher the scrambled data bytes with a 1-bit shift to get the flag:
data = [0xde,0x99,0xcb,0x95,0xcb,0x84,0x18,0x52,0x67,0x62,0x48,0xc8,0xaa,0xc1,0x06,0x8e,0x38,0xde,0x6e,0x74,0xd5,0x4f,0xb4,0x0c,0x5a,0x83]
clear = []
def rotr8(byte, n):
return ((byte >> n) | (byte << (8-n))) & 0xFF
def unscramble(byte, i):
x = (i*7+13) % 256
y = (i % 5) + 1
s1 = rotr8(byte, y)
return s1 ^ x
for byte in data:
clear_byte = unscramble(byte, 1)
clear.append(clear_byte)
print(f"clear={clear}")
print(f"cleartext={bytes(clear).decode("latin1")}")
However, executing this yields poor results:
$ python test.py
clear=[98, 193, 232, 199, 232, 79, 1, 36, 190, 60, 41, 105, 88, 237, 14, 74, 17, 98, 58, 55, 231, 170, 87, 11, 32, 204]
cleartext=bÁèÇèO$¾<)iXíJb:7çªW
Ì
We know that the flag format begins with brb{ for this CTF, and the first deciphered cleartext byte is indeed a b. We can try incrementing the amount of bits shifted on each iteration instead, shifting 2 bits for byte 2, 3 for byte 3, etc.. by adding a i variable on our loop:
i=0
for byte in data:
clear_byte = unscramble(byte, i)
clear.append(clear_byte)
i+=1
Finally, we get the flag:
$ python test.py
clear=[98, 114, 98, 123, 119, 114, 49, 116, 51, 95, 119, 104, 52, 116, 95, 49, 115, 95, 109, 49, 115, 115, 49, 110, 103, 125]
cleartext=brb{wr1t3_wh4t_1s_m1ss1ng}