lunatic
Author: 0xIDA
Platform: FlagYard · Category: rev · Difficulty: Hard
Solvers
Summary
Custom Lua 5.4 interpreter ((((LUnAtic 1.0)))) based on commit c33b1728... with a backdoored luaV_equalobj for length-32 tables. The script mutates both tables through 0x1336 equality ops before a final check; invert the transform chain against the hardcoded XOR targets to recover the flag.
Challenge files
lua— patched ELF64 Lua 5.4 (not stripped)lunatic.lua— flag checker
Description: Am I going insane? (lua base c33b1728aeb7dfeec4013562660e07d32697aa6b)
Recon
./lua -v
# (((LUnAtic 1.0)))
cat lunatic.lua
The script builds two tables of bytes from:
magic = "lua is a cool language right??!?"(length 32)- user
flag
then:
for i = 1,0x1336 do
test[i] = (k1 == k2)
end
if k1 == k2 then print('Correct :)') else print('Wrong :(') end
Stock Lua tables compare by reference, so same contents still fail. The patched binary is required.
Vulnerability / backdoor
In luaV_equalobj, case 5 (LUA_TTABLE):
- If both tables have
luaH_getn == 0x20(32), take the custom path; else pointer compare. - Process array part in 8 groups of 4 TValues (low byte of each integer):
p1 = b0 | b1<<8 | b2<<16 | b3<<24
t1 = (p1 + 0x1337beef) ^ 0x12345678 # table1 / magic
p2 = ...
t2 = (p2 + 0x4444c0de) ^ 0xabbabaab # table2 / flag
require t1 ^ t2 == TARGET[i]
- Write t1/t2 back into the four slots as separate integer bytes (mutating equality).
Targets (first hardcoded, then stack constants little-endian):
0x6978c552, 0xa1f4e69e,
0xe1148ded, 0xc9dde4b7, # 0xc9dde4b7e1148ded
0x63875f42, 0x5554fc02, # 0x5554fc0263875f42
0x0150b8f7, 0xfc60f536 # 0xfc60f5360150b8f7
The loop runs equality 0x1336 times (each applies f1/f2 once), then the final if applies one more transform and checks. Only the final check matters for the printed result.
Solution
Let f1(x)=(x+0x1337beef)^0x12345678, f2(x)=(x+0x4444c0de)^0xabbabaab, f2_inv(y)=(y^0xabbabaab)-0x4444c0de (mod 2^32).
For each 4-byte magic chunk a0:
t1 = f1^{0x1337}(a0)
t2 = t1 ^ TARGET[i]
b0 = f2_inv^{0x1337}(t2)
Solver
#!/usr/bin/env python3
magic = b"lua is a cool language right??!?"
targets = [
0x6978c552, 0xa1f4e69e, 0xe1148ded, 0xc9dde4b7,
0x63875f42, 0x5554fc02, 0x0150b8f7, 0xfc60f536,
]
C1, K1 = 0x1337beef, 0x12345678
C2, K2 = 0x4444c0de, 0xabbabaab
MASK = 0xFFFFFFFF
def f1(x): return ((x + C1) & MASK) ^ K1
def f2_inv(y): return ((y ^ K2) - C2) & MASK
def pack4(bs, i): return bs[i] | (bs[i+1]<<8) | (bs[i+2]<<16) | (bs[i+3]<<24)
def unpack4(x): return bytes([x&0xff,(x>>8)&0xff,(x>>16)&0xff,(x>>24)&0xff])
rounds = 0x1337
flag = bytearray(32)
for i in range(8):
a = pack4(magic, i*4)
for _ in range(rounds): a = f1(a)
b = a ^ targets[i]
for _ in range(rounds): b = f2_inv(b)
flag[i*4:i*4+4] = unpack4(b)
print(bytes(flag).decode())
Live verification
$ printf 'FlagY{Lua_backd00r_1s_co0l3r_:)}\n' | ./lua lunatic.lua
Enter flag:
Checking...
Correct :)
Platform submit: success (isSuccess: true).
Flag
FlagY{Lua_backd00r_1s_co0l3r_:)}
Key takeaways
- Diff / decompile equality paths when a “simple” Lua/scripting challenge ships a custom interpreter.
- Mutating operators + loop count means the final comparison sees fully transformed state; solve for
f^{n}notf^1. - Magic constants (
0x1337beef,0x4444c0de,0xabbabaab) and version stringLUnAticare strong RE hints.
Tools used
- Flagyard MCP (download/submit)
file,strings,nm,r2(disasmluaV_equalobj)- Python 3 (transform inversion)
- Local
./lua lunatic.luafor live check