# PricelessL3ak mobile challenge - L3akCTF 2025
## Synopsis
This challenge involves identifying a lifecycle intent vulnerability. The flags from the lifecycle intent serve as the decryption key for a file containing VM instructions. These instructions are executed in the background using IPC parcel communication.
<br>
## Description
Basic flag checker app, or is it?
<br>
## Enumeration & Exploitation
### Running the app & overview
First, we'll use Android Studio to examine the challenge and understand its functionality. We can see that it's simply a flag checker - when we input any text, it displays an "Wrong" message.
<br>
### Analyzing the source code
+ When examining the challenge, we're provided with an Android APK that can be decompiled using tools like JADX. The code appears to be obfuscated with ProGuard, as evidenced by the shortened class names and restructured packages.
Looking at the MainActivity, we can see that it takes user input, calculates the SHA256 hash of that input, and compares it with a hardcoded hash. Strange, right?
```java
private void checkFlag(String str) {
String calculateSHA256 = calculateSHA256(str);
if (calculateSHA256 == null || !calculateSHA256.equals(TARGET_HASH)) {
this.resultText.setText("Wrong!");
this.resultText.setTextColor(getColor(R.color.holo_red_dark));
} else {
this.resultText.setText("Correct!");
this.resultText.setTextColor(getColor(R.color.holo_green_dark));
}
}
```
Let's go back and analyze the AndroidManifest.xml file to check for any exported components:
```java
<activity
android:theme="@android:style/Theme.Translucent.NoTitleBar"
android:name="ctf.l3akctf.pricelessl3ak.h1832fla12"
android:exported="true"/>
```
From this, we can see there's an exported activity. Let's navigate to that class and examine its functionality.
```java
public class h1832fla12 extends Activity {
public static final
int f1490b = 0;
public h f1491a;
@Override
public final void onCreate(Bundle bundle) {
super.onCreate(bundle);
h hVar = new h();
hVar.f360a = this;
c cVar = new c();
cVar.f5a = this;
hVar.f361b = cVar;
this.f1491a = hVar;
if (!"BINGO".equals(getIntent().getAction())) {
finish();
return;
}
h hVar2 = this.f1491a;
h1832fla12 h1832fla12Var = (h1832fla12) hVar2.f360a;
try {
InputStream open = h1832fla12Var.getAssets().open("data.enc");
byte[] bArr = new byte[open.available()];
hVar2.f362c = bArr;
open.read(bArr);
open.close();
} catch (Exception unused) {
h1832fla12Var.finish();
}
}
@Override
public final void onNewIntent(Intent intent) {
String stringExtra;
super.onNewIntent(intent);
setIntent(intent);
if ("BANGO".equals(intent.getAction())) {
h hVar = this.f1491a;
if (((byte[]) hVar.f362c) == null || intent.getFlags() == 0 || (stringExtra = intent.getStringExtra("f")) == null) {
return;
}
int flags = intent.getFlags();
byte[] bArr = (byte[]) hVar.f362c;
C0003d c0003d = new C0003d(9, hVar);
c cVar = (c) hVar.f361b;
cVar.getClass();
try {
cVar.h(c0003d, stringExtra, c.j(c.g(bArr, flags)));
} catch (Exception e2) {
c0003d.v(" " + e2.getMessage());
}
}
}
}
```
From this class, we can observe a two-stage lifecycle intent vulnerability:
+ Stage 1 (onCreate): The activity receives an intent and checks if the action equals "BINGO". If successful, it loads an encrypted file (data.enc) from the assets into memory, but performs no other operations.
+ Stage 2 (onNewIntent): The activity receives a second intent with action "BANGO" and an extra parameter "f". If the loaded data exists, intent flags are present, and the "f" parameter is not empty, it processes the encrypted data by passing the loaded bytes, intent flags, and the "f" parameter to cVar.h(c0003d, stringExtra, c.j(c.g(bArr, flags))).
This creates a lifecycle intent vulnerability. We'll build our own exploit app to trigger this behavior.
<br>
### Exploiting the lifecycle intent
Here's the code to exploit the lifecycle intent vulnerability:
```java
@Override
public void onClick(View v) {
String f = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa";
Intent openIntent = new Intent();
openIntent.setComponent(new ComponentName("ctf.l3akctf.pricelessl3ak", "ctf.l3akctf.pricelessl3ak.h1832fla12"));
openIntent.setAction("BINGO");
openIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
startActivity(openIntent);
Intent reopenIntent = new Intent();
reopenIntent.setComponent(new ComponentName("ctf.l3akctf.pricelessl3ak", "ctf.l3akctf.pricelessl3ak.h1832fla12"));
reopenIntent.setAction("BANGO");
reopenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
reopenIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
reopenIntent.putExtra("f", f);
new Handler().postDelayed(() -> {
startActivity(reopenIntent);
}, 1000);
}
```
after building and running the app, clicking the button successfully exploits the lifecycle intent vulnerability. We receive a popup message indicating "failed," which suggests there's an additional flag validation process occurring. We'll now proceed to decrypt the file and analyze what operations are being performed.
First, the method processes the encrypted data using two parameters: the encrypted data itself and the intent flags. The intent flags we're using are:
```java
reopenIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
reopenIntent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
```
These flags have the following hex values:
FLAG_ACTIVITY_NEW_TASK → 0x10000000
FLAG_ACTIVITY_SINGLE_TOP → 0x20000000
Since the method doesn't check for a specific flag, the two flags will be combined into a single value with or operation:
FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_SINGLE_TOP → 0x10000000 | 0x20000000 = 0x30000000
Now we have the key (0x30000000) used for decrypting the file. Here's the decryption algorithm:
```java
private static final long SEED = 0x30000000L;
public static byte[] decrypt(byte[] data) {
byte[] result = data.clone();
for (int i = result.length - 1; i >= 1; i--) {
result[i] ^= result[i-1];
}
for (int i = 0; i < result.length; i++) {
int rotAmount = (i % 7) + 1;
result[i] = (byte)rotateRight(result[i] & 0xFF, rotAmount);
}
for (int i = 0; i < result.length; i++) {
int addValue = (i * 0x13 + (int)(SEED & 0xFF)) & 0xFF;
result[i] = (byte)((result[i] - addValue) & 0xFF);
}
for (int i = 0; i < result.length; i++) {
int keyByte = (int)((SEED >> ((i % 4) * 8)) & 0xFF);
result[i] ^= keyByte;
}
return result;
}
```
we now have the decrypted file.
<br>
### IPC Parcel Communication
The next step involves understanding how the decrypted data is processed using Android's Parcel mechanism. The c.j method is responsible for parsing the decrypted bytes into a structured format:
```java
public static ArrayList j(byte[] bArr) {
ArrayList arrayList = new ArrayList();
ByteBuffer wrap = ByteBuffer.wrap(bArr);
wrap.order(ByteOrder.LITTLE_ENDIAN);
while (wrap.remaining() >= 7) {
arrayList.add(new v27a8612b(wrap.get() & 255, wrap.get() & 255, wrap.get() & 255, wrap.getInt()));
}
if (arrayList.isEmpty()) {
throw new Exception("?");
}
return arrayList;
}
```
This method parses the decrypted data by:
1 - Creating a ByteBuffer with little-endian byte order
2 - Reading chunks of 7 bytes at a time (3 bytes + 1 integer = 7 bytes total)
3 - Creating v27a8612b objects from each 7-byte chunk
4 - Returning an ArrayList of these parsed objects
The v27a8612b class is a Parcelable object that stores structured data:
```java
public class v27a8612b implements Parcelable {
public final int f1498a;
public final int f1499b;
public final int f1500c;
public final int f1501d;
public v27a8612b(int i2, int i3, int i4, int i5) {
this.f1498a = i2;
this.f1499b = i3;
this.f1500c = i4;
this.f1501d = i5;
}
@Override
public void writeToParcel(Parcel parcel, int i2) {
parcel.writeInt(this.f1498a);
parcel.writeInt(this.f1499b);
parcel.writeInt(this.f1500c);
parcel.writeInt(this.f1501d);
}
}
```
the Parcelable implementation allows these objects to be efficiently serialized and passed between Android components via IPC (Inter-Process Communication). Each v27a8612b object represents a structured data element with four integer fields, and the collection of these objects forms the complete parsed dataset from the decrypted file.
The h method acts as an entry point that forwards the processed data to the next stage:
```java
public void h(C0003d c0003d, String str, ArrayList arrayList) {
try {
if (((X.c) this.f6b) == null) {
h1832fla12 h1832fla12Var = (h1832fla12) this.f5a;
X.c cVar = new X.c();
cVar.f353c = false;
cVar.f352b = h1832fla12Var.getApplicationContext();
this.f6b = cVar;
cVar.b();
}
((X.c) this.f6b).a(new C0003d(8, c0003d), str, arrayList);
} catch (Exception e2) {
c0003d.v("Error: " + e2.getMessage());
}
}
```
The method first checks if some component (X.c) is already set up. If not, it creates one and configures it with the app context - kind of like making sure you have the right tool for the job before you start working.
Then it takes everything we've gathered so far - our flag input, those parsed bytes objects, and a way to report back what happens - and hands it all off to this X.c component to do.
The X.c class is essentially a communication bridge that handles the transition from our exploited data to whatever processing system comes next:
```java
public final class c {
public a f351a;
public Context f352b;
public boolean f353c;
public final void a(C0003d c0003d, String str, ArrayList arrayList) {
if (!this.f353c) {
b();
}
h k2 = h.k();
p2a1672ac p2a1672acVar = !arrayList.isEmpty() ? new p2a1672ac(str, arrayList) : new p2a1672ac(str, (ArrayList) null);
b bVar = new b(c0003d);
ConcurrentHashMap concurrentHashMap = (ConcurrentHashMap) k2.f360a;
int i2 = p2a1672acVar.f1492a;
f fVar = (f) concurrentHashMap.get(Integer.valueOf(i2));
if (fVar == null) {
bVar.b("No handler registered for message type: " + i2);
return;
}
Parcel obtain = Parcel.obtain();
p2a1672acVar.writeToParcel(obtain, 0);
Message obtainMessage = fVar.obtainMessage();
obtainMessage.obj = obtain;
int incrementAndGet = ((AtomicInteger) k2.f362c).incrementAndGet();
obtainMessage.what = incrementAndGet;
((ConcurrentHashMap) k2.f361b).put(Integer.valueOf(incrementAndGet), bVar);
fVar.sendMessageDelayed(obtainMessage, 50L);
}
public final void b() {
if (this.f353c) {
return;
}
h k2 = h.k();
this.f351a = new a();
a aVar = this.f351a;
HandlerThread handlerThread = new HandlerThread("BackgroundProcessor", 10);
handlerThread.start();
f fVar = new f(handlerThread.getLooper());
new WeakReference(aVar);
fVar.f357a = this.f352b.getApplicationContext();
((ConcurrentHashMap) k2.f360a).put(4919, fVar);
this.f353c = true;
}
}
```
What this class actually does:
+ Initialization (b method): Sets up a background processing system with a dedicated HandlerThread called "BackgroundProcessor". It registers a handler for message type 4919 in some global registry.
+ Message Processing (a method): Takes our flag string and parsed instruction list, wraps them into a p2a1672ac message object, and sends it through the parcel communication system with a 50ms delay.
+ Handler Lookup: It looks up a registered handler for the message type and forwards the parceled data to it. If no handler exists, it reports an error.
The interesting part is that this creates a background thread specifically for processing our data, and uses Android's Parcel system to serialize and send messages between components. However, we still don't know what the actual handler (the f class) does with our instruction data once it receives it.
Moving to the f class, we can see that JADX failed to decompile the code properly and is showing some garbage comments. Don't worry - we can fix this by enabling the 'Show inconsistent code' option in the preferences.

