Post

Palantine Pack (SunshineCTF 2025)

Palantine Pack (SunshineCTF 2025)

Steps:

  1. Investigate binary and identify the transform pipeline in main.
  2. Note the binary reads palantinepackflag.txt, then three class to an expand routine with a shuffle, and one earlier flipBits pass.
  3. Obserse the ouput is printed and written to flag.txt. This is a Ciphertext, not the flag.
  4. Derive exact inverse functions for expand and flipBits.
  5. Apply inverse expand three times, then inverse flipBits once, and decode the bytes as UTF-8
  6. Recover the plaintext flag in format sunshine{…}

Detailed Explanation:

We are given an x86_64 ELF that performs a layred bitwise transform on the contents of a local file, the prints and saves the bytes.

Key functions:
flipBits(buf, n)

  • Alternates two operations across 0..n-1
    • even index: b = ~b
    • odd index: b = b ^ K with K starting at 0x69, then K += 0x20 after each odd index

expand(buf, n)

  • Produces 2*n bytes by mixing nibbles with a running key K that evolves as K = K * 0x0b mod 256. It also toggles a flag for each byte. For each source byte x it emits two bytes, placing x’s high and low nibbles in complement positions and filling the other nibble positions from K.

The pipeline in main:

read(“palatinepackflag.txt”) -> buf length L+1 including newline flipBits(buf, L+1) buf1 = expand(buf, L+1)
buf2 = expand(buf1, 2(L+1))
buf3 = expand(buf2, 4
(L+1))
print buf3 write buf3 to “flag.txt”

Therefore, the encryption is:
cipher = expand(expand(expand(flipBits(plain))))

We need to invert this to get the flag.

Inversion

For expand

  • Forward for each source byte x with toggle t and key K:
    • if t is False:
      • out0 = (x & 0x0F) | (K << 4)
      • out1 = (x & 0xF0) | (K >> 4)
    • if t is True:
      • out0 = (x & 0xF0) | (K >> 4)
      • out1 = (x & 0x0F) | (K << 4)
  • The nibble from x is always preserved in one nibble of out0 and the complement nibble of out1. So we can recover x without knowing K:
    • if t is False: x = (out1 & 0xF0) | (out0 & 0x0F)
    • if t is True: x = (out0 & 0xF0) | (out1 & 0x0F)

For flipBits

  • Forward index i:
    • even i: b = ~b
    • odd i: b = b ^ K, then K = (K + 0x20) mod 256

With this we can make a minimal solve:

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
def inv_expand(buf: bytes) -> bytes:
    out = bytearray()
    toggle = False
    for i in range(0, len(buf), 2):
        b0, b1 = buf[i], buf[i+1]
        if toggle:
            x = ((b0 & 0xF0) | (b1 & 0x0F)) & 0xFF
        else:
            x = ((b1 & 0xF0) | (b0 & 0x0F)) & 0xFF
        out.append(x)
        toggle = not toggle
    return bytes(out)

def inv_flip(buf: bytes) -> bytes:
    out = bytearray()
    toggle = False
    k = 0x69
    for b in buf:
        if toggle:
            out.append(b ^ k)
            k = (k + 0x20) & 0xFF
        else:
            out.append((~b) & 0xFF)
        toggle = not toggle
    return bytes(out)

# usage on the file that the binary outputs
with open("flag.txt", "rb") as f:
    c = f.read()
for _ in range(3):
    c = inv_expand(c)
p = inv_flip(c)
print(p.rstrip(b"\x00").decode("utf-8"))

Output : sunshine{C3A5ER_CR055ED_TH3_RUB1C0N}

This post is licensed under CC BY 4.0 by the author.