Je continue sur le CTF Protostar mais cette fois sur les exercices net
. Il s’agit plus d’exercices de reverse-engineering.
Les binaires correspondants écoutent sur le réseau d’où leur nom :
1
2
3
4
tcp 0 0 0.0.0.0:2996 0.0.0.0:* LISTEN 1432/net3
tcp 0 0 0.0.0.0:2997 0.0.0.0:* LISTEN 1430/net2
tcp 0 0 0.0.0.0:2998 0.0.0.0:* LISTEN 1428/net1
tcp 0 0 0.0.0.0:2999 0.0.0.0:* LISTEN 1426/net0
il y a aussi un binaire net4
sur le système mais il s’avère qu’il s’agit juste de la version dépouillée des autres binaires : il met bien un port en écoute mais la fonction de callback attendue pour gérer les communications est vide.
Level 0
Le programme donne un entier et demande à ce qu’on lui envoit en little-endian sur 32 bits comme un entier serait représenté en mémoire dans un programme informatique :
1
2
3
4
$ ncat 192.168.56.95 2999 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.95:2999.
Please send '88717732' as a little endian 32bit int
L’entier demandé change à chaque fois.
On peut résoudre le problème directement depuis Python :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python
Python 2.7.18 (default, Apr 23 2020, 09:27:04) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> import struct
>>> sock = socket.socket()
>>> sock.connect(('192.168.56.95', 2999))
>>> sock.recv(1024)
"Please send '1664743977' as a little endian 32bit int\n"
>>> sock.send(struct.pack("<I", 1664743977))
4
>>> sock.recv(1024)
'Thank you sir/madam\n'
>>> sock.close()
Level 1
Cette fois pas d’indications mais c’est bien sûr l’inverse qui est demandé :
1
2
3
4
ncat 192.168.56.95 2998 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.95:2998.
H�h
C’est parti :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
$ python
Python 2.7.18 (default, Apr 23 2020, 09:27:04) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> import struct
>>> sock = socket.socket()
>>> sock.connect(('192.168.56.95', 2998))
>>> data = sock.recv(1024)
>>> num = struct.unpack("<I", data)[0]
>>> sock.send(str(num).encode())
10
>>> sock.recv(1024)
'you correctly sent the data\n'
>>> sock.close()
Level 2
Ca semble plus énigmatique :
1
2
3
4
$ ncat 192.168.56.95 2997 -v
Ncat: Version 7.93 ( https://nmap.org/ncat )
Ncat: Connected to 192.168.56.95:2997.
R���DnE
Via le décompilateur de Cutter
il semble que le programme donne 4 entiers qu’il faut additionner :
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
39
40
41
42
#include <stdint.h>
uint32_t run (void) {
int32_t var_24h;
int32_t var_20h;
int32_t var_10h;
unsigned long var_ch;
const char * buf;
size_t nbytes;
var_ch = 0;
var_10h = 0;
while (var_10h <= 3) {
ebx = var_10h;
eax = random ();
*((ebp + ebx*4 - 0x20)) = eax;
eax = var_10h;
eax = *((ebp + eax*4 - 0x20));
var_ch += eax;
eax = var_10h;
edx = eax*4;
eax = &var_20h;
eax += edx;
eax = write (0, 4, eax);
if (eax != 4) {
errx (1, 0x8049c94);
}
var_10h++;
}
eax = &var_24h;
eax = read (0, 4, eax);
if (eax != 4) {
errx (1, 0x8049c98);
}
eax = var_24h;
if (var_ch == eax) {
puts ("you added them correctly");
} else {
puts ("sorry, try again. invalid");
}
return eax;
}
La difficulté en Python réside dans le fait que les entiers ne sont pas bornés à 32 bits, il faut donc utiliser un masque :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
$ python
Python 2.7.18 (default, Apr 23 2020, 09:27:04) [GCC] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> import socket
>>> import struct
>>> sock = socket.socket()
>>> sock.connect(('192.168.56.95', 2997))
>>> numbers = sock.recv(1024)
>>> len(numbers)
16
>>> result = sum(struct.unpack("<IIII", numbers)) # On additionne les 4 nombres
>>> sock.send(struct.pack("<I", result))
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
struct.error: 'I' format requires 0 <= number <= 4294967295
>>> sock.send(struct.pack("<I", 0xffffffff & result))
4
>>> sock.recv(1024)
'you added them correctly\n'
Level 3
Toujours plus compliqué, cet exercice demande de bonnes connaissances en reverse-engineering et/ou debugging.
La fonction run()
qui gère le client est la suivante :
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
run (int32_t arg_8h);
; var int32_t var_12h @ ebp-0x12
; var void *var_10h @ ebp-0x10
; var unsigned long var_ch @ ebp-0xc
; arg int32_t arg_8h @ ebp+0x8
; var const char *var_4h @ esp+0x4
; var const char *var_8h @ esp+0x8
0x08049a26 push ebp
0x08049a27 mov ebp, esp
0x08049a29 sub esp, 0x28
0x08049a2c mov dword [var_8h], 2 ; int32_t arg_8h ; Lit 2 octets dans l'ordre reseau et les converti en ordre host
0x08049a34 lea eax, [var_12h]
0x08049a37 mov dword [var_4h], eax ; int32_t arg_10h
0x08049a3b mov eax, dword [arg_8h]
0x08049a3e mov dword [esp], eax ; int32_t arg_ch
0x08049a41 call nread ; sym.nread ; sym.nread(const char *arg_ch, unsigned long arg_10h, int fildes)
0x08049a46 movzx eax, word [var_12h]
0x08049a4a movzx eax, ax
0x08049a4d mov dword [esp], eax
0x08049a50 call ntohs ; sym.imp.ntohs
0x08049a55 mov word [var_12h], ax
0x08049a59 movzx eax, word [var_12h]
0x08049a5d movzx eax, ax
0x08049a60 mov dword [esp], eax ; size_t size
0x08049a63 call malloc ; sym.imp.malloc ; alloue la taille qui a ete recue ; void *malloc(size_t size)
0x08049a68 mov dword [var_10h], eax
0x08049a6b cmp dword [var_10h], 0
0x08049a6f jne 0x8049a90
0x08049a71 movzx eax, word [var_12h]
0x08049a75 movzx eax, ax
0x08049a78 mov dword [var_8h], eax
0x08049a7c mov dword [var_4h], str.malloc_failure_for__d_bytes ; 0x8049fd8
0x08049a84 mov dword [esp], 1 ; int eval
0x08049a8b call errx ; sym.imp.errx ; void errx(int eval)
0x08049a90 movzx eax, word [var_12h]
0x08049a94 movzx eax, ax
0x08049a97 mov dword [var_8h], eax ; int32_t arg_8h
0x08049a9b mov eax, dword [var_10h]
0x08049a9e mov dword [var_4h], eax ; int32_t arg_10h
0x08049aa2 mov eax, dword [arg_8h]
0x08049aa5 mov dword [esp], eax ; int32_t arg_ch
0x08049aa8 call nread ; sym.nread ; sym.nread(const char *arg_ch, unsigned long arg_10h, int fildes)
0x08049aad mov eax, dword [var_10h]
0x08049ab0 movzx eax, byte [eax]
0x08049ab3 movzx eax, al
0x08049ab6 cmp eax, 0x17 ; 23
0x08049ab9 jne 0x8049b09
0x08049abb movzx eax, word [var_12h]
0x08049abf sub eax, 1
0x08049ac2 movzx eax, ax
0x08049ac5 mov edx, dword [var_10h]
0x08049ac8 add edx, 1
0x08049acb mov dword [var_4h], eax ; int32_t arg_8h
0x08049acf mov dword [esp], edx ; int32_t arg_ch
0x08049ad2 call login ; sym.login
0x08049ad7 mov dword [var_ch], eax
0x08049ada cmp dword [var_ch], 0
0x08049ade je 0x8049ae7
0x08049ae0 mov eax, str.successful ; 0x8049ff4
0x08049ae5 jmp 0x8049aec
0x08049ae7 mov eax, str.failed ; 0x8049fff
0x08049aec mov dword [var_8h], eax ; int32_t arg_8h
0x08049af0 mov dword [var_4h], 0x21 ; '!' ; 33 ; int32_t arg_10h
0x08049af8 mov eax, dword [arg_8h]
0x08049afb mov dword [esp], eax ; int32_t arg_ch
0x08049afe call send_string ; sym.send_string
0x08049b03 nop
0x08049b04 jmp 0x8049a2c
0x08049b09 mov dword [var_8h], str.what_you_talkin_about_willis ; 0x804a006 ; int32_t arg_8h
0x08049b11 mov dword [var_4h], 0x3a ; ':' ; 58 ; int32_t arg_10h
0x08049b19 mov eax, dword [arg_8h]
0x08049b1c mov dword [esp], eax ; const char *arg_ch
0x08049b1f call send_string ; sym.send_string
0x08049b24 jmp 0x8049a2c
J’ai balancé ce code a ChatGPT qui a donné l’interprétation suivante :
This is a function written in C programming language. It appears to be reading in a value from an input stream and then allocating memory on the heap based on that value.
The value is read in using the
nread
function and then converted from network byte order to host byte order using thentohs
function. If the memory allocation fails, the function prints an error message and exits.After the memory is successfully allocated, the function reads in another value from the input stream and checks if it is equal to 23.
If it is, the function continues execution with some additional processing. Otherwise, it frees the previously allocated memory and exits.
Effectivement le code utilise d’abord 2 octets sur le réseau avec la fonction nread
qui a le prototype suivant :
1
int nread(int socket, char *buffer, int length);
Ces deux octets sont convertis de la notation réseau (big-endian) vers la notation de l’architecture machine (ici little-endian).
L’entier ainsi obtenu sert de taille pour faire un malloc
puis un second appel à nread
pour écrire les données dans le buffer alloué.
Sur cette seconde lecture le programme s’attend à ce que le premier octet corespond à 0x17
.
Ensuite un appel à la fonction login()
est effectué. Cette fonction reçoit deux arguments :
le buffer à partir de son second octet donc sans le
0x17
la taille telle qu’annoncée moins un (du au retrait du premier octet)
La fonction login()
a été l’une des plus difficile à comprendre :
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
login (void *arg_8h, const char *arg_ch);
; var unsigned long var_2ch @ ebp-0x2c
; var const char *var_1ch @ ebp-0x1c
; var const char *var_18h @ ebp-0x18
; var const char *ptr @ ebp-0x14
; var int32_t var_10h @ ebp-0x10
; var unsigned long var_ch @ ebp-0xc
; arg void *arg_8h @ ebp+0x8
; arg const char *arg_ch @ ebp+0xc
; var const char *s2 @ esp+0x4
; var int32_t var_8h @ esp+0x8
0x08049861 push ebp
0x08049862 mov ebp, esp
0x08049864 sub esp, 0x48
0x08049867 mov eax, dword [arg_ch]
0x0804986a mov word [var_2ch], ax
0x0804986e cmp word [var_2ch], 2
0x08049873 ja 0x8049889
0x08049875 mov dword [s2], str.invalid_login_packet_length ; 0x8049f78
0x0804987d mov dword [esp], 1 ; int eval
0x08049884 call errx ; sym.imp.errx ; void errx(int eval)
0x08049889 mov dword [var_1ch], 0
0x08049890 mov eax, dword [var_1ch]
0x08049893 mov dword [var_18h], eax
0x08049896 mov eax, dword [var_18h]
0x08049899 mov dword [ptr], eax
0x0804989c movzx eax, word [var_2ch]
0x080498a0 mov dword [var_8h], eax ; void **dest
0x080498a4 mov eax, dword [arg_8h]
0x080498a7 mov dword [s2], eax ; int32_t arg_ch
0x080498ab lea eax, [ptr]
0x080498ae mov dword [esp], eax ; int32_t arg_10h
0x080498b1 call get_string ; sym.get_string
0x080498b6 mov dword [var_10h], eax
0x080498b9 mov eax, dword [var_10h]
0x080498bc movzx edx, word [var_2ch]
0x080498c0 mov ecx, edx
0x080498c2 sub cx, ax
0x080498c5 mov eax, ecx
0x080498c7 movzx edx, ax
0x080498ca mov eax, dword [var_10h]
0x080498cd add eax, dword [arg_8h]
0x080498d0 mov dword [var_8h], edx ; void **dest
0x080498d4 mov dword [s2], eax ; const char *arg_ch
0x080498d8 lea eax, [var_18h]
0x080498db mov dword [esp], eax ; int32_t arg_10h
0x080498de call get_string ; sym.get_string
0x080498e3 add dword [var_10h], eax
0x080498e6 mov eax, dword [var_10h]
0x080498e9 movzx edx, word [var_2ch]
0x080498ed mov ecx, edx
0x080498ef sub cx, ax
0x080498f2 mov eax, ecx
0x080498f4 movzx edx, ax
0x080498f7 mov eax, dword [var_10h]
0x080498fa add eax, dword [arg_8h]
0x080498fd mov dword [var_8h], edx ; void **dest
0x08049901 mov dword [s2], eax ; const char *arg_ch
0x08049905 lea eax, [var_1ch]
0x08049908 mov dword [esp], eax ; int32_t arg_10h
0x0804990b call get_string ; sym.get_string
0x08049910 add dword [var_10h], eax
0x08049913 mov dword [var_ch], 0
0x0804991a mov eax, dword [ptr]
0x0804991d mov dword [s2], str.net3 ; 0x8049f94 ; const char *s2
0x08049925 mov dword [esp], eax ; const char *s1
0x08049928 call strcmp ; sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
0x0804992d or dword [var_ch], eax
0x08049930 mov eax, dword [var_18h]
0x08049933 mov dword [s2], str.awesomesauce ; 0x8049f99 ; const char *s2
0x0804993b mov dword [esp], eax ; const char *s1
0x0804993e call strcmp ; sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
0x08049943 or dword [var_ch], eax
0x08049946 mov eax, dword [var_1ch]
0x08049949 mov dword [s2], str.password ; 0x8049fa6 ; const char *s2
0x08049951 mov dword [esp], eax ; const char *s1
0x08049954 call strcmp ; sym.imp.strcmp ; int strcmp(const char *s1, const char *s2)
0x08049959 or dword [var_ch], eax
0x0804995c mov eax, dword [ptr]
0x0804995f mov dword [esp], eax ; void *ptr
0x08049962 call free ; sym.imp.free ; void free(void *ptr)
0x08049967 mov eax, dword [var_18h]
0x0804996a mov dword [esp], eax ; void *ptr
0x0804996d call free ; sym.imp.free ; void free(void *ptr)
0x08049972 mov eax, dword [var_1ch]
0x08049975 mov dword [esp], eax ; void *ptr
0x08049978 call free ; sym.imp.free ; void free(void *ptr)
0x0804997d cmp dword [var_ch], 0
0x08049981 sete al
0x08049984 movzx eax, al
0x08049987 leave
0x08049988 ret
Ici ChatGPT était assez à côté de la plaque.
Pour bien comprendre ce que fait cette fonction il faut comprendre comment fonctione getstring()
. Cette fonction a le prototype suivant :
1
int getstring(void **dest, char *buffer, int size);
Cette fonction reçoit un paramètre size
mais ce dernier sert uniquement comme une protection : si la chaine à extraire (explications après) est plus grande que la totalité du buffer alors le programme indique que la paquet est malformé.
Ce qui se passe c’est que le premier octet de buffer est lu comme étant la taille de la chaine de caractère qui suit. Cette taille est utilisée pour faire un malloc
et la chaine qui se trouve après la taille est copiée via strcpy()
.
La fonction retourne ensuite la taille de la chaine extraite + 1 :
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
get_string (void **dest, const char *arg_ch, int32_t arg_10h);
; var unsigned long var_1ch @ ebp-0x1c
; var size_t size @ ebp-0x9
; arg void **dest @ ebp+0x8
; arg const char *arg_ch @ ebp+0xc
; arg int32_t arg_10h @ ebp+0x10
; var const char *src @ esp+0x4
0x080497fa push ebp
0x080497fb mov ebp, esp
0x080497fd sub esp, 0x38
0x08049800 mov eax, dword [arg_10h]
0x08049803 mov word [var_1ch], ax
0x08049807 mov eax, dword [arg_ch] ; <- la chaine d'entrée
0x0804980a movzx eax, byte [eax] ; <- récupération de la taille sur le premier octet
0x0804980d mov byte [size], al
0x08049810 movzx eax, byte [size]
0x08049814 cmp ax, word [var_1ch] ; <- compare avec la taille reçue en argument
0x08049818 jbe 0x804982e
0x0804981a mov dword [src], str.badly_formed_packet ; 0x8049f64
0x08049822 mov dword [esp], 1 ; int eval
0x08049829 call errx ; sym.imp.errx ; void errx(int eval)
0x0804982e movzx eax, byte [size]
0x08049832 mov dword [esp], eax ; size_t size
0x08049835 call malloc ; sym.imp.malloc ; void *malloc(size_t size)
0x0804983a mov edx, eax
0x0804983c mov eax, dword [dest]
0x0804983f mov dword [eax], edx
0x08049841 mov eax, dword [arg_ch]
0x08049844 lea edx, [eax + 1]
0x08049847 mov eax, dword [dest]
0x0804984a mov eax, dword [eax]
0x0804984c mov dword [src], edx ; const char *src
0x08049850 mov dword [esp], eax ; char *dest
0x08049853 call strcpy ; sym.imp.strcpy ; char *strcpy(char *dest, const char *src)
0x08049858 movzx eax, byte [size] ; <- prend la taille lut, incrémente et retourne
0x0804985c add eax, 1
0x0804985f leave
0x08049860 ret
Dans login()
il y a plusieurs appels successifs à getstring()
avec le premier argument qui change et qui doit correspondre à un tableau de pointeurs où sont stockées les adresses retournées par malloc()
.
Le premier appel est tout simple, il passe la chaine qui a été reçue sur le réseau avec sa taille, en revanche juste après cet appel il prend la taille initiale et y retranche le résultat de getstring()
pour obtenir la taille restante et incrémente aussi le pointeur sur la chaine d’autant de caractères.
Ainsi sur l’appel suivant à getstring()
il extrait une seconde chaine depuis le buffer reçu. Il y a donc plusieurs chaines présentes dans le paquet qui commence par 0x17
, chacune précédée d’un octet correspondant à sa taille… mais pas uniquement !
Pour que tout fonctionne sans problèmes il faut aussi que ces chaines aient un octet nul terminal (en raison de l’utilisation de strcpy()
) sans quoi on peut écraser des données sur le tas (et provoquer un crash lors d’un appel à free()
) et ici notre objectif est juste que tout fonctionne correctement.
Par conséquent quand on spécifie la longueur d’une chaine dans le paquet on doit aussi compter son '\0'
terminal.
La fin de login()
procéde à 3 strcmp()
avec les chaines suivantes : net3
, awesomesauce
, password
.
Au final la solution est :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import socket
import struct
sock = socket.socket()
sock.connect(("192.168.56.95", 2996))
words = ["net3", "awesomesauce", "password"]
packet = b"\x17"
for word in words:
packet += struct.pack("<b", len(word) + 1) + word.encode() + b"\0"
sock.send(struct.pack(">H", len(packet))) # envoie la taille du paquet sur 2 octets
sock.send(packet) # envoie le paquet avec son header 0x17
print(sock.recv(2048))
sock.close()
Ce qui nous donne l’output b'\x00\x0b!successful'
Après avoir résolu les exercices j’ai regardé sur le web et découvert que certains ont pu le faire en s’aidant du code source… Mais ceux-ci ne sont pas présents sur l’image fournit par VulnHub.
Voir par exemple Exploit-Exercises: Protostar (Net Levels) pour les curieux.
Publié le 6 janvier 2023