1680 words
8 minutes
TexSAW 2026 - Switcheroo Read - Reverse Engineering Writeup

Category: Reverse Engineering Flag: texsaw{pAt1ence!!_W0rKn0w?}

Challenge Description#

Whoopsie, some wild functions started switching my string. Please determine a string to fit their confusion.

Analysis#

The challenge file was a small stripped 64-bit ELF, which usually means the interesting part is in the control flow rather than in symbols or external assets. A quick string pass showed only two useful messages, which immediately suggested a password gate rather than anything more elaborate.

strings "/home/rei/Downloads/switcheroo/switcheroo" | rg -i 'flag|texsaw|password|switch|compatible|entered|please'
You have entered the flag
Please make a compatible password:

Listing the functions in radare2 showed that the binary was tiny enough to reverse statically. The interesting part was that main was only a thin wrapper around a single validation function.

r2 -A -q -c 'afl' "/home/rei/Downloads/switcheroo/switcheroo"
0x00401850    3     96 main
0x00401729   11    295 fcn.00401729
0x004012df    9    286 fcn.004012df
0x004013fd   10    812 fcn.004013fd
0x0040129c    9     67 fcn.0040129c
0x004011b6    4    154 fcn.004011b6

Decompiling main made the first hard requirement obvious: the program reads at most 27 characters and refuses to continue unless the input length is exactly 0x1b. That fixed the size of the string we had to recover.

r2 -A -q -c 'pdg @main' "/home/rei/Downloads/switcheroo/switcheroo"
sym.imp.printf("Please make a compatible password: ");
sym.imp.__isoc99_scanf("%27[^\n]",&s);
iVar1 = sym.imp.strlen(&s);
if (iVar1 == 0x1b) {
    fcn.00401729(&s);
}

The main validator at 0x401729 was where the challenge name started to make sense. It repeatedly called the same helper with the sequence 5, 6, 13, 3, 24, 10, 7, and after several of those transformations it checked a few specific byte positions. That meant the input was being switched around in a reversible way rather than hashed.

r2 -A -q -c 'pdg @ 0x401729' "/home/rei/Downloads/switcheroo/switcheroo"
fcn.004012df(arg1,5);
fcn.004012df(arg1,6);
if (arg1[0xb] == 'o') {
    fcn.004012df(arg1,0xd);
    if (arg1[0xe] == 'R') {
        fcn.004012df(arg1,3);
        fcn.004012df(arg1,0x18);
        if ((*arg1 == -0x65) && (arg1[0x1a] + 0x8dU < 5)) {
            fcn.004012df(arg1,10);
            if ((arg1[8] == 'Y') && ((arg1[0xb] == 'Y' && (arg1[0xc] + 0x8cU < 4)))) {
                fcn.004012df(arg1,7);
                if ((arg1[0x14] == -0x4b) && (arg1[0xd] == 's')) {
                    fcn.004013fd(arg1);
                }
            }
        }
    }
}

The next decompilation answered what each helper was doing. fcn.004011b6 rotates the 27-byte buffer by k positions modulo 27. fcn.004012df wraps that rotation and, depending on whether k is even or odd, also adds or subtracts k from selected indices. Once that behavior was clear, the whole binary reduced to a sequence of byte-level equations.

r2 -A -q -c 'pdg @ 0x4012df; pdg @ 0x4011b6' "/home/rei/Downloads/switcheroo/switcheroo"
void fcn.004012df(int arg1,int arg2)
{
    if ((arg2 & 1U) == 0) {
        for (...) {
            iVar1 = (i * arg2) % 0x1b;
            arg1[iVar1] = arg1[iVar1] + arg2;
        }
        fcn.004011b6(arg1,arg2);
    } else {
        fcn.004011b6(arg1,arg2);
        for (...) {
            iVar1 = (i + arg2) % 0x1b;
            arg1[iVar1] = arg1[iVar1] - arg2;
        }
    }
}

void fcn.004011b6(char *arg1,int arg2)
{
    strcpy(&dest,arg1);
    for (i = 0; i < 0x1b; i++) {
        arg1[(i + arg2) % 0x1b] = dest[i];
    }
    arg1[0x1b] = '\0';
}

