Category: Reverse Engineering
Flag: EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}
Challenge Description
meh yet another crackme challenge
Analysis
file "chall"chall: ELF 64-bit LSB executable, UCB RISC-V, RVC, double-float ABI, version 1 (SYSV), statically linked, ... strippedchecksec "chall"Arch: riscv64-64-little
RELRO: Partial RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x10000)The first pass showed a static, stripped RISC-V binary. Since qemu-riscv64 was not present in this environment, I treated it as a static reversing problem and leaned on r2 decompilation plus constant extraction instead of runtime execution.
strings -a "chall" | rg -i "I Guess Bro|Wrong length|Correct!|Flag: %s|EH4X\{"'I Guess Bro' - Hard Mode
Wrong length! Keep guessing...
Correct! You guessed it!
Flag: %s
EH4X{n0t_th3_r34l_fl4g}
EH4X{try_h4rd3r_buddy}Seeing two ready-made EH4X{...} candidates this early looked suspicious, and the challenge style screamed decoy checks.
r2 -e scr.color=0 -A -q -c "s 0x1037e; af; pdg" "chall" | rg "fcn.00010732|Wrong length|Correct!|Flag: %s|== 0x23|Input error"fcn.000151c4("Input error!");
if (iVar1 == 0x23) {
iVar1 = fcn.00010732(auStack_90);
fcn.000151c4("\nš Correct! You guessed it!\n");
fcn.000110d4("Flag: %s\n",auStack_90);
fcn.000151c4("Wrong length! Keep guessing...");r2 -e scr.color=0 -A -q -c "s 0x10732; af; pdg" "chall" | rg "n0t_th3_r34l_fl4g|try_h4rd3r_buddy|Debugger detected|0xc351|fcn.000105cc|fcn.00010622|fcn.00010574|0x1fb53791|0xcab|-0x7e30f90b5f734a11"iVar1 = fcn.0001eaf0(param_1,"EH4X{n0t_th3_r34l_fl4g}");
iVar1 = fcn.0001eaf0(param_1,"EH4X{try_h4rd3r_buddy}");
if (iVar2 - iVar1 < 0xc351) {
iVar1 = fcn.000105cc(param_1);
if ((iVar1 != 0) && (iVar1 = fcn.00010622(param_1), iVar1 != 0)) {
iVar1 = fcn.00010574(param_1,0x23);
return iVar1 == -0x7e30f90b5f734a11;
}
fcn.000151c4("Debugger detected! Exiting...");That decompilation confirmed the trick: both visible flags are explicitly rejected first, then real validation happens through three helper checks. So the shortest path was to reverse those helpers and recover the expected 35-byte input directly.
r2 -e scr.color=0 -A -q -c "s 0x105cc; af; pdg" "chall" | rg "0x57bc8|0x57beb|\^ 0xa5|uVar4 = uVar4 \+ 7"puVar5 = 0x57bc8;
*puVar3 = uVar1 ^ uVar4 ^ 0xa5;
uVar4 = uVar4 + 7;
} while (puVar5 != 0x57beb);r2 -q -c "pxj 35 @ 0x57bc8" "chall"[224,234,159,232,194,255,191,225,194,253,150,219,130,141,244,168,138,166,179,20,93,105,77,53,126,105,76,123,19,90,20,23,40,113,54]# decode_table.py
data = [
224,234,159,232,194,255,191,225,194,253,150,219,130,141,244,168,138,
166,179,20,93,105,77,53,126,105,76,123,19,90,20,23,40,113,54
]
out = []
k = 0
for b in data:
out.append((b ^ k ^ 0xA5) & 0xFF)
k = (k + 7) & 0xFF
print(bytes(out).decode())data=[224,234,159,232,194,255,191,225,194,253,150,219,130,141,244,168,138,166,179,20,93,105,77,53,126,105,76,123,19,90,20,23,40,113,54]
out=[]
k=0
for b in data:
out.append((b^k^0xa5)&0xff)
k=(k+7)&0xff
print(bytes(out).decode())EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}The decoded string looked right, but I still verified it against every remaining constraint from fcn.00010622 and fcn.00010574 so it was not just a plausible-looking decode.
r2 -e scr.color=0 -A -q -c "s 0x10622; af; pdg" "chall" | rg "param_1\[0x22\] == 0x7d|iVar3 == 0xcab|0x3b9aca07|0x1fb53791|aiStack_18\[0\] = 5|aiStack_18\[5\] = 0x1e"((param_1[4] == 0x7b && (param_1[0x22] == 0x7d)))) {
if (iVar3 == 0xcab) {
aiStack_18[0] = 5;
aiStack_18[5] = 0x1e;
uVar5 = (param_1[iVar3] * uVar5) % 0x3b9aca07;
return uVar5 == 0x1fb53791;# verify_constraints.py
flag = b"EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}"
print("len", len(flag))
print("prefix", flag[:5] == b"EH4X{" and flag[0x22] == 0x7D)
print("sum", hex(sum(flag)))
idx = [5, 10, 15, 20, 25, 30]
mod = 0x3B9ACA07
prod = 1
for i in idx:
prod = (prod * flag[i]) % mod
print("prod", hex(prod))
def mix(x: int, i: int) -> int:
x = ((0x5851F42D4C957F2D >> (i & 0x3F)) ^ x) & 0xFFFFFFFFFFFFFFFF
return (((x >> 0x33) + ((x * 0x2000) & 0xFFFFFFFFFFFFFFFF)) ^ 0xEBFA848108987EB0) & 0xFFFFFFFFFFFFFFFF
x = 0xDEADBEEF
for i, b in enumerate(flag):
x = mix(x ^ ((b << ((i & 7) << 3)) & 0xFFFFFFFFFFFFFFFF), i)
print("hash", hex(x))
print("target", hex((-0x7E30F90B5F734A11) & 0xFFFFFFFFFFFFFFFF))flag=b'EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}'
print('len',len(flag))
print('prefix', flag[:5]==b'EH4X{' and flag[0x22]==0x7d)
print('sum', hex(sum(flag)))
idx=[5,10,15,20,25,30]
mod=0x3b9aca07
prod=1
for i in idx:
prod=(prod*flag[i])%mod
print('prod', hex(prod))
def mix(x,i):
x=((0x5851f42d4c957f2d >> (i & 0x3f)) ^ x) & 0xffffffffffffffff
return (((x >> 0x33) + ((x * 0x2000)&0xffffffffffffffff)) ^ 0xebfa848108987eb0) & 0xffffffffffffffff
x=0xdeadbeef
for i,b in enumerate(flag):
x=mix(x ^ ((b << ((i & 7)<<3)) & 0xffffffffffffffff), i)
print('hash', hex(x))
print('target', hex((-0x7e30f90b5f734a11) & 0xffffffffffffffff))len 35
prefix True
sum 0xcab
prod 0x1fb53791
hash 0x81cf06f4a08cb5ef
target 0x81cf06f4a08cb5efFinally I re-extracted from raw file bytes at the decoded table offset to make sure the recovered flag came straight from the challenge artifact, not from any decompiler artifact.

# extract_flag.py
from pathlib import Path
p = Path("/home/rei/Downloads/chall").read_bytes()
enc = p[0x47BC8:0x47BC8 + 35]
flag = bytes((b ^ ((i * 7) & 0xFF) ^ 0xA5) & 0xFF for i, b in enumerate(enc))
print(flag.decode())from pathlib import Path
p=Path('/home/rei/Downloads/chall').read_bytes()
enc=p[0x47bc8:0x47bc8+35]
flag=bytes((b ^ ((i*7)&0xff) ^ 0xa5) & 0xff for i,b in enumerate(enc))
print(flag.decode())EH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}Solution
# solve.py
from pathlib import Path
def decode_flag(binary_path: str) -> str:
data = Path(binary_path).read_bytes()
enc = data[0x47BC8:0x47BC8 + 35]
out = bytes((b ^ ((i * 7) & 0xFF) ^ 0xA5) & 0xFF for i, b in enumerate(enc))
return out.decode()
if __name__ == "__main__":
print(decode_flag("chall"))python3.12 solve.pyEH4X{y0u_gu3ss3d_th4t_r1sc_cr4ckm3}