Skip to main content

Powershelly picoCTF 2021 Solution

Reverse engineer a PowerShell script that processes the flag through multiple encoding or encryption transformations.

Published: April 2, 2026Updated: August 13, 2026

Description

We give you the PowerShell transformation script and the output file. Can you reverse it to find the input?

Download the PowerShell script and the output file.

bash
wget <url>/script.ps1
bash
wget <url>/output.txt

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
  1. Step 1Read the script to understand the transformation
    Observation
    The challenge gives both the transformation script and its output file. So reverse the process by working out exactly which operations the script applies, and in what order.
    Open script.ps1. It reads an input file as UTF-8, verifies each block of 6 characters consists of only '0' and '1', generates a list of pseudo-random seeds (one per block), then for each block calls a scramble function and XORs the result cumulatively, writing each cumulative XOR to output.txt. The output.txt has 264 lines.
    bash
    cat script.ps1
    What didn't work first

    Tried: Treat the output.txt values as plain binary strings and convert them directly to ASCII without reversing the XOR.

    output.txt holds cumulative XOR results across all 264 blocks, not the raw encoded blocks. Convert those lines directly and you get meaningless bytes, because every line carries the XOR of everything before it. XOR adjacent lines first to isolate the per-block data.

    Tried: Assume the scramble function is a Caesar cipher or simple substitution and try to brute-force the shift value.

    The scramble function is not a Caesar cipher - it uses a seeded pseudo-random permutation that reorders bits within each 6-character block differently for every block. Brute-forcing a fixed shift produces no consistent plaintext because each block's scramble key changes based on its position in the seed sequence.

    Learn more

    The transformation has three stages:

    1. The input is 264 blocks of 6 binary digits (ones and zeros).
    2. Each block is scrambled using a seeded random number generator (the seed sequence is deterministic given the block count).
    3. Results are XORed cumulatively and written to output.txt.

    To reverse it, undo the cumulative XOR first (XOR adjacent lines), then unscramble each block, then interpret each 6-bit block as a binary digit.

  2. Step 2Reverse the cumulative XOR
    Observation
    The script accumulates XOR results across every block before writing to output.txt. So isolating one block's contribution means XORing each adjacent pair of output lines, not reading the raw values.
    The script accumulates XOR results across blocks. The first line of output is the XOR of block 0 alone. The second line is that XOR block 1. To undo, XOR each line with the previous line to isolate individual block results. Write a PowerShell (or Python) script that reproduces the same seed sequence and unscramble function.
    bash
    # Reproduce the seed generation (copy from script.ps1):
    # RandomGen generates the same seed list given blocks.count = 264
    # Run the unscramble logic in PowerShell:
    pwsh -File reverse.ps1
    What didn't work first

    Tried: XOR every line with the first line instead of XORing each line with the one immediately before it.

    The script accumulates XOR progressively: line N is the XOR of blocks 0 through N. To recover block N individually, you XOR line N with line N-1, not with line 0. XORing everything against line 0 produces the XOR of blocks 1 through N for each position, which is still a combined result rather than an isolated block.

    Tried: Skip regenerating the seed sequence and try to unscramble each block using a fixed permutation order.

    The RandomGen function produces a distinct seed per block position, meaning every block is scrambled with a different permutation. A single fixed permutation only correctly unscrambles block 0 and produces garbage for all 263 remaining blocks. You must reproduce the full seed list from script.ps1 using blocks.count = 264 to get the right permutation for each block.

    Learn more

    The seed sequence is deterministic: copy the RandomGen function directly from script.ps1 with blocks.count = 264. The unscramble function is the inverse of scramble: where scramble replaces one binary character with a longer pattern, unscramble maps those patterns back to 0 or 1.

  3. Step 3Interpret the unscrambled blocks as binary and convert to ASCII
    Observation
    Each unscrambled line holds exactly two distinct 6-character patterns, one built from '11' pairs and one from '00' pairs. The scramble function is a bit substitution, so map those two patterns to 1 and 0, group into 8-bit values, and read the ASCII.
    After reversing, each line of the recovered data contains only two distinct 6-character patterns. One pattern maps to bit 0 and the other to bit 1. This is a fixed substitution, not a vote: the scramble function encodes each input bit as a 2-char pair ('00' for 0, '11' for 1) at a seed-shuffled position. To decode, collect the two unique patterns per line, determine which is which by checking their 2-bit pairs, assign bits, then decode 8-bit groups as ASCII.
    python
    python3 << 'EOF'
    # Read the unscrambled blocks output from the PowerShell reverse step
    with open("unscrambled.txt") as f:
        lines = f.read().splitlines()
    
    bits = ""
    for line in lines:
        parts = line.split()
        # Each line has exactly two distinct 6-char patterns.
        # The scramble encodes bit-1 as "11" pairs, bit-0 as "00" pairs.
        # Identify which of the two patterns encodes a 1 by checking inner pairs.
        seen = list(dict.fromkeys(parts))  # preserve order, deduplicate
        # A pattern encoding 1 will have all "11" 2-bit pairs (e.g. "111111")
        # A pattern encoding 0 will have all "00" 2-bit pairs (e.g. "000000")
        # After unscrambling, pairs are either "11" or "00"; count "11" pairs
        def is_one_pattern(p):
            pairs = [p[i:i+2] for i in range(0, len(p), 2)]
            return all(pair == "11" for pair in pairs)
        one_pat = seen[0] if is_one_pattern(seen[0]) else seen[1]
        for block in parts:
            bits += "1" if block == one_pat else "0"
    
    # Decode 8 bits at a time to ASCII
    flag = ""
    for i in range(0, len(bits), 8):
        byte = bits[i:i+8]
        if len(byte) == 8:
            flag += chr(int(byte, 2))
    print(flag)
    EOF

    Expected output

    picoCTF{2018highw@y_2_pow3r$hel!}
    What didn't work first

    Tried: Decode each 6-character block directly as a 6-bit binary number and convert groups of bits to ASCII.

    The blocks are not raw binary - each block is a scrambled encoding where bit values are represented as 2-character pairs ('11' for 1, '00' for 0) placed at shuffled positions. Reading the six characters as a literal 6-bit number gives a numeric value that has no correspondence to the original bit, because the meaningful signal is in which 2-char pair is present, not the positional value of each character.

    Tried: Take the first of the two distinct patterns per line as bit 0 and the second as bit 1 based on appearance order.

    Appearance order depends on how the scramble permutation happened to arrange patterns for that block and is not a reliable indicator of which pattern encodes 0 versus 1. The correct approach is to inspect the 2-character pairs inside each pattern - a pattern made entirely of '11' pairs encodes bit 1 and a pattern made entirely of '00' pairs encodes bit 0, regardless of which appeared first in the line.

    Learn more

    The scramble function encodes each input bit as a 2-character pair: "11" for a 1 bit and "00" for a 0 bit, placed at a seed-scrambled position in the output. Because of this, each line of the reversed output contains exactly two distinct 6-character patterns - one made entirely of "11" pairs (representing 1) and one made entirely of "00" pairs (representing 0). Decoding means identifying which pattern is which per line and mapping to bits, then reading 8-bit groups as ASCII characters to reveal the flag.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.
  • Hex ViewerView text or raw hex bytes as a xxd-style hex dump with byte offset, hex columns, and ASCII sidebar. Highlights printable characters and null bytes.

Flag

Reveal flag

picoCTF{2018highw@y_2_pow3r$hel!}

Reverse the cumulative XOR, unscramble each block using the same seed sequence, decode the two-pattern substitution encoding (each line has exactly two distinct 6-char patterns, one for bit 0 and one for bit 1), then convert 8-bit groups to ASCII.

Key takeaway

Reversing a custom obfuscation scheme requires reading the transformation code carefully and inverting each operation in reverse order. Cumulative XOR is undone by XORing adjacent output lines, seeded-random shuffling is undone by regenerating the same seed sequence and applying the inverse permutation, and a bit-encoding substitution is undone by identifying the two symbol patterns and mapping them back to bits. This pattern of layered, invertible transforms appears frequently in malware obfuscation, license-key schemes, and CTF challenge design. The key insight is that any deterministic transformation applied to data can be reversed if you have the algorithm and its parameters.

Related reading

Useful tools for Reverse Engineering

Where to go next