Now we have a clean code of that class:

we can see this is actually a vm! Looking at the f class (which is the VM executor), we can finally understand what all that parcel communication was leading up to.
Now let's break down the actual parcel communication classes to understand the complete picture:
**p2a1672ac - The Message Container:**
```java
public class p2a1672ac implements Parcelable {
public final int f1492a; // message type
public final String f1493b; // flag
public final ArrayList f1494c; // vm instruction list (v27a8612b objects)
public final byte[] f1495d; // raw binary data
public final int f1496e; // flags field
}
```
this is the main envelope that carries our data. It can hold either parsed instructions (f1494c) or raw bytes (f1495d), along with our flag (f1493b).
**v27a8612b - VM Instruction Objects:**
```java
public class v27a8612b implements Parcelable {
public final int f1498a; // opcode
public final int f1499b; // reg 1
public final int f1500c; // reg 2
public final int f1501d; // immediate value
}
```
these represent individual VM instructions. Each 7-byte chunk from our decrypted file becomes one of these objects.
**v1289a0d - VM Result:**
```java
public class v1289a0d implements Parcelable {
public final boolean f1497a; // success/failure result
}
```
simple boolean wrapper for the VM execution result.
**c9a7d02a - Control Flow State:**
```java
public class c9a7d02a implements Parcelable {
public boolean f1482a; // zero flag
public boolean f1483b; // negative/carry flag
public int f1484c; // PC
}
```
**a0da01 - Arithmetic State:**
```java
public class a0da01 implements Parcelable {
public int[] f1479a; // register array (16 registers)
public boolean f1480b; // zero flag
public boolean f1481c; // negative flag
}
```
**da012da - VM Data Management State:**
```java
public class da012da implements Parcelable {
public int[] f1485a; // register array (16 registers)
public HashMap f1486b; // memory map (address -> value)
public String f1487c; // flag
public ArrayList f1488d; // store tracking
public int f1489e; // flag length
}
```
This class essentially represents the "data processing unit" of the vm - it's what tracks all the memory operations, flag characters, and intermediate results during the complex multi-phase algorithm execution.
**handleMessage:**
```java
@Override
public final void handleMessage(Message message) {
Object obj = message.obj;
if (obj instanceof Parcel) {
Parcel parcel = (Parcel) obj;
parcel.setDataPosition(0);
try {
try {
p2a1672ac e2 = e(p2a1672ac.CREATOR.createFromParcel(parcel));
g gVar = (g) ((ConcurrentHashMap) h.k().f361b).get(Integer.valueOf(message.what));
if (gVar != null && e2 != null) {
gVar.a(e2);
} else if (gVar != null) {
gVar.b("Processing returned null result");
}
} catch (Exception e3) {
}
} finally {
parcel.recycle();
}
}
}
```
+ Takes the incoming Message object and extracts the Parcel from it
+ Uses p2a1672ac.CREATOR.createFromParcel(parcel) to reconstruct our message object from the parceled data
+ Calls method e() which contains the actual VM execution logic we saw earlier
+ Finds the appropriate callback (gVar) using the message ID (message.what)
+ Calls gVar.a(e2) to deliver the VM result back to whoever sent the original request
+ Recycles the parcel and removes the callback from the pending list
This parcel system essentially creates a complete VM execution environment that can be serialized and passed between Android components.
### Reversing the VM
Looking at this VM structure, we can now rewrite our disassembler to properly analyze the bytecode:
```python
OPCODE_MAP = {
0x30: "LOAD", 0x31: "STORE", 0x32: "LOAD_CHAR", 0x33: "MOVE",
0x10: "ADD", 0x11: "SUB", 0x12: "MUL", 0x14: "MOD",
0x20: "AND", 0x21: "OR", 0x22: "XOR", 0x23: "NOT",
0x26: "ROL", 0x27: "ROR", 0x28: "CMP",
0x40: "JMP", 0x41: "JZ", 0x42: "JNZ", 0x43: "JL",
0x44: "JG", 0x45: "JLE", 0x46: "JGE", 0x4B: "NOP",
0x70: "HALT", 0x71: "PRINT",
}
def load_instructions(path):
ext = os.path.splitext(path)[1].lower()
data = open(path,'rb').read()
insts=[]; off=0
while off+7 < len(data):
opc,r1,r2,imm = struct.unpack_from('<BBBI', data, off)
name = OPCODE_MAP.get(opc)
if not name: raise ValueError(f"Unknown opcode 0x{opc:02X}")
insts.append({'op_name':name,'reg1':r1,'reg2':r2,'immediate':imm})
off += 7
return insts
def disassemble(insts):
for idx, inst in enumerate(insts):
addr = idx
op = inst['op_name']
r1 = inst['reg1']
r2 = inst['reg2']
imm = inst['immediate']
if op in ("JMP","JZ","JNZ","JL","JG","JLE","JGE"):
operand = str(imm)
else:
operand = f"r{r2}" if r2 else str(imm)
print(f"{addr}: {op} r{r1}, {operand}")
insts = load_instructions("data.dec")
disassemble(insts)
```
Analyzing the disassembly, we can see the first phase implements ASCII validation and normalization:
```ass
1: CMP r0, 32
2: JL r0, 7
3: CMP r0, 126
4: JG r0, 7
5: SUB r0, 32
6: JMP r0, 8
7: LOAD r0, 31
8: STORE r0, 4096
9: LOAD_CHAR r0, 1
10: CMP r0, 32
11: JL r0, 16
12: CMP r0, 126
13: JG r0, 16
14: SUB r0, 32
15: JMP r0, 17
16: LOAD r0, 31
17: STORE r0, 4097
18: LOAD_CHAR r0, 2
19: CMP r0, 32
20: JL r0, 25
21: CMP r0, 126
22: JG r0, 25
23: SUB r0, 32
24: JMP r0, 26
25: LOAD r0, 31
26: STORE r0, 4098
...
```
This pattern repeats until address 269, processing all 30 bytes (the flag length). Each character is:
+ Loaded from the flag string
+ Validated to be in the printable ASCII range (0x20-0x7E)
+ Normalized by subtracting 0x20 if valid, or set to 0x1F if invalid
+ Stored at memory addresses 0x1000-0x101D
```python
norm = []
for i, c in enumerate(flag):
v = ord(c)
if v < 0x20 or v > 0x7E:
norm.append(0x1F) character marker
else:
norm.append((v - 0x20) & 0xFF)
```
The next phase processes each normalized character from addresses 0x1000-0x101D:
```ass
270: LOAD r0, 4096
271: XOR r0, 0
272: XOR r0, 0
273: ROL r0, 1
274: AND r0, 255
275: STORE r0, 4352
276: LOAD r0, 4097
277: XOR r0, 23
278: XOR r0, 1
279: ROL r0, 2
280: AND r0, 255
281: STORE r0, 4353
282: LOAD r0, 4098
283: XOR r0, 46
284: XOR r0, 4
285: ROL r0, 3
286: AND r0, 255
287: STORE r0, 4354
...
```
**Pattern Analysis:**
First XOR: Each character is XORed with (index * 0x17) & 0xFF
Index 0: 0 * 0x17 = 0
Index 1: 1 * 0x17 = 23
Index 2: 2 * 0x17 = 46
Index 3: 3 * 0x17 = 69
Second XOR: Result is XORed with (index * index) & 0xFF
Index 0: 0² = 0
Index 1: 1² = 1
Index 2: 2² = 4
Index 3: 3² = 9
Rotation: Value is rotated left by (index % 8) + 1 bits
Storage at addresses 0x1100-0x111D
This phase can be implemented as:
```python
r1 = []
for i, v in enumerate(norm):
v ^= (i * 0x17) & 0xFF
v ^= (i * i) & 0xFF
v = rol8(v, (i % 8) + 1)
r1.append(v)
```
The final transformation phase implements a complex polynomial algorithm with dynamic rotation based on dependencies:
```ass
450: LOAD r0, 4352
451: LOAD r1, 4352
452: LOAD r2, 4352
453: MOVE r3, r1
454: MUL r3, r3
455: AND r3, r3
456: MOVE r4, 0
457: ADD r4, r3
458: AND r4, r4
459: MOVE r5, 0
460: MUL r5, r2
461: AND r5, r5
462: XOR r4, r5
463: MOVE r6, r1
464: ADD r6, r2
465: MOD r6, 8
466: CMP r6, 0
467: JZ r0, 482
...
482: ROR r4, 0
483: JMP r0, 498
484: ROR r4, 1
485: JMP r0, 498
...
498: AND r4, r4
499: STORE r4, 4608
```
**pattern analysis:**
dependencies: Each position i depends on values at (i*3)%30 and (i*7)%30
Polynomial: (current + dep1²) ⊕ (current * dep2)
Dynamic Rotation: ROR by (dep1 + dep2) % 8 using jump table dispatch
Storage: Results stored at 0x1200-0x121D
Equivalent code:
```python
r2 = []
for i, a in enumerate(r1):
dep1 = r1[(i * 3) % 30]
dep2 = r1[(i * 7) % 30]
part1 = (a + (dep1 * dep1)) & 0xFF
part2 = (dep2 * a) & 0xFF
v = part1 ^ part2
rot = (dep1 + dep2) % 8
v = ror8(v, rot)
r2.append(v & 0xFF)
```
finally, the algorithm stores the target ciphertext at addresses 0x4000-0x401D and performs validation:
```ass
1949: STORE r4, 4637
1950: LOAD r12, 216
1951: STORE r12, 16384
1952: LOAD r12, 80
1953: STORE r12, 16385
1954: LOAD r12, 35
1955: STORE r12, 16386
1956: LOAD r12, 22
1957: STORE r12, 16387
1958: LOAD r12, 129
1959: STORE r12, 16388
1960: LOAD r12, 176
1961: STORE r12, 16389
1962: LOAD r12, 231
1963: STORE r12, 16390
1964: LOAD r12, 76
1965: STORE r12, 16391
...
```
validation phase compares computed values (0x1200+) against target values (0x4000+):
```ass
2013: CMP r0, r1
2014: JZ r0, 2016
2015: LOAD r14, 0
2016: LOAD r0, 4609
2017: LOAD r1, 16385
2018: CMP r0, r1
2019: JZ r0, 2021
2020: LOAD r14, 0
2021: LOAD r0, 4610
2022: LOAD r1, 16386
2023: CMP r0, r1
2024: JZ r0, 2026
2025: LOAD r14, 0
2026: LOAD r0, 4611
2027: LOAD r1, 16387
2028: CMP r0, r1
2029: JZ r0, 2031
2030: LOAD r14, 0
2031: LOAD r0, 4612
2032: LOAD r1, 16388
2033: CMP r0, r1
2034: JZ r0, 2036
2035: LOAD r14, 0
2036: LOAD r0, 4613
2037: LOAD r1, 16389
...
```
Any mismatch sets r14 to 0 (failure). If all 30 bytes match, r14 remains 1 (correct flag)
## Getting the Flag
Now we have everything needed to solve the challenge. I wrote a z3 solver, but it's generating around 3000 potential flags. Since we're provided with the correct sha256 hash of the flag, we can use it to get the right flag:
```python
import sys
import hashlib
from z3 import *
def rol8_dyn(x, r):
expr = None
for i in range(8):
branch = RotateLeft(x, i)
cond = (r == BitVecVal(i, r.size()))
expr = branch if expr is None else If(cond, branch, expr)
return expr
def get_flag(ct):
n = len(ct)
s = Solver()
r1 = [BitVec(f"r1_{i}", 8) for i in range(n)]
for i in range(n):
a = r1[i]
dep1 = r1[(i * 3) % n]
dep2 = r1[(i * 7) % n]
rot = BitVec(f"rot_{i}", 8)
s.add(rot == (dep1 + dep2) & BitVecVal(0x07, 8))
ct_bv = BitVecVal(ct[i], 8)
temp = rol8_dyn(ct_bv, rot)
rhs = ((a + (dep1 * dep1)) & BitVecVal(0xFF, 8)) ^ ((dep2 * a) & BitVecVal(0xFF, 8))
s.add(temp == rhs)
for i in range(n):
shift = (i % 8) + 1
inv_rol = RotateLeft(r1[i], 8 - shift)
k1 = (i * 0x17) & 0xFF
k2 = (i * i) & 0xFF
norm = BitVec(f"norm_{i}", 8)
s.add(norm == inv_rol ^ BitVecVal(k1, 8) ^ BitVecVal(k2, 8))
ascii_i = BitVec(f"ascii_{i}", 8)
s.add(ascii_i == norm + BitVecVal(0x20, 8))
s.add(BitVec("ascii_0", 8) == ord("L"))
s.add(BitVec("ascii_1", 8) == ord("3"))
s.add(BitVec("ascii_2", 8) == ord("A"))
s.add(BitVec("ascii_3", 8) == ord("K"))
s.add(BitVec("ascii_4", 8) == ord("{"))
s.add(BitVec(f"ascii_{n-1}", 8) == ord("}"))
while s.check() == sat:
m = s.model()
flag = ''.join(chr(m[BitVec(f"ascii_{i}", 8)].as_long()) for i in range(n))
flag_hash = hashlib.sha256(flag.encode()).hexdigest()
if flag_hash == "f3bdd9f68a198756b96c5cf8207db63a11507e50fb0d29be609ff678ef721935":
print(f"FLAG: {flag}")
exit()
block = Or(*[ BitVec(f"ascii_{i}", 8) != BitVecVal(ord(flag[i]), 8) for i in range(n)])
s.add(block)
return
ct = [0xd8, 0x50, 0x23, 0x16, 0x81, 0xb0, 0xe7, 0x4c, 0x9a, 0xb5, 0x4b, 0xd9, 0x2a, 0x98, 0x58, 0x14, 0xea, 0xc6, 0x90, 0x51, 0x20, 0x48, 0x43, 0x72, 0x28, 0x85, 0x4b, 0xad, 0xa5, 0x80]
get_flag(ct)
# Flag: L3AK{P4rc3l_cycl3_1Nt3nt_VM!!}
```
and finally we got the flag.