The Boss Needs Help
Wow you are extremely good at internet!
Maybe you can help us. We just got a call from the management of a true rock-and-roll legend. This artist, famous for his blue-collar anthems and marathon live shows, fears his home studio machine in New Jersey has been compromised.
Our client is a master of the six-string, not the command line. We've isolated a suspicious binary from his machine, hopeanddreams.exe, that appears to be phoning home. We've also collected suspicious HTTP traffic and are passing that along.
Can you uncover what happened?
We are provided with a 64-bit PE file, hopeanddreams.exe, and a PCAP traffic capture containing HTTP traffic between the infected host and the domains twelve.flare-on.com:8000 and theannualtraditionofstaringatdisassemblyforweeks.torealizetheflagwasjustxoredwiththefilenamethewholetime.com:8080.
The HTTP requests and responses consist of JSON objects containing encrypted data.
Now that we've briefly gone over the PCAP, let's turn our attention to the main function of the executable file.
Oh no!
Deobfuscation
We've encountered yet another obfuscated binary! This time, junk instructions have been inserted into key functions to deter disassembly and decompilation.
The junk instructions vary, and it's not immediately obvious which, if any, have a real impact on the program's function.
However, I noticed a pattern: the junk instructions use 32-bit registers (eax, ecx), while legitimate instructions use 64-bit registers (rax, rcx) as expected in a 64-bit binary.
I quickly cooked up a IDAPython script to nuke all the junk instructions:
import ida_bytes
import idc
import ida_ua
from ida_funcs import get_func
start = idc.here()
f = get_func(start)
start = f.start_ea
end = start + f.size()
comparison = ["test", "cmp", "cdq"]
blacklist_mnemonics = ["xor", "mov", "not", "add", "sub", "and", "or", "inc", "dec", "idiv", "imul", "sar", "shl", "shr", "cdg", "movsx", "movzx", "cdq", "btc"]
blacklist_registers = ["eax", "ecx", "edx", "e8", "e9", "e11", "e10", "e12", "e13", "e14", "e15", "e16", "e17", "e18", "edi", "esi", "ebx"]
good_registers = ["rdi", "rsi", "rdx", "rcx", "rax", "rbx", "r8", "r9", "r10", "r11", "r12", "r13", "r14", "r15"]
def delete_instruction(ea, size):
ida_bytes.patch_bytes(ea, b"\x90" * size)
ea = start
nop_count = 0
idc.del_items(start, 0, end-start)
while ea < end:
out = ida_ua.insn_t()
size = ida_ua.create_insn( ea, out)
if size == 0:
assert False
ea += 1
continue
line = idc.GetDisasm(ea)
line = line.replace("r8d", "e8").replace("r9d", "e9").replace("r10d", "e10").replace("r11d", "e11").replace("r12d", "e12").replace("r13d", "e13").replace("r14d", "e14").replace("r15d", "e15")
assert "db" not in line
if "r12" in line:
print(hex(ea), line)
mnemonic = line.split(" ")[0]
if mnemonic in blacklist_mnemonics:
if any(reg in line for reg in blacklist_registers) and not any(reg in line for reg in good_registers):
delete_instruction(ea, out.size)
nop_count += 1
ea += out.size
continue
if mnemonic in comparison or mnemonic.startswith("j"):
print(hex(ea) )
if nop_count > 2 :
delete_instruction(ea, out.size)
nop_count += 1
ea += out.size
continue
nop_count = 0
ea += out.size
print(hex(end), hex(ea))As you can see, there are bunch of edge cases that need to be handled. For example, some legitimate instructions like test eax, eax need to be explicitly whitelisted.
However, the deobfuscation isn't perfect and likely contains false positives, though it's sufficient to solve the challenge.
Despite replacing all the junk instructions with nops, IDA still refuses to decompile the function due to its size. To address this, I wrote another IDAPython script to replace long sequences of nops with a jmp to the end of the sequence.
ea = start
nop_start = None
ma = 0
total_nop_start = 0
idc.del_items(start, 0, end-start)
while ea < end:
out = ida_ua.insn_t()
idc.del_items(ea, 0, 1)
size = ida_ua.create_insn( ea, out)
if size == 0:
ea += 1
continue
line = idc.GetDisasm(ea)
assert "db" not in line
if line == "nop":
if nop_start is None:
nop_start = ea
total_nop_start = ea
else:
if nop_start and ea - nop_start > 20:
print(hex(ea - nop_start), end=" ")
ida_bytes.patch_bytes(nop_start, b"\xe9" + (ea - nop_start - 5).to_bytes(4, 'little'))
else:
if nop_start:
for b in range(nop_start, ea):
ida_bytes.revert_byte(b)
nop_start = None
total_nop_start = None
ea += out.size
print(nop_start)
print(hex(start), hex(end), hex(ea))
print("Max nop sequence:", hex(ma))We still can't decompile the main function, but at least it's more readable now!
Dynamic analysis
After configuring C:\Windows\System32\drivers\etc\hosts to point both C2 domains to 127.0.0.1, I set up a Python HTTP server listening on ports 8000 and 8080 to replay the JSON messages found in the PCAP.
import http.server
import socketserver
import json
class JSONHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
# Set response status
self.send_response(200)
# Set headers
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
# Create JSON response
response_data ={"d": "085d8ea282da6cf76bb2765bc3b26549a1f6bdf08d8da2a62e05ad96ea645c685da48d66ed505e2e28b968d15dabed15ab1500901eb9da4606468650f72550483f1e8c58ca13136bb8028f976bedd36757f705ea5f74ace7bd8af941746b961c45bcac1eaf589773cecf6f1c620e0e37ac1dfc9611aa8ae6e6714bb79a186f47896f18203eddce97f496b71a630779b136d7bf0c82d560"}
# Send JSON response
self.wfile.write(json.dumps(response_data).encode('utf-8'))
def run_server(port=8000):
with socketserver.TCPServer(("", port), JSONHandler) as httpd:
print(f"Server running on port {port}")
print(f"Access at: http://localhost:{port}")
print("Press Ctrl+C to stop the server")
try:
httpd.serve_forever()
except KeyboardInterrupt:
print("\nServer stopped")
httpd.shutdown()
if __name__ == "__main__":
import sys
run_server(int(sys.argv[1]))Monitoring the traffic in Wireshark, I observed that the Authorization headers sent during local testing differed from those in the provided PCAP:

Interestingly, there was a significant number of common bytes at the start of the encrypted string. Time for more static analysis to determine what's happening!
Cross-referencing some network-related strings led me to 0x140081590, which I renamed network_thingy. Unlike main, IDA was able to decompile network_thingy after deobfuscation.
While the decompilation isn't perfect, it's sufficient to understand what's happening.
encrypt_for_authtoken contains a basic substitution-based encryption algorithm.
Setting a breakpoint on this function revealed the input to be a combination of a timestamp, my username, and the VM's hostname.
This makes sense, as the 4-digit year at the start of the string to be encrypted should be the same both in local testing and in the provided PCAP, thus resulting in a ciphertext with 4 common starting bytes.
At the same time, I extracted the sbox from memory and implemented the decryption algorithm in Python:
sbox = bytes.fromhex("52096AD53036A538BF40A39E81F3D7FB7CE339829B2FFF87348E4344C4DEE9CB547B9432A6C2233DEE4C950B42FAC34E082EA16628D924B2765BA2496D8BD12572F8F66486689816D4A45CCC5D65B6926C704850FDEDB9DA5E154657A78D9D8490D8AB008CBCD30AF7E45805B8B34506D02C1E8FCA3F0F02C1AFBD0301138A6B3A9111414F67DCEA97F2CFCEF0B4E67396AC7422E7AD3585E2F937E81C75DF6E47F11A711D29C5896FB7620EAA18BE1BFC563E4BC6D279209ADBC0FE78CD5AF41FDDA8338807C731B11210592780EC5F60517FA919B54A0D2DE57A9F93C99CEFA0E03B4DAE2AF5B0C8EBBB3C83539961172B047EBA77D626E169146355210C7D")
reverse_sbox = {}
for i, val in enumerate(sbox):
reverse_sbox[val] = i
def decrypt(encrypted_data):
decrypted = []
for i, encrypted_byte in enumerate(encrypted_data):
sbox_index = reverse_sbox[encrypted_byte]
original_xored = (sbox_index - (i + 1)) % 256
original_byte = original_xored ^ 0x5a
decrypted.append(original_byte)
return bytes(decrypted)Decrypting the authorization token from packets.pcapng (e4b8058f06f7061e8f0f8ed15d23865ba2427b23a695d9b27bc308a26d) reveals the plaintext 2025082006TheBoss@THUNDERNODE.
This indicates that the program ran at 6am UTC on 2025-08-20 by user TheBoss on a machine with hostname THUNDERNODE.
We can satisfy these conditions by changing the hostname, creating a new TheBoss user, and manually setting the time.
Unfortunately, the same encryption algorithm doesn't decrypt the response from the C2 server.
More Dynamic Analysis
To determine how the main C2 traffic is encrypted, I traced calls to json_get_string (0x000140431B20). This function extracts a string from the JSON payload, so its return value will be the encrypted string. We can then trace the string to find where it's decrypted.
Fortunately, this function has few call sites, and 2 of them are in network_thingy, which we've already analyzed. The next call is at 0x14010BA01:
Jumping past all the nops, we quickly find a call to a function that IDA MCP identifies as aes256_init (0x140050530)!
Setting a breakpoint on aes256_init allows us to extract the key and IV:
KEY = 95AF8B095B7465F9059D0358BACC2238504059A0BD79B49B6790A6620ADD6D96
IV = 000102030405060708090A0B0C0D0E0FPlugging these parameters into CyberChef, we can begin to decrypt some of the C2 communications:
We can write a Python script to decrypt all the C2 messages. Initially, everything decrypts correctly:
response {"msg": "cmd", "d": {"cid": 2, "line": "whoiam"}}
request {"op":""}
response {"msg": "no_op"}
response {"msg": "no_op"}
response {"msg": "cmd", "d": {"cid": 2, "line": "whoami"}}
request {"op":"thundernode\\theboss\n"}
response {"msg": "no_op"}
response {"msg": "no_op"}
response {"msg": "cmd", "d": {"cid": 2, "line": "systeminfo | findstr /B /C:\"OS Name\" /C:\"OS Version\""}}
request {"op":"OS Name: Microsoft Windows 10 Pro\nOS Version: 10.0.19045 N/A Build 19045\n"}
response {"msg": "no_op"}
response {"msg": "cmd", "d": {"cid": 2, "line": "dir /b C:\\Users\\%USERNAME%\\"}}
request {"op":"3D Objects\nContacts\nDesktop\nDocuments\nDownloads\nFavorites\nLinks\nMusic\nOneDrive\nPictures\nSaved Games\nSearches\nVideos\n"}However, after the message {"msg": "cmd", "d": {"cid": 6, "dt": 20, "np": "TheBoss@THUNDERNODE"}}, decryption fails.
This is likely a command instructing the C2 agent to rekey the cipher. Rather than reverse-engineering the rekey process, I replayed this message and checked if the aes256_init breakpoint is triggered again.
This requires a minor modification to the HTTP server code:
x = 0
class JSONHandler(http.server.BaseHTTPRequestHandler):
def do_GET(self):
global x
print(x)
# Set response status
self.send_response(200)
# Set headers
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
#Initial message
response_data ={"d": "085d8ea282da6cf76bb2765bc3b26549a1f6bdf08d8da2a62e05ad96ea645c685da48d66ed505e2e28b968d15dabed15ab1500901eb9da4606468650f72550483f1e8c58ca13136bb8028f976bedd36757f705ea5f74ace7bd8af941746b961c45bcac1eaf589773cecf6f1c620e0e37ac1dfc9611aa8ae6e6714bb79a186f47896f18203eddce97f496b71a630779b136d7bf0c82d560"}
if x:
# Rekey message
response_data["d"] = "8c58be3320873de641f94200087e77cfe3904ea49d90924454cb06e2fcc98023480053b247db8a06e4475475d231d5b7e34649a851657664e7ede1a222984735f22987bff7d8a4298a23cba90d297d5d"
# Send JSON response
self.wfile.write(json.dumps(response_data).encode('utf-8'))
def do_POST(self):
global x
x = 1
# Set response status
self.send_response(200)
# Set headers
self.send_header('Content-type', 'application/json')
self.send_header('Access-Control-Allow-Origin', '*')
self.end_headers()
# Create JSON response
response_data ={"d": "5134c8a46686f2950972712f2cd84174"} # STA Ok
# Send JSON response
self.wfile.write(json.dumps(response_data).encode('utf-8'))After some waiting, the breakpoint at aes256_init is indeed called with a new key:
848A5E071203CC8E8F476C25A3D1825FD5582AE7AAADD39BBA70C994F9757CD9Using this key, we can decrypt more traffic, including a request for the file rocknroll.zip (returned as base64):
Unfortunately:
- This zip file is encrypted, though we can see it contains the file
guitar_sax_flag.jpg - We've hit another re-key request
To obtain the new decryption key, we update the HTTP server to return the new re-key request. However, since this re-key request is encrypted with the key from the first re-key, we need to re-encrypt it with the original key.
#Initial message
response_data ={"d": "085d8ea282da6cf76bb2765bc3b26549a1f6bdf08d8da2a62e05ad96ea645c685da48d66ed505e2e28b968d15dabed15ab1500901eb9da4606468650f72550483f1e8c58ca13136bb8028f976bedd36757f705ea5f74ace7bd8af941746b961c45bcac1eaf589773cecf6f1c620e0e37ac1dfc9611aa8ae6e6714bb79a186f47896f18203eddce97f496b71a630779b136d7bf0c82d560"}
if x:
# Rekey message
response_data["d"] = "8c58be3320873de641f94200087e77cfe3904ea49d90924454cb06e2fcc98023c79ef7287cc26ead2f60159411689d6acccfbd236989a1111e807514ebde4ee6"Fortunately, new keys don't depend on the state from previous keys, and we can obtain the final decryption key:
CF923BE8DA52631113752D5B32CEF80B9D2BDADAC85130811BEE86868FE97204Decrypting the remaining traffic with this key reveals a request for password.txt:

Base64 decoding reveals its contents:
Email: BornToRun!75
Bank: TheRiver##1980
ComputerLogin: TheBossMan
Other: TheBigM@n1942!Using the password TheBigM@n1942!, we can decrypt rocknroll.zip and extract guitar_sax_flag.jpg, which contains the flag:

The full decryption script is included below:
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
import json
data = json.load(open('http_bodies.json', 'r', encoding='utf-8'))
def decrypt(key, ct):
iv = bytes(range(16))
try:
cipher = AES.new(key, AES.MODE_CBC, iv)
pt = unpad(cipher.decrypt(ct), AES.block_size)
except:
return None
return pt
keys = ["95af8b095b7465f9059d0358bacc2238504059a0bd79b49b6790a6620add6d96", "848A5E071203CC8E8F476C25A3D1825FD5582AE7AAADD39BBA70C994F9757CD9", "CF923BE8DA52631113752D5B32CEF80B9D2BDADAC85130811BEE86868FE97204"]
out = []
last = None
# Skip first entry as it is encrypted differently
for entry in data[1:]:
body = entry['body']
# Convert body to bytes
body = json.loads(body)
if "d" not in body:
continue
ct = bytes.fromhex(body['d'])
results = [decrypt(bytes.fromhex(k), ct) for k in keys]
if any(r is not None for r in results):
for r in results:
if r is not None:
out.append({
'type': entry['type'],
'body': r.decode('utf-8', errors='ignore')
})
else:
print("Decryption failed for all keys")
print(last.hex())
break
last = ct
with open('decrypted_http_bodies.json', 'w', encoding='utf-8') as f:
json.dump(out, f, indent=2, ensure_ascii=False)