Desclavando espinas 2/3: UAD360 go4fun writeup

El reto

Revisamos la información básica sobre el binario:

file go4fun.uu
go4fun.uu: ELF 32-bit MSB executable, MIPS, MIPS32 version 1 (SYSV), statically linked, not stripped

Estamos ante un binario ELF (Linux), con arquitectura MIPS de 32 bits. Además, es Big Endian (MSB).

Ejecutando el binario con arm_now

Durante la UAD360, no conocía el proyecto arm_now. @julianjm me lo chivó (luego también lo haría @oreos_ES). Basta con instalar arm_now y luego levantar una instancia emulada con qemu de una máquina MIPS32 Big Endian:

arm_now start mips32 –sync

El binario nos pide una clave, luego parece estar haciendo algún tipo de comprobación y finalmente acaba sin ningún mensaje de error:

Ejecutando el binario MIPS32

Reverseando

Ejecutamos r2 y lo primero que nos sorprende es la cantidad de funciones que tiene este binario. Se ve claramente que estamos ante un binario escrito en golang, compilado para MIPS32. Leemos un poco sobre binarios Go para saber dónde comenzar nuestra búsqueda. La primera función que realmente nos interesa es sym.main.main, que a su vez llama a sym.main.check_key. Pedimos a r2 que nos muestre el callgraph desde sym.main.check_key:

El callgraph generado nos muestra claramente el flujo del programa con una llamada a Sleep (seguramente el “Please Wait….”) y una llamada a Decrypt. Luego, el binario desencripta probablemente la flag, que estará encriptada y almacenada en el propio binario, a partir de la key que se nos pide.

La function callgraph para check_key

En la función sym.main.main encontramos la flag cifrada:

La flag cifrada.

Recordemos que esto es Go, y las cadenas no terminan con \x00. Si observamos claramente la imagen, en realidad la flag cifrada está compuesta de los siguientes bytes:

7fd4000ff0ae3bdd14ff3bd70aa12ce69636366c77c1a7298ae9f5aeab69a0f4739f4f069561c44b2d6597bfc4834236f1756290ae

Si miramos el código de la función Decrypt, vemos claras llamadas a las funciones en Go crypto_aes.NewCipher y crypto_cipher.NewGCM:

[0x0010050c]> s sym.main.decrypt
[0x000ff758]> pdf
Do you want to print 747 lines? (y/N) 
[0x000ff758]> pdf~jal
| |: 0x000ff770 0c01b2cc jal sym.runtime.morestack_noctxt
| 0x000ff798 0c03fd8a jal sym.main.createHash
| 0x000ff7b8 0c016a6b jal sym.runtime.stringtoslicebyte ; sym.runtime.rawstringtmp+0xc4
| 0x000ff7d8 0c0222e1 jal sym.crypto_aes.NewCipher
| | 0x000ff800 0c01d9fa jal sym.crypto_cipher.NewGCM

Así que ya sabemos lo que hace el código a groso modo. A partir de nuestra entrada, hace algunas comprobaciones sobre la misma. Si la key introducida es válida, entonces genera un hash en MD5 de la clave (llamada a createHash) y la utiliza como clave AES en modo GCM (llamadas a sym.crypto_aes.NewCipher y sym.crypto_cipher.NewGCM) . Con esto descifra la flag y entonces se hace una llamada a show_flag que la imprime por pantalla.

Leyendo un poco sobre Go y AES en modo GCM, vemos que se suele concatenar el nonce delante del texto cifrado. Dentro del binario tenemos claramente la flag cifrada, pero el nonce no lo vemos por ninguna parte. Al final, con ayuda de r2, vemos claramente que hay una llamada a gcm.NonceSize() justo aquí:

0x000ff828 8c230010 lw v1, 0x10(at)
| || 0x000ff82c afa20004 sw v0, (var_4h)
| || 0x000ff830 0060f809 jalr v1; gcm.NonceSize()

Así que la flag cifrada tiene seguramente el nonce concatenado al principio. La siguiente llamada es la que ejecuta el descifrado:

0x000ff8c0 0080f809 jalr a0

La documentación sobre AES en modo GCM indica que el número de bytes para el nonce es de 12 (0xc en hexadecimal). Así pues, consideramos los 12 primeros bytes de la flag como el nonce, y el resto como la flag cifrada:

nonce =7fd4000ff0ae3bdd14ff3bd7

