Random IRC quote :      <elfinimin> recordais que alguna vez os he dicho que voy a mataros a todos? <elfinimin> pues ahora lo retiro <elfinimin> ya no es necesario <elfinimin> ya lo hara [censored]

Jugando con Samepage Merging

ACTUALIZACIÓN: 1/5/2010 Incluyo un pequeño cambio en el codigo del PoC que había hecho el cual mejora muchisimo los resultados, lamentablemente no tengo tiempo de rehacer las graficas y las conclusiones. Pruebenlo que ahora los resultados son mucho mas notables 😉

Hola audiencia de 48bits, hoy les voy a contar un poco sobre una cualidad que tienen ciertos entornos de virtualizacion, la cual podemos aprovechar para detectar los mismos.

El método que propongo esta basado en timing analysis, pero no sobre el tiempo de ejecución de determinadas instrucciones, sino que sobre los tiempos de acceso a memoria. Se han utilizado técnicas similares para detectar VMMs (incluyendo VT). Un método conocido es medir el tiempo de acceso a memoria con cache on/off, por lo general el VMM no permite desactivar el cache, entonces si ambas mediciones dan resultados similares significa que estamos dentro de un entorno virtual.

A diferencia de esa técnica, la manera que he encontrado no requiere ring0 y además es bastante sencilla. Claro que tiene ciertas limitaciones, no sirve para detectar cualquier VM, solo aquellas que implementen samepage merging (hasta ahora solo hice pruebas con KVM-KSM y VMware).

Supongo que tendré que explicar un poco en que consiste esto del samepage merging (desde ahora SM porque soy vago). El SM esta muy relacionado con el CoW (Copy on Write). Básicamente se trata de un thread que periódicamente recorre la memoria y une todas las páginas cuyo contenido es exactamente el mismo. Una vez unidas las páginas, el resto del proceso es exactamente un CoW, se comparte la misma page frame y se marca la página como read-only, cuando se intenta realizar una escritura, el manejador de excepciones se encarga de asignar una nueva página.

Este tipo de estrategia es bastante costosa por lo que no se suele utilizar sobre toda la memoria del sistema operativo. Sin embargo es muy tentador utilizarla en entornos virtualizados ya que se ahorran cantidades considerables de ram, el beneficio es máximo cuando se corren varios guests simultanea-mente.

En linux disponemos de KSM (Kernel Samepage Merging), este se puede activar y desactivar en el vuelo. Cualquier versión reciente de KVM saca provecho de KSM si se encuentra activado. La idea original sobre este tipo de tecnologías parece pertenecer a VMware y al parecer hubo ciertos problemas de patentes con KSM, lo importante a destacar es que si podemos aprovecharnos de KSM seguramente también podemos hacerlo de VMware.

Ya con todo esto un poco explicado, algunos deben estarse preguntando que pasaría si medimos los tiempos de acceso a memoria antes y después de que el KSM (o VMware) actue sobre la memoria del guest. Es justamente lo que vamos a averiguar 😉

Para logralo he escrito un PoC bastante cutre pero útil a nuestros propósitos. A continuación, el código (que no es muy bonito que digamos), y le sigue su descripción.

Nuevo PoC!

/*
 * smdetect.cpp
 *
 * Created by Daniel Fernandez (soyfeliz@48bits.com) on 19/03/2010
 *
 * Copyright (c) 2010  48BITS
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 * Redistributions of source code must retain the above copyright notice,
 * this list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright
 * notice, this list of conditions and the following disclaimer in the
 * documentation and/or other materials provided with the distribution.
 *
 * Neither the name of the project’s author nor the names of its
 * contributors may be used to endorse or promote products derived from
 * this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED
 * TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
 * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 */

#include <iostream>
#include <cstdlib>
#include <sys/time.h>
#include <unistd.h>

#ifdef _WIN32
#include <windows.h>
#else
#include <sys/mman.h>
#endif

class Buffer
{
    int *buffer;
    unsigned int size;

    unsigned long long gettime()
    {
        struct timeval tv;

        gettimeofday(&tv, NULL);
        return ((unsigned long long) tv.tv_sec) * 1000000ULL +
                (unsigned long long) tv.tv_usec;
    }

public:

