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).