flag  = 0aa12ce69636366c77c1a7298ae9f5aeab69a0f4739f4f069561c44b2d6597bfc4834236f1756290ae

La única manera de descifrar la flag es, pues, con la key válida de AES. Por si acaso, revisando el código del binario en MIPS, nos encontramos con que la función createHash, que entendemos calcula el MD5 de la clave AES (aparentemente, la que introducimos como entrada), tiene un código que llama la atención.

La falsa clave AES

Si observamos el código con r2 de la función createHash, vemos un snippet de código que acaba generando 32 bytes hexadecimales (que podría perfectamente ser una clave AES válida):

0x67452301efcdab8998badcfe10325476

Las operaciones lui cargan inmediatos en el registro v0, y las operaciones ORI hacen el or con el mismo registro y otro inmediato, almacenando el resultado en el mismo registro v0. Tras cada operación, se guardan 4 bytes en at + offset. Tras el código mostrado en la siguiente captura, $at contendrá los 32 bytes del falso hash:

Este código genera un hash falso de 32 bytes.

Como ya sabemos que el código descifra la flag, cifrada con AES en modo GCM; tenemos la flag y el nonce, sabemos que la clave será un hash md5. Con todo esto, probamos el hash generado con las operaciones OR con un código Go típico para descifrar. El resultado no puede ser más desolador:

Nope, @oreos_ES nos ha jodido bien …

Por desgracia, no hay más remedio que enfrentarse a la lógica de la función check_key … A base de mucho café, claro.

Sobre check_key, retdec y otros quebrantos

Mis conocimientos de MIPS32 son bastante nulos, aunque han mejorado tras este reto :). Usando r2, el código de comprobación de la clave se me hizo bastante infumable, así que tiré de retdec para intentar obtener una representación a más alto nivel de la función. Tras instalar retdec, de-compilé la función check_key:

retdec-decompiler.py -a mips -e big –select-functions main.check_key go4fun.uu

El archivo de código C resultante mejoró considerablemente, aunque seguía siendo algo duro de roer. Más bien tedioso en su lectura e interpretación, aunque a simple vista ya se podía ver que se estaba usando una variable como base y diferentes constantes numéricas como OFFSET para realizar ciertas comprobaciones sobre lo que parecían ser las diferentes posiciones de nuestra clave. Por ejemplo:

Primera comprobación de las posiciones key[15] y key[4]

En el código anterior, v2 contendrá el valor entero de la posición 15 de la clave, o sea, key[15]. v6, en cambio, contendrá el valor entero de la posición 4 de la clave, es decir, key[4]. La primera comprobación es, si substituimos v6 por key[15] y v2 por key[4], la siguiente:

key[15] + key[4] != 12

Evidentemente, si esto se cumple se retorna y no se comprueban el resto de posiciones de la clave, con lo que no es válida y no se procede a descifrar la flag. Por lo tanto, nuestra clave debe satisfacer todo lo contrario:

key[15] + key[4] == 12

Si seguimos mirando el resto del código, podemos ver que esta realizando más comprobaciones con diferentes posiciones de la clave. Esto cada vez más parece un sistema de ecuaciones dónde diferentes operaciones sobre diferentes posiciones de la clave deben dar un valor concreto para seguir con la enésima comprobación. Si cualquiera de estas operaciones no da el resultado correspondiente, se retorna y no se llama a decrypt. Tras armarme de paciencia, llegué a identificar hasta 24 ecuaciones con diferentes posiciones de la clave. El archivo en código C generado con retdec y analizado y documentado lo podéis ver aquí.

Todas las ecuaciones se podían identificar con relativa facilidad gracias a retdec, aunque en algunos casos era algo más obtuso. También es cierto que he simplificado algunas, porque por ejemplo esto:

((v12 > 9 ? -1 : v12) – (v13 > 9 ? -1 : v13) ^ 8) == 0

es totalmente equivalente a esto:

((v12 > 9 ? -1 : v12) – (v13 > 9 ? -1 : v13) ) == 8

Entra z3 (gracias @julianjm)

Estas son todas las ecuaciones (24) que identifiqué leyendo el código de-compilado de la función check_key (os recomiendo mirar el archivo check_key.c):

