Énoncé (résumé)
Une sonde a intercepté une transmission (transmission.bin) destinée à un vieux
« traducteur universel ». Le traducteur a perdu la table qui associe les identifiants
numériques aux mots de son dictionnaire. On dispose de 4 programmes d’exemple
(samples/sample_0{1..4}.bin) avec la sortie exacte produite à l’époque.
Analyse
Le README décrit une machine Forth :
- pile de cellules 64 bits, tokens d’un octet lus de gauche à droite ;
0x00–0x07= DUP, DROP, SWAP, OVER, ROT, NIP, TUCK, EMIT (ordre inconnu) ;0x10–0x19= +, -, *, XOR, AND, OR, MAX, MIN, LSHIFT, RSHIFT (ordre inconnu) ;- tout autre octet = littéral non signé poussé sur la pile ;
- une sortie n’est enregistrée que si le programme s’est terminé « sans incident » (donc un underflow de pile invalide une table).
Sorties attendues :
| Programme | Sortie |
|---|---|
| sample_01 | 41 41 0B 20 30 |
| sample_02 | 29 2A 58 43 58 43 |
| sample_03 | 18 37 39 39 23 |
| sample_04 | 3B 33 11 08 3E 10 01 40 08 00 08 |
Il faut retrouver les deux permutations, puis exécuter la transmission.
Point clé
- Séparer le problème. Les samples 1 et 2 n’utilisent que
0x00–0x07. On brute-force 8! = 40 320 permutations, en considérant l’underflow comme un échec. Une seule table convient :00 ROT, 01 TUCK, 02 OVER, 03 DUP, 04 EMIT, 05 NIP, 06 SWAP, 07 DROP. - Les 10! permutations arithmétiques en non signé donnent 0 solution
(
brute.c, ~1,5 s). La sémantique supposée est donc fausse quelque part. - La fin de sample_04 contient de petits calculs isolés (
lit lit op EMIT) qui fixent six opérations à la main :08 08 19→ 0x10 :19 = +0a 09 17→ 0x01 :17 = -08 08 13→ 0x40 :13 = *09 0a 14→ 0x08 :14 = AND08 08 12 08 10→ 0x08 :12 = LSHIFT,10 = RSHIFT(8<<8 = 0x800, puis >>8 = 8 ; l’inverse donnerait 0)08 09 10→ 0x00 : confirme10 = RSHIFT
- Le piège : MIN et MAX comparent en signé (complément à deux sur 64 bits). Les
longues chaînes de sample_03/04 produisent des valeurs négatives via
-et*. En testant les 4! ordres restants avec MIN/MAX signés, une seule table reproduit exactement les samples 3 et 4 :11 XOR, 15 MIN, 16 MAX, 18 OR.
Table finale complète :
| Octet | Mot | Octet | Mot |
|---|---|---|---|
| 00 | ROT | 10 | RSHIFT |
| 01 | TUCK | 11 | XOR |
| 02 | OVER | 12 | LSHIFT |
| 03 | DUP | 13 | * |
| 04 | EMIT | 14 | AND |
| 05 | NIP | 15 | MIN (signé) |
| 06 | SWAP | 16 | MAX (signé) |
| 07 | DROP | 17 | - |
| 18 | OR | ||
| 19 | + |
EMIT sort l’octet de poids faible du sommet de la pile.
Exploitation (reproductible)
cd .
python3 -I solve.py # table des opérations de pile (solution unique)
cc -O2 -o brute brute.c && ./brute # 10! en non signé : 0 solution (montre le piège)
python3 -I brute2.py | grep True # variantes signé/non signé, une seule table valide
python3 -I brute2.py | tail -1 # exécute transmission.bin → flag
Sortie :
b'BA26{0n3_st4ck_f0rth_4ll}'
Scripts
solve.py: interpréteur et brute-force des 8! opérations de pile (samples 1–2).brute.c: brute-force en C des 10! opérations arithmétiques, en non signé (aucune solution).brute2.py: six opérations fixées à la main, test des 4! restantes avec MIN/MAX signés ou non signés, puis exécution detransmission.bin.files/universal_translator/: contenu du zip.
Code
import itertools, sys
D='./files/universal_translator/'
S=[open(D+f'samples/sample_0{i}.bin','rb').read() for i in range(1,5)]
T=open(D+'transmission.bin','rb').read()
EXP=[bytes.fromhex(x) for x in ['41410B2030','292A58435843','1837393923','3B3311083E1001400800 08'.replace(' ','')]]
M=(1<<64)-1
STK=['DUP','DROP','SWAP','OVER','ROT','NIP','TUCK','EMIT']
ARI=['+','-','*','XOR','AND','OR','MAX','MIN','LSHIFT','RSHIFT']
class E(Exception):pass
def run(prog,tab,maxout=None):
s=[];out=[]
for b in prog:
w=tab.get(b)
if w is None: s.append(b); continue
if w=='DUP':
if len(s)<1: raise E
s.append(s[-1])
elif w=='DROP':
if len(s)<1: raise E
s.pop()
elif w=='SWAP':
if len(s)<2: raise E
s[-1],s[-2]=s[-2],s[-1]
elif w=='OVER':
if len(s)<2: raise E
s.append(s[-2])
elif w=='ROT':
if len(s)<3: raise E
s.append(s.pop(-3))
elif w=='NIP':
if len(s)<2: raise E
del s[-2]
elif w=='TUCK':
if len(s)<2: raise E
s.insert(-2,s[-1])
elif w=='EMIT':
if len(s)<1: raise E
out.append(s.pop()&0xff)
else:
if len(s)<2: raise E
b2=s.pop();a=s.pop()
if w=='+':r=a+b2
elif w=='-':r=a-b2
elif w=='*':r=a*b2
elif w=='XOR':r=a^b2
elif w=='AND':r=a&b2
elif w=='OR':r=a|b2
elif w=='MAX':r=max(a,b2)
elif w=='MIN':r=min(a,b2)
elif w=='LSHIFT':r=a<<b2 if b2<64 else 0
elif w=='RSHIFT':r=a>>b2 if b2<64 else 0
s.append(r&M)
return bytes(out)
sols=[]
for p in itertools.permutations(STK):
tab=dict(zip(range(8),p))
try:
if run(S[0],tab)==EXP[0] and run(S[1],tab)==EXP[1]: sols.append(tab)
except E: pass
print(len(sols)); [print(t) for t in sols] import itertools
D='files/universal_translator/'
S=[open(D+f'samples/sample_0{i}.bin','rb').read() for i in (3,4)]
EXP=[bytes.fromhex('1837393923'),bytes.fromhex('3B3311083E100140080008')]
M=(1<<64)-1
def sg(x): return x-(1<<64) if x>>63 else x
STK={0:'ROT',1:'TUCK',2:'OVER',3:'DUP',4:'EMIT',5:'NIP',6:'SWAP',7:'DROP'}
fixed={0x19:'+',0x17:'-',0x13:'*',0x14:'AND',0x10:'RSHIFT',0x12:'LSHIFT'}
rest=['XOR','OR','MAX','MIN']; free=[0x11,0x15,0x16,0x18]
def run(prog,tab,signed,shmod):
s=[];out=[]
for b in prog:
w=tab.get(b)
if w is None: s.append(b);continue
if w=='DUP': s.append(s[-1])
elif w=='DROP': s.pop()
elif w=='SWAP': s[-1],s[-2]=s[-2],s[-1]
elif w=='OVER': s.append(s[-2])
elif w=='ROT': s.append(s.pop(-3))
elif w=='NIP': del s[-2]
elif w=='TUCK': s.insert(-2,s[-1])
elif w=='EMIT': out.append(s.pop()&0xff)
else:
c=s.pop();a=s.pop()
f=sg if signed else (lambda x:x)
if w=='+':r=a+c
elif w=='-':r=a-c
elif w=='*':r=a*c
elif w=='XOR':r=a^c
elif w=='AND':r=a&c
elif w=='OR':r=a|c
elif w=='MAX':r=a if f(a)>f(c) else c
elif w=='MIN':r=a if f(a)<f(c) else c
elif w=='LSHIFT':r=(a<<(c&63)) if shmod else (a<<c if c<64 else 0)
elif w=='RSHIFT':r=(a>>(c&63)) if shmod else (a>>c if c<64 else 0)
s.append(r&M)
return bytes(out)
for signed in (0,1):
for shmod in (0,1):
for p in itertools.permutations(rest):
tab=dict(STK);tab.update(fixed);tab.update(zip(free,p))
o=[run(x,tab,signed,shmod) for x in S]
print(signed,shmod,p,[x.hex() for x in o], o==EXP)
tab=dict(STK);tab.update(fixed);tab.update({0x11:'XOR',0x15:'MIN',0x16:'MAX',0x18:'OR'})
T=open(D+'transmission.bin','rb').read()
for sh in (0,1):
try: print(run(T,tab,1,sh))
except Exception as e: print('err',e) #include <stdio.h>
#include <stdint.h>
#include <string.h>
#include <stdlib.h>
// stack table: 0 ROT,1 TUCK,2 OVER,3 DUP,4 EMIT,5 NIP,6 SWAP,7 DROP
// arith words: 0 + 1 - 2 * 3 XOR 4 AND 5 OR 6 MAX 7 MIN 8 LSH 9 RSH
static uint8_t P[2][1024]; static int PL[2]; static uint8_t X[2][32]; static int XL[2];
static int run(int k,int *perm){
uint64_t s[1024]; int sp=0, o=0;
for(int i=0;i<PL[k];i++){ int b=P[k][i];
if(b<8){ switch(b){
case 0: if(sp<3)return 0; {uint64_t t=s[sp-3]; s[sp-3]=s[sp-2]; s[sp-2]=s[sp-1]; s[sp-1]=t;} break;
case 1: if(sp<2)return 0; {uint64_t a=s[sp-2],c=s[sp-1]; s[sp-2]=c; s[sp-1]=a; s[sp]=c; sp++;} break;
case 2: if(sp<2)return 0; s[sp]=s[sp-2]; sp++; break;
case 3: if(sp<1)return 0; s[sp]=s[sp-1]; sp++; break;
case 4: if(sp<1)return 0; sp--; if(o>=XL[k] || (uint8_t)s[sp]!=X[k][o]) return 0; o++; break;
case 5: if(sp<2)return 0; s[sp-2]=s[sp-1]; sp--; break;
case 6: if(sp<2)return 0; {uint64_t t=s[sp-1]; s[sp-1]=s[sp-2]; s[sp-2]=t;} break;
case 7: if(sp<1)return 0; sp--; break; }
} else if(b>=0x10 && b<=0x19){ if(sp<2) return 0; uint64_t c=s[--sp], a=s[sp-1], r;
switch(perm[b-0x10]){
case 0:r=a+c;break; case 1:r=a-c;break; case 2:r=a*c;break; case 3:r=a^c;break;
case 4:r=a&c;break; case 5:r=a|c;break; case 6:r=a>c?a:c;break; case 7:r=a<c?a:c;break;
case 8:r=c<64?a<<c:0;break; default:r=c<64?a>>c:0;break; }
s[sp-1]=r;
} else s[sp++]=b;
}
return o==XL[k];
}
static int perm[10], used[10]; static long cnt=0;
static void rec(int d){
if(d==10){ if(run(0,perm)&&run(1,perm)){ cnt++; for(int i=0;i<10;i++)printf("%d ",perm[i]); printf("\n"); } return; }
for(int v=0;v<10;v++) if(!used[v]){used[v]=1;perm[d]=v;rec(d+1);used[v]=0;}
}
static void load(int k,const char*f,const char*hex){
FILE*fp=fopen(f,"rb"); PL[k]=fread(P[k],1,1024,fp); fclose(fp);
XL[k]=0; for(const char*h=hex;*h;h+=2) {unsigned v; sscanf(h,"%2x",&v); X[k][XL[k]++]=v;}
}
int main(){
load(0,"files/universal_translator/samples/sample_03.bin","1837393923");
load(1,"files/universal_translator/samples/sample_04.bin","3B3311083E100140080008");
rec(0); fprintf(stderr,"%ld sols\n",cnt);
}