L'Off-by-one et l'Off-by-few
Jusqu'ici on écrivait 200 octets dans un buffer de 16, un débordement franc. Un off-by-one, c'est l'inverse, un seul octet de trop, souvent sans intention, juste une erreur de calcul. Un off-by-few suit la même logique avec quelques octets en plus. Dans les deux cas, ça reste un buffer overflow, seule la taille du dépassement change.
C'est quoi un Off-by-one / Off-by-few ?
Un off-by-one, c'est écrire un octet de trop dans un buffer, ni plus ni moins. L'exemple classique vient d'une mauvaise condition de boucle.
char buffer[16];
for (int i = 0; i <= 16; i++) {
buffer[i] = 'A';
}
i <= 16 fait tourner la boucle de 0 à 16 inclus, soit 17 itérations pour un buffer de 16 cases. Le dernier tour écrit dans buffer[16], une case qui n'existe pas. Un off-by-few, c'est exactement la même idée mais avec un écart de plusieurs octets au lieu d'un seul, souvent à cause d'un mauvais calcul de taille plutôt que d'une simple erreur de borne.
Comment survient le débordement
Ce genre de bug vient rarement d'une intention, contrairement à un strcpy sans limite qu'on utilise en connaissance de cause. Les causes les plus courantes sont une condition de boucle avec <= au lieu de <, un \0 qu'on a oublié de compter dans la taille réservée, ou une taille recalculée à la main qui se trompe de quelques unités.
Exemple concret : Off-by-one
#include <stdio.h>
#include <string.h>
char id[16];
char role[16] = "invite";
void copier(const char *source) {
size_t len = strlen(source);
for (size_t i = 0; i <= len; i++) {
id[i] = source[i];
}
}
int main(void) {
copier("AAAAAAAAAAAAAAAA"); // 16 caractères
printf("role = %s\n", role);
return 0;
}
source fait 16 caractères, len vaut donc 16. La boucle i <= len écrit dans id[0] jusqu'à id[16], soit 17 octets dans un buffer qui n'en fait que 16. Le dernier octet écrit correspond à source[16], le \0 de fin de chaîne, et il atterrit juste après id, dans le premier octet de role.
$ ./off_by_one
role =
role valait "invite". Un seul octet a été écrasé, mais c'était le premier, et c'était un \0. Pour tout ce qui lit role comme une chaîne, elle est devenue vide. Un octet a suffi à effacer complètement son contenu apparent.
Off-by-few
Le même bug avec plusieurs octets de trop suit le même principe, sauf que l'impact touche plus large.
#include <stdio.h>
#include <string.h>
char id[16];
char role[16] = "invite";
void enregistrer(const char *source) {
for (int i = 0; i < 20; i++) {
id[i] = source[i];
}
}
int main(void) {
enregistrer("AAAAAAAAAAAAAAAAAAAA"); // 20 caractères
printf("role = %s\n", role);
return 0;
}
La boucle est bornée en dur à 20 au lieu de 16, une erreur de calcul assez courante quand la taille du buffer n'est pas relue au moment d'écrire la condition. id reçoit 20 octets au lieu de 16, les 4 en trop débordent sur les 4 premiers octets de role.
$ ./off_by_few
role = AAAAte
Les quatre premiers caractères de role (i, n, v, i) sont remplacés par des A, le reste (te) n'a pas été touché puisque le débordement s'arrête après 4 octets.
Observer le débordement avec GDB
Pour repérer précisément ce qui dépasse, on compare la mémoire de role avant et après l'appel fautif.
gdb ./off_by_one
(gdb) break copier
(gdb) run
(gdb) x/16bx &role
0x404050 <role>: 0x69 0x6e 0x76 0x69 0x74 0x65 0x00 0x00 ...
0x69 0x6e 0x76 0x69 0x74 0x65 correspond à "invite" en ASCII. On laisse la fonction se terminer et on regarde à nouveau.
(gdb) finish
(gdb) x/16bx &role
0x404050 <role>: 0x00 0x6e 0x76 0x69 0x74 0x65 0x00 0x00 ...
Un seul octet a changé, le premier, passé de 0x69 à 0x00. C'est exactement l'octet en trop écrit par la boucle, ni plus ni moins, et c'est la comparaison avant/après qui permet de le confirmer avec certitude plutôt que de le déduire du résultat affiché par le programme.
Pourquoi quelques octets peuvent suffire
L'impact d'un off-by-one ou d'un off-by-few ne dépend pas du nombre d'octets débordés, mais de ce qu'ils touchent. Plus haut, un seul \0 a suffi à vider une chaîne entière. Si l'octet voisin avait été un champ de taille, un booléen, ou un octet de pointeur, l'effet aurait été tout aussi marquant pour la même quantité de données écrites en trop. Un débordement de 200 octets sans conséquence peut faire moins de dégâts qu'un seul octet tombé au bon endroit.
Comment l'éviter
La plupart des off-by-one viennent d'une condition de boucle mal posée, <= là où il fallait <, ou d'une taille de buffer qui ne réserve pas de place pour le \0 final. Le réflexe à avoir est de toujours vérifier ses bornes une par une, et de compter le caractère de fin de chaîne dans la taille du buffer plutôt que de l'ajouter après coup.
Conclusion
Un octet ou quelques octets de trop restent un buffer overflow au même titre qu'un débordement massif, la seule chose qui change c'est la taille du dépassement, pas sa nature. Ce qui détermine réellement l'impact, c'est ce qui se trouve juste après le buffer, pas le nombre d'octets qu'on a écrit en trop pour l'atteindre.