One-byte stack overflow in gpredict rotctld client (arm64 stack protector abort)
Nadie ha tomado este issue todavía.
Evaluación
- Dificultad
- 2/5
- Tiempo estimado
- 1-3 horas
- Aptitud para principiantes
- 82/100
- Tipo de issue
- Error
- Claridad
- Bien especificado
- Estado de actividad
- Activo
- Stack tecnológico
- c
- Área
- networking, security
Línea de trabajo
Inspecciona rotctld_socket_rw() en src/gtk-rot-ctrl.c, especialmente las escrituras de recv() y del terminador, y compáralo con la comprobación de límites existente en src/gtk-rig-ctrl.c. El issue identifica a los llamadores afectados y un controlador local en lab/rot_oob.c; reproduce el caso de una respuesta de 128 bytes antes de modificar el helper. Terminar significa que el helper nunca escribe más allá del búfer proporcionado, maneja de forma segura una respuesta completa y conserva el comportamiento de retorno documentado.
Escrito por el modelo de indexación a partir del texto del issue.
Descripción
Summary
get_pos(), set_pos(), and rot_locked_cb() each pass a gchar buffback[128] and a length of 128 into rotctld_socket_rw(). That function does:
size = recv(sock, buffout, sizeout, 0);
buffout[size] = '\0';
recv is allowed to fill all 128 bytes. When it returns 128, buffout[128] is one past the array, and the stored byte is 0x00.
A reply of 127 bytes or fewer writes the NUL inside the array. I measured that. A reply of 128 or more makes recv return 128 and writes the NUL into the next byte. On this machine’s arm64 ABI, with Apple clang at -O0 and at -O2 -fstack-protector-all, that byte is the stack canary. The process dies in __stack_chk_fail (exit status 134). The written byte is always NUL. I did not get a controlled pointer write or instruction-pointer control.
The radio helper in src/gtk-rig-ctrl.c already reserves the extra byte. The rotator helper does not.
gpredict does not open a rotator socket until the user fills in a host and port and presses Engage. The new-rotator dialog defaults the host to localhost and the port to 4533. The exchange is cleartext TCP. This client does not check the peer.
Code
src/gtk-rot-ctrl.c, rotctld_socket_rw(), lines 180 and 189:
size = recv(sock, buffout, sizeout, 0);
...
buffout[size] = '\0';
Callers, all gchar buffback[128] and size 128:
| Caller | Line | Command written to the socket |
|---|---|---|
get_pos() |
311 | p\n (g_strdup_printf("p\x0a")) |
set_pos() |
371 | P <az> <el>\n |
rot_locked_cb() |
1209 | S\n (stop, when Engage is released) |
Radio path, src/gtk-rig-ctrl.c line 1492, already correct:
size = recv(sock, buffout, sizeout - 1, 0);
buffout[size] = '\0';
A normal rotctld position reply is two short decimal lines and a RPRT line, far below 128 bytes. The overflow needs a peer that returns at least 128 bytes in one recv. Any process that accepts the TCP connection can do that. The client does not authenticate it.
Local reproduction
GTK and hamlib were not linked. I did not connect to a rotator on the public internet. The send, the recv, and the buffout[size] = '\0' in the driver are the body of rotctld_socket_rw(), with the sat_log_log calls compiled out so the file links. The peer is a socketpair child in the same process.
Build and run
clang -arch arm64 -O2 -g -fstack-protector-all lab/rot_oob.c -o /tmp/rot_oob_arm2
arch -arm64 /tmp/rot_oob_arm2
-O0 arm64 was built and run the same way. x86_64 builds: /tmp/rot_oob and /tmp/rot_oob_o2.
What the client sent
Every shot uses the get_pos() command, 2 bytes, sent with send(sock, buff, strlen(buff), 0):
0000 70 0a p.
ASCII p\n. set_pos / stop use the same recv and the same 128-byte buffer; only the outbound command string differs.
What the peer sent back
The child fills a buffer with Z (0x5a) and sends a chosen length, then shutdown(SHUT_WR).
Reply A, 127 bytes (NUL stays inside the array):
0000 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a ZZZZZZZZZZZZZZZZ
...
48 bytes of 0x5a per line, 7 full lines (112 bytes), then:
0070 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a ZZZZZZZZZZZZZZZ
Reply B, 128 bytes (the case that writes off the end):
0000 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a ZZZZZZZZZZZZZZZZ
...
0070 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a 5a ZZZZZZZZZZZZZZZZ
128 × 0x5a. No trailing NUL in the reply. The client adds one, at index 128.
Reply C, 200 bytes of 0x5a. recv is capped by sizeout, so the client still accepts 128 and writes buffout[128].
The neighbor byte next to the 128-byte array was set to 0x42 before each call so a missed write is visible.
Program output (arm64, -O2 -fstack-protector-all)
recv returned 127, sizeout 128
nsend=127 ok=1 neighbor=0x42 buf0=0x5a
recv returned 128, sizeout 128
nsend=128 ok=1 neighbor=0x00 buf0=0x5a
recv returned 128, sizeout 128
nsend=200 ok=1 neighbor=0x00 buf0=0x5a
summary 127=0x42 128=0x00 200=0x00
recv returned 128, sizeout 128
Then the process exits with status 134.
| Reply length | recv return |
buffout[0] |
byte after the array | result |
|---|---|---|---|---|
127 × 0x5a |
127 | 0x5a |
stays 0x42 |
write is inside the array |
128 × 0x5a |
128 | 0x5a |
0x42 → 0x00 |
one byte past the array |
200 × 0x5a |
128 | 0x5a |
0x42 → 0x00 |
same write, extra bytes stay in the socket |
buf0=0x5a is the first byte of the reply actually stored, so this is the buffer recv filled, not a stale 0x41 from the memset I did before the call.
Stack protector, arm64
Fourth call uses a frame shaped like get_pos(): a gchar *, a gchar **, a gchar buffback[128], and a gboolean, and it passes 128 as the length. The peer sends reply B (128 × 0x5a).
lldb -- /tmp/rot_oob_arm2
(lldb) breakpoint set -n __stack_chk_fail
(lldb) run
Stopped in libsystem_c.dylib__stack_chk_fail. The caller frame is get_pos_likeinrot_oob.c. The abort happens on return from that function, after the line recv returned 128, sizeout 128`.
arm64 -O0 is the same exit status 134, and it stops at the same point: the 127-byte reply leaves the neighbor at 0x42, the 128-byte reply changes it to 0x00, and the following get_pos_like return is where the protector fires.
Frame layout I read back from this clang:
| Build | buffback |
canary | buffout[128] |
|---|---|---|---|
arm64 -O2 |
sp+24 |
x29-40, which is sp+152 |
sp+152 |
arm64 -O0 |
sp+40 |
sp+168 |
sp+168 |
In both arm64 builds the NUL lands on the canary.
x86_64, same source, no abort
/tmp/rot_oob and /tmp/rot_oob_o2 still change the neighbor from 0x42 to 0x00 for the 128-byte and 200-byte replies. Apple clang inserts 8 bytes of padding between this 128-byte array and the canary, so the NUL hits the padding. get_pos_like prints get_pos_like returned and does not enter __stack_chk_fail.
The overflow is in the source either way. Whether that one NUL hits the canary depends on how the compiler lays out the frame. The native architecture here is arm64, and that build aborts.
Driver
lab/rot_oob.c. The socket function matches gtk-rot-ctrl.c lines 157–197.
size = strlen(buff);
written = send(sock, buff, size, 0);
size = recv(sock, buffout, sizeout, 0);
fprintf(stderr, "recv returned %d, sizeout %d\n", size, sizeout);
buffout[size] = '\0'; /* size == sizeout is legal for recv */
/* probe: array and the following byte are adjacent */
struct probe { char buf[128]; unsigned char next; };
memset(p.buf, 'A', 128);
p.next = 0x42;
rotctld_socket_rw(sv[0], "p\n", p.buf, 128);
/* canary: locals match get_pos, peer sends 128 bytes of 'Z' */
static void get_pos_like(int sock) {
gchar *buff, **vbuff;
gchar buffback[128];
gboolean retcode;
buff = "p\n";
retcode = rotctld_socket_rw(sock, buff, buffback, 128);
}
Suggested fix
Do what _send_rigctld_command() already does:
if (sizeout < 1)
return FALSE;
size = recv(sock, buffout, sizeout - 1, 0);
if (size < 0)
return FALSE;
buffout[size] = '\0';
Treat a full buffer as a hard error and drop that reply. Do not assume the peer only returns a few digits.
Credit
Found and reproduced locally against commit da7243fe7aaa14b20782d127b0fa5bd3c21e2dfe. The send bytes, reply bytes, recv returns, neighbor values, and the __stack_chk_fail stop are from that run.
- Lenguaje dominante
- C
- Estrellas
- 1.2k
- Forks
- 305
- Merge medio
- 8 h 48 min
- PR fusionados (30 d)
- 3
Preparar el entorno
- Sin Dockerfile ni archivo de Docker Compose
- Sin plantilla de pull request
- Leer la guía de contribución
Primeros pasos
- Lee el issue completo y luego la guía de contribución del proyecto.
- Comenta en el issue que vas a ocuparte — evita que dos personas hagan lo mismo.
- Haz un fork del repositorio y trabaja en una rama.
- Abre un pull request que haga referencia al número del issue.
Más de csete/gpredict
-
bug
Dificultad 2/5 1-3 horas Aptitud para principiantes 76/100
-
Dificultad 2/5 Menos de una hora Aptitud para principiantes 60/100
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
-
Dificultad 4/5 3-5 días Aptitud para principiantes 78/100
-
rotator
Dificultad 4/5 3-5 días Aptitud para principiantes 30/100
Todos los issues de csete/gpredict
Issues similares
-
Dificultad 1/5 Menos de una hora Aptitud para principiantes 91/100
FujiNetWIFI/fujinet-firmware#2045 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 70/100
OpenPrinting/cups#1772 ·
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 88/100
snowballstem/snowball#311 ·
-
tech-debt
Dificultad 2/5 1-3 horas Aptitud para principiantes 78/100
Los mantenedores suelen responder en 1 día
-
Dificultad 2/5 1-3 horas Aptitud para principiantes 68/100
Los mantenedores suelen responder en 1 día