Hello guys, this is 5h1kh4r back with another CTF writeups post, this time covering the challenges I solved during the CRACCON CTF Qualifiers held between 6th September 8am IST to 7th September 8am IST (24 Hours long). I played for team BlitzHack which ranked 2nd in the Qualifiers and all of BlitzHack is really proud of this achievement since it was just 3 teenagers from BlitzHack playing CRACCON CTF.

Also a big congratulations to all top 10 teams for securing their spots at the competition and a big big thank you to the orgz for organizing such an amazing CTF, eagerly looking forward to the finals
Without any further ado let’s start with the writeups and in this post I will be covering the following challenges:
1. Misc/knock-knock
2. Rev/JSQuizTime
3. Web/Extension Unchained
4. Web/Orderly Chaos
5. Mobile/Easy RASP
6. Web/Method Madness
7. Rev/ShellCode
8. Threat Research/Pow3r
9. Crypto/ECG
10. Rev/Flatearth
- Misc/knock-knock
This challenge was the simple welcome challenge for which the flag was in the discord server of the CTF

The flag becomes: CRACCON{knock_knock_q8r5m2z7t1x9}
2. Rev/JSQuizTime
This challenge was also a good beginner challenge in the reverse engineering category. For this challenge we are given an html file which has javascript code in it’s script tags. We can deobfuscate the javascript code using: https://obf-io.deobfuscate.io/ to get the below js code which is much cleaner to read and interpret
function tungtungtungsahur() {
if (document.getElementById(“flag”).value == atob(“dGhlcmUgYXJlIG1hbnk=”)) {
document.getElementById(‘q2’).removeAttribute(“hidden”);
document.getElementById(‘q1’).setAttribute(“hidden”, “hidden”);
document.getElementById(“overlay”).style.display = “block”;
document.getElementById(“overlay”).innerHTML = “
Right Answer!
document.getElementById(“question”).innerText = atob(atob(“TWk0Z1NHOTNJRzFoYm5rZ1puVnVZM1JwYjI1eklHVjRhWE4wSUdsdUlIUm9hWE1nY1hWbGMzUnBiMjV1WVdseVpUOD0=”));
} else {
document.getElementById(“overlay”).style.display = “block”;
document.getElementById(“overlay”).innerHTML = “
Wrong Answer!
}
}
function closeOverlay() {
document.getElementById(“overlay”).style.display = “none”;
}
function aatob(_0xed91c) {
return atob(atob(_0xed91c));
}
function BrrBrrPatapim() {
if (document.getElementById(“flag”).value == “there are too many, I was lazy to count”) {
document.getElementById(‘q3’).removeAttribute(“hidden”);
document.getElementById(‘q2’).setAttribute(“hidden”, “hidden”);
document.getElementById(“overlay”).style.display = “block”;
document.getElementById(“overlay”).innerHTML = “
Right Answer!
document.getElementById(“question”).innerText = atob(“My4gV2hhdCBpcyB0aGUgZmxhZyBmb3IgdGhpcyBjaGFsbGVuZ2U/”);
} else {
document.getElementById(“overlay”).style.display = “block”;
document.getElementById(“overlay”).innerHTML = “
Wrong Answer!
}
}
function skjdhfk1(_0x5b5217) {
const _0x481a78 = new Array(0x1c);
for (let _0x25b793 = 0x0; _0x25b793 < 0x1c; _0x25b793++) {
const _0xd242c9 = _0x25b793 * 0x5 % 0x1c;
_0x481a78[_0xd242c9] = _0x5b5217[_0x25b793];
}
return _0x481a78.join(‘’);
}
function bombardinocrocodilo(_0x303704) {
let _0x50a14d = ‘’;
for (let _0x52a601 = 0x0; _0x52a601 < _0x303704.length; _0x52a601++) {
const _0x276c07 = _0x303704[_0x52a601];
const _0x22ab92 = parseInt(_0x276c07, 0x10);
if (isNaN(_0x22ab92)) {
throw new Error(“Invalid hex character: “ + _0x276c07);
}
const _0x2ccdb8 = _0x22ab92 * 0x3 % 0x10;
_0x50a14d += _0x2ccdb8.toString(0x10);
}
return _0x50a14d;
}
function tralalelotralala(_0x3e4647) {
let _0x2ea8bc = ‘’;
for (let _0x233e8a = 0x0; _0x233e8a < _0x3e4647.length; _0x233e8a++) {
_0x2ea8bc += _0x3e4647.charCodeAt(_0x233e8a).toString(0x10);
}
return _0x2ea8bc;
}
const durability2 = () => {
if (bombardinocrocodilo(String(tralalelotralala(skjdhfk1(document.getElementById(“flag”).value)))) == “299c2a9926565c51932223992e935f292c599d5929fdfd57292d5590”) {
alert(“flag is correct!”);
} else {
alert(“wrong flag, try again”);
}
};
from here on, we can either read the code manually and reverse the functions (which is very trivial and easy) or just give this thing to any LLM and it will do the hardwork for us… either ways we solve the challenge and below is my solve script for this one:
Python script to recover the original input (flag) for the obfuscated JS check.
It inverts the three transformations:
1) bombardinocrocodilo: each hex digit y = (x3) % 16 -> inverse multiply by 11 mod 16 (since 311 = 33 ≡ 1 mod 16)
2) tralalelotralala: concatenation of each char code in hex (2 hex digits per char here)
3) skjdhfk1: permutation where dest[(i*5) % 28] = input[i] for i in [0..27]
The script prints the recovered original input (flag).
Replace target with a different hex target if needed.
target = “299c2a9926565c51932223992e935f292c599d5929fdfd57292d5590” # skibiditoilet from the JS
def invert_bombardinocrocodilo(transformed_hex):
# 3 * x % 16 = y -> x = y * inv3 (mod 16), inv3 = 11 because 3*11 = 33 ≡ 1 (mod 16)
inv3 = 11
out_digits = []
for ch in transformed_hex:
y = int(ch, 16)
x = (y * inv3) % 16
out_digits.append(format(x, ‘x’)) # single hex digit
return ‘’.join(out_digits)
def hex_to_string(prehex):
# prehex is expected to be even-length; each pair is the hex of a character code
if len(prehex) % 2 != 0:
raise ValueError(“prehex has odd length, can’t pair into bytes”)
chars = []
for i in range(0, len(prehex), 2):
byte = int(prehex[i:i+2], 16)
chars.append(chr(byte))
return ‘’.join(chars)
def invert_skjdhfk1(permuted):
# skjdhfk1 sets dest[(i5) % 28] = input[i]
# So input[i] = dest[(i5) % 28].
n = 28
if len(permuted) != n:
raise ValueError(f“expected permuted length {n}, got {len(permuted)}”)
original = []
for i in range(n):
pos = (i * 5) % n
original.append(permuted[pos])
return ‘’.join(original)
Run the inversion pipeline
prehex_digits = invert_bombardinocrocodilo(target)
permuted_str = hex_to_string(prehex_digits)
recovered = invert_skjdhfk1(permuted_str)
print(“target (transformed):”, target)
print(“prehex (after inverse digit-map):”, prehex_digits)
print(“permuted string (from hex pairs):”, permuted_str)
print(“recovered original input (flag):”, recovered)
Optional verification: reapply the forward pipeline from the recovered input and check equals target
def forward_pipeline(s):
# skjdhfk1: dest[(i5)%28] = s[i]
n = 28
dest = [‘’] * n
for i, ch in enumerate(s):
dest[(i5) % n] = ch
permuted = ‘’.join(dest)
# tralalelotralala: concat charCode.toString(16) for each char (we’ll use two hex digits for consistency)
hexcat = ‘’.join(format(ord(c), ‘x’) for c in permuted)
# bombardinocrocodilo: for each hex digit -> (int(d,16)*3)%16
transformed = ‘’.join(format((int(d,16)*3)%16, ‘x’) for d in hexcat)
return transformed
verification = forward_pipeline(recovered)
print(“verification equals target?:”, verification == target)
craccon{js_0bfusc4t3d_w311?}
3. Web/Extension Unchained
This was a tricky challenge which was related to a chrome browser extension. For this challenge we are given a zip file containing two javascript files (important) and an html and .json file (not so important). I will only discuss about the javascript files because they hold importance and not the html and json file. We’ll first analyze background.js file:
// background.js (service worker)
// There are no flags here. Definitely not. 😉
const decoyA = “RkxBR3tub3RoaW5nX3RvX3NlZV9oZXJlfQ==”; // fake flag
const decoyB = “U1lOVHtsYmhfanJhZ19ndXJfamViYXRfam5sfQ==”; // also fake
const decoyC = “+br3q+lnBk7ZaB9lGTxPSA85pxY+xtgJykoF15JB2tKd”; //
const vault = “=tTEUuSnNu1HJ9SEbA0GFkHr4EUq2ITq”;
self.addEventListener(‘activate’, () => {
chrome.storage.local.set({ vault });
});
self.addEventListener(‘install’, () => {
chrome.storage.local.set({ vault });
self.skipWaiting();
});
function pointlessHasher(s) {
let h = 0;
for (let i=0;i<s.length;i++) h = (h + s.charCodeAt(i) * (i+3)) % 0xFFFFFF;
return h.toString(16).padStart(6,‘0’);
}
console.debug(“Booster active:”, pointlessHasher(decoyB));
There is some stuff but for now let’s just interpret this as a fake flag js code which contains 3 fake flags and one vault constant which will be important going ahead so make a note of it. Now let’s look at the other javascript file (popup.js):
// popup.js
// Konami
const status = document.getElementById(‘status’);
const reveal = document.getElementById(‘reveal’);
const checkBtn = document.getElementById(‘check’);
reveal.setAttribute(‘data-ctf’, “=0He1R2Xr12Z4Bje59lYrh3XvNHN0BTc”);
checkBtn.addEventListener(‘click’, async () => {
const st = await chrome.storage.local.get([“vault”]);
status.textContent = “Storage OK: “ + (st && typeof st.vault === ‘string’) + “\n”
+ “Hint: Nothing visible? Try looking deeper than the UI.”;
});
const KONAMI = [38,38,40,40,37,39,37,39,66,65];
const keys = [];
window.addEventListener(‘keydown’, (e) => {
keys.push(e.keyCode);
if (keys.length > KONAMI.length) keys.shift();
if (KONAMI.every((k,i)=>k===keys[i])) {
decodeAndShow();
}
});
function decodeAndShow() {
const blob = reveal.getAttribute(‘data-ctf’);
const rev = blob.split(‘’).reverse().join(‘’);
const base = atob(rev);
const plain = caesarShift(base, -6);
reveal.style.display = ‘block’;
reveal.textContent = “[Unlocked] “ + plain;
}
function caesarShift(text, k) {
return text.replace(/[A-Za-z]/g, (c) => {
const base = (c <= ‘Z’) ? ‘A’.charCodeAt(0) : ‘a’.charCodeAt(0);
return String.fromCharCode(base + ((c.charCodeAt(0) - base + (k%26) + 26) % 26));
});
}
Now this file is pretty self-explanatory and you can run the python script below to get the latter half of the flag:
import base64, codecs
popup_blob = “=0He1R2Xr12Z4Bje59lYrh3XvNHN0BTc”
decode popup suffix -> reverse, base64, caesar shift -6
rev_p = popup_blob[::-1]
b_p = base64.b64decode(rev_p).decode()
apply caesar shift -6 (letters only)
def caesar(s,k):
out=[]
for c in s:
if ‘A’<=c<=‘Z’:
base=ord(‘A’); out.append(chr(base + ((ord(c)-base + k)%26)))
elif ‘a’<=c<=‘z’:
base=ord(‘a’); out.append(chr(base + ((ord(c)-base + k)%26)))
else:
out.append(c)
return ‘’.join(out)
suffix = caesar(b_p, -6) # -> k0n4mi_rev_st0rage_xor}
print(suffix)
Now where are we supposed to get the first part? Remember the vault constant? Yes we will use that here, what we have to do is take the vault constant and decode it using rot13 and then reverse it to get the base64 data and then decode it to get : “tevttxyLROChD_VSX@hXGDh”, Now what should we do with this? Is this a dead end? Not at all! Now the next step was a bit tricky to identify but we already have the flag format which is “CRACCON{” and we have to use this to xor the string we just got and we will notice that we get a constant output 0x37 which is the key! So now putting all of that in a python script we can get the first part of the flag too
import base64, codecs
vault = “=tTEUuSnNu1HJ9SEbA0GFkHr4EUq2ITq”
1. reverse
rev = vault[::-1]
2. ROT13 the reversed string
rot = codecs.decode(rev, ‘rot_13’)
3. base64-decode the ROT13 result -> printable
intermediate = base64.b64decode(rot).decode(‘latin1’) # -> ‘tevttxyLROChD_VSX@hXGDh’
4. XOR each char with 0x37
prefix = ‘’.join(chr(ord(c) ^ 0x37) for c in intermediate)
print(prefix) # CRACCON{ext_shadow_ops_
CRACCON{ext_shadow_ops_k0n4mi_rev_st0rage_xor}
4. Web/Orderly Chaos
This challenge was also a Web challenge and I was shocked that I was able to do web in this ctf too…. Quite surprising, isn’t it? For this challenge we are given the following instance: http://ecom.ctf.defhawk.com/shop
We first register a user and login with it and then I placed one or two sample orders but still don’t find anything interesting in the responses and requests alike. The description tells us to focus on the orders api so that’s why we’re putting our main aim onto that. After a bit of tinkering we see that there’s a “My Orders” page in the account settings, I open that and intercept the request using Burpsuite

So now we are asked to look for the user ID: 678b366000a47f4fa1ecc432 in the hint so what we have to do is make an array and append this user id to the array and in the response we should get the flag.

The flag is dynamic though so I won’t be copy pasting the flag directly here.
5. Mobile/Easy RASP
Runtime application self-protection, or RASP, is a security technology that operates within software applications to monitor, detect, analyze, and protect against malicious activity automatically in real-time while the application is running.
So now we have to work on decompiling this apk given for which I use jadx gui and see that the MainActivity is not doing much except load a native library.. So we can export that native library and continue working on that now we can open the library in any decompiler, I use IDA. From all the two functions of interest are decrypt_flag() and confuse_and_map(). From here on you can automate the steps and use AI to solve the rest aur if you want just manually do it. Below is my solve script:
reproduce the native behaviour
KEY = bytes([0x0F,0x17,0x15,0x0E,0x06,0x06,0x00,0x55,0x7C,0x2D,
0x36,0x24,0x32,0x54,0x1A,0x31,0x6B,0x4F,0x1F,0x1C,0x09])
password = b“letmein..”
flag = bytes(password[i % len(password)] ^ KEY[i] for i in range(21))
print(flag) # b’craccon{RASP_1s_Easy}’
print(flag.decode()) # craccon{RASP_1s_Easy}
6. Web/Method Madness
Another Web challenge which was fairly easy so I won’t spend much time on this one, just fire up burpsuite and see the description it says to check the discount code related methods or something like that so we go to the coupon code functionality of the website which is available at checkout and that is what we need to exploit. Now we can just add random items to the cart and apply a random coupon code and intercept the request in burp.

Now all we have to do is change the POST method to GET and we will have the flag.

7. Rev/ShellCode
This was an interesting reverse engineering challenge. In a nutshell this challenge had a binary inside a binary. What the given executable was doing basically was creating another executable at the location /tmp/coderun and then executing that and then deleting that. So with enough static analysis we can extract the coderun executable from the binary (or another option would’ve been to patch the binary to not delete coderun but I didn’t try this). After having the coderun file we can decompile it and see that the main function looks something like:
int __fastcall main(int argc, const char **argv, const char **envp)
{
char v3; // cl
char v5[32]; // [rsp+0h] [rbp-110h] BYREF
__int64 v6; // [rsp+20h] [rbp-F0h]
__int64 v7; // [rsp+28h] [rbp-E8h]
__int64 v8; // [rsp+30h] [rbp-E0h]
__int64 v9; // [rsp+38h] [rbp-D8h]
__int64 v10; // [rsp+40h] [rbp-D0h]
__int64 v11; // [rsp+48h] [rbp-C8h]
__int64 v12; // [rsp+50h] [rbp-C0h]
__int64 v13; // [rsp+58h] [rbp-B8h]
__int64 v14; // [rsp+60h] [rbp-B0h]
__int64 v15; // [rsp+68h] [rbp-A8h]
__int64 v16; // [rsp+70h] [rbp-A0h]
__int64 v17; // [rsp+78h] [rbp-98h]
__int64 v18; // [rsp+80h] [rbp-90h]
__int64 v19; // [rsp+88h] [rbp-88h]
__int64 v20; // [rsp+90h] [rbp-80h]
__int64 v21; // [rsp+98h] [rbp-78h]
__int64 v22; // [rsp+A0h] [rbp-70h]
__int64 v23; // [rsp+A8h] [rbp-68h]
__int64 v24; // [rsp+B0h] [rbp-60h]
__int64 v25; // [rsp+B8h] [rbp-58h]
__int64 v26; // [rsp+C0h] [rbp-50h]
__int64 v27; // [rsp+C8h] [rbp-48h]
__int64 v28; // [rsp+D0h] [rbp-40h]
__int64 v29; // [rsp+D8h] [rbp-38h]
__int64 v30; // [rsp+E0h] [rbp-30h]
__int64 v31; // [rsp+E8h] [rbp-28h]
__int64 v32; // [rsp+F0h] [rbp-20h]
__int64 v33; // [rsp+F8h] [rbp-18h]
__int64 v34; // [rsp+100h] [rbp-10h]
__int64 v35; // [rsp+108h] [rbp-8h]
strcpy(v5, “cy_iCljBNxlayp>RcveLjNyac>hRN”);
printf((unsigned int)&unk_487018, (unsigned int)v5, 1634498638, v3);
v35 = calloc(174LL, 1LL);
v34 = v35;
v33 = v35 + 6;
v32 = v35 + 12;
v31 = v35 + 18;
v30 = v35 + 24;
v29 = v35 + 30;
v28 = v35 + 36;
v27 = v35 + 42;
v26 = v35 + 48;
v25 = v35 + 54;
v24 = v35 + 60;
v23 = v35 + 66;
v22 = v35 + 72;
v21 = v35 + 78;
v20 = v35 + 84;
v19 = v35 + 90;
v18 = v35 + 96;
v17 = v35 + 102;
v16 = v35 + 108;
v15 = v35 + 114;
v14 = v35 + 120;
v13 = v35 + 126;
v12 = v35 + 132;
v11 = v35 + 138;
v10 = v35 + 144;
v9 = v35 + 150;
v8 = v35 + 156;
v7 = v35 + 162;
v6 = v35 + 168;
*(_BYTE *)(v35 + 1) = v5[28];
*(_BYTE *)(v33 + 2) = v5[2];
*(_BYTE *)(v32 + 3) = v5[19];
*(_BYTE *)(v31 + 4) = v5[21];
*(_BYTE *)(v30 + 5) = v5[8];
*(_BYTE *)(v29 + 1) = v5[7];
*(_BYTE *)(v28 + 2) = v5[4];
*(_BYTE *)(v27 + 3) = v5[17];
*(_BYTE *)(v26 + 4) = v5[9];
*(_BYTE *)(v25 + 5) = v5[24];
*(_BYTE *)(v24 + 1) = v5[22];
*(_BYTE *)(v23 + 2) = v5[10];
*(_BYTE *)(v22 + 3) = v5[16];
*(_BYTE *)(v21 + 4) = v5[20];
*(_BYTE *)(v20 + 5) = v5[23];
*(_BYTE *)(v19 + 1) = v5[26];
*(_BYTE *)(v18 + 2) = v5[27];
*(_BYTE *)(v17 + 3) = v5[12];
*(_BYTE *)(v16 + 4) = v5[18];
*(_BYTE *)(v15 + 5) = v5[25];
*(_BYTE *)(v14 + 1) = v5[15];
*(_BYTE *)(v13 + 2) = v5[1];
*(_BYTE *)(v12 + 3) = v5[5];
*(_BYTE *)(v11 + 4) = v5[0];
*(_BYTE *)(v10 + 5) = v5[6];
*(_BYTE *)(v9 + 1) = v5[11];
*(_BYTE *)(v8 + 2) = v5[14];
*(_BYTE *)(v7 + 3) = v5[3];
*(_BYTE *)(v6 + 4) = v5[13];
return free(v35);
}
We can easily reverse this to get the flag script for which is given below:
v5 = “cy_iCljBNxlayp>RcveLjNyac>hRN”
permutation & placement encoded in the binary (from decompilation)
assigns = [
(0,1,28),(1,2,2),(2,3,19),(3,4,21),(4,5,8),(5,1,7),(6,2,4),(7,3,17),
(8,4,9),(9,5,24),(10,1,22),(11,2,10),(12,3,16),(13,4,20),(14,5,23),
(15,1,26),(16,2,27),(17,3,12),(18,4,18),(19,5,25),(20,1,15),(21,2,1),
(22,3,5),(23,4,0),(24,5,6),(25,1,11),(26,2,14),(27,3,3),(28,4,13)
]
build buffer of 29*6 bytes
buf = bytearray(296)
ptrs = [6i for i in range(29)]
for ptr_idx, offset, src_idx in assigns:
buf[ptrs[ptr_idx] + offset] = ord(v5[src_idx])
read row-major printable bytes
row_major = “”.join(chr(b) for b in buf if 32 <= b < 127)
xor with 13 to get the flag
flag = “”.join(chr(ord(c) ^ 13) for c in row_major)
print(flag)
normalize prefix to lowercase if needed:
print(“canonical:”, flag[0:7].lower() + flag[7:])
8. Threat Research/Pow3r
This was not a super difficult challenge, instead the flag was a bit guessy. This was basic powershell command interpretation but the contents you get after interpreting the powershell command goes something like: CRAccONcl3t\th3\shellb_e\dithb.0ua
from here you can figure out the part of the flag to be CRACCON{l3t_th3_shell_be_with_y0u} but now the correct flag is CRACCON{l3t_th3_sh3ll_be_with_y0u} with one character changed.
9. Crypto/ECG
This challenge was the only crypto I solved in this CTF and it does have quite a LOT of solves so I’ll just give the script for this thing directly
recover_craccon.py
Requires: pycryptodome (for long_to_bytes / bytes_to_long helpers)
from Crypto.Util.number import long_to_bytes
from math import gcd, isqrt
import random
from collections import defaultdict
given sequence
s = [1471188920,8580498,528503476,577384753,534687615,631132756,
1181691587,494356384,450508778,224733577,240456085]
––––– utils –––––
def pollard_rho(n):
if n % 2 == 0:
return 2
if n % 3 == 0:
return 3
# f(x) = x^2 + c
while True:
x = random.randrange(2, n-1)
y = x
c = random.randrange(1, n-1)
d = 1
while d == 1:
x = (xx + c) % n
y = (yy + c) % n
y = (y*y + c) % n
d = gcd(abs(x-y), n)
if d == n:
break
if d > 1 and d < n:
return d
def factor(n):
“”“Return prime factorization as a dict {p:exp}”“”
if n == 1:
return {}
# trial divide small primes first
res = {}
small_primes = [2,3,5,7,11,13,17,19,23,29,31,37,41,43,47]
for p in small_primes:
while n % p == 0:
res[p] = res.get(p,0)+1
n//=p
if n==1:
return res
# if n is prime
def is_probable_prime(n):
if n<2: return False
# Miller-Rabin bases good for 32-bit integers
d = n-1
s = 0
while d%2==0:
s+=1; d//=2
for a in (2,7,61):
if a >= n: continue
x = pow(a,d,n)
if x==1 or x==n-1:
continue
for _ in range(s-1):
x = (x*x) % n
if x==n-1:
break
else:
return False
return True
stack = [n]
while stack:
m = stack.pop()
if m==1: continue
if is_probable_prime(m):
res[m] = res.get(m,0)+1
continue
d = pollard_rho(m)
if d is None:
# fallback
raise RuntimeError(“rho failed”)
stack.append(d)
stack.append(m//d)
return res
Baby-step giant-step discrete log: find x s.t. g^x = h (mod p)
def discrete_log(g, h, p):
g %= p
h %= p
if h == 1:
return 0
m = isqrt(p) + 1
table = {}
e = 1
for j in range(m):
if e not in table:
table[e] = j
e = (e * g) % p
factor = pow(g, p-1-m, p) # g^{-m} mod p (since g^{p-1}=1)
gamma = h
for i in range(m+1):
if gamma in table:
return i*m + table[gamma]
gamma = (gamma * factor) % p
return None
find primitive root modulo p (p must be prime)
def primitive_root(p):
if p == 2:
return 1
# factor p-1
fac = factor(p-1)
for g in range(2, p):
ok = True
for q in fac:
if pow(g, (p-1)//q, p) == 1:
ok = False; break
if ok:
return g
raise RuntimeError(“no primitive root found”)
––––– Step 1: recover n –––––
def recover_n(seq):
diffs = [ (seq[i+1]-seq[i]) for i in range(len(seq)-1) ]
vals = []
for i in range(len(diffs)-1):
for j in range(i+1, len(diffs)-1):
vals.append(abs(diffs[i+1]*diffs[j] - diffs[i]*diffs[j+1]))
g = 0
for v in vals:
g = gcd(g, v)
return g
n = recover_n(s)
print(“Recovered n =”, n)
––––– Step 2: A, c, s0_block –––––
diffs = [ (s[i+1]-s[i]) % n for i in range(len(s)-1) ]
handle potential non-invertible diffs: pick i where invertible
inv_i = None
for i in range(len(diffs)-1):
if gcd(diffs[i], n) == 1:
inv_i = i
break
if inv_i is None:
raise RuntimeError(“No invertible diff found; can’t recover A directly”)
A = (diffs[inv_i+1] * pow(diffs[inv_i], -1, n)) % n
c = (s[1] - A * s[0]) % n
s0_block = ((s[0] - c) * pow(A, -1, n)) % n
print(“A =”, A)
print(“c =”, c)
print(“s0_block =”, s0_block)
––––– Step 3: recover m_block i.e. solve x^{e} = A (mod n) –––––
reduce the exponent modulo phi(p) per prime factorization
e_big = 2**65537
fac = factor(n)
print(“n factorization:”, fac)
For each prime factor compute solutions mod p
per_prime_solutions = []
for p, expv in fac.items():
if expv != 1:
raise NotImplementedError(“This solver expects square-free modulus (exp=1).”)
if p == 2:
# small special-case: brute force mod 2
sols = [x for x in range(2) if pow(x, e_big, p) == A % p]
per_prime_solutions.append((p, sols))
continue
k = pow(2, 65537, p-1) # e mod (p-1)
A_mod = A % p
# quick check: solutions exist only if A^{(p-1)/g} == 1 mod p where g = gcd(k, p-1)
g = gcd(k, p-1)
if pow(A_mod, (p-1)//g, p) != 1:
print(f"No solution modulo prime {p}")
per_prime_solutions.append((p, []))
continue
# find generator and discrete log of A
g0 = primitive_root(p)
a = discrete_log(g0, A_mod, p)
if a is None:
raise RuntimeError("discrete log failed for prime %d" % p)
# Solve k * x ≡ a (mod p-1)
mod = p-1
gg = gcd(k, mod)
# there are gg solutions, step = mod/gg
base = pow(k//gg, -1, mod//gg) # inverse of k/gg modulo mod/gg
particular = ( (a//gg) * base ) % (mod//gg)
sols = []
for t in range(gg):
x = (particular + t*(mod//gg)) % mod
root = pow(g0, x, p)
sols.append(root)
per_prime_solutions.append((p, list(set(sols)))) # unique
print(“Per-prime solutions counts:”, [(p, len(sol)) for p, sol in per_prime_solutions])
––––– CRT combine –––––
produce all combinations of per-prime solutions
def crt_combine(sol_list):
# sol_list = [(p1, [r11,r12,…]), (p2, […]), …]
# returns list of integers modulo n
mods = [p for p,_ in sol_list]
residues_options = [solutions for ,solutions in sol_list]
# quick bail if any prime has zero solutions
for sols in residues_options:
if len(sols) == 0:
return []
# iterative combining
combos = [ (0,1) ] # (residue, modulus)
for p, sols in sol_list:
new_combos = []
for residue, mod_curr in combos:
for r in sols:
# solve x ≡ residue (mod mod_curr)
# x ≡ r (mod p)
# find via simple brute force for small mod_currp (fast because mod sizes are small)
# use standard CRT since mod_curr and p are coprime
m1 = mod_curr
m2 = p
inv = pow(m1, -1, m2)
x = ( (r - residue) * inv ) % m2
combined = residue + m1 * x
new_combos.append( (combined, m1m2) )
combos = new_combos
# reduce residues modulo full modulus
return [res % n for res, in combos]
m_candidates = crt_combine(per_prime_solutions)
m_candidates = sorted(set(m_candidates))
print(“Total m candidates modulo n:”, len(m_candidates))
Filter candidates to 32-bit (original block is 4 bytes)
m_candidates_32 = [m for m in m_candidates if 0 <= m < (1<<32)]
print(“m candidates fitting 4-bytes:”, m_candidates_32)
––––– Build message bytes –––––
blocks = [n, s0_block, c]
final_flags = []
for m in m_candidates_32:
msg_bytes = b’’.join(long_to_bytes(b, 4) for b in (blocks + [m]))
flag = b“craccon{” + msg_bytes + b“}“
final_flags.append(flag)
if not final_flags:
print(“No 4-byte candidate m found. Printing all m candidates (mod n):”)
for m in m_candidates:
print(hex(m))
else:
print(“Candidate flags:”)
for f in final_flags:
print(f)
10. Rev/Flatearth
This challenge was nothing but just interpreting the conditions (case switches) set by the check_flag function properly. What I did was just prompt AI to make an interpreter for directly interpreting the Decompiled C to the conditions and then make the flag so that there is no manual effort of putting in the conditions manually and then solving for each. Below is the script
solve_checkflag.py
import re
path to your decompiled file
PATH = “checkflag_c.txt”
with open(PATH, ‘r’, encoding=‘utf-8’) as f:
text = f.read()
extract initial v2 and v3
m_v2 = re.search(r“v2 = (\d+);”, text)
m_v3 = re.search(r“v3 = (\d+);”, text)
v2_init = int(m_v2.group(1))
v3_init = int(m_v3.group(1))
split into case blocks
cases = {}
for m in re.finditer(r“case\s+(\d+):”, text):
case_num = int(m.group(1))
start = m.end()
br = text.find(“break;”, start)
block = text[start:br]
cases[case_num] = block
parse blocks into typed operations
parsed = {}
for case, block in cases.items():
nxt_m = re.search(r“v3 = (\d+);”, block)
nxt = int(nxt_m.group(1)) if nxt_m else None
b = block
# xor then add: ((v2 ^ (a1+idx)) + C) == T
if “v2 ^ (_BYTE )(a1 +” in b:
m_idx = re.search(r“a1 + (\d+))”, b)
m_plus = re.search(r“+\s(\d+))\s==\s(\d+)“, b)
if m_idx and m_plus:
parsed[case] = (“xor_add”, int(m_idx.group(1)), int(m_plus.group(1)), int(m_plus.group(2)), nxt)
continue
# add then xor: ((v2 + (a1+idx)) ^ K) == T
if “v2 + (_BYTE )(a1 +” in b and “^” in b:
m_idx = re.search(r“a1 + (\d+))”, b)
m_hex = re.search(r“^\s0x([0-9A-Fa-f]+))\s==\s(\d+)“, b) or re.search(r”^\s*(\d+))\s*==\s*(\d+)“, b)
if m_idx and m_hex:
K = int(m_hex.group(1),16) if ‘0x’ in m_hex.group(0) else int(m_hex.group(1))
target = int(m_hex.group(2))
parsed[case] = (“add_xor”, int(m_idx.group(1)), K, target, nxt)
continue
# sub then xor: ((v2 - (a1+idx)) ^ K) == T
if “v2 - (_BYTE )(a1 +” in b and “^” in b:
m_idx = re.search(r“a1 + (\d+))”, b)
m_hex = re.search(r“^\s0x([0-9A-Fa-f]+))\s==\s(\d+)“, b) or re.search(r”^\s*(\d+))\s*==\s*(\d+)“, b)
if m_idx and m_hex:
K = int(m_hex.group(1),16) if ‘0x’ in m_hex.group(0) else int(m_hex.group(1))
target = int(m_hex.group(2))
parsed[case] = (“sub_xor”, int(m_idx.group(1)), K, target, nxt)
continue
# inequality-equality special (like: if ((unsigned __int8)(v2 + (a1+6)) != 0x3D) … else v2 = (s + v2) ^ 0x3D)
if “!= 0x” in b or “!= “ in b:
m_idx = re.search(r”a1 + (\d+))“, b)
idx = int(m_idx.group(1)) if m_idx else (0 if “v2 + (_BYTE )a1” in b or “v2 ^ (_BYTE )a1” in b else None)
m_val = re.search(r“!=\s0x([0-9A-Fa-f]+)“, b)
val = int(m_val.group(1),16) if m_val else None
m_x = re.search(r”^\s0x([0-9A-Fa-f]+)“, b)
xor_const = int(m_x.group(1),16) if m_x else None
if idx is not None and val is not None:
parsed[case] = (“neq_const”, idx, val, xor_const, nxt)
continue
# fallback: try patterns without +N (a1)
if “v2 ^ (_BYTE )a1” in b:
m_plus = re.search(r“+\s(\d+))\s==\s(\d+)“, b)
if m_plus:
parsed[case] = (“xor_add”, 0, int(m_plus.group(1)), int(m_plus.group(2)), nxt)
continue
if “v2 + (_BYTE )a1” in b and “^” in b:
m_hex = re.search(r“^\s0x([0-9A-Fa-f]+))\s==\s(\d+)“, b) or re.search(r”^\s(\d+))\s*==\s*(\d+)“, b)
if m_hex:
K = int(m_hex.group(1),16) if ‘0x’ in m_hex.group(0) else int(m_hex.group(1))
parsed[case] = (“add_xor”, 0, K, int(m_hex.group(2)), nxt)
continue
parsed[case] = (“raw”, b, nxt)
parse default block (used when switch doesn’t have that case)
m_def = re.search(r“default:\s*(.?)\n\sbreak;”, text, re.S)
default_entry = None
if m_def:
db = m_def.group(1)
m_idx = re.search(r“a1 + (\d+))”, db)
m_hex = re.search(r“^\s0x([0-9A-Fa-f]+))\s==\s*(\d+)“, db) or re.search(r”^\s*(\d+))\s*==\s*(\d+)“, db)
nxt = int(re.search(r”v3 = (\d+);“, db).group(1))
if m_idx and m_hex:
K = int(m_hex.group(1),16) if ‘0x’ in m_hex.group(0) else int(m_hex.group(1))
default_entry = (“sub_xor”, int(m_idx.group(1)), K, int(m_hex.group(2)), nxt)
else:
default_entry = (“raw”, db, nxt)
simulation
v2 = v2_init
v3 = v3_init
flag = [None]*31
steps = 0
while v3 and v3 != -1 and steps < 5000:
steps += 1
entry = parsed.get(v3, default_entry)
typ = entry[0]
if typ == “xor_add”:
_, idx, plus, target, nxt = entry
s = v2 ^ ((target - plus) & 0xFF)
flag[idx] = s
v2 = ((s ^ v2) + plus) & 0xFF
v3 = nxt
elif typ == “add_xor”:
_, idx, K, target, nxt = entry
s = ((target ^ K) - v2) & 0xFF
flag[idx] = s
v2 = ((s + v2) ^ K) & 0xFF
v3 = nxt
elif typ == “sub_xor”:
_, idx, K, target, nxt = entry
s = (v2 - (target ^ K)) & 0xFF
flag[idx] = s
v2 = ((v2 - s) ^ K) & 0xFF
v3 = nxt
elif typ == “neq_const”:
_, idx, val, xor_const, nxt = entry
s = (val - v2) & 0xFF
flag[idx] = s
xor_const = xor_const or 0
v2 = ((s + v2) ^ xor_const) & 0xFF
v3 = nxt
else:
raise RuntimeError(“unparsed block encountered”)
flag_str = ‘’.join(chr(b) for b in flag)
print(flag_str)
The checkflag_c.txt file basically contains the full decompiled text from IDA for the check_flag function only.
And I think that’s it for my writeups. Thank you for reading so far and I hope you enjoyed reading the writeups! Till we meet again…