There was one more routine after the transform chain. Decompiling it showed that the mutated bytes were used to rebuild the filename README.txt, then the code opened that file and derived several hex nibble checks that had to land on 0x57, 0x34, 0x61, and 0x29. The file contents themselves were not part of the logic, but the presence of README.txt was necessary to avoid an early exit.

r2 -A -q -c 'pdg @ 0x4013fd' "/home/rei/Downloads/switcheroo/switcheroo"
iVar1 = strcmp(&filename,"README.txt");
if (iVar1 != 0) exit(1);
stream = fopen(&filename,"rb");
...
var_ch = strtol(&str,0,0x10);
var_10h = strtol(&var_2ah,0,0x10);
var_1ah._2_4_ = strtol(&var_30h,0,0x10);
if ((((stack0xffffffffffffffe4 == 0x61) && (var_10h == 0x34)) && (var_ch == 0x57)) && (var_1ah._2_4_ == 0x29)) {
    printf("You have entered the flag");
}

At that point the fastest path was to model the transform exactly and let Z3 solve backwards from the observed constraints. The first pass left the prefix unconstrained, so it produced a valid-looking string with texsax{...} instead of the required texsaw{...}.

from z3 import *

MOD = 27

def op(arr, k):
    arr = list(arr)
    if k % 2 == 0:
        for i in range(k):
            idx = (i * k) % MOD
            arr[idx] = (arr[idx] + BitVecVal(k, 8)) & BitVecVal(0xFF, 8)
        old = arr[:]
        new = [None] * MOD
        for i in range(MOD):
            new[(i + k) % MOD] = old[i]
        return new
    old = arr[:]
    new = [None] * MOD
    for i in range(MOD):
        new[(i + k) % MOD] = old[i]
    arr = new
    for i in range(k):
        idx = (i + k) % MOD
        arr[idx] = (arr[idx] - BitVecVal(k, 8)) & BitVecVal(0xFF, 8)
    return arr

s = Solver()
S0 = [BitVec(f's0_{i}', 8) for i in range(MOD)]
for c in S0:
    s.add(UGE(c, 0x20), ULE(c, 0x7E))

S1 = op(S0, 5)
S2 = op(S1, 6)
s.add(S2[11] == BitVecVal(ord('o'), 8))
S3 = op(S2, 13)
s.add(S3[14] == BitVecVal(ord('R'), 8))
S4 = op(S3, 3)
S5 = op(S4, 24)
s.add(S5[0] == BitVecVal(0x9B, 8))
s.add(UGE(S5[26], BitVecVal(0x73, 8)), ULE(S5[26], BitVecVal(0x77, 8)))
S6 = op(S5, 10)
s.add(S6[8] == BitVecVal(ord('Y'), 8))
s.add(S6[11] == BitVecVal(ord('Y'), 8))
s.add(UGE(S6[12], BitVecVal(0x74, 8)), ULE(S6[12], BitVecVal(0x77, 8)))
F = op(S6, 7)
s.add(F[20] == BitVecVal(0xB5, 8))
s.add(F[13] == BitVecVal(ord('s'), 8))
s.add(F[0] == BitVecVal(0x73, 8))
s.add(F[1] == BitVecVal(0x65, 8))
s.add(F[2] == BitVecVal(0x69, 8))
s.add(Or(F[3] == BitVecVal(0x1E, 8), F[3] == BitVecVal(0x9E, 8)))
s.add(F[12] == BitVecVal(ord('1'), 8))
s.add(F[11] == BitVecVal(0xAB, 8))
s.add(F[10] == BitVecVal(ord('&'), 8))
s.add(F[9] == BitVecVal(ord('`'), 8))
s.add(F[8] == BitVecVal(0x7F, 8))
s.add(Or(F[26] == BitVecVal(0x40, 8), F[26] == BitVecVal(0xC0, 8)))
s.add(Or(F[5] == BitVecVal(0x92, 8), F[5] == BitVecVal(0x93, 8)))
s.add(F[6] == BitVecVal(ord('3'), 8))
s.add(F[7] == BitVecVal(ord('^'), 8))
s.add(F[25] == BitVecVal(ord('e'), 8))
s.add(F[24] == BitVecVal(ord('1'), 8))
s.add(Or(F[23] == BitVecVal(0xAE, 8), F[23] == BitVecVal(0xAF, 8)))
s.add(F[22] == BitVecVal(ord('A'), 8))
s.add(F[21] == BitVecVal(ord('v'), 8))

