Writeup Capture The Flag HIDC 2026 (PART I)
For me, HIDC 2026 was more than just a Capture The Flag competition, it was a journey filled with challenges, unexpected discoveries and a fair amount of trial and error. Throughout the event, my team and I had the opportunity to tackle various cybersecurity challenges, test our skills, learn new techniques and experience the pressure and excitement of competing against other top teams. In this article, I want to share not only how I approached and solved these challenges but also the experiences, thought processes, mistakes, and lessons I gained along the way.
About HIDC 2026
HIDC 2026 is a cybersecurity CTF organized by PT Hacktrace Siber Indonesia, held on 17 August 2026. The competition featured Boot2Root, Forensics, and Jeopardy formats with 200+ participants competing across categories such as AI, Misc, Forensics, Boot2Root, Web Exploitation, Hardware, SDR, Reverse Engineering, Cryptography, and Mobile.
During the competition, I managed to solve 8 challenges across four categories: Misc (2), SDR (2), Cryptography (2), and Mobile (2), finishing in Top 44.
In this writeup, I’ll share how I approached each challenge, the obstacles I encountered and what I learned along the way.
Challenge Writeup
- MISC
The MISC category in HIDC 26 featured challenges that required a combination of binary analysis, anti-analysis investigation, signal analysis, OSINT, cryptography, and steganography.
Our team successfully solved two MISC challenges:
- IP Chall Unidentified
- speXound
Both challenges required following clues through multiple stages rather than relying on a single vulnerability or technique.
- IP Chall Unidentified
The challenge provided a remote binary service and described it as follows:
An old operator left a suspicious binary service behind. The visible bugs look promising, but the real validation logic is reconstructed only at runtime. Reverse the staged loader, account for its anti-analysis checks, and recover the correct access key.
- Several clues immediately stood out:
Suspicious binary
Staged loader
Anti-analysis
Runtime-reconstructed validation logic
Access key recovery
This indicates that the challenge is fundamentally a reverse-engineering problem, rather than merely a search for a hardcoded password. - Initial Binary Analysis
The binary was identified as an ELF 64-bit x86–64 executable with the following characteristics:
1. PIE executable
2. Dynamically linked
3. Not stripped
The fact that the binary was not stripped was particularly useful because function names were still available, making static analysis significantly easier. Some interesting strings can also be found using the ‘strings’ command.
At first glance, S3CR3T_P4SSW0RD looked like an obvious candidate. However, the challenge description specifically mentioned runtime reconstruction, so these strings could potentially be intentional distractions.
- Looking at the Available Functions
The binary contained several functions that immediately caught my attention:
The presence of check_tracerpid, check_timing, and execute_stage2 strongly suggested that the program contained anti-analysis logic and a second execution stage.
- Analyzing Function main()
Disassembly of main() showed that user input was read using:
fgets()
The newline character was then removed using:
strcspn()
The resulting input was passed to:
execute_stage2()
This was an important discovery.
main() itself did not perform the actual access-key validation. Instead, the validation logic was delegated to the second stage.
- Anti-Analysis Mechanism #1 — TracerPid
The first anti-analysis function was:
check_tracerpid
Examining its disassembly showed that the binary opened:
/proc/self/status
and searched for:
TracerPid:
The value was then parsed using:
strtol()
Conceptually, the logic was equivalent to:
if (TracerPid != 0) detected = true;
This mechanism attempts to detect whether the process is being traced, for example by a debugger such as GDB.
Therefore, simply running the binary directly under a debugger could potentially trigger the anti-analysis path.
- Anti-Analysis Mechanism #2 — Timing Check
Another interesting function was:
check_timing
This function used the x86 RDTSC instruction.
RDTSC reads the CPU timestamp counter, allowing the program to measure how many CPU cycles elapsed between two points.
The logic was approximately:
The purpose was to detect unusually slow execution that could occur because of debugging or instrumentation.
At this point, two anti-analysis mechanisms had been identified:
This made a purely dynamic debugging approach less attractive.
- Reconstructing the Key Material
The next interesting function was:
build_key
Several constants were constructed directly inside the function.
The first constant was:
movabs rax,0xdec03713efbeadde
Because x86–64 uses little-endian representation, this corresponds to:
deadbeef1337c0de
Another constant resolved to:
HIDC2026
A further string fragment was:
Undentif
However, there was another important detail.
One anti-analysis/timing path modified a byte using:
mov BYTE PTR […],0xff
Combining the recovered components resulted in the following 32-byte SHA-256 input:
deadbeef1337c0de
HIDC2026
ff 00 00 00 00 00 00 00
UndentifIn raw bytes:
de ad be ef 13 37 c0 de
48 49 44 43 32 30 32 36
ff 00 00 00 00 00 00 00
55 6e 64 65 6e 74 69 66The binary then called:
sha256_init
sha256_update
sha256_update
sha256_update
sha256_update
sha256_finalThis confirmed that the recovered material was used to generate the decryption key through SHA-256.
The same process could be reproduced offline with Python:
import hashlib
data = (
bytes.fromhex("de ad be ef 13 37 c0 de") +
b"HIDC2026" +
bytes.fromhex("ff 00 00 00 00 00 00 00") +
b"Undentif"
)
key = hashlib.sha256(data).digest()
print(key.hex())The resulting key was: 25c67e4e60a71b60b105b7a69a89599146d77e8e4aeb4dba0fd540825074180a
This gave us the key material required to analyze the second execution stage.
- Finding the stage2_blob
Further analysis revealed two important values:
stage2_size = 0x40a0
stage2_blob = 0x40c0
The relevant encrypted payload was approximately 466 bytes.
The overall design could now be visualized as:
This explained why the validation logic was not immediately visible as normal executable code inside the binary.
- Analyzing execute_stage2()
Disassembly of execute_stage2() revealed the actual staged-loader workflow.
First, the program called:
build_key()
It then allocated memory using:
mmap()
with executable permissions:
RWX
The encrypted stage2_blob was copied into this memory using:
memcpy()
After that, the program called:
aes256_cbc_decrypt()
Finally, execution was transferred to the newly decrypted memory.
The complete flow was therefore:
This was the core of the staged-loader mechanism.
- The “AES” Wasn’t Actually AES
One of the most interesting parts of the challenge was the function named:
aes256_cbc_decrypt
The name strongly suggested AES-256-CBC.
However, examining the assembly revealed operations such as:
and edx,0x1f
and ecx,0xf
movzx edx,BYTE PTR [r8+rdx]
xor dl,BYTE PTR [r9+rcx]
xor BYTE PTR [rsi+rax],dlThere were no actual AES rounds or AES transformations.
Instead, the operation was essentially:
ciphertext[i]
XOR
key[i & 0x1f]
XOR
iv[i & 0x0f]The IV was stored in .rodata as:
0123456789abcdef0123456789abcdef
Therefore, the effective decryption operation was:
plaintext[i] = ciphertext[i] ^ key[i & 0x1f] ^ iv[i & 0x0f]The function name was simply a form of misdirection.
This was an important reminder not to blindly trust function names during reverse engineering. The actual behavior of the code is what matters.
- Recovering the Access Key
After reconstructing the second-stage validation logic, the actual access key was finally revealed:
Unseen_Self_2026
This also explained why the previously discovered string:
S3CR3T_P4SSW0RD
should not be trusted.
The binary contained several misleading paths and strings, including:
fake_crypto_logic
unlock_secret
win
S3CR3T_P4SSW0RDbut the actual access key originated from the runtime stage-2 validation logic.
The local binary itself returned: FLAG_NOT_SET
which indicated that the actual flag was stored on the remote service rather than hardcoded inside the local binary.
The recovered flag was: HIDC2026{You’ve_Found_The_Unseen_Part_Of_Yourself}
- speXound
Its description stated:
A forgotten transmission carries more than static. Hidden among the frequencies lies a trail left by someone who never intended it to be easily followed. Decode the signal, follow the breadcrumbs, and uncover what remains concealed.
The challenge provided two files:
1. multiband_broadcast.wav
2. leak_capture.jpg
The description suggested that the audio signal contained the first clue, while the JPEG would become relevant later in the investigation.
The challenge ultimately combined signal analysis, OSINT, AES-256-CBC decryption, metadata analysis, and steganography.
- Analyzing the WAV File
The first step was to inspect the audio using a spectrogram.
The spectrogram revealed hidden text:
RECOVERY NODE
id.pinterest.com/hidden_but_leaked
THE REMAINING PAYLOAD
LIES BEYOND THE SPECTRUMThis immediately provided two important clues.
First, the challenge pointed toward a recovery node.
Second, it provided a Pinterest URL: https://id.pinterest.com/hidden_but_leaked/
The audio therefore acted as the first breadcrumb in a multi-stage investigation.
- Following the Pinterest Breadcrumb
Following the recovered URL led to a Pinterest account named: n0w u h3ar m3
Two images were particularly relevant: th3 f1srt!.jpg & Th3 s3cond!.jpg
However, the images themselves were not the most important part.
Their pin descriptions contained hexadecimal strings.
- Recovering the AES Key
The description of the first image contained: 0f2ffc05e0942b37d03c6f4e25d1ca2af56ce329d946221a197b496356d55a5d
Get Aditya Pranoto’s stories in your inbox
Join Medium for free to get updates from this writer.
The string contained 64 hexadecimal characters.
Since: 64 hex characters = 32 bytes
this matched the expected size of an AES-256 key.
Therefore: AES Key = 0f2ffc05e0942b37d03c6f4e25d1ca2af56ce329d946221a197b496356d55a5d
- Recovering the AES IV
The second image contained another hexadecimal string: 0965eaaffe683abb4ca9a092e2e2ea6b
This contained: 32 hex characters = 16 bytes, which matched the expected size of an AES initialization vector.
Therefore: AES IV = 0965eaaffe683abb4ca9a092e2e2ea6b
At this point, we had:
Key = 0f2ffc05e0942b37d03c6f4e25d1ca2af56ce329d946221a197b496356d55a5d
IV = 0965eaaffe683abb4ca9a092e2e2ea6b
- Investigating the WAV Metadata
The next step was to return to the original WAV file and inspect its metadata.
The metadata contained a field labeled: CIPHERTEXT
The data following this field appeared to be Base64-encoded ciphertext.
Instead of manually copying the ciphertext, it could be extracted directly from the metadata for decryption.
Using the AES key and IV obtained from the Pinterest breadcrumbs, the ciphertext was decrypted using: AES-256-CBC
- Decrypting the Ciphertext
From Pinterest, I have previously obtained:
Key : 0f2ffc05e0942b37d03c6f4e25d1ca2af56ce329d946221a197b496356d55a5d
IV : 0965eaaffe683abb4ca9a092e2e2ea6b
The ciphertext is then decrypted using: AES-256-CBC
The decrypted plaintext revealed:
====================================
HIDC SIGNAL RECOVERY
====================================
Recovery Status : SUCCESS
Recovered Carrier :
leak_capture.jpg
Authentication Phrase :
m3t4dat4_l34k_2026
Operation :
GARUDA-1945This was the key turning point of the challenge. The decrypted message explicitly pointed back to:
1. leak_capture.jpg
2. Recovered Carrier:
3. Authentication Phrase:
4. m3t4dat4_l34k_2026
This means the challenge now directs us back to the JPG file provided from the start.
- Extracting the Hidden Payload
The recovered authentication phrase was: m3t4dat4_l34k_2026
Since the JPEG had been identified as the recovered carrier, the next step was to inspect it for embedded data using steghide.
The phrase was used as the passphrase to extract the hidden file. The embedded data contained the final flag: HIDC2026{sp3ctr0_m3ets_st3g0}
2. Cryptography
Cryptography is the science of securing information through encryption and decryption. In the HIDC CTF, this category focuses on algorithm analysis and the exploitation of cryptographic weaknesses to obtain the flag.
I successfully completed two cryptography challenges :
- Blacksite
- Deadmint
- Blacksite
Description :
The old world’s crypto died with the grid.
But rumors say a pre-collapse military vault still runs an autonomous encryption daemon, guarding supplies, schematics, and perhaps even a way out of the wastes.
A scavenger crew managed to drag a terminal close enough to ping it. The vault challenges anyone who connects: it presents an encrypted token, then allows you to query its oracle.
Nobody has made it past the handshake yet.
The vault doesn’t care about your radiation count. It only cares whether you can break the cipher.
- Identifying File Types Using the “file” Command
Why is this step important? So that we know which tools are suitable for the next stage and how “friendly” the binary is for analysis.
ELF 64-bit — This is an x86–64 Linux binary. You can use tools like objdump, which are designed for this format.
“not stripped” — this is good news. It means the symbol table the list of internal function names is still present in the binary. If the binary were “stripped,” all function names would be removed, leaving us with only raw addresses like “sub_1234”, which are much harder to read. Since it hasn’t been stripped, we can still see the original function names (even if they’ve been randomized or obfuscated by the challenge creator) at the very least, the structure “this is a specific function, called from here” remains clear.
“pie executable” — Position Independent Executable; this means the addresses within the binary are relative (shifting when the program runs due to ASLR). It does not significantly affect our static analysis; it is simply something to be aware of.
- Viewing strings using the “strings” command
The second step almost always taken is extracting all “readable” text from the binary file. Such text often reveals sensitive information, error messages, library function names, output formats, and more.
Use “-n 6” to specify a minimum string length of 6 characters, which helps avoid being overwhelmed by noise; meanwhile, the “-t x” option displays the position offset in hex, which is useful for later cross-referencing with the disassembly.
Based on the results obtained, we can draw several conclusions:
1. This is a network server program. The sequence of socket, bind, listen, accept, fork represents the standard TCP server pattern in C: open a socket → bind to a port → listen → accept a connection → typically fork() a child process to serve each client in parallel.
2. There is a flag system. The string flag.txt and an error message regarding the FLAG environment variable indicate that the program reads the flag from either the flag.txt file or the FLAG environment variable.
3. There is an oracle mechanism involving state and limits. The messages “Invalid query state” and “Query limit exceeded” indicate a request-response loop (Oracle) with a specific state and a limit on the number of queries per session confirming the problem description (“allows you to query its oracle”).
Why is this step important? “strings” is cheap (fast to run) but often provides a roadmap indicating which functions are “interesting” enough to examine in detail first, rather than reading through thousands of lines of assembly from the beginning without direction.
- Viewing the actual code in assembly form using the objdump command.
After gaining a general overview from the previous step, we will now look at the code in assembly form.
1. -d = disassemble the executable section (.text)
2. -M intel = use Intel syntax (mov dst, src), which is generally easier to read than the default AT&T syntax (mov src, dst)
3. --demangle = convert mangled C++ function names back into a more readable format- Reading function main()
Here is the initial snippet of main() :
Lines 136d–1374 = this is a very common pattern C.
srand(time(NULL));time(NULL) returns the current Unix time, which is then used as the seed for rand().
322 Lines 1388–13c3 constitute a "generate key, check if weak, repeat if weak" loop. Note line 13c3: “jne 138d” — this instruction jumps back to address 138d (the start of the loop) only if the newly generated key is still considered weak. Once the key is deemed “strong” (passes QrLp), the loop terminates and the program proceeds to the subsequent code.
The most important observation : This generate-key loop (138d–13c3) is located BEFORE the socket setup section (socket(), bind(), listen(), accept()) and is NOT part of the accept-loop (the loop that repeatedly accepts client connections).
- Dissecting the “Encryption” Algorithm — Searching for wZGracS
Now we know when the key was created. Next, we need to know how that key is used to “encrypt” the data, so we can understand what can be reversed.
- Track which functions the oracle calls.
After the socket setup in `main()`, the program enters the “eMqywudU” function (the connection handler) whenever a new client connects. Inside, there is a call to “wZGracS” — used both to display the initial token (flag encryption) and for every oracle query. This means we only need to focus on analyzing this single function, as the logic is reused in exactly the same way in both instances. - Dismantle the supporting functions first, before the primary function.
Instead of jumping straight to the lengthy “wZGracS”, it is easier to first identify the small functions it calls, as small functions are quicker to understand: Function aiIHgPkz(int a, int b) & YEcwG(int a, int b):
The pattern idiv of performing integer division twice with normalization like this implements a strictly positive modulo operation — specifically “(((a % b) + b) % b)” — unlike the standard “%” operator in C, which can yield a negative result if “a” is negative. Thus, “aiIHgPkz(a, b)” computes “a mod b” (the true modulo, with a result in the range “0” to “b-1”).
“YEcwG(a, b)” calls “aiIHgPkz” and then performs a subtraction followed by another integer division, this is a common pattern for floor division.
3. Function wNyyh(unsigned char byte, int n):
The pattern “(byte << n) | (byte >> (8-n))”, with “n” constrained to the range 0–7, is a classic implementation of an 8-bit left bit rotation (circular left rotation), commonly referred to as ROL8.
So far, we know two important “raw materials”:
- There are “correct” (always positive) modulo and division operations — typically used to break down large numbers into a quotient and a remainder.
- There is an 8-bit bit-rotation operation — typically used as an additional layer of “scrambling.”
4. Reading wZGracS equipped with the background knowledge mentioned earlier.
There are several important parts that will be discussed this time.
Part 1 — plaintext “copying”, lines 1–4 (local_c loop):
At first glance, it looks like a simple XOR cipher (^ 0xa5). But look closely: the same byte is XORed with the same constant (0xa5) twice in succession. Due to the mathematical properties of XOR — where (X ^ K) ^ K always results in X again — the second XOR operation cancels out the first. So, abStack_39 ends up being just an identical copy of param_1, without any transformation at all.
Part 2 — building the rotation array rot[], lines 5–8 (local_10 loop):
param_4 is a 16-character hex key. The loop takes the characters at indices 8–15, processes them using psZD() to generate a hex digit value modulo 8, and stores the result in aiStack_68. Thus, aiStack_68 constitutes an array rot[0..7] where rot[i] = (hex digit value of the (8+i)-th character) mod 8.
Part 3 — matrix multiplication core and output encoding, lines 9–19 (local_14 loop):
local_14 and local_24 serve as output and summation indices (0–7), while local_20 is a 64-bit accumulator used to calculate the dot product of matrix “param_3” and the plaintext vector abStack_39. The result is split into a remainder modulo 256 (0–255) and an integer quotient; the remainder is then bit-rotated by rot[i] positions and recombined with the quotient to produce the final output param_2[i].
- After gathering all the information from the previous stage, it is now time to design an exploit strategy.
1. Step 1 — Capture the initial token (ciphertext flag)
2. Step 2 — Recover matrix M via the oracle, using basis vectors.
3. Step 3 — Recover the rotation vector rot[i]
4. Step 4 — Decrypt the flag token
Finally, the flag was successfully obtained: HIDC2026{br41d1n6_sp1n_m4tr1c3s_p4ul1_3/2_6d3474685f31355f333473795f72316768743f}
- Deadmint
Description :
A fintech company managing digital assets through their encrypted API. They claim their system is “unbreakable.” Prove them wrong.
- Step 1, Reconnaissance using command file & ldd
The provided file is an ELF binary, not source code.
Since the binary offers no option to run as a standalone server on a specific port, I ran it directly for observation:
chmod +x deadmint
./deadmint &It turns out this binary listens on a local TCP port. The menu displayed upon connecting:
- Register Client : The server generates a new 1024-bit RSA key pair, then returns the client_id, modulus n, and public exponent e. The value of e is always 0x10001.
- Get Public Key : The user enters the client_id, and the server returns the modulus n and public exponent e associated with that client.
- Encrypt Message : The user encrypts a message using the client’s public key via the textbook RSA scheme with PKCS#1 padding.
- Authenticate : The user enters the `client_id`. The server then sends a random nonce in hexadecimal format. The user is required to provide a signature in hex format to prove possession of the corresponding private key.
- Get Flag : The feature to retrieve the flag is accessible only after successful authentication.
- Quit : Terminates the program and exits the menu.
- Static Binary Analysis using Ghidra (searching for bugs in key generation)
void FUN_001030c0(undefined8 param_1,long param_2,undefined8 param_3,undefined8 param_4,
undefined8 param_5,undefined8 param_6)
{
byte bVar1;
int __fd;
ssize_t sVar2;
int *piVar3;
byte *pbVar4;
byte *pbVar5;
long lVar6;
ulong uVar7;
long in_FS_OFFSET;
undefined1 local_68 [8];
byte local_60 [32];
long local_40;
DAT_00106028 = param_2 << 0x10;
local_40 = *(long *)(in_FS_OFFSET + 0x28);
DAT_00106020 = 0xabed1234;
FUN_00102f80(param_5);
__fd = open("/dev/urandom",0);
if (__fd < 0) {
perror("open /dev/urandom");
/* WARNING: Subroutine does not return */
exit(1);
}
uVar7 = 0;
do {
while( true ) {
sVar2 = read(__fd,local_68 + uVar7,0x20 - uVar7);
if (-1 < sVar2) break;
piVar3 = __errno_location();
if (*piVar3 != 4) {
perror("read /dev/urandom");
/* WARNING: Subroutine does not return */
exit(1);
}
}
uVar7 = uVar7 + sVar2;
} while (uVar7 < 0x20);
close(__fd);
pbVar5 = local_60;
lVar6 = 0;
DAT_00106028 = 0;
do {
pbVar4 = pbVar5 + -8;
uVar7 = 0;
do {
bVar1 = *pbVar4;
pbVar4 = pbVar4 + 1;
uVar7 = uVar7 << 8 | (ulong)bVar1;
} while (pbVar5 != pbVar4);
lVar6 = lVar6 + 8;
DAT_00106028 = DAT_00106028 ^ uVar7;
pbVar5 = pbVar5 + 8;
} while (lVar6 != 0x20);
FUN_00102f80(param_6);
__gmpz_mul(param_3,param_5,param_6);
if (local_40 == *(long *)(in_FS_OFFSET + 0x28)) {
__gmpz_set_ui(param_4,0x10001);
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();void FUN_00102f80(undefined8 param_1)
{
int iVar1;
ulong uVar2;
uint uVar3;
ulong uVar4;
ulong uVar5;
long in_FS_OFFSET;
uint local_9c;
undefined1 local_98 [16];
undefined1 local_88 [64];
undefined1 local_48 [8];
long local_40;
local_40 = *(long *)(in_FS_OFFSET + 0x28);
__gmpz_init(local_98);
do {
uVar2 = 0x40;
do {
DAT_00106028 = DAT_00106028 * 0xda662d15 + (ulong)DAT_00106020;
local_9c = (uint)((ulong)DAT_00106028 >> 0x20) | (uint)DAT_00106028;
uVar4 = 4;
if (uVar2 < 5) {
uVar4 = uVar2;
}
if ((uint)uVar4 != 0) {
uVar3 = 0;
do {
uVar5 = (ulong)uVar3;
uVar3 = uVar3 + 1;
local_48[uVar5 - uVar2] = local_98[uVar5 - 4];
} while (uVar3 < (uint)uVar4);
}
uVar2 = uVar2 - 4;
} while (uVar2 != 0);
__gmpz_import(local_98,0x40,1,1,1,0);
__gmpz_setbit(local_98,0x1ff);
__gmpz_setbit(local_98,0);
iVar1 = __gmpz_probab_prime_p(local_98,0x19);
} while (iVar1 == 0);
__gmpz_set(param_1,local_98,local_88,0x102fc2);
__gmpz_clear(local_98);
if (local_40 == *(long *)(in_FS_OFFSET + 0x28)) {
return;
}
/* WARNING: Subroutine does not return */
__stack_chk_fail();
}- DAT_00106028 = global variable STATE, DAT_00106020 = global variable INC — exactly as mentioned in section 2.
- The line local_48[uVar5 — uVar2] = local_98[uVar5–4] in FUN_00102f80 appears strange because Ghidra misinterprets overlapping stack buffers (where the same memory area is alternately used as an mpz_t struct and a raw byte buffer), this is a common decompiler artifact in code involving such low-level memory reinterpretation, rather than an analysis error.
- __gmpz_import(local_98,0x40,1,1,1,0) should have 7 parameters according to the original mpz_import signature, Ghidra only displays 6 because the last argument (the source buffer pointer) is passed via the stack and is not fully resolved by the decompiler.
- param_2 in FUN_001030c0 is a Unix timestamp (the result of time(NULL) from the caller), confirming directly that the prime seed p originates from the server time, not the client_id.
- Building an Exploit
- Complete exploit workflow
- Connect & Register client record local timestamps t_before / t_after flanking the request.
- Brute-force a small time window (t_before — 5 to t_before + 20, adjusted for network latency): for each candidate t, generate p candidates and check if n % p == 0.
- Once p is found → q = n // p → phi = (p-1)*(q-1) → d = inverse(e, phi).
- Construct the complete RSA key object (`n`, `e`, `d`, `p`, `q`) using pycryptodome
- Authenticate: request a nonce, calculate digest = SHA256(nonce_hex + client_id).
- Forge the signature using pkcs1_15.new(key).sign(digest) — this signature is valid because we possess the actual private key (derived from reconstructing the real key, not a mathematical guess).
- Send the signature → server responds with “Authentication successful!”. Call menu option [5] “Get flag” → flag obtained.
- Execution on the Target Server
pip install gmpy2 pycryptodome --break-system-packages
python3 exploit.py 10.1.2.218 1339The exploit runs automatically: register → brute-force the timing → recover p, q, and d → authenticate using a forged (yet actually valid) signature → retrieve the flag.
Flag successfully obtained: HIDC2026{GCD_5h4r3d_pr1m35_k1ll5_r54_UzRZNF80SzROX0w0VzRO}
Those four challenges across Misc and Cryptography were a great reminder that most real vulnerabilities hide in small implementation details, not exotic math.
In Part II, I’ll cover the remaining categories: SDR and Mobile. Stay tuned!