    enum Exceptions
    {
        ALLOC_FAILED,
        LOCK_FAILED,
    };

    Buffer(unsigned int sz, bool locking, unsigned int seconds) : size(sz)
    {
#ifdef _WIN32
        HANDLE hProcess(GetCurrentProcess());
        SIZE_T minws, maxws;

        buffer = (int *) VirtualAlloc(NULL, size, MEM_COMMIT, PAGE_READWRITE);

        if (buffer == NULL)
            throw ALLOC_FAILED;

        if (locking == true)
        {
            GetProcessWorkingSetSize(hProcess, &minws, &maxws);
            SetProcessWorkingSetSize(hProcess, minws + size, maxws + size);

            if (VirtualLock(buffer, size) == 0)
                throw LOCK_FAILED;
        }
#else
        buffer = (int *) mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_PRIVATE |
                              MAP_ANONYMOUS, 0, 0);

        if (buffer == MAP_FAILED)
            throw ALLOC_FAILED;

        if (locking == true && mlock(buffer, size) == -1)
            throw LOCK_FAILED;
#endif
        for (unsigned int x(0); x != size/sizeof(int); ++x)
            buffer[x] = 0x443d3d38;
#ifdef _WIN32
        Sleep(seconds * 1000);
#else
        sleep(seconds);
#endif
    }

    ~Buffer()
    {
#ifdef _WIN32
        VirtualUnlock(buffer, size);
        VirtualFree(buffer, 0, MEM_RELEASE);
#else
        munmap(buffer, size);
#endif
    }

    unsigned long long measureWrite()
    {
        unsigned long long start(gettime());

        for (unsigned int x(0); x != size/sizeof(int); x += 4096/sizeof(int))
            buffer[x] = x;

        return gettime() – start;
    }
};

unsigned long long getTime(unsigned int bsize, bool locking,
                           unsigned int sleep_seconds = 0)
{
    Buffer buf(bsize, locking, sleep_seconds);
    return buf.measureWrite();
}

int main(int argc, char *argv[])
{
    unsigned int buffer_size(15*1024*4096);
    unsigned int sleep_time(60);
    unsigned int threshold(500);
    bool locking(true);
    char c;
   
    while ((c = getopt(argc, argv, "pm:s:t:")) != -1)
    {
        switch(c)
        {
        case ‘p’:
            locking = false;
            break;
        case ‘m’:
            buffer_size = (atoi(optarg) * 1024 * 1024) & ~4095;
            break;
        case ‘s’:
            sleep_time = atoi(optarg);
            break;
        case ‘t’:
            threshold = atoi(optarg);
            break;
        case ‘?’:
            std::cout << "\nOptions:\n"
                    << "-m size\t buffer size in Mb (default 60mb)\n"
                    << "-s time\t sleep time in second (default 60secs)\n"
                    << "-t num\t threshold (default 98)\n"
                    << "-p\t disable locking (default enabled)"
                    << std::endl;
            return -1;
        }
    }

    try
    {
        unsigned long long t1(getTime(buffer_size, locking));
        unsigned long long t2(getTime(buffer_size, locking, sleep_time));

        // avoid division by zero
        if (t1 == 0)
                t1 = 1;
       
        int variation((std::abs((long long)(t2 – t1))*100)/t1);

        std::cout << "First write:  " << t1
                << "\nSecond write: " << t2
                << "\nVariation:    " << variation << "%\n"
                << (variation >= threshold ? "VMM detected!" : "no VMM detected")
                << std::endl;
    }
    catch (Buffer::Exceptions e)
    {
        if (e == Buffer::ALLOC_FAILED)
        {
            std::cout << "Can’t alloc buffer, try with a smaller size (-m)\n"
                    "Or check your capabilities" << std::endl;
        }

        if (e == Buffer::LOCK_FAILED)
        {
            std::cout << "Can’t lock buffer, check your capabilities\n"
                    "Or use -p flag" << std::endl;
        }
    }

    return 0;
}
 

El código puede ser compilado con visual studio, y lo ideal es compilarlo para release.

Ahora se compila con: g++ -O3 smdetect.cpp -o smdetect
Funciona tanto en linux como en windows (mingw).

Ahora unas gráficas con los resultados en distintos entornos:

Las gráficas corresponden a 10 ejecuciones de smdetect por cada entorno. Si todo fuera perfecto deberían ser lineas horizontales, pero factores externos como la carga del sistema hacen variar bastante los resultados.

Se puede apreciar una diferencia notable entre los casos donde no hay VMM y en los que la hay. Un caso curioso se da con OSX, donde la diferencia es apenas un 100% estando más cerca de los resultados donde no hay VMM. Incluso en una prueba llego a dar ~30% lo cual lo hace un target muy dificil de detectar.

Mirando las gráficas y sin considerar OSX, podriamos establecer un umbral de 150% para la detección. Incluyendo los datos de OSX he decidido utilizar un 98%, aún asi pueden darse falsos negativos en OSX y un umbral tan bajo podría llegar a dar falsos positivos si se dan ciertas condiciones.

Conclushion!

Tenemos un nuevo método, que si bien no es 100% confiable, puede ser utilizado de forma práctica y es posible refinarlo mucho más. Una posibilidad es la utilización de heurísticas para ajustar el umbral en función a información sobre la carga del sistema. Otro tema pendiente es la enorme espera que hay que realizar entre mediciones, pero esto no creo que sea un impedimento para muchos autores de malware…

Aqui se acaba 🙂

PD: lo siento Javi y Marconi por meter otro post en tan poco tiempo (parece ser que siempre ocurre lo mismo, pueden pasar meses sin movimiento y luego abalancha de posts).

8 Comentarios para “Jugando con Samepage Merging”

  1. Comment por Shaddy | 03/17/10 at 7:01 am

    muy buena tío, lo que me pregunto es como exactamente lo aplicarías para una detección de entorno virtualizado (sin tener acceso al host). Porque meter «hardcodeado» un rango de estadísticas de un entorno virtualizado puede llegar a implicar que en máquinas antiguas se detecten como virtualizadas, no? Quiero decir, que es un método válido para X máquina en contreto . . .

    Un saludo ;).

  2. Comment por Mario Vilas | 03/17/10 at 9:44 am

    Para lo que pregunta Shaddy, se me ocurre que en vez de detectar estadisticas harcodeadas se puede tratar de relacionar la media de variaciones con la velocidad del procesador, que tambien es medible. Pero no estoy tan ducho en matematicas como para proponer algo en concreto. 🙂

    Offtopic, de donde salio el capcha? Recien tuve que tipear la palabra «chalchichon» 😀 😀 😀

  3. Comment por erg0t | 03/17/10 at 10:00 am

    @Shaddy, si te fijas, lo que se mide es la variacion en los tiempos, no los tiempos en si. Es decir no importa la velocidad del procesador la relacion se va a mantener igual. Si el procesador es mas lento ambas mediciones van a dar mas counts pero la diferencia entre la primera y segunda medicion se va a mantener igual.

    Si hay una diferencia considerable, digamos de un 80% para arriba podriamos decir que estamos en una VM. Tambien se podria calcular mas de una vez por si llegara a ocurrir algo como el pico en la grafica que comente, aunque es poco probable pero ya con tomar las medidas por lo menos dos veces las posibilidades de un falso positivo serian practicamente nulas.

  4. Comment por Karcrack | 03/17/10 at 9:51 pm

    Muy interesante el tema =D

    A ver si consigo hacer un par de test en un par de maquinas viejas que tengo por casa, ya os contaré! ;D

  5. Comment por Shaddy | 03/18/10 at 4:39 am

    Entonces la idea es determinar el entorno en función de la varianza estadística?

  6. Comment por erg0t | 03/18/10 at 10:14 am

    @Shaddy, si algo asi. Si miras la ultima grafica vas a ver que las variaciones en el caso de no-VM son muy pequeñas, en cambio en el caso de VMWare y KVM-KSM son bastante grandes (inclusive mucho mas de lo que parece en la grafica, ya que la grafica es un promedio de la variacion de las 3 medidas, y el segundo read tiene una variacion ~0% por lo cual bajo bastante el promedio).

  7. Comment por Dreg | 03/21/10 at 8:18 pm

    mu chulo tio

  8. Comment por Putex | 03/25/10 at 10:57 am

    Oye espero con ansiedad un articulo sobre los Futex, que se nos estan atragantando!!

Se han cerrado los comentarios