print(s.check())
if s.check() == sat:
    m = s.model()
    out = bytes(m[c].as_long() for c in S0)
    print(out)
    print(out.decode('latin1'))
    print('hex', out.hex())
python solve.py
sat
b'texsax{pAt1ence!!_W0rKn0w?}'
texsax{pAt1ence!!_W0rKn0w?}
hex 7465787361787b70417431656e636521215f5730724b6e30773f7d

That made the final adjustment straightforward: constrain the prefix to texsaw{ and solve again. The binary also needed a local README.txt because the last routine checks that the reconstructed name matches exactly before opening it.

from z3 import *

MOD = 27

def op(arr, k):
    arr = list(arr)
    if k % 2 == 0:
        for i in range(k):
            idx = (i * k) % MOD
            arr[idx] = (arr[idx] + BitVecVal(k, 8)) & BitVecVal(0xFF, 8)
        old = arr[:]
        new = [None] * MOD
        for i in range(MOD):
            new[(i + k) % MOD] = old[i]
        return new
    old = arr[:]
    new = [None] * MOD
    for i in range(MOD):
        new[(i + k) % MOD] = old[i]
    arr = new
    for i in range(k):
        idx = (i + k) % MOD
        arr[idx] = (arr[idx] - BitVecVal(k, 8)) & BitVecVal(0xFF, 8)
    return arr

s = Solver()
S0 = [BitVec(f's0_{i}', 8) for i in range(MOD)]
for c in S0:
    s.add(UGE(c, 0x20), ULE(c, 0x7E))
for i, b in enumerate(b'texsaw{'):
    s.add(S0[i] == b)
S1 = op(S0, 5)
S2 = op(S1, 6)
s.add(S2[11] == 0x6F)
S3 = op(S2, 13)
s.add(S3[14] == 0x52)
S4 = op(S3, 3)
S5 = op(S4, 24)
s.add(S5[0] == 0x9B)
s.add(UGE(S5[26], 0x73), ULE(S5[26], 0x77))
S6 = op(S5, 10)
s.add(S6[8] == 0x59, S6[11] == 0x59, UGE(S6[12], 0x74), ULE(S6[12], 0x77))
F = op(S6, 7)
s.add(F[20] == 0xB5, F[13] == 0x73)
vals = {0: 0x73, 1: 0x65, 2: 0x69, 12: 0x31, 11: 0xAB, 10: 0x26, 9: 0x60, 8: 0x7F, 6: 0x33, 7: 0x5E, 25: 0x65, 24: 0x31, 22: 0x41, 21: 0x76}
for i, v in vals.items():
    s.add(F[i] == v)
s.add(Or(F[3] == 0x1E, F[3] == 0x9E), Or(F[26] == 0x40, F[26] == 0xC0), Or(F[5] == 0x91, F[5] == 0x92), Or(F[23] == 0xAD, F[23] == 0xAE))

print(s.check())
if s.check() == sat:
    m = s.model()
    out = bytes(m.eval(c).as_long() for c in S0)
    print(out)
python solve_prefix.py
sat
b'texsaw{pAu1ence!!_W0rKn0w?}'

The solver still had multiple satisfying assignments because the binary does not pin down every byte exactly. So the last step was to enumerate several texsaw{...} candidates and run them against the program. The executable bit was missing, so invoking the loader directly was the cleanest workaround.

stat "/home/rei/Downloads/switcheroo/switcheroo"
Access: (0644/-rw-r--r--)
from z3 import *
import subprocess

MOD = 27

def op(arr, k):
    arr = list(arr)
    if k % 2 == 0:
        for i in range(k):
            idx = (i * k) % MOD
            arr[idx] = (arr[idx] + BitVecVal(k, 8)) & BitVecVal(0xFF, 8)
        old = arr[:]
        new = [None] * MOD
        for i in range(MOD):
            new[(i + k) % MOD] = old[i]
        return new
    old = arr[:]
    new = [None] * MOD
    for i in range(MOD):
        new[(i + k) % MOD] = old[i]
    arr = new
    for i in range(k):
        idx = (i + k) % MOD
        arr[idx] = (arr[idx] - BitVecVal(k, 8)) & BitVecVal(0xFF, 8)
    return arr

s = Solver()
S0 = [BitVec(f's0_{i}', 8) for i in range(MOD)]
for c in S0:
    s.add(UGE(c, 0x20), ULE(c, 0x7E))
for i, b in enumerate(b'texsaw{'):
    s.add(S0[i] == b)
S1 = op(S0, 5)
S2 = op(S1, 6)
s.add(S2[11] == 0x6F)
S3 = op(S2, 13)
s.add(S3[14] == 0x52)
S4 = op(S3, 3)
S5 = op(S4, 24)
s.add(S5[0] == 0x9B)
s.add(UGE(S5[26], 0x73), ULE(S5[26], 0x77))
S6 = op(S5, 10)
s.add(S6[8] == 0x59, S6[11] == 0x59, UGE(S6[12], 0x74), ULE(S6[12], 0x77))
F = op(S6, 7)
s.add(F[20] == 0xB5, F[13] == 0x73)
vals = {0: 0x73, 1: 0x65, 2: 0x69, 12: 0x31, 11: 0xAB, 10: 0x26, 9: 0x60, 8: 0x7F, 6: 0x33, 7: 0x5E, 25: 0x65, 24: 0x31, 22: 0x41, 21: 0x76}
for i, v in vals.items():
    s.add(F[i] == v)
s.add(Or(F[3] == 0x1E, F[3] == 0x9E), Or(F[26] == 0x40, F[26] == 0xC0), Or(F[5] == 0x91, F[5] == 0x92), Or(F[23] == 0xAD, F[23] == 0xAE))

for _ in range(4):
    assert s.check() == sat
    m = s.model()
    txt = bytes(m.eval(c).as_long() for c in S0).decode('ascii')
    p = subprocess.run(
        ['bash', '-lc', f"printf '%s\\n' '{txt}' | /lib64/ld-linux-x86-64.so.2 ./switcheroo"],
        cwd='/home/rei/Downloads/switcheroo',
        capture_output=True,
        text=True,
        timeout=5,
    )
    print(txt, repr(p.stdout + p.stderr))
    s.add(Or(*[c != m.eval(c) for c in S0]))
python solve.py
texsaw{pAs1ence!!_W0rKn0w?} 'Please make a compatible password: You have entered the flag'
texsaw{pAs1ence!!_V0rKn0w?} 'Please make a compatible password: You have entered the flag'
texsaw{pAt1ence!!_W0rKn0w?} 'Please make a compatible password: You have entered the flag'
texsaw{pAt1ence!!_V0rKn0w?} 'Please make a compatible password: You have entered the flag'

Any of those four strings passes, but the recorded solve selected texsaw{pAt1ence!!_W0rKn0w?}. The important trick was realizing that the binary was only shuffling and biasing bytes, so the whole validator could be expressed as reversible constraints instead of brute force.

Solution#

Create a solve.py script containing the solver and candidate test logic below, place a README.txt file in the binary directory, then run the script to enumerate valid texsaw{...} inputs and choose one of the passing candidates.

texsaw{pAt1ence!!_W0rKn0w?}
TexSAW 2026 - Switcheroo Read - Reverse Engineering Writeup
https://blog.rei.my.id/posts/136/texsaw-2026-switcheroo-read-reverse-engineering-writeup/
Author
Reidho Satria
Published at
2026-03-30
License
CC BY-NC-SA 4.0