int(key[4]) + int(key[15]) == 12
int(key[1]) * int(key[18]) == 15
int(key[15]) / int(key[9]) ^ 1 == 0 => int(key[15]) / int(key[9]) == 1
int(key[17]) – int(key[0]) ^ 8 == 0 => int(key[17]) – int(key[0]) == 8
int(key[5]) – int(key[17]) == -1
int(key[15]) – int(key[1]) == 6
int(key[10]) * int(key[1]) == 24
int(key[13]) + int(key[8]) == 11
int(key[8]) * int(key[18]) == 15
int(key[11]) * int(key[4]) == 18
int(key[8]) + int(key[9]) == 12
int(key[12]) – int(key[19]) == -4
int(key[9]) % int(key[17]) == 1
int(key[16]) * int(key[14]) == 2
int(key[7]) – int(key[4]) == 1
int(key[6]) + int(key[0]) == 0
int(key[2]) – int(key[16]) == -1
int(key[4]) – int(key[6]) == -3
int(key[0]) % int(key[5] == 0
int(key[11]) * int(key[5]) == 42
int(key[10]) % int(key[15]) ^ 8 == 0 => int(key[10]) % int(key[15]) == 8
int(key[11]) / int(key[3]) == 2
int(key[14]) – int(key[13]) == -7
int(key[19]) + int(key[18] == 12

Necesitaba algo que resolviera todas estas ecuaciones. Entonces fue cuando @julianjm me dijo que existía el z3 solver. Así que lo instalé, y escribí el solver a partir de las ecuaciones anteriores. El archivo del solver en z3 lo podéis descargar aquí. Para resolver estas ecuaciones, las introduje una a una y ejecuté el solver:

[s[8] = 3,
s[4] = 3,
s[19] = 7,
s[17] = 8,
s[16] = 2,
s[2] = 1,
s[9] = 9,
s[1] = 3,
s[3] = 3,
s[15] = 9,
s[11] = 6,
s[10] = 8,
s[12] = 3,
s[18] = 5,
s[0] = 0,
s[14] = 1,
s[7] = 4,
s[6] = 0,
s[5] = 7,
s[13] = 8]

El resultado me daba el valor numérico de cada posición de la clave. Por lo tanto, re-ordenando las soluciones se obtenía la clave:

03133704398638192857

Tras mi análisis de lo que hacía el código, era evidente que había que calcular el hash MD5 de la clave y usarla como clave de 32 byres AES de descifrado en mi programa escrito en go “decrypt.go”. El resultado:

The flag is mine!

Reto superado, eso sí, cerca de 72 horas más tarde de lo esperado …

Desclavando espinas 1/3: UAD360 SuperSafeWeb writeup (sin wasm2c o wasmdec)

El reto

Descargamos el archivo rev1.zip y obenemos 3 archivos: index.jsp, index.wasn y main.html. El archivo index.jsp es el envoltorio para poder ejecutar las llamadas a bajo nivel escritas en WASM del archivo index.wasm, y no tiene mayor interés. El archivo main.html es un simple formulario web con una caja de texto y un botón que nos insta a introducir una flag:

main.html muestra un simple formulario web.

Si miramos el código por debajo, vemos que hay una llamada de JavaScript a la función check_flag(). Si escribimos cualquier cosa, se hace una llamada a check_flag y, si la flag introducida no es la correcta, se nos devuelve el siguiente error:

Si la flag no es la correcta, mensaje de error.

Depurando con Chrome

La función check_flag() está implementada a bajo nivel en el archivo index.wasm. Para resolver el reto, utilizaremos las opciones de debugging de la consola web de Google Chrome y un poco de reversing. Con Google Chrome no se permite la apertura y ejecución de archivos WASM via file://, así que levantamos un pequeño servicio web con Python en el mismo directorio donde tengamos los 3 archivos descomprimidos:

python -m SimpleHTTPServer

Usando Google Chrome, abrimos la url: http://localhost:8000/main.html y nos aparece el formulario web. Abrimos las “Developer Tools” para poder depurar el código WASM. Antes que nada, tenemos que identificar la función check_flag() dentro de todo el código a bajo nivel para poder poner ahí algunos puntos de interrupción.

Localizando la función check_flag()

Descargamos y compilamos wabt . Convertimos el código binario a formato humano con:

wasm2wat index.wasm -o index.wat

Abrimos el archivo generado con el editor ASCII que más nos guste y buscamos la función check_flag():

(export “_check_flag” (func 19))

Vemos que la función check_flag, dentro del código WASM, está implementada como func 19. Ya sabemos dónde poner el primer punto de interrupción en el navegador, justo en la primera línea de la func 19:

Punto de interrupción en la función check_flag

Identificando los argumentos de check_flag

La función check_flag() recibe 2 argumentos de tipo integer 32 bits. Escribimos cualquier cosa en la caja de texto y pulsamos “Check flag!”. Automáticamente veremos claramente identificados en Chrome los dos valores de estos argumentos. El segundo parece ser la longitud de la cadena introducida; el primero podría ser la dirección dentro de la memoria donde se encuentra la cadena introducida. Eso se puede comprobar escribiendo diferentes cadenas con diferentes longitudes:

Argumentos de check_flag() para “hola”.

El primer argumento siempre tiene el mismo valor, y es la dirección dentro de la memoria dónde se almacena nuestra cadena. Podemos verlo navegando a dicha posición dentro de “globals/memory” en Chrome, tal y como se muestra a continuación (cada carácter ASCII se representa con su valor decimal, así “hola” pasa a ser: 104 111 108 97):

Apoyándonos en la depuración y el código del archivo WAT, podemos ir determinando lo que hace realmente la función, paso a paso.

Longitud de la flag

Los dos argumentos pasados a la función son local 0 y local 1. El siguiente código, líneas 18-21, cargan en el stack el primer argumento y lo asignan a la variable local 31, después carga al stack el segundo argumento y lo asigna a la variable local 42:

get_local 0;    en WASM, local 0 es #arg0.
set_local 31
get_local 1;   en WASM, local 1 es #arg1.
set_local 42

local42 tiene, por tanto, la longitud de la cadena apuntada por local31. La primera comparación tiene lugar poco después. Tras asignar el valor de la longitud a la variable local42, esta se vuelve a cargar en el stack y se asigna a la variable local53. Finalmente, se pone en la pila la constante 20 (i32.const 20) y se comparan ambos valores (i32.ne). Si es diferente, no se entra en el siguiente bloque y se retorna de la función check_flag con un 1 (el resultado de i32.ne, 1, se almacena en local64), lo que implica que la flag no es válida (líneas 23-33):

set_local 53
get_local 53;   ponemos en el stack la longitud de la cadena.
i32.const 20;   ponemos en el stack 20.
i32.ne;            comparamos los 2 valores del stack.
set_local 64:  el resultado lo guardamos en local64.
block
get_local 64
if
i32.const 0
set_local 20
else;           este bloque ya no se ejecuta; se sale retornando error.

La constante 20 es, evidentemente, la longitud de la flag esperada.

Repeticiones

Aunque WASM es francamente poco amigable de leer, es bastante básico. Si observamos lo que sucede tras la comparación de la longitud de la cadena, podemos ver claramente que hay varios bloques con instrucciones repetitivas. Por ejemplo, si la longitud es de 20 caracteres exactos, entonces en el código WASM se entra en el bloque del “else” del snippet de código anterior. Y ahí tenemos una estructura que se va repitiendo varias veces hasta el final de la función (con ligeros cambios, claro):

get_local 31
set_local 75
get_local 75
i32.load8_s offset=0 align=1
set_local 86
get_local 86
i32.const 24
i32.shl
i32.const 24
i32.shr_s
set_local 97
get_local 97
i32.const 119
i32.ne
set_local 2
get_local 2
if
i32.const 0
set_local 20
br 2
end

A priori puede parecer un código difícil de leer, pero no lo es en absoluto.  La primera línea pone en el stack la dirección de nuestra cadena. Asigna dicho valor a la variable local75, y luego la pone de nuevo en el stack. Y ahora viene la parte interesante: la instrucción i32.load8_s offset=0 align=1 lo que hace es poner en el stack el byte apuntado por la dirección que tenemos en el stack, que es precisamente el primer byte de nuestra cadena. En nuestro caso, y para el ejemplo: “12345678901234567890” cargará en el stack el valor decimal 49 (“1”). Después asigna dicho valor a la variable local86, vuelve a ponerla en el stack, y realiza 2 operaciones de rotación de bits que se anulan entre ellas, por lo que a efectos prácticos es como si no hiciese nada:

get_local 86;   el primer byte de nuestra cadena.
i32.const 24;   ponemos 24 en el stack.
i32.shl;           byte_de_la_cadena << 24. Resultado en stack.
i32.const 24;  ponemos 24 en el stack.
i32.shr_s;      el resultado de la operación anterior se vuelve a rotar 24 bits a la derecha.
set_local 97:  el resultado se guarda en local97.

Por lo tanto, la variable local97 tendrá, en realidad, el mismo valor original que tenía la variable local86, es decir, el primer byte de nuestra cadena. Rotar 24 bits a la izquierda y dicho resultado rotarlo 24 bits a la derecha de nuevo es equivalente a no hacer nada sobre el valor original. Sin duda, esto era para despistar.

Tras esta operación absurda, se hace una comparación con un valor constante, decimal, que es claramente un valor ASCII imprimible:

get_local 97;               local97 se pone en la pila, es nuestro byte.
i32.const 119;             se pone en el stack el valor 119, que es “w”.
i32.ne;                         se comparan los dos valores.

Si los valores coinciden, entonces se pasa al siguiente bloque que es una repetición casi idéntica del código ya estudiado, haciendo exactamente la misma rotación de bits pero esta vez con dos cambios importantes: el byte de la cadena que se usa, y el valor con el que se compara. En caso contrario, se ejecuta la instrucción br 2 y se retorna error:

set_local 2;      el resultado de la comparación se guarda en local2.
get_local 2;     se pone dicho valor en el stack.
if;                    si los valores no son iguales, entramos en este if.
i32.const 0
set_local 20
br 2;                saltamos y retornamos error.
end

Resto de bytes de la cadena

A estas alturas ya nos podemos imaginar que se van a ir comparando todos los bytes de nuestra cadena, desde el primero hasta el último, con diferentes valores constantes, todos ellos valores en decimal que se traducen en códigos ASCII imprimibles. Si coinciden todos, es decir, si la comparación tiene éxito, entonces esa será la única flag correcta. Para cada bloque, las únicas diferencias importantes respecto del bloque ya analizado para el primer byte serán:

get_local 31          ponemos en el stack la dirección de nuestra cadena.
set_local 11;         asignamos la dirección de la cadena en local11 (primer byte).
get_local 11;         la ponemos en el stack.
i32.const <idx>;    ponemos el indice (de 1 hasta 19) en el stack del byte que queremos usar.
i32.add;                 sumamos esto al OFFSET que tenemos ya en el stack.
set_local 12:         guardamos resultado en local12.
get_local 12;        ponemos el valor de local12 en el stack.
i32.load8_s offset=0 align=1;  con esto, cargaremos el byte apuntado por offset+indice en el stack.

Se usará en todos los bloques el valor de constante correspondiente para operar con el enésimo byte de nuestra cadena. Por ejemplo, el carácter en la posición 13 de nuestra cadena:

get_local 31
set_local 71
get_local 71
i32.const 13;     índice del carácter: 13.
i32.add;            sumamos esto al OFFSET que ya tenemos en el stack.
set_local 72
get_local 72
i32.load8_s offset=0 align=1;  en el stack ahora cargamos el byte de la posición 13.
set_local 73
get_local 73
i32.const 24
i32.shl
i32.const 24
i32.shr_s
set_local 74
get_local 74
i32.const 53;   el carácter 13 de la flag debe ser 53, es decir, “5”.
i32.ne
set_local 76
get_local 76
if
i32.const 0
set_local 20
br 2
end

Sabiendo esto, basta con revisar todos los bloques y los valores constantes con los que se compara para poder saber cuál es la flag correcta. Cuidado que no están totalmente consecutivos; por ejemplo: los dos últimos bloques comparan las posiciones 11 y 14 con el carácter “_” (95 en decimal).

La flag.

La flag del reto.

 

The libvirtd nightmare

Preamble

Coming from outdated OpenVZ containers, one would surely think that migrating every one of those old containers to virtual machines with native and mainstream Linux Kernel KVM technology would be stable, easy to manage and absolutely not prone to corruption, kernel panics and the like.

Dream on.

The chosen setup: one powerful server with lots of RAM and hard-disk space running on Debian Stretch with an AMD Ryzen 7 processor having native SVM and IOMMU support, native KVM support coming right out from the stable Kernel shipped with Debian out-of-the-box, and libvirtd to make things easier to manage. So far so good, we managed (easily) to migrate the old-fashioned OpenVZ containers to native KVM machines, and we tested them successfully using qemu-system. After that, some trivial stuff like using virt-install to install them and virsh list, virsh start and virsh autostart commands to integrate them within the libvirtd daemon. And, by the way, every single VM running as a non-privileged user.

All of the VMs were running like a charm and smoothly thanks to their bare-metal fast solid state disk. Stable and reliable.

Dream on.

And then, all hell broke loose

After some time with all the VMs running with no issues at all, I logged myself to the host server using the non-privileged account the VMs were running as. Everything looked normal: the total amount of physical RAM used, the pagination and so on. Then, by the heck of me, I escalated privileges (I performed a su command) to perform some basic sysadmin stuff on the box. At some point, instead of returning to the non-privileged shell with <CTRL+d>, exit or whatever, I did this:

su – kvmuser

Then, magically, another instance of every single VM started up all at once, for all those VMs marked as “autostart” by libvirtd. So in no time at all, all the VMs started complaining about data integrity and instability on their file systems. No wonder why, because we had two instances of qemu-system per VM, accessing the very same qcow2 image! In no time at all, all the filesystems were mounted read-only for the first VM instances. At some point, funny things started to happen and data corruption became the norm. Chaos aplenty. Data loss and an incredible headache as I have never experienced in my whole life as a Sysadmin.

What the fuck man?

What is wrong with libvirtd

Once the VMs were restored and everything was again under perfect control, I did some research and tests. Of course we had two main problems here, the first one being not having a locking system preventing the VMs from accesing read/write the very same image twice. Heck, as far as I’m concerned, there are different ways to implement that. The second issue here was, of course, why on earth libvirtd was starting the already running VMs twice.

On another box mirroring the exact same setup, I logged as kvmuser, listed the running VMs, then became root and performed the su – kvmuser command and ran virsh list again to list the running VMs. Instead of getting the same ones, I got a suspicious and buggy empty list! See and marvel:

kvmuser@kvmbox:~$ virsh list
Id Name State
—————————————————-
1 Test running

kvmuser@kvmbox:~$ su
Password:
root@kvmbox:/home/kvmuser# su – kvmuser
kvmuser@kvmbox:~$ virsh list
Id Name State
—————————————————-

So that was it. The second virsh list command was not seeing the same VM “Test” running, although with a virst list –all it did see all the installed ones alright.

Tracing the socket

The virsh command connects to the libvirtd daemon, of course. And libvirtd is listening for connections from clients on a socket. Unless you tell libvirtd otherwise, it listens to connections on a UNIX socket. So I used strace to determine if the virsh command was using the same socket, something I suspected was not the case. The first time, when virsh was correctly seeing the “Test” VM running, this is what I got:

connect(5, {sa_family=AF_UNIX, sun_path=”/var/run/nscd/socket”}, 110) = -1 ENOENT (No such file or directory)
connect(5, {sa_family=AF_UNIX, sun_path=”/var/run/nscd/socket”}, 110) = -1 ENOENT (No such file or directory)
connect(6, {sa_family=AF_UNIX, sun_path=”/run/user/1001/libvirt/libvirt-sock”}, 110) = 0

So the socket was /run/user/1001/libvirt/libvirt-sock. Then, I became root and then I performed the su – kvmuser again, and I traced the second virsh list command:

connect(5, {sa_family=AF_UNIX, sun_path=”/var/run/nscd/socket”}, 110) = -1 ENOENT (No such file or directory)
connect(5, {sa_family=AF_UNIX, sun_path=”/var/run/nscd/socket”}, 110) = -1 ENOENT (No such file or directory)
connect(6, {sa_family=AF_UNIX, sun_path=”/home/kvmuser/.cache/libvirt/libvirt-sock”}, 110) = 0

The second time, the UNIX socket used was a different one, which explained why the virsh command did not see the same running VMs as the first one!

A trivial fix

According to libvirtd manpage:

$XDG_RUNTIME_DIR/libvirt/libvirt-sock
The socket libvirtd will use.

If $XDG_RUNTIME_DIR is not set in your environment, libvirtd will use $HOME/.cache

So, the first time, $XDG_RUNTIME_DIR was correctly set, and thus the socket was created on the /run/user/UID/ directory. After executing su and then su – kmvuser, though, this variable was not set at all, therefore libvirtd was using the UNIX socket under /home/$USER/.cache, as shown above.

This absolutely trivial thing rendered all of our KVMs corrupt in a matter of seconds. To fix it, I added the following line to the .bashrc file of the “kvmuser” user:

export XDG_RUNTIME_DIR=/run/user/$UID

And now, everything (but locking) works like a charm.

So far.