Writeup for the Huntress CTF challenge Rustline
It was October again, Cybersecurity Awareness Month again. Just like last year, we participated in the Huntress CTF, which runs throughout the month. Here’s how the CTF is described:
For this specific Capture the Flag competition, we hope to offer hands-on and practical exercises based around malware analysis, digital forensics and incident response, threat hunting or cyber threat intelligence, and general security. We will be releasing new challenges for you to play every single day throughout the month of October.
I chose to write about this challenge not because it was technically complex, but for the opposite reason. I solved it within a few minutes and was surprised to discover that only 3% of teams completed it. Sometimes, the simplest challenges can be the most confusing.
Recon
First of all we read through the description which looked like this.
We can download a password-protected ZIP archive using the password rustline. It’s supposed to contain encrypted files resulting from a malware infection. Once we’ve downloaded it, we can get started right away.
Scanning
Examining the downloaded files
First of all let’s extract all the files from the archive (challenge.zip).
~/Downloads/rustline/ unzip challenge.zip
Archive: challenge.zip
[challenge.zip] Application.docx.scr password:
inflating: Application.docx.scr
creating: challenge-files/
inflating: challenge-files/password.txt
inflating: challenge-files/id_rsa_webserver
inflating: challenge-files/id_rsa_dba_srv.pub
inflating: challenge-files/id_rsa_dba_srv
inflating: challenge-files/id_rsa_aws_ec2.pub
inflating: challenge-files/id_rsa_webserver.pub
inflating: challenge-files/id_rsa_aws_ec2
inflating: challenge-files/ovpn_config.ovpn
creating: encrypted-files/
extracting: encrypted-files/password.txt
inflating: encrypted-files/flag.txt
extracting: encrypted-files/id_rsa_webserver
extracting: encrypted-files/id_rsa_dba_srv.pub
extracting: encrypted-files/id_rsa_dba_srv
extracting: encrypted-files/id_rsa_aws_ec2.pub
extracting: encrypted-files/id_rsa_webserver.pub
extracting: encrypted-files/id_rsa_aws_ec2
extracting: encrypted-files/ovpn_config.ovpn
That’s a whole bunch of files! There was a lot of mention of Rust, but I don’t see any reason to focus on that just yet. First, let’s take a look at what an encrypted file looks like.
~/Downloads/rustline/ cat encrypted-files/flag.txt
??J??-e????s?كF??-!կ??s?^ф[??61????h?
??F??.)?????^ٔL??91?????u?ʒB??=W)????n?
????,(?????{?ʞ??x$?????j???Z??x'???:?ΒA??*e???{???J??)$????y?̒L??x ?????l?
ْ??;$?????:?țF??:k????:?њ??+(????j?̒B??-$????j???\??x6????n?
??Z??7,į???:?
ߞ[ڵ+!????:?քJ??-1????}???@??*6????k???N??7 ????j?̒B??=0ٯ??y?
փ??=0կ??h?^ɂF??-(????:?
??K??7 ݯ???o?^ɂF??<)?????n?Ւ[ڵ;+????n?
??N??(6?????v?
??\??x0?????t?͚^??5W ?????u???J??7$???~???Z??4'?????n?כ@??x$????{?ɂN??)$?????l?͇[??=k?ڨ??t???K??1,?????t?????1e????o?^ݏJ??1$????w?
ԛN??;7???:?
˔F??,W)?????u?????+e????s?
ѓ??x$???u???@??=0?????:?
˒??9e???:?Ԓ\??9e????k?̂]ڵ.)???w?͞??47????w?͐F??x0߯??o??t?ʞ-e????:?Ԃ_??,e???:?
ل??4?????{?
??>????+?ٓKǦa ֻ???|???NæaEq?롄?8??Z??(7????{???Z??=W*????s?
??A??-e????:???Y??-1?????{?̈́N??1(???h?ɂJ??9!???w?^̘[??x ݯ???h?????) ????:?
ْ??x)????l?̘]??.7?????i??^??+e????n?̘??9$կ??{?^ܞL??x0????j?ۖM??x9 ????s?^ч\??x*?????n???Z??xу??(7????:?
??@??,W$????}?
??\??x0????t?ɂ??-e????:?Ԙ]??x*ï???:?̞@??x*?????n???J??1W+????t?P??J??=W5?????k?ˆZ??x6ģ???s?כ@??5W,?????k???K??7e?樮?w?
ݓ??1e?ಮ?o?͖B??16???:?Շ??x+????n?
??C??7 ?ꨮ?u?ʒ???$ݯ??k???^??=$į??o?
كJ??x"1???:???F??5e???w?^ɂF??66??????ʔ??,*?걮?v???L??(7?????i?Ȟ[??9*????w?^֞\??-e????s?^ݏ??x*???:?քJ??90°?ߒs?^ق[??x ܯ???:?
ʒ??( ???h?
̞N??;+????n??v?̖[??.)????i?^ɂN??6-????v?
??Y??x)?????o?^ܘC??=e?????o?ك??7W3????{?^ւC??x$????h?\
That was not a pleasant sight. Let’s examine one of the unencrypted files to make sure there’s nothing unusual going on.
~/Downloads/rustline/ cat challenge-files/id_rsa_webserver
-----BEGIN OPENSSH PRIVATE KEY-----
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
Owf/klh9RKEFAAAAAAEC
-----END OPENSSH PRIVATE KEY-----
That looks like a regular private key. It seems legit. We don’t know the type of encryption used, but we have some unencrypted text we can use for comparison. Let’s start by checking the size of the unencrypted files.
~/Downloads/rustline/ ls -l challenge-files
total 64
-rw-------@ 1 chgr staff 3357 Oct 3 14:21 id_rsa_aws_ec2
-rw-r--r--@ 1 chgr staff 726 Oct 3 14:21 id_rsa_aws_ec2.pub
-rw-------@ 1 chgr staff 3357 Oct 3 14:21 id_rsa_dba_srv
-rw-r--r--@ 1 chgr staff 726 Oct 3 14:21 id_rsa_dba_srv.pub
-rw-------@ 1 chgr staff 3357 Oct 3 14:21 id_rsa_webserver
-rw-r--r--@ 1 chgr staff 726 Oct 3 14:21 id_rsa_webserver.pub
-rw-r--r--@ 1 chgr staff 146 Oct 3 14:20 ovpn_config.ovpn
-rw-r--r--@ 1 chgr staff 42 Oct 3 14:19 password.txt
Next, let’s compare that with the size of the encrypted files.
~/Downloads/rustline/ ls -l encrypted-files
total 72
-rw-r--r--@ 1 chgr staff 1776 Oct 3 14:31 flag.txt
-rw-r--r--@ 1 chgr staff 3357 Oct 3 14:31 id_rsa_aws_ec2
-rw-r--r--@ 1 chgr staff 726 Oct 3 14:31 id_rsa_aws_ec2.pub
-rw-r--r--@ 1 chgr staff 3357 Oct 3 14:31 id_rsa_dba_srv
-rw-r--r--@ 1 chgr staff 726 Oct 3 14:31 id_rsa_dba_srv.pub
-rw-r--r--@ 1 chgr staff 3357 Oct 3 14:31 id_rsa_webserver
-rw-r--r--@ 1 chgr staff 726 Oct 3 14:31 id_rsa_webserver.pub
-rw-r--r--@ 1 chgr staff 146 Oct 3 14:31 ovpn_config.ovpn
-rw-r--r--@ 1 chgr staff 42 Oct 3 14:31 password.txt
We can see that all the files have the same length, both encrypted and unencrypted. My conclusion is that we’re dealing with a very simple encryption method. My initial guess is that it’s not “real” encryption, but perhaps a basic XOR cipher. Let’s test that theory.
Gaining Access
XOR has the property that applying it again with the known plaintext reverses the operation, revealing the key. The challenge here is that we don’t know the length of the key used. But we can write some Python to help us solve this.
Writing the decrypt code
I’ll use the unencrypted id_rsa_webserver
file and XOR each character with the corresponding character in the encrypted id_rsa_webserver
file to extract the key. We’ll then identify when the key starts repeating and isolate only the necessary part. Here’s our script:
def xor_decrypt(encrypted_bytes, key):
# Decrypts using XOR and the provided key
decrypted = bytearray()
key_length = len(key)
for i in range(len(encrypted_bytes)):
decrypted.append(encrypted_bytes[i] ^ key[i % key_length])
return decrypted
def find_xor_key(plain_path, encrypted_path):
# Load files as byte arrays
with open(plain_path, 'rb') as f:
plain_bytes = bytearray(f.read())
with open(encrypted_path, 'rb') as f:
encrypted_bytes = bytearray(f.read())
# Derive the XOR key
key = bytearray()
for i in range(len(plain_bytes)):
key.append(plain_bytes[i] ^ encrypted_bytes[i])
# Remove repeating sections of the key
for i in range(1, len(key)):
if key[:i] == key[i:2*i]:
key = key[:i]
break
return key
def decrypt_flag(flag_path, key):
with open(flag_path, 'rb') as f:
encrypted_flag = bytearray(f.read())
decrypted_flag = xor_decrypt(encrypted_flag, key)
return decrypted_flag.decode('utf-8', errors='ignore')
# Define file paths
plain_text_file = "challenge-files/id_rsa_webserver"
encrypted_file = "encrypted-files/id_rsa_webserver"
flag_file = "encrypted-files/flag.txt"
# Find the XOR key
key = find_xor_key(plain_text_file, encrypted_file)
print("Derived XOR Key (hex):", key.hex())
# Decrypt and print the flag content
flag_content = decrypt_flag(flag_file, key)
print("Decrypted Flag Content:", flag_content)
Let’s just run the script and see what happens.
~/Downloads/rustline/ python3 decrypt.py
Derived XOR Key (hex): b8f72ff695587745b08fdc8ee71adf7e
Decrypted Flag Content: "Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
flag{bfe12aadd139def4d47f5f51a539249d}
"Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?"
Just like that, we obtained a key (b8f72ff695587745b08fdc8ee71adf7e
) and used it to recover the flag (flag{bfe12aadd139def4d47f5f51a539249d}
). We could go on to decrypt the other files using the same key if we wanted, but since this is a CTF, our work here is done."
Summary
In my opinion, this was one of the easier challenges in the competition. The key takeaway here isn’t about fancy code or advanced cryptanalysis. It’s about keeping things simple! Start with small steps and try straightforward solutions before diving into complex techniques.
Perhaps people were thrown off by the mention of Rust and ended up going down completely wrong paths and falling into rabbit holes.
Of course, we could have used simple visual tools like CyberChef to solve this manually. However, if it turned out not to be a basic XOR, having a script would allow us to quickly test different variations. But Im sure there’s someone out there with a nice oneliner for this.
We solved every single challenge and followed up last year’s 7th place with 32nd this year. Clearly, Rustline wasn’t one of the challenges that brought us down on the scoreboard! This year included a lot of tough reverse engineering tasks that took considerable time. While I feel last year’s challenges were perhaps more real-world oriented, the difficulty of some of this year’s challenges made the CTF even better. We’ll be back for 2025!
Until the next time, happy hacking!
/f1rstr3am