SmileyCTF 2025
I played a few challenges in smileyCTF.
rev/Success
We are given Haskell code of this form, consisting of 70 nested if statements involving arithmetic and bitwise operations on random character pairs:
checkFlag :: String -> IO ()
checkFlag flag = do
if length flag /= 39
then do
putStrLn "bruh its 39 characters long"
exitFailure
else do
let chars :: [Integer]
chars = map fromIntegral . map fromEnum $ flag
if (*) (chars !! 37) (chars !! 15) == 3366 then do
if (+) (chars !! 8) (chars !! 21) == 197 then do
if (*) (chars !! 8) (chars !! 13) == 9215 then do
if (*) (chars !! 0) (chars !! 3) == 2714 then do
if (+) (chars !! 3) (chars !! 21) == 159 then do
if (*) (chars !! 1) (chars !! 20) == 5723 then do
if (.|.) (chars !! 6) (chars !! 37) == 105 then do
if (*) (chars !! 11) (chars !! 7) == 11990 then do
if (.&.) (chars !! 29) (chars !! 25) == 100 then do
... etc.
We can simply copy these constraints into a Z3 script to solve for the flag.
from z3 import *
s = Solver()
v = [BitVec(f"v{i}", 32) for i in range(39)]
s.add(v[37] * v[15] == 3366)
s.add(v[8] + v[21] == 197)
s.add(v[8] * v[13] == 9215)
s.add(v[0] * v[3] == 2714)
s.add(v[3] + v[21] == 159)
s.add(v[1] * v[20] == 5723)
s.add(v[6] | v[37] == 105)
s.add(v[11] * v[7] == 11990)
s.add(v[29] & v[25] == 100)
... # more conditions
assert s.check() == sat
print("".join(chr(s.model().eval(c).as_long()) for c in v))
# result: .;,;.{imagine_if_i_made_it_compiled!!!}
rev/fORtran
IDA gives this code for the main function:
__int64 sub_1246()
{
__int64 v0; // rdx
__int64 v1; // rcx
__int64 v2; // r8
__int64 v3; // r9
__int64 v4; // rdx
__int64 v5; // rcx
__int64 v6; // r8
__int64 v7; // r9
int v8; // eax
__int64 v9; // rdx
__int64 v10; // rcx
__int64 v11; // r8
__int64 v12; // r9
_BYTE *v13; // rsi
int v14; // ebx
unsigned int *v15; // rdi
__int64 v16; // rcx
__int64 v17; // r8
__int64 v18; // r9
_BOOL8 v19; // rdx
__int64 v20; // rdx
__int64 v21; // rcx
__int64 v22; // r8
__int64 v23; // r9
__int64 v24; // rdx
__int64 v25; // rcx
__int64 v26; // r8
__int64 v27; // r9
__int64 i; // rbx
int v30; // [rsp+0h] [rbp-310h] BYREF
int v31; // [rsp+4h] [rbp-30Ch]
const char *v32; // [rsp+8h] [rbp-308h]
int v33; // [rsp+10h] [rbp-300h]
char *v34; // [rsp+50h] [rbp-2C0h]
__int64 v35; // [rsp+58h] [rbp-2B8h]
char v36; // [rsp+21Fh] [rbp-F1h] BYREF
__int64 v37; // [rsp+220h] [rbp-F0h]
unsigned int v38; // [rsp+22Ch] [rbp-E4h] BYREF
_BYTE v39[72]; // [rsp+230h] [rbp-E0h] BYREF
unsigned int v40; // [rsp+278h] [rbp-98h] BYREF
_DWORD v41[30]; // [rsp+27Ch] [rbp-94h] BYREF
int v42; // [rsp+2F4h] [rbp-1Ch]
int v43; // [rsp+2F8h] [rbp-18h]
int v44; // [rsp+2FCh] [rbp-14h]
v37 = 0LL;
sub_19BD("*");
v44 = sub_1954((__int64)dword_4020, &dword_201C);
v44 = sub_190F(&v38);
v44 = sub_18C2(&dword_2020);
sub_185D((__int64)v39, dword_4020, 64LL);
v32 = "bfl.f90";
v33 = 21;
v30 = 128;
v31 = 6;
_gfortran_st_write(&v30, dword_4020, v0, v1, v2, v3);
_gfortran_transfer_character_write(&v30, "key:(A)", 4LL);
_gfortran_st_write_done(&v30);
v32 = "bfl.f90";
v33 = 22;
v34 = "(A)";
v35 = 3LL;
v30 = 4096;
v31 = 5;
((void (__fastcall *)(int *, const char *, __int64, __int64, __int64, __int64))_gfortran_st_read)(
&v30,
"key:(A)",
v4,
v5,
v6,
v7);
_gfortran_transfer_character(&v30, v39, 64LL);
_gfortran_st_read_done(&v30);
v43 = 24;
v8 = _gfortran_string_len_trim(64LL, v39);
if ( v43 > v8 )
{
v32 = "bfl.f90";
v33 = 26;
v30 = 128;
v31 = 6;
_gfortran_st_write(&v30, v39, v9, v10, v11, v12);
_gfortran_transfer_character_write(&v30, "*", 0LL);
_gfortran_st_write_done(&v30);
_gfortran_stop_string(0LL, 0LL, 0LL);
}
v38 = 0;
v13 = v39;
v14 = _gfortran_string_len_trim(64LL, v39);
for ( v41[0] = 1; v14 >= v41[0]; ++v41[0] )
{
v38 += (unsigned __int8)v39[v41[0] - 1];
v13 = v41;
v44 = sub_17A4(&v38, v41);
}
v15 = &v38;
v44 = sub_19BD(&v38);
v40 = v38;
for ( v41[0] = 1; v41[0] <= 28; ++v41[0] )
{
v44 = sub_18C2(&v40);
v40 = (4919 * v40) ^ 0xDEADBEEF;
v41[v41[0]] = dword_4020[v41[0] - 1] ^ (unsigned __int8)v40;
v15 = &v40;
v44 = sub_190F(&v40);
}
for ( v41[0] = 1; v41[0] <= 28; ++v41[0] )
{
v42 = v41[v41[0]];
v19 = v42 <= 31;
if ( v19 || v42 > 126 )
{
sub_11C9((__int64)v15, (__int64)v13, v19, v16, v17, v18);
v32 = "bfl.f90";
v33 = 49;
v30 = 128;
v31 = 6;
_gfortran_st_write(&v30, v13, v20, v21, v22, v23);
_gfortran_transfer_character_write(&v30, "*", 0LL);
_gfortran_st_write_done(&v30);
v13 = 0LL;
v15 = 0LL;
_gfortran_stop_string(0LL, 0LL, 0LL);
}
}
v44 = sub_19BD(&dword_201C);
v32 = "bfl.f90";
v33 = 55;
v30 = 128;
v31 = 6;
_gfortran_st_write(&v30, v13, v24, v25, v26, v27);
for ( i = 1LL; i <= 28; ++i )
{
v36 = v41[i];
_gfortran_transfer_character_write(&v30, &v36, 1LL);
}
return _gfortran_st_write_done(&v30);
}
It seems all of the function calls are red herrings, as they do nothing to effect the program. Cleaning up the decompilation, we are left with essentially this:
int dword_4020[28] = {0xBF, 0xF3, 0x3B, 0x25, 0xB3, 0x2F, 0x97,
0x1A, 0xD9, 0xBF, 0xAA, 0xA2, 0xA6, 0x55,
0xC4, 0xCA, 0x15, 0x90, 0x93, 0x51, 0x8B,
0x34, 0x41, 0x6E, 0x0B, 0x24, 0xF1, 0xBB};
int main() {
char input[64] = {0};
unsigned int v25 = 0;
unsigned int v27 = 0;
int result[29] = {0};
printf("key:");
if (!fgets(input, sizeof(input), stdin))
exit(1);
// .strip()
size_t len = strcspn(input, "\n");
input[len] = 0;
while (len > 0 && input[len - 1] == ' ')
input[--len] = 0;
if (len < 24) {
printf("*\n");
exit(1);
}
for (int i = 1; i <= len; ++i) {
v25 += (unsigned char)input[i - 1];
}
v27 = v25;
for (int i = 1; i <= 28; ++i) {
v27 = (4919 * v27) ^ 0xDEADBEEF;
result[i] = dword_4020[i - 1] ^ (v27 & 0xFF);
}
for (int i = 1; i <= 28; ++i) {
if (result[i] < 32 || result[i] > 126) {
printf("*\n");
exit(1);
}
}
for (int i = 1; i <= 28; ++i) {
putchar(result[i]);
}
putchar('\n');
}
The program collapses the input into a single number (by summing) then iterates some process on this number, where the residuals must be ASCII. Again, a simple Z3 script is sufficient.
from z3 import *
dword_4020 = [...]
s = BitVec("s", 32)
solver = Solver()
v = s
for i in range(28):
v = (4919 * v) ^ 0xDEADBEEF
res = dword_4020[i] ^ (v & 0xFF)
solver.add(res >= 32)
solver.add(res <= 126)
assert solver.check() == sat
v = solver.model()[s].as_long()
for i in range(28):
v = (4919 * v) ^ 0xDEADBEEF
res = dword_4020[i] ^ (v & 0xFF)
print(end=chr(res))
print()
# result: .;,;.{t00_e4sy_h4h4_f0rtr4n}
rev/Easy Come Easy Go
So IDA gives this disgusting code:
// main.main
// local variable allocation has failed, the output may be wrong!
void __golang main_main()
{
runtime_hchan_0 *v0; // rcx
__int128 v1; // xmm15
uintptr v2; // rax
runtime_hchan_0 *v3; // rcx
uintptr v4; // rax
runtime_hchan_0 *v5; // rcx
uintptr v6; // rax
runtime_hchan_0 *v7; // rcx
uintptr v8; // rax
runtime_hchan_0 *v9; // rcx
runtime_hchan_0 *v10; // rax
int v11; // rax
int v12; // r8
os_File_0 *v13; // rbx OVERLAPPED
__int64 v14; // r8
__int64 v15; // r9
int v16; // r10
__int64 v17; // rcx
int v18; // r8
error_0 v19; // r9
runtime_funcval *v20; // rax
uintptr v21; // rcx
uintptr v22; // rdx
uintptr *v23; // r11
runtime_funcval *v24; // rax
uintptr v25; // rcx
uintptr v26; // rdx
uintptr *v27; // r11
runtime_funcval *v28; // rax
int v29; // rcx
uintptr v30; // rdx
uintptr v31; // rbx
uintptr v32; // rsi
uintptr v33; // rdi
runtime_hchan_0 *v34; // r8
int *v35; // r11
int v36; // r8
error_0 v37; // r9
os_File_0 *v38; // rbx
const internal_abi_ITab *v39; // rax OVERLAPPED
void *p_cap; // rcx OVERLAPPED
__int64 v41; // rdi
__int64 v42; // rsi
error_0 v43; // [rsp-3Eh] [rbp-E8h]
char elem; // [rsp+1h] [rbp-A9h] BYREF
runtime_hchan_0 *c; // [rsp+2h] [rbp-A8h]
uintptr v46; // [rsp+Ah] [rbp-A0h]
uintptr v47; // [rsp+12h] [rbp-98h]
uintptr v48; // [rsp+1Ah] [rbp-90h]
uintptr v49; // [rsp+22h] [rbp-88h]
_slice_interface_ v50; // [rsp+2Ah] [rbp-80h] BYREF
char **v51; // [rsp+42h] [rbp-68h]
_slice_interface_ v52; // [rsp+4Ah] [rbp-60h] BYREF
char **v53; // [rsp+62h] [rbp-48h]
__int128 v54; // [rsp+6Ah] [rbp-40h]
_slice_interface_ a; // [rsp+7Ah] [rbp-30h] BYREF
char **v56; // [rsp+92h] [rbp-18h]
int v57; // [rsp+9Ah] [rbp-10h]
error_0 v58; // 0:r9.16
io_Writer_0 v59; // 0:rax.8,8:rbx.8
io_Writer_0 v60; // 0:rax.8,8:rbx.8
io_Writer_0 v61; // 0:rax.8,8:rbx.8
_slice_interface_ v62; // 0:rcx.8,8:rdi.16 OVERLAPPED
_slice_interface_ v63; // 0:rcx.8,8:rdi.16
_slice_interface_ v64; // 0:rcx.8,8:rdi.16
runtime_makechan((internal_abi_ChanType *)&RTYPE_chan_int, 0LL, v0);
v49 = v2;
runtime_makechan((internal_abi_ChanType *)&RTYPE_chan_uint8, 0LL, v3);
v48 = v4;
runtime_makechan((internal_abi_ChanType *)&RTYPE_chan_uint8, 0LL, v5);
v47 = v6;
runtime_makechan((internal_abi_ChanType *)&RTYPE_chan_uint8, 0LL, v7);
v46 = v8;
runtime_makechan((internal_abi_ChanType *)&RTYPE_chan_bool, 0LL, v9);
c = v10;
runtime_newobject((internal_abi_Type *)&RTYPE__slice_uint8, 0LL);
v57 = v11;
a.cap = (int)&RTYPE_string;
v56 = &off_4EB170;
v59.data = os_Stdout;
v59.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
v62.array = (interface__0 *)&a.cap;
v62.len = 1LL;
v62.cap = 1LL;
fmt_Fprint(v59, v62, v12, v58);
a.array = (interface__0 *)&RTYPE__ptr__slice_uint8;
a.len = v57;
v13 = os_Stdin;
v59.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Reader;
v62.array = (interface__0 *)"%s";
v62.len = 2LL;
v62.cap = (int)&a;
v14 = 1LL;
v15 = 1LL;
fmt_Fscanf(*(io_Reader_0 *)&(&v13)[-1], *(string_0 *)&v62.array, *(_slice_interface_ *)&v62.cap, v16, v43);
if ( v13 )
{
v54 = v1;
v52.cap = (int)&RTYPE_string;
v53 = &off_4EB180;
*(os_File_0 *)&v54 = v13[1];
*((_QWORD *)&v54 + 1) = v17;
v60.data = os_Stdout;
v60.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
v63.array = (interface__0 *)&v52.cap;
v63.len = 2LL;
v63.cap = 2LL;
fmt_Fprintln(v60, v63, v18, v19);
}
else if ( *(_QWORD *)(v57 + 8) == 24LL )
{
runtime_newobject((internal_abi_Type *)&stru_4B5BC0, 0LL);
v20->fn = (uintptr)main_main_func1;
if ( *(_DWORD *)&runtime_writeBarrier.enabled )
{
runtime_gcWriteBarrier2();
v21 = v49;
*v23 = v49;
v22 = v47;
v23[1] = v47;
}
else
{
v21 = v49;
v22 = v47;
}
v20[1].fn = v21;
v20[2].fn = v22;
runtime_newproc(v20);
runtime_newobject((internal_abi_Type *)&stru_4B5C60, 0LL);
v24->fn = (uintptr)main_main_func2;
if ( *(_DWORD *)&runtime_writeBarrier.enabled )
{
runtime_gcWriteBarrier2();
v25 = v48;
*v27 = v48;
v26 = v46;
v27[1] = v46;
}
else
{
v25 = v48;
v26 = v46;
}
v24[1].fn = v25;
v24[2].fn = v26;
runtime_newproc(v24);
runtime_newobject((internal_abi_Type *)&stru_4BB720, 0LL);
v28->fn = (uintptr)main_main_func3;
if ( *(_DWORD *)&runtime_writeBarrier.enabled )
{
runtime_gcWriteBarrier6();
v29 = v57;
*v35 = v57;
v30 = v49;
v35[1] = v49;
v31 = v47;
v35[2] = v47;
v32 = v48;
v35[3] = v48;
v33 = v46;
v35[4] = v46;
v34 = c;
v35[5] = (int)c;
}
else
{
v29 = v57;
v30 = v49;
v31 = v47;
v32 = v48;
v33 = v46;
v34 = c;
}
v28[1].fn = v29;
v28[2].fn = v30;
v28[3].fn = v31;
v28[4].fn = v32;
v28[5].fn = v33;
v28[6].fn = (uintptr)v34;
runtime_newproc(v28);
elem = 0;
runtime_chanrecv1(c, &elem);
if ( elem )
{
v50.cap = (int)&RTYPE_string;
v51 = &off_4EB1A0;
v38 = os_Stdout;
v39 = &go_itab__ptr_os_File_comma_io_Writer;
p_cap = &v50.cap;
}
else
{
v50.array = (interface__0 *)&RTYPE_string;
v50.len = (int)&off_4EB190;
v38 = os_Stdout;
v39 = &go_itab__ptr_os_File_comma_io_Writer;
p_cap = &v50;
}
v41 = 1LL;
v42 = 1LL;
fmt_Fprintln(*(io_Writer_0 *)&v39, *(_slice_interface_ *)&p_cap, v36, v37);
}
else
{
v52.array = (interface__0 *)&RTYPE_string;
v52.len = (int)&off_4EB190;
v61.data = os_Stdout;
v61.tab = (internal_abi_ITab *)&go_itab__ptr_os_File_comma_io_Writer;
v64.array = (interface__0 *)&v52;
v64.len = 1LL;
v64.cap = 1LL;
fmt_Fprintln(v61, v64, v18, v19);
}
}
From it we see what appears to be a length check on the input followed by goroutines running on the input. These are the goroutines:
// main.main.func1
void __golang main_main_func1()
{
__int64 v0; // rdx
runtime_hchan_0 *v1; // rcx
runtime_hchan_0 *v2; // rax
char v3; // al
__int64 v4; // rax
_QWORD v5[3]; // [rsp+0h] [rbp-38h] BYREF
__int64 elem; // [rsp+18h] [rbp-20h] BYREF
runtime_hchan_0 *v7; // [rsp+20h] [rbp-18h]
runtime_hchan_0 *c; // [rsp+28h] [rbp-10h]
v1 = *(runtime_hchan_0 **)(v0 + 16);
v7 = v1;
v2 = *(runtime_hchan_0 **)(v0 + 8);
c = v2;
v5[0] = 0x47300FC1F251843ELL;
v5[1] = 0x6C1AB14C445D2D6FLL;
v5[2] = 0x257CC82AC421251LL;
while ( 1 )
{
runtime_chanrecv2(v2, &elem, (bool)v1);
if ( !v3 )
break;
v4 = elem;
elem = 24LL;
if ( v4 >= 24 )
{
runtime_chansend1(v7, &runtime_egcbss);
}
else
{
if ( (unsigned __int64)v4 >= 0x18 )
runtime_panicIndex();
runtime_chansend1(v7, (char *)v5 + v4);
}
v2 = c;
}
}
// main.main.func2
void __golang main_main_func2()
{
__int64 v0; // rdx
runtime_hchan_0 *v1; // rcx
runtime_hchan_0 *v2; // rax
unsigned __int64 v3; // rdx
char v4; // al
_QWORD v5[3]; // [rsp+1h] [rbp-39h]
char elem; // [rsp+19h] [rbp-21h] BYREF
unsigned __int64 v7; // [rsp+1Ah] [rbp-20h]
runtime_hchan_0 *c; // [rsp+22h] [rbp-18h]
runtime_hchan_0 *v9; // [rsp+2Ah] [rbp-10h]
v1 = *(runtime_hchan_0 **)(v0 + 16);
c = v1;
v2 = *(runtime_hchan_0 **)(v0 + 8);
v9 = v2;
v5[0] = 0x912EFA6C49BD6BELL;
v5[1] = 0x6A163B0583444D46LL;
v5[2] = 0x24E61FF4643EA395LL;
v3 = 0LL;
while ( 1 )
{
v7 = v3;
runtime_chanrecv2(v2, &elem, (bool)v1);
if ( !v4 )
break;
if ( v7 >= 0x18 )
runtime_panicIndex();
elem ^= *((_BYTE *)v5 + v7);
runtime_chansend1(c, &elem);
v3 = v7 + 1;
v2 = v9;
}
}
// main.main.func3
void __golang main_main_func3()
{
__int64 v0; // rdx
__int64 *v1; // rsi
runtime_hchan_0 *v2; // rax
runtime_hchan_0 *v3; // rdi
__int64 v4; // r10
signed __int64 v5; // rsi
unsigned __int64 v6; // rcx
char v7; // r11
char v8; // [rsp+0h] [rbp-122h]
char v9; // [rsp+1h] [rbp-121h] BYREF
_QWORD v10[3]; // [rsp+2h] [rbp-120h]
unsigned __int64 v11; // [rsp+1Ah] [rbp-108h]
_QWORD elem[24]; // [rsp+22h] [rbp-100h] BYREF
signed __int64 v13; // [rsp+E2h] [rbp-40h]
runtime_hchan_0 *v14; // [rsp+EAh] [rbp-38h]
runtime_hchan_0 *v15; // [rsp+F2h] [rbp-30h]
runtime_hchan_0 *c; // [rsp+FAh] [rbp-28h]
runtime_hchan_0 *v17; // [rsp+102h] [rbp-20h]
runtime_hchan_0 *v18; // [rsp+10Ah] [rbp-18h]
__int64 v19; // [rsp+112h] [rbp-10h]
v1 = *(__int64 **)(v0 + 8);
v2 = *(runtime_hchan_0 **)(v0 + 48);
v14 = v2;
v3 = *(runtime_hchan_0 **)(v0 + 16);
v18 = v3;
c = *(runtime_hchan_0 **)(v0 + 24);
v17 = *(runtime_hchan_0 **)(v0 + 32);
v15 = *(runtime_hchan_0 **)(v0 + 40);
elem[0] = 21LL;
elem[1] = 23LL;
elem[2] = 1LL;
elem[3] = 3LL;
elem[4] = 14LL;
elem[5] = 18LL;
elem[6] = 13LL;
elem[7] = 0LL;
elem[8] = 20LL;
elem[9] = 10LL;
elem[10] = 6LL;
elem[11] = 11LL;
elem[12] = 17LL;
elem[13] = 2LL;
elem[14] = 15LL;
elem[15] = 8LL;
elem[16] = 9LL;
elem[17] = 12LL;
elem[18] = 16LL;
elem[19] = 4LL;
elem[20] = 22LL;
elem[21] = 7LL;
elem[22] = 5LL;
elem[23] = 19LL;
v10[0] = 0x58C4D6920D33EF5CLL;
v10[1] = 0x77250476F61923ACLL;
v10[2] = 0xF5903C93901FDBD0LL;
v4 = *v1;
v19 = *v1;
v5 = v1[1];
v13 = v5;
v6 = 0LL;
while ( (__int64)v6 < v5 )
{
v7 = *(_BYTE *)(v4 + v6);
if ( v6 >= 0x18 )
runtime_panicIndex();
v11 = v6;
v8 = v7;
runtime_chansend1(v3, &elem[v6]);
v9 = 0;
runtime_chanrecv1(c, &v9);
v9 ^= v8;
runtime_chansend1(v17, &v9);
v9 = 0;
runtime_chanrecv1(v15, &v9);
if ( *((_BYTE *)v10 + v11) != v9 )
{
runtime_chansend1(v14, &runtime_egcbss);
return;
}
v6 = v11 + 1;
v5 = v13;
v3 = v18;
v4 = v19;
v2 = v14;
}
runtime_chansend1(v2, &runtime_gcbits__ptr_);
}
The third goroutine is important; it seems to use the other two. The elem
array forms a permutation. Each index i
is permuted by elem
and passed into the first channel. The result is XORed with the input byte. Then, this data is passed into the second channel which XORs with some array. We want this byte to be equal to expected_results
. Essentially, its just a bunch of XORs. We could probably solve this by XORing manually, but here is a Z3 script nevertheless.
from z3 import *
magic_data = [...] # v5 from main_main_func1
xor_data = [...] # v5 from main_main_func2
expected_results = [...] # v10 from main_main_func3
elem = [...] # elem from main_main_func3
s = Solver()
input = [BitVec(f"v{i}", 8) for i in range(24)]
for i in range(24):
s.add(magic_data[elem[i]] ^ input[i] ^ xor_data[i] == expected_results[i])
assert s.check() == sat
print("".join(chr(s.model()[c].as_long()) for c in input))
# result: .;,;.{goh3m1an_rh4p50dy}
crypto/never enough
The point of this challenge is quite obvious, the difficultly lies in the fact that we are given occluded (top 20 bits) samples. Indeed, the program notes itself that we are 328 bits short of the 19968 bits needed to fully solve for the state.
from random import getrandbits
from Crypto.Cipher import AES
from hashlib import sha256
danger = 624*32 # i hear you need this much.
given = []
key = ""
for _ in range(danger//20 - 16): # should be fine if im only giving u this much :3
x = getrandbits(32)
# we share <3
key += str(x % 2**12)
given.append(x >> 12)
key = key[:100]
key = sha256(key.encode()).digest()
flag = open("flag.txt", "rb").read().strip()
cipher = AES.new(key, AES.MODE_ECB)
print(given)
print(cipher.encrypt(flag + b"\x00" * (16 - len(flag) % 16)).hex())
This challenge actually gave me an opportunity to look at Mersenne twister in more depth. I understand it better now. I wrote this general solver, adopted from the code given by icemonster:
from itertools import count
from random import Random
import z3
counter = count()
BitVec = lambda n: z3.BitVec(f'x{next(counter)}', n) # for convenience
def temper(y):
y = y ^ (y >> 11)
y = y ^ ((y << 7) & 0x9D2C5680)
y = y ^ ((y << 15) & 0xEFC60000)
y = y ^ (y >> 18)
return y
# based off of https://github.com/icemonster/symbolic_mersenne_cracker/tree/main
class Untwister:
def __init__(self):
self.MT = [BitVec(32) for _ in range(624)]
self.index = 0
self.solver = z3.Solver()
def untemper(self, num):
def temper(y):
y = y ^ (z3.LShR(y, 11))
y = y ^ ((y << 7) & 0x9D2C5680)
y = y ^ ((y << 15) & 0xEFC60000)
y = y ^ (z3.LShR(y, 18))
return y
y = BitVec(32)
self.solver.add(temper(y) == num)
return y
def twist(
self,
n=624,
upper_mask=0x80000000,
lower_mask=0x7FFFFFFF,
a=0x9908B0DF,
m=397,
):
for i in range(n):
x = (self.MT[i] & upper_mask) + (self.MT[(i + 1) % n] & lower_mask)
xA = z3.LShR(x, 1)
xB = z3.If(x & 1 == 0, xA, xA ^ a)
self.MT[i] = self.MT[(i + m) % n] ^ xB
next_mt = self.MT
self.MT = [BitVec(32) for _ in range(624)]
for i in range(624):
self.solver.add(self.MT[i] == next_mt[i])
def submit(self, guess):
if self.index >= 624: # we need to refresh the state by twisting.
self.twist()
self.index = 0
assert len(guess) <= 32
guess = guess.zfill(32)[::-1]
x = BitVec(32)
for i, bit in enumerate(guess):
if bit != '?':
self.solver.add(z3.Extract(i, i, x) == bit)
x = self.untemper(x)
self.solver.add(self.MT[self.index] == x)
self.index += 1
def solve(self):
import bitwuzla
options = bitwuzla.Options()
options.set(bitwuzla.Option.PRODUCE_MODELS, True)
tm = bitwuzla.TermManager()
parser = bitwuzla.Parser(tm, options)
parser.parse(self.solver.to_smt2(), parse_file=False, parse_only=True)
solver = parser.bitwuzla()
if solver.check_sat() != bitwuzla.Result.SAT:
return None
model = {
term.symbol(): solver.get_value(term) for term in parser.get_declared_funs()
}
# generated numbers (useful for partially occluded input)
return [temper(int(model[f'x{i}'].value(), 2)) for i in range(624)]
# # try to recover complete state
# state = [int(model[str(x)].value(), 2) for x in self.MT]
# r = Random()
# r.setstate((3, (*state, self.index), None))
# return r
Its ~30x faster since I swapped out Z3 for bitwuzla. Also, we don't need to fully solve for the state (its not going to be right anyway since we have so few bits). Instead, we just extract the numbers from the first internal state. Empirically, I found it to be completely correct except for the first number, which is wrong for some reason. Not a problem; we can just bruteforce it. Here is the code for the challenge, it takes only 8 seconds:
ut = Untwister()
for x in given:
ut.submit(bin(x)[2:] + 12 * '?')
guess = ut.solve()
for i in range(2**12):
key = str(i)
for x in guess[1:]:
key += str(x % 2**12)
key = key[:100]
key = sha256(key.encode()).digest()
cipher = AES.new(key, AES.MODE_ECB)
s = cipher.decrypt(bytes.fromhex(encrypted_data))
if s[0] == ord('.'):
print(s) # prints 17 lines, one of which contains the actual flag.