Llamar o no a GC.Collect directamente

Hace ya un tiempo publiqué un post donde hablaba de liberación de memoria en código manejado, y cómo ayudar a quién es nuevo en .net y le dijeron que la memoria se liberaba sola. En éste, hacía una aclaración, la cual sigo manteniendo hasta ahora. Esta dice relación con el llamar o no a el metodo collect del garbage collector (GC.Collect()).

En esa oportunidad decía que uno jamás debería llamar al recolector de memoria manualmente, sino dejar que la recolección se hiciera en su tiempo, permitiendo que el garbage collector se adiestre sólo. También abría la posibilidad de que alguien diera una buena justificación para hacerlo. Hasta el día de hoy no había dado con ninguna, hasta ahora. Hago hincapié en que no me creo ni soy dueño de la verdad, pero me baso en lo que veo y leo día a día.

Podcast

Estando suscrito a podcasts, rss y cuanta información podamos y creamos que vamos a ser capaces de digerir en algún momento, algunos fueron recibiendo polvo hasta que un largo viaje arriba de un avión me permitió escucharlos, en mi Zune :). Si, tengo un Zune, igual que muy poca gente en este mundo porque nadie la conoce. Bueno, volvamos al grano.

El podcast pertenece a la serie de arcast.tv, el título de éste no era muy atractivo, no así la descripción.

El título decía “Steve Michelotti of e.imagination on High Performance Web Solutions”. Al igual que muchos, tu seguramente habrás encontrado documentación o podcasts con títulos impresionantes, que terminaban siendo una recopilación de recomendaciones que puedes encontrar en cualquier buscador, utilizando tres palabras como web, performance y .net.

La descripción por otra parte, detallaba con lujo de que se trataba. El tema principal era problemas con el recolector de basura en arquitecturas de 64 bits. No voy a incluir acá la descripción porque no podría justificar el contenido de lo que escribiré ahora y perdería la gracia.

Si el inglés no te abandona, te recomiendo que veas el podcast, como otros también en el mismo sitio. El de RESTful services está muy bueno, al menos para los que creemos en REST y sus cualidades. Si ves el podcast, puedes evitar seguir leyendo ésto.

¿Entonces?

Si no vas a ver el podcast y sigues acá, te cuento de qué se trata. No haré una traducción literal porque no tendría sentido, sino algo donde explique los puntos y complemente la información.

Mencionaba hace un rato que el garbage collector se adiestra automáticamente de acuerdo al patrón de petición de memoria de tu aplicación (allocation pattern en inglés). Para el caso de el sitio mencionado, varios problemas sacudían el día a día, todos relacionados con la petición y liberación de memoria. Veamos cada uno punto a punto y cómo los fueron tratando.

Gran cantidad de objetos creados en memoria

La aplicación era capaz de crear miles, millones de objetos en breve tiempo, los cuales eran descartados una vez que se dejaban de usar. Esto, en general, es esperable en cualquier aplicación. Los objetos se crean, se usan y luego se le dejan al GC para que reclame la memoria. El problema de ellos es que creaban muchos, MUCHOS objetos, aumentando la administración de memoria y disparando la recolección de basura con una frecuencia más breve. Si no haz visto el post de adiestramiento del GC, te lo recomiendo para clarificar este punto.

¿Cómo mitigaron el problema?, fácil, o ni tanto. Reutilizando objetos en vez de dejárselos al GC para que liberara la memoria utilizada. Debo reconocer que cuando lo escuchaba, me costaba asumir que era una opción que alguna vez podría considerar debido a la cantidad de trabajo que involucra. De esta forma, se crean menos objetos y se destruyen menos. Por lo tanto, menos presión al GC.

¿Cuál es el problema con el GC?

Mmm, en teoría, ninguno. En la práctica, debido a que el espacio de memoria virtual de un proceso de 64 bits es muy grande, en dónde grande significa aproximadamente 8TB de espacio de memoria virtual en modo usuario y 8TB en modo kernel (con diferencias leves entre IA64 y x64), los bloques de memoria manejados por el garbage collector pueden ser muchos más que en una arquitectura de 32 bits, donde los espacios de memoria eran 2GB para usuario y 2GB para kernel.

Como los bloques son muchos más , para el caso de la aplicación en cuestión, si el GC debía hacer una recoleción completa, es decir, incluir las 3 generaciones y el heap de objetos grandes (Large Object Heap o LOH en inglés), esta podía tomar entre 5 y 8 segundos en completarse. Para recolecciones de la generación 0 y 1, no es un problema porque ambas generaciones estan un único segmento, que por lo general no supera los 64 MBs.

Es importante rescatar que mientras se realiza una recolección de memoria, todos los hilos que están ejecutando código manejado son detenidos. Esto impactaba sevéramente en el SLA (Contrato de nivel servicio) de ellos. Si cada cierto tiempo se debe “detener” un servidor entre 5 y 8 segundos, será dificil poder cumplir con un SLA de 99,99%.

Otras acciones

Entonces, a la reutilización de objetos mencionada, se le agregó además la verificación que tanto operaciones de boxing como unboxing fuesen mínimas en el código. Boxing y unboxing se refieren a convertir un objeto de un tipo específico (de tipo valor) al tipo object y viceversa. Esto ocurre generalmente fuera de nuestra vista, cuando utilizamos funciones o tipos que reciben object en vez del tipo específico. En el siguiente post se muestra cómo detectar y minimizar este patrón de uso.

Otra acción para minimizar el problema del consumo de memoria, fue utilizare object pooling, funcionalidad generalmente utilizada para mantener conexiones a la base de datos sin ser destruidas, con el fin de minimizar la creación y destrucción de los objetos, proceso que ya sabemos es costoso.

¿Dónde entra GC.Collect y el llamado de forma explícita?

Habiendo minimizado la creación y destrucción de objetos, el problema continuaba ocurriendo en menor medida, pero cuando ocurría, nuevamente se sufría de varios segundos sin servicio.

Implementaron entonces un mecanismo de broadcasting para la aplicación, con el que se informaba del estado de un servidor, a todas las aplicaciones corriendo en los servidores que componían la capa de aplicación.

Cada cierto período de tiempo, y cuando ya “se creía” que podría ocurrir una recolección de memoria completa en una aplicación del servidor X, ésta le avisaba a todas las aplicaciones de los servidores restantes que iba a recolectar basura, y que por lo tanto no le fueran enviados requerimientos. Luego, cuando estaba “fuera de línea”, llamaba manualmente a GC.Collect y una vez finalizada la recolección, avisaba a las otras aplicaciones que ya estaba “en línea”.

Si bien la solución ayuda a mitigar el problema, se basa en la creencia (y esperanza) de que pronto será recolectada la memoria, esto, con el fin de actuar antes y evitar el impacto de quedar fuera por varios segundos. Sin embargo, ya sabemos que la recolección ocurre de forma no deterministica. Sólo el GC sabe cuándo será recolectada la memoria… hasta ahora.

… Hasta ahora …

La ayuda final que le da el toque de elegancia al modelo de ellos, (que ya lo tenía, para ser justos), vino de la persona que desarrolla el GC (Maoni Stephens). Ella ayudó con la implementación de una funcionalidad que permite, como aplicación, ser notificado con un margen de tiempo especificable por uno, cuándo podría ocurrir la recolección de basura, permitiendoles a ellos no tener que “adivinar” o “creer” cuando ocurrirá la recolección, si no que tener certeza absoluta.

Esta nueva funcionalidad, más la implementación del mecanismo de broadcasting, de puesta y sacada de línea de las aplicaciones, se puede minimizar totalmente el impacto de éstas.

La funcionalidad del GC la pueden ver en el siguiente articulo de MSDN.

Al menos para mí, este es el primer caso donde veo que el llamado de GC.Collect() de forma manual es la solución a algún problema específico.

Saludos,
Patrick

Alto consumo de memoria y cursores de datos

Sorpresas te llevas en la vida, siempre. A pesar de lo que parezca, hoy no ando sermoneador ni nada por el estilo. Es sólo que no se me ocurre como comenzar este post así que escribo lo primero que se me ocurre [:)]. Total, lo interesante viene ahora.


Viaje de emergencia, aplicación ASP con [OOM], servicio interrumpido.
Resumen: problemas… un poco de entretención para unos meses muy aburridos.


Síntomas


Como mencionaba, tenemos una aplicación ASP que de vez en cuando lanza excepciones por falta de memoria, mas conocidos como error 500 ASP 147. Obviamente con el reinicio del proceso, todo vuelve a la normalidad, pero luego de la calma, llega la tormenta.


Como ya es costumbre, se capturaron algunos dumps de memoria cuando se estaba produciendo el error y se analizaron. Los resultados fueron sorprendentes, los que pasan a ser mostrados ahora.


Lo primero muy interesante es que el dump apenas sobrepasaba los 100 megabytes. Un dump contiene, sin entrar en grandes detalles, los datos privados del proceso y las librerías cargadas entre otras cosas. Si son un poco más de 100 megabytes, ¿cómo es posible que haya falta de memoria?. El administrador de tareas confirmaba que el proceso estaba utilizando algo más de 100 megabytes en working set y un poco menos en memoria privada


¿Entonces?


[windbg] entra a ayudarnos. Revisando el estado de cada heap, nos encontramos con lo que muestra el bloque de más abajo. Hay muchas columnas y muchos datos, pero fijemos la atención en las columnas que hacen mención a la memoria reservada y comprometida.


Recordemos que en un sistema operativo Windows, la memoria puede estar en tres estados: libre, reservada y comprometida. Para que una aplicación la pueda utilizar necesita primero reservarla, y luego comprometerla. Después de usarla, la debe des-comprometer (fea palabra, lo sé) y luego liberar (des-reservar, también es fea).





  Heap     Flags   Reserv  Commit  Virt   Free  List   UCR  Virt  Lock  Fast
(k) (k) (k) (k) length blocks cont. heap
—————————————————————————–
00080000 00000002 15360 13424 14512 2362 514 137 0 9d L
External fragmentation 17 % (514 free blocks)
00180000 00008000 64 12 12 10 1 1 0 0
002b0000 00001002 22080 9560 17536 1629 231 124 25 5b L
External fragmentation 17 % (231 free blocks)
Virtual address fragmentation 45 % (124 uncommited ranges)
00550000 00000002 1024 20 20 2 1 1 0 0 L
00690000 00001002 256 32 32 2 1 1 0 0 L
01bb0000 00001002 256 12 12 4 1 1 0 0 L
01bf0000 00001002 39872 11200 29608 722 62 60 0 7 LFH
01c30000 00001002 256 12 12 4 1 1 0 0 L
01c70000 00001002 256 12 12 4 1 1 0 0 L
<recortado>
02630000 00001002 256 12 12 2 1 1 0 0 L
02670000 00001002 256 12 12 4 1 1 0 0 L
02730000 00001002 64 32 32 4 1 1 0 0 L
02a20000 00001002 3328 2084 2396 110 23 16 0 0 LFH
02a60000 00001002 19968 7164 8076 35 6 10 21 0 LFH
02f60000 00001003 1280 1152 1152 2 1 1 0 bad
03470000 00001003 1280 512 512 1 1 1 0 bad
034b0000 00001003 1280 524 524 2 1 1 0 bad
034f0000 00001003 256 96 96 0 0 1 0 bad
03730000 00001003 1280 356 356 1 1 1 0 bad
03970000 00001003 1280 264 264 0 0 1 0 bad
049b0000 00001003 256 204 204 3 1 1 0 bad
049f0000 00001003 130304 128 300 63 11 12 0 bad
04a30000 00001003 441600 112 188 101 11 12 0 bad <-éste
04a70000 00001003 167204 352 424 264 49 67 0 bad
04ab0000 00001003 465716 128 36532 103 20 27 0 bad
04af0000 00001003 469708 164 1696 92 13 32 0 bad
04b30000 00001003 46312 324 328 254 47 65 0 bad
04b70000 00001003 9700 372 372 348 62 64 0 bad
039c0000 00001002 64 16 16 2 1 1 0 0 L
04c30000 00001003 256 148 148 92 36 1 0 bad
<recortado>

En el listado anterior, vemos que hay un par de heaps que han reservado (memoria en estado reservado) más de 400 megabytes, pero que sólo están utilizando (memoria en estado comprometido) un poco más de 100 kilobytes. Entre varios, el heap 04a30000, indicado más arriba en negrilla y con la palabra “<- éste”, es uno de los más grandes.


Veamos el detalle de este heap y sus segmentos, listados a continuación.





Index   Address  Name      Debugging options enabled
111: 04a30000
Segment at 04a30000 to 04a70000 (00010000 bytes committed)
Segment at 0f940000 to 0fa40000 (00003000 bytes committed)
Segment at 0fa40000 to 0fc40000 (00001000 bytes committed)
Segment at 100d0000 to 104d0000 (00001000 bytes committed)
Segment at 104d0000 to 10cd0000 (00001000 bytes committed)
Segment at 10cd0000 to 11cd0000 (00001000 bytes committed)
Segment at 11cd0000 to 13cd0000 (00001000 bytes committed)
Segment at 13cd0000 to 17cd0000 (00001000 bytes committed)
Segment at 17ed0000 to 1fed0000 (00001000 bytes committed)
Segment at 4dbd0000 to 55bd0000 (00001000 bytes committed)
Segment at 5bb60000 to 5eb60000 (00001000 bytes committed)

Mmm… mmm…mmm…mmm (esto me recuerda una canción de hace unos años), la mayoría de ellos no tiene más de 4 kilobytes usados para bloques de varios megabytes reservados. Si las matemáticas no te ayudan ahora, 1000 en hexadecimal es equivalente a 4096 en decimal.


Análisis de la situación


Recordemos que el manejo de la memoria lo realiza generalmente el sistema operativo aunque algunas aplicaciones pueden utilizar sus propios manejadores de memoria. Desde código ASP (VBScript) o Visual Basic 6.0, como también desde código manejado NO es posible trabajar a este nivel con la memoria. Lo anterior es un problema en un manejador de memoria.


Si no es ASP, VB. 6.0, ¿qué puede ser? (considerando que no hay componentes desarrollados por el cliente en C o C++)


La respuesta la da [DebugDiag]. Quien creo el heap es “Microsoft Data Access Runtime”, es decir, MDAC. Revisando la versión instalada, comprobamos que es la última con Windows Server 2003 SP2. El camino se pone difícil.


Investigación y resolución


Involucrando a las personas adecuadas, aprendimos que este comportamiento es considerado “esperado” cuando se cumplen las siguientes condiciones:



  • Se utilizan recordset del lado del cliente (client-side cursor)

  • Se obtienen muchos datos, muchos datos de una tabla

Ok ¿client-side cursor?¿que significa “muchos datos”?


“Cliente” es quien consulta la base de datos, que para este caso es IIS/ASP. En ese caso, los datos se llevan al cliente para ser luego procesados.


Después de investigar en el código, se encontró que una consulta estaba retornando más de 2 millones de registros. Eso es mucho [:)]


Reproducción


Decidido a demostrarlo, procedí a hacer unas pruebas con el siguiente código en mi “servidor.”



Y le agregué a mi tabla algo así como 4 millones de registros.


Después de varias ejecuciones, tanto en paralelo como en serie, los contadores de memoria reservada, comprometida y utilización de procesador mostraron esto:



Se puede ver que la memoria comprometida (verde) llegó como mucho hasta 300 megabytes, pero la memoria reservada (roja) aumentó sin mostrar intención de disminuir, llegando casi hasta 900 megabytes.


¿Cuál es la explicación a que no reutilice la memoria reservada y siga reservando más? Al menos yo no tengo la respuesta.


¿Que sucede cuando llegue a 2 gigabytes? [OOM]


Conclusiones


1.- Nunca desplegar “muchos” registros en una página. Mejor aún, nunca pedir muchos registros a la base de datos.


2.- Utilicen server-side cursors. Hagan la prueba con el mismo código y comparen los resultados. [;)]


Saludos,
Patrick

Internet Explorer se cuelga por algunos segundos

A diferencia de los otros casos descritos, en esta oportunidad quien estaba en problemas era yo mismo, con un síntoma que seguro a alguno de ustedes le ha ocurrido antes. Veamos de que se trata.


Síntomas


Cada vez que abría una nueva instancia de Internet Explorer, como también al abrir una nueva pestaña de una instancia que ya llevase corriendo, algunas veces se demoraba una buena cantidad de segundos (entre 10 y 15 segundos) en quedar disponible para poder usarla. Si bien el proceso se creaba y se pintaba rápidamente (menos de 1 segundo), quedaba como “esperando” algo.


Esta es una ventana del navegador en ese estado intermedio de espera.



Una vez que la espera termina, el navegador funciona como se espera. Sin embargo, si se deja pasar un tiempo sin hacer nada en la ventana de éste, al tratar de usarlo nuevamente, ya sea creando un tabulador nuevo o incluso cerrando el proceso, otra vez debo esperar una cantidad de segundos similar a la anterior.


En resumen, mientras esté usando la ventana (haciendo clics), todo bien. Dejo de usarla un rato, hay que esperar para que reaccione.


Intención de detección del problema


Aunque sabía que no sería nada fácil encontrar el problema, hice mi intento usando [windbg] y atachándome al proceso una vez creara un nuevo tab mientras estaba “esperando”.


Un listado de los threads relevantes (0 y 5) y sus stacks correspondientes en el momento de espera mostraba lo siguiente.





   0  Id: 12f0.123c Suspend: 1 Teb: 7efdd000 Unfrozen
ChildEBP RetAddr
002ce690 7d4e286c ntdll!NtWaitForMultipleObjects+0x15
002ce738 7d94d299 kernel32!WaitForMultipleObjectsEx+0x11a
002ce794 02596029 user32!RealMsgWaitForMultipleObjectsEx+0x152
002ce7b4 0259632d ieui!CoreSC::Wait+0x49
002ce7dc 025960d8 ieui!CoreSC::WaitMessage+0x54
002ce7e8 4640994d ieui!WaitMessageEx+0x33
002ce818 463fabcc ieframe!CBrowserFrame::FrameMessagePump+0x199
002ce824 463fbc3b ieframe!BrowserThreadProc+0x3f
002ce848 463fbb89 ieframe!BrowserNewThreadProc+0x7b
002cf8b8 463fba39 ieframe!SHOpenFolderWindow+0x188
002cfae8 00401464 ieframe!IEWinMain+0x2d9
002cff2c 004012ff iexplore!wWinMain+0x2c1
002cffc0 7d4e7d2a iexplore!_initterm_e+0x1b1
002cfff0 00000000 kernel32!BaseProcessStart+0x28

5 Id: 12f0.12e8 Suspend: 1 Teb: 7efa9000 Unfrozen
ChildEBP RetAddr
02fee038 7d4d8c82 ntdll!ZwWaitForSingleObject+0x15
02fee0a8 7da3936a kernel32!WaitForSingleObjectEx+0xac
02fee0c4 7da3ba11 rpcrt4!UTIL_WaitForSyncIO+0x20
02fee0e8 7da3b9eb rpcrt4!UTIL_GetOverlappedResultEx+0x1d
02fee104 7da3b9a9 rpcrt4!UTIL_GetOverlappedResult+0x17
02fee124 7da3ad05 rpcrt4!NMP_SyncSendRecv+0x73
02fee14c 7da3af2d rpcrt4!OSF_CCONNECTION::TransSendReceive+0x7d
02fee1d4 7da3aea2 rpcrt4!OSF_CCONNECTION::SendFragment+0x2ae
02fee22c 7da3b1b9 rpcrt4!OSF_CCALL::SendNextFragment+0x1e2
02fee274 7da3b834 rpcrt4!OSF_CCALL::FastSendReceive+0x148
02fee290 7da3b7b7 rpcrt4!OSF_CCALL::SendReceiveHelper+0x5b
02fee2c0 7da37d3e rpcrt4!OSF_CCALL::SendReceive+0x41
02fee2cc 7da37cf0 rpcrt4!I_RpcSendReceive+0x24
02fee2e0 7dac01b6 rpcrt4!NdrSendReceive+0x2b
02fee6c8 71c4b685 rpcrt4!NdrClientCall2+0x22e
02fee6e0 71c4b644 netapi32!NetrLogonGetTrustRid+0x1c
02fee720 7da4af08 netapi32!I_NetlogonGetTrustRid+0x1e
02fee768 7da4ae60 rpcrt4!GetMachineAccountSid+0xcb
02fee780 7da3f97e rpcrt4!NormalizeAccountSid+0x4c
02fee88c 7da4aaa4 rpcrt4!LRPC_CASSOCIATION::OpenLpcPort+0x1e9
02feeb88 7da4a40b rpcrt4!LRPC_CASSOCIATION::CreateBackConnection+0x74
02feebc4 7da456fb rpcrt4!LRPC_CASSOCIATION::ActuallyDoBinding+0x32
02feec3c 7da3843a rpcrt4!LRPC_CASSOCIATION::AllocateCCall+0x190
02feec70 7da38366 rpcrt4!LRPC_BINDING_HANDLE::AllocateCCall+0x1f2
02feec9c 7da37a1c rpcrt4!LRPC_BINDING_HANDLE::NegotiateTransferSyntax+0xd3
02feecb4 7778c956 rpcrt4!I_RpcGetBufferWithObject+0x5b
02feecf8 7778c629 ole32!CRpcChannelBuffer::ClientGetBuffer+0x31c
02feed08 776c0e9e ole32!CRpcChannelBuffer::GetBuffer+0x20
02feed28 776c0f7a ole32!CAptRpcChnl::GetBuffer+0x209
02feed8c 7dac0fda ole32!CCtxComChnl::GetBuffer+0x1e5
02feeda8 7dac0f89 rpcrt4!NdrProxyGetBuffer+0x47
02fef190 776a3717 rpcrt4!NdrClientCall2+0x173
02fef1a8 7778b6e4 ole32!IClassFactory_RemoteCreateInstance_Proxy+0x1c
02fef1c4 776ad8ac ole32!IClassFactory_CreateInstance_Proxy+0x41
02fef24c 776aaf7e ole32!CServerContextActivator::CreateInstance+0x175
02fef28c 776ad9b6 ole32!ActivationPropertiesIn::DelegateCreateInstance+0xf7
02fef2e0 776ad92d ole32!CApartmentActivator::CreateInstance+0x110
02fef300 776acb27 ole32!CProcessActivator::CCICallback+0x6d
02fef320 776acad8 ole32!CProcessActivator::AttemptActivation+0x2c
02fef35c 776ada17 ole32!CProcessActivator::ActivateByContext+0x4f
02fef384 776aaf7e ole32!CProcessActivator::CreateInstance+0x49
02fef3c4 776aaf19 ole32!ActivationPropertiesIn::DelegateCreateInstance+0xf7
02fef614 776aaf7e ole32!CClientContextActivator::CreateInstance+0x8f
02fef654 776ab10f ole32!ActivationPropertiesIn::DelegateCreateInstance+0xf7
02fefe08 776a679a ole32!ICoCreateInstanceEx+0x3f8
02fefe3c 776a6762 ole32!CComActivator::DoCreateInstance+0x6a
02fefe60 776a6963 ole32!CoCreateInstanceEx+0x23
02fefe90 4635a747 ole32!CoCreateInstance+0x3c
02fefec0 4638f691 ieframe!SHCoCreateInstanceAC+0x9e
02feff08 463c167a ieframe!WinList_RegisterPending+0x2c
02feff4c 463ee4b9 ieframe!CTabWindow::_RegisterPendingWindow+0x149
02feffb8 7d4dfe21 ieframe!CTabWindow::_TabWindowThreadProc+0x99
02feffec 00000000 kernel32!BaseThreadStart+0x34


El thread 0 es el thread principal y aparentemente no está haciendo mucho, salvo esperar que el kernel le avise que ciertos eventos han ocurrido.


Sin embargo, el thread 5 sí está haciendo algo y es exactamente lo que yo le había pedido. Crear un tabulador. Esto lo puedo inferir por las llamadas a las funciones con nombres acordes, como se puede apreciar ahora





<cortado …>
02feff4c 463ee4b9 ieframe!CTabWindow::_RegisterPendingWindow+0x149
02feffb8 7d4dfe21 ieframe!CTabWindow::_TabWindowThreadProc+0x99
02feffec 00000000 kernel32!BaseThreadStart+0x34

Ok, tenemos el thread que crea el tabulador. ¿qué más?


Mirando el stack, se puede observar creación de objetos COM, posiblemente una creación de contexto de COM o una reutilización de uno existente (no estoy del todo seguro de cual opción), una llamada RPC, interacción con el sub-sistema de Windows (kernel32.dll) y el kernel (ntdll.dll).


A diferencia de una revisión de aplicaciones como asp.net o asp tradicional, donde uno espera encontrar código del cliente que no está optimizado y que consume recursos, en esta oportunidad no había código de terceros. El camino se veía difícil.


Golpes de suerte


Con dos sucesivos golpes de suerte logré dar con el problema, pero jamás podría haberlo detectado sin un post de Mark Russinovich, donde él experimentó un problema similar. El primer golpe de suerte fue elegir alguna función que pareciese sospechosa y luego, livear en internet. La función elegida fue GetMachineAccountSid y la búsqueda retornó este post en primera opción.


http://blogs.technet.com/markrussinovich/archive/2006/08/31/453100.aspx
The Case of the Process Startup Delays


Con ese título del post, claramente estábamos enfrentando el mismo problema. Eso sí, la diferencia es que Mark sabe reconocer el problema, pero uno debe apelar a la suerte y a los buscadores de internet. [:)]


Haciendo pruebas similares a las del post mencionado, obtuve respuestas similares.



Bien. Traté de atacharme a lsass.exe (local security authority subsystem) con nefastos resultados. Una vez atachado windbg a lsass, hice una mala maniobra y windbg dejó de responder, haciendo que el sistema quedase medianamente bloqueado (respondía muy lento) ya que muchos procesos depende de lsass.exe. Como no hubo más opción que matar windbg, esto traería como consecuencia que el proceso que se estaba debugueando también moriría. Y si muere lsass.exe, el sistema operativo te pide amablemente reiniciar, con una pantalla similar a la pantalla del virus que atacaba XP antes de SP2 y que te daba 48 segundos para cerrar todo.


La consecuencia.



Como dirían en algunos programas de televisión; “niños, no traten de hacer esto en casa,” o mejor dicho, no lo hagan mientras visitan a un cliente y deben trabajar con su equipo.


Resolución


A diferencia del post de Mark, en donde el causante del problema era Defender, en este caso no había un tercer involucrado que remover del sistema.


La solución no fue la más elegante, pero no tuve más alternativa que ejecutar Internet Explorer en el contexto de un usuario local de la máquina, para así evitar que se trate de obtener información desde el dominio, el cual no puede ser alcanzado.


Lamentablemente esto trajo como consecuencia que no pudiese accesar a mi historial ni favoritos. Espero que cuando logre conectarme al dominio se acaben los problemas.


Saludos,
Patrick

Signos vitales de un servidor: Parte III (disco)

Después de unos días de descanso debido a las fiestas ampliamente conocidas, continuamos con la tercera entrega de la serie de monitoreo de signos vitales de un servidor, en la cual presentaremos la información necesaria para detectar problemas de rendimiento en las unidades lógicas de disco de un servidor.


Si bien es posible determinar problemas en unidades físicas, las diferentes configuraciones físicas de arreglos RAID 0, 1, 5, 1+0 y 0+1 y la creación posterior de unidades lógicas en el arreglo, hace muy difícil, si es que no imposible, el determinar si es o no un disco físico específico.


Antes de comenzar, hago un mini corte para recomendar este link de una presentación de David Solomon sobre cómo maneja la memoria Windows y como hacer un troubleshooting básico de los problemas. Además, se da tiempo de romper algunos mitos. Definitivamente un video imperdible. Está en inglés y dura 1 hora y 37 minutos.


http://www.microsoft.com/uk/technet/itsshowtime/sessionh.aspx?videoid=64


Diferentes caminos


Para la verificación de los contadores de disco, existen dos caminos conocidos. Uno de ellos el difícil y otro mucho más simple y agnóstico del hardware.


El primer camino, el cual NO veremos aquí corresponde a obtener información de los IOs de los discos, calcular la cantidad de operaciones de lectura y escritura que se realizan por cada tipo de RAID, la cantidad de discos físicos (ejes) involucrados en el RAID y hacer una cantidad importantes de cálculos y asumir con poca certeza algunas constantes o valores.


El segundo camino, mucho más simple, es agnóstico del tipo de RAID, la cantidad de discos y otras variables. ¿Qué es lo que mide? muy simple y básico:


¿Cuánto tiempo se demora el sistema de discos en realizar una operación de escritura o lectura?


Si se demora “mucho,” hay un problema. Si se demora “poco,” estamos bien. Si tienes picos elevados constantes, podemos estar en presencia de algún inconveniente o escasez de capacidad de procesamiento.


Además de la información relacionada con escrituras y lecturas, se debe complementar con información general del disco como el porcentaje de tiempo disponible y largo de la cola de requerimientos.


Veamos los contadores y contra qué compararlos, es decir, que es “bueno” y “malo.”


Nota: Antes de comenzar aclaro que cuando digo disco “malo,” no estoy mencionando que el disco está malo físicamente o que presenta alguna falla sino que realmente me refiero a que el disco no responde de acuerdo a lo que es espera de él, generalmente producto de una tasa muy elevada de requerimientos.


Contadores


Los contadores a medir del objeto Logical Disk, para la instancia específica (c:, d:, etc.), son los siguientes:




  • Avg Disk Sec/read: milisegundos en que una operación de lectura es llevada a cabo.



  • Avg disk Sec/write: milisegundos en que una operación de escritura es llevada a cabo.



  • % Idle time: porcentaje de tiempo que el disco está desocupado.



  • Avg disk queue length: largo promedio de la cola de requerimientos del disco



  • Current disk queue length: largo actual de la cola de requerimientos del disco


¿Contra que los comparamos?


Los dos primeros contadores se comparan contra la misma tabla de valores, la cual despliego a continuación:















Valor medido Significado
entre 1 milisegundo y 15 milisegundos Perfecto estado
entre 16 milisegundos y 25 milisegundos Se debe monitorear
más de 25 milisegundos Disco degradado

Se deben considerar valores promedios. Es esperable que un disco tenga picos por sobre los 25 milisegundos pero el valor promedio debe ser bajo.


Nota: Si tienes un storage con caché de escritura, deberás ser más estricto en las comparaciones y reducir un poco los valores. Esto se debe a que el caché de escritura mejora sustancialmente el rendimiento. Si antes 15 ms era el tope para considerar el valor como perfecto, entonces si mides 15 ms y tienes caché habilitado, no estarás dentro del grupo de perfecto estado sino que el rendimiento será medio y deberás monitorear la actividad.


Anécdota: El valor más alto que he presenciado en algunos casos ha sido de más de 900 milisegundos. Por supuesto, el servicio que se estaba exponiendo estaba severamente afectado, con tiempos de respuesta muy bajos.


El contador del tiempo porcentaje de tiempo disponible es simple también. Más de 50%, perfecto estado. Entre 20% y 50%, se deberá monitorear y menor a 20%, el disco está degradado.


Por último, para los contadores de largo de cola de requerimientos promedio y actual, estos dos valores se comparan y calculan de la misma forma. Si la cantidad de discos físicos que componen el arreglo de discos es conocida, se deberá comparar contra el valor medido menos la cantidad de discos y dividirlo por dos.


Por ejemplo: Si el valor medido del largo promedio de la cola de requerimientos es 25 y tengo 10 discos en el arreglo, la formula nos retorna: (25-10)/2 = 7,5.


Este valor es menor a 1, el rendimiento es excelente. Entre 1 y 3, se deberá monitorear. Mayor a 3, crítico o degradado.


Por supuesto, la formula presenta inconvenientes cuando los valores medidos son menores a la cantidad de discos, pero en ese caso el disco claramente no tiene problemas.


¿Qué sucede si no conozco la cantidad de discos? y bueno, en ese caso se recomienda dividir por 2 el valor medido en el contador y aplicar la misma regla de comparación. Entendemos que no es un valor fidedigno, pero tampoco es “tan errado.” Para el mismo ejemplo, 25/2  es 12,5, un valor mayor a 7,5, pero igual de malo de acuerdo a el parámetro de comparación.


Como pueden ver, monitorear un disco es muy simple. Si no responde en tiempos breves, claramente le estamos pidiendo más de lo que puede dar.


Queda pendiente una o dos entregas más de signos vitales, las que llegarán espero durante este mes.


Saludos,
Patrick

Traza de asp.net y el consumo de memoria

El siguiente caso a presentar está relacionado con el alto consumo de memoria de una aplicación. Como el título lo dice, está relacionado con el uso de la traza de asp.net (trace en web.config.)


El escenario era similar a lo descrito ahora. La aplicación analizada empezaba a consumir memoria y aunque tenía momentos donde la liberaba, la impresión general era que en el largo plazo, siempre subía. Este es un típico comportamiento de un memory leak o pérdida de memoria; la tendencia al alza, aunque con momentos donde baja un poco.


Como ya es tradición, un dump del proceso y mi amigo [windbg] nos darían las pistas necesarias. También nos apoyamos en performance monitor, pero hay veces que ya sabes que es una pérdida de memoria y éste no te dirá mucho más de lo que ya sabes.


Con el dump en mis manos, hacemos una revisión del proceso afectado y el estado de la memoria virtual del éste.


Recolección de información general


Tamaño del dump: ~300 MB. No es muy grande, pero tampoco es descartable. Tradicionalmente se espera a que la memoria privada del proceso llegue al menos hasta 500 MB para declararlo como sospechoso.


Información del proceso:





System Uptime: 180 days 15:52:41.140
Process Uptime: 0 days 2:08:23.000
Kernel time: 0 days 0:00:13.000
User time: 0 days 0:08:48.000

El sistema operativo lleva corriendo 180 días sin reiniciarse. El proceso en cuestión lleva del orden de 2 horas y 8 minutos corriendo y que ha consumido 13 segundos en modo privilegiado (kernel) y 8 minutos y 48 segundos en modo usuario.

Con 8 minutos de procesamiento ya llegó a 300 MB. Interesante.







El estado de la memoria virtual concuerda con el tamaño del dump, reflejado gráficamente en la imagen de la derecha, la cual pueden pinchar para agrandar.

Efectivamente hay aproximadamente 300 MB de memoria virtual, de los cuales casi 190 son de memoria manejada.


Memoria manejada


Haciendo una revisión de los heaps del GC, obtenemos la información de la siguiente lista. El servidor tiene 4 procesadores por lo que se crean cuatro heaps del GC, cada uno con sus propias generaciones, heaps efímeros y heaps de objetos grandes.





Number of GC Heaps: 4
——————————
Heap 0 (0x000d01c8)
generation 0 starts at 0x121135c8
generation 1 starts at 0x12109a44
generation 2 starts at 0x102d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x102d0000 0x102d0030 0x121135d4 0x01e435a4(31,733,156) 0x00d79000
Large object heap starts at 0x202d0030
segment begin allocated size reserved
0x202d0000 0x202d0030 0x205d2168 0x00302138(3,154,232) 0x00cdd000
Heap Size 0x2245770(35,936,112)
——————————
Heap 1 (0x000d08d8)
generation 0 starts at 0x16009a28
generation 1 starts at 0x15eaacac
generation 2 starts at 0x142d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x142d0000 0x142d0030 0x16009a34 0x01d39a04(30,644,740) 0x014c7000
Large object heap starts at 0x212d0030
segment begin allocated size reserved
0x212d0000 0x212d0030 0x21449a38 0x00179a08(1,546,760) 0x00e66000
Heap Size 0x1f2b4a0(32,683,168)
——————————
Heap 2 (0x000d13d8)
generation 0 starts at 0x24461404
generation 1 starts at 0x243e6e2c
generation 2 starts at 0x182d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x182d0000 0x182d0030 0x1998c7dc 0x016bc7ac(23,840,684) 0x028ef000
0x242d0000 0x242d0030 0x24461410 0x001913e0(1,643,488) 0x033c4000
Large object heap starts at 0x2a2d0030
segment begin allocated size reserved
0x2a2d0000 0x2a2d0030 0x2a2d0030 0x00000000(0) 0x047df000
Heap Size 0x198fb8c(26,803,084)
——————————

Heap 3 (0x000d1ca0)
generation 0 starts at 0x087954cc
generation 1 starts at 0x083dd820
generation 2 starts at 0x1c2d0030
ephemeral segment allocation context: none
segment begin allocated size reserved
0x1c2d0000 0x1c2d0030 0x1df48b8c 0x01c78b5c(29,854,556) 0x02310000
0x08330000 0x08330030 0x087954d8 0x004654a8(4,609,192) 0x01fff000
Large object heap starts at 0x232d0030
segment begin allocated size reserved
0x232d0000 0x232d0030 0x2344ed50 0x0017ed20(1,568,032) 0x00e61000
0x0edc0000 0x0edc0030 0x0f195f40 0x003d5f10(4,022,032) 0x00c2a000
Heap Size 0x277ac50(41,397,328)
——————————
Reserved segments:
——————————
GC Heap Size 0x827b3ec(136,819,692)

De esta información se puede ver que hay muchos objetos en generación 2, lo que significa que hay objetos que no se han liberado y han ido envejeciendo más de lo necesario.


¿Qué hay en la memoria?


Haciendo un vaciado estadístico de los objetos, utilizando [windbg] y sos, obtenemos un listado como el de a continuación, donde se ha eliminado parte del comienzo y sólo se ha dejado el final, que contiene la información relevante para el caso.





MT        Count TotalSize Class Name
…..<recortado por el bien de esta página>…
0x028665f0 22,807 273,684 System.Xml.Xsl.EndEvent
0x02784adc 14,800 296,000 System.Data.DataRowCollection
0x027844dc 14,800 296,000 System.Data.DataRowBuilder
0x027862d4 14,911 298,220 System.Data.ColumnQueue
0x02782a74 11,824 331,072 System.ComponentModel.CollectionChangeEventHandler
0x0230159c 14,866 356,784 System.Xml.NameTable/Entry
0x02925aec 7,847 408,044 System.Xml.Xsl.XsltCompileContext
0x02926bfc 11,589 417,204 System.Xml.XPath.XPathChildIterator
0x02863d04 8,692 417,216 System.Xml.XPath.ChildrenQuery
0x027846d4 14,800 473,600 System.Data.RecordManager
0x0278001c 14,800 473,600 System.Data.ConstraintCollection
0x027842e4 14,913 477,216 System.ComponentModel.PropertyDescriptorCollection
0x0280cddc 16,754 670,160 System.Xml.XPath.XPathWhitespace
0x0280c924 10,044 682,992 System.Xml.XPath.XPathElement
0x02866564 22,807 729,824 System.Xml.Xsl.BeginEvent
0x0252ebe4 14,800 769,600 System.Data.DataColumnCollection
0x79bda488 14,813 888,780 System.Threading.ReaderWriterLock
0x79bab93c 17,498 909,896 System.Collections.Hashtable
0x02923770 28,877 924,064 System.Xml.DocumentXPathNavigator
0x027882e4 40,203 964,872 System.Data.Common.StringStorage
0x0280cba4 21,157 1,015,536 System.Xml.XPath.XPathAttribute
0x0278489c 29,600 1,184,000 System.Data.DataRelationCollection/DataTableRelationCollection
0x01ba2c28 17,561 2,839,896 System.Collections.Hashtable/bucket[]
0x0252e4b0 14,800 3,433,600 System.Data.DataTable
0x79ba2ee4 145,618 3,494,832 System.Collections.ArrayList
0x0278670c 54,976 3,518,464 System.Data.DataColumnPropertyDescriptor
0x01ba2964 24,879 5,024,760 System.Int32[]
0x0278513c 136,364 5,454,560 System.Data.DataRow
0x0252f5d0 54,760 7,009,280 System.Data.DataColumn
0x01ba26b0 1,262 8,513,840 System.Byte[]
0x000cff48 2,829 14,897,824 Free
0x79b94638 238,884 23,296,388 System.String
0x01ba209c 235,235 40,966,392 System.Object[]
Total 1,509,853 objects, Total size: 136,798,976

Existe un poco más de 1,5 millones de objetos en memoria, que consumen cerca de 136 MB, similar al resultado entregado en la lista anterior.


De esos 1,5 millones de objetos, existe una gran cantidad de strings y arreglos de objeto, como también de filas de datos y columnas de datos. Este comportamiento se presenta cuando se han creado Datasets y no se han liberado.


Un DataSet contiene filas y columnas, y en cada celda encontramos objetos de los tipos básicos como strings, enteros, decimales, objetos, etc. Por lo tanto, estos objetos que se ven al final no viven solos sino que están referenciados desde otro objeto, como podría ser un dataset. No significa que todos estén referenciados, pero con 136 mil filas (DataRow) y 54 mil columnas (DataColumn), varios de esos miles de objetos si lo están.


¿Cuantos DataSets hay?





MT         Count TotalSize Class Name
0x0252ce84 1,597 127,760 System.Data.DataSet
Total 1,597 objects, Total size: 127,760

Bueno, tomemos uno al azar y veamos donde nos lleva. Elegimos el objeto en la posición de memoria 0x0834d148.





Name: System.Data.DataSet
MethodTable 0x0252ce84
EEClass 0x0275b43c
Size 80(0x50) bytes
GC Generation: 2
mdToken: 0x0200003b (c:\windows\assembly\gac\system.data\1.0.5000.0__b77a5c561934e089\system.data.dll)
FieldDesc*: 0x0252c4b4
MT Field Offset Type Attr Value Name
0x0252c23c 0x4000583 0x4 CLASS instance 0x00000000 site
0x0252c23c 0x4000584 0x8 CLASS instance 0x00000000 events
0x0252c23c 0x4000582 0 CLASS shared static EventDisposed
>> Domain:Value 0x000c4ac8:NotInit 0x00138380:0x1c331c58 <<
0x0252ce84 0x40003d3 0xc CLASS instance 0x15cee704 defaultViewManager
0x0252ce84 0x40003d4 0x10 CLASS instance 0x0834d198 tableCollection
0x0252ce84 0x40003d5 0x14 CLASS instance 0x0834d220 relationCollection
0x0252ce84 0x40003d6 0x18 CLASS instance 0x00000000 extendedProperties
0x0252ce84 0x40003d7 0x1c CLASS instance 0x1c331c30 dataSetName
0x0252ce84 0x40003d8 0x20 CLASS instance 0x102d0224 _datasetPrefix
0x0252ce84 0x40003d9 0x24 CLASS instance 0x102d0224 namespaceURI
0x0252ce84 0x40003da 0x40 System.Boolean instance 0 caseSensitive
0x0252ce84 0x40003db 0x28 CLASS instance 0x1c2e69c8 culture
0x0252ce84 0x40003dc 0x41 System.Boolean instance 1 enforceConstraints
0x0252ce84 0x40003dd 0x42 System.Boolean instance 0 fInReadXml
0x0252ce84 0x40003de 0x43 System.Boolean instance 0 fInLoadDiffgram
0x0252ce84 0x40003df 0x44 System.Boolean instance 0 fTopLevelTable
0x0252ce84 0x40003e0 0x45 System.Boolean instance 0 fInitInProgress
0x0252ce84 0x40003e1 0x46 System.Boolean instance 1 fEnableCascading
0x0252ce84 0x40003e2 0x47 System.Boolean instance 0 fIsSchemaLoading
0x0252ce84 0x40003e3 0x2c CLASS instance 0x00000000 rowDiffId
0x0252ce84 0x40003e4 0x48 System.Boolean instance 0 fBoundToDocument
0x0252ce84 0x40003e5 0x30 CLASS instance 0x00000000 onPropertyChangingDelegate
0x0252ce84 0x40003e6 0x34 CLASS instance 0x00000000 onMergeFailed
0x0252ce84 0x40003e7 0x38 CLASS instance 0x00000000 onDataRowCreated
0x0252ce84 0x40003e8 0x3c CLASS instance 0x00000000 onClearFunctionCalled
0x0252ce84 0x40003e9 0 CLASS shared static zeroTables
>> Domain:Value 0x000c4ac8:NotInit 0x00138380:0x1c331c20 <<

Mmm, nada más que un dataset. No se si podría haber encontrado otra cosa [:)]. Veamos el nombre de éste, en la posición 0x1c331c30 (línea blanca arriba).





String: NewDataSet

[:s]. El nombre genérico no da pistas para encontrar alguna parte del código que lo esté creando. Vemos entonces quién lo está apuntando.





Scan Thread 10 (0x1cac)
Scan Thread 16 (0xa00)
Scan Thread 17 (0x1908)
Scan Thread 5 (0xa0c)
Scan Thread 4 (0x1dcc)
Scan Thread 19 (0x1a6c)
Scan Thread 3 (0x1584)
Scan Thread 2 (0x444)
Scan Thread 20 (0x15f4)
ESP:36ff548:Root:0x1c6b59a8(System.Threading.Thread)->0x15cecad4(System.Runtime.Remoting.Messaging.IllogicalCallContext)->0x15cecae0(System.Collections.Hashtable)->0x15cecb14(System.Collections.Hashtable/bucket[])->0x15cec81c(System.Web.HttpContext)->0x15cec608(System.Web.Hosting.ISAPIWorkerRequestInProcForIIS6)->0x182d81c4(System.Web.HttpWorkerRequest/EndOfSendNotification)->0x14339e70(System.Web.HttpRuntime)->0x182d7fac(System.Web.Util.Profiler)->0x14358454(System.Collections.ArrayList)->0x1d89d7b0(System.Object[])->0x834d148(System.Data.DataSet)
Scan Thread 22 (0x1098)
Scan Thread 24 (0xcd8)
…..<recortado por el bien de esta página>…
Scan Thread 67 (0x1938)
Scan Thread 54 (0x618)
Scan HandleTable 0xc9ec0
Scan HandleTable 0xcd008
Scan HandleTable 0x147008

El thread 20, que está procesando un requerimiento en la extensión ISAPI de asp.net en IIS6 (System.Web.Hosting.ISAPIWorkerRequestInProcForIIS6) tiene una referencia fuerte al objeto. Vemos también un HashTable, el Contexto y Runtime de http, los cuales se pueden considerar normales en el ciclo de vida de un requerimiento web.


Sin embargo, hay un invitado especial en esta lista de referencias. Éste es System.Web.Util.Profiler, del cual no hay mucha información disponible, pero después de examinarlo, ya podemos inferir de quién se trata. Veamos su contenido.





Name: System.Web.Util.Profiler
MethodTable 0x021c49d0
EEClass 0x0215912c
Size 28(0x1c) bytes
GC Generation: 2
mdToken: 0x0200029d (c:\windows\assembly\gac\system.web\1.0.5000.0__b03f5f7f11d50a3a\system.web.dll)
FieldDesc*: 0x021c4704
MT Field Offset Type Attr Value Name
0x021c49d0 0x4001076 0x8 System.Int32 instance 1478 _requestNum
0x021c49d0 0x4001077 0xc System.Int32 instance 7500 _requestsToProfile
0x021c49d0 0x4001078 0x4 CLASS instance 0x14358454 _requests
0x021c49d0 0x4001079 0x14 System.Boolean instance 0 _pageOutput
0x021c49d0 0x400107a 0x15 System.Boolean instance 1 _isEnabled
0x021c49d0 0x400107b 0x16 System.Boolean instance 1 _oldEnabled
0x021c49d0 0x400107c 0x17 System.Boolean instance 0 _localOnly
0x021c49d0 0x400107d 0x10 System.Int32 instance 0 _outputMode

Recordando las propiedades de la traza de asp.net, podemos ver asombrosas similitudes con las propiedades. Veamos la definición de la traza en el archivo web.config. La información oficial dice:





<trace
enabled=”true|false”
localOnly=”true|false”
pageOutput=”true|false”
requestLimit=”integer”
mostRecent=”true|false”
writeToDiagnosticsTrace=”true|false”
traceMode=”SortByTime|SortByCategory”
/>

El dump dice que la traza estaba habilitada (_isEnabled), no estaba local (_localOnly), que tenía marcado para almacenar hasta 7.500 requerimientos (segunda línea blanca) y que habían 1478 almacenados (primera línea blanca). El resto de las propiedades las pueden chequear ustedes. Recordemos que habían 1597 DataSets en memoria.


El objeto _requests es un System.Collections.ArrayList y los 1478 objetos dentro suman casi la totalidad de memoria manejada, que si bien recordarán, sumaba cerca de 136 MB.





sizeof(0x14358454) = 91,258,512 (0x5707e90) bytes (System.Collections.ArrayList)

Entonces, la condición de memoria elevada, se da en parte porque hay ~90 MB de objetos referenciados por la traza de asp.net. También hay una cantidad de importante de memoria (~70 MB) consumida por dlls y ensamblados (ver la primera imagen.)


Conclusiones


Las conclusiones que se pueden obtener en este caso son:




  1. No todos las pérdidas de memoria son producto de errores en la codificación.



  2. Si vas a habilitar la traza en producción, recuerda deshabilitarla una vez que termines.



  3. También, si no vas a poder leer la información de 7500 trazas, configúralo para que almacene menos.


Saludos,
Patrick

Signos vitales de un servidor: Parte II (procesador)

Continuando con los signos vitales de un servidor, hoy día veremos en la segunda parte, el procesador.


El análisis del procesador es bastante más simple que el de la memoria. Hay dos contadores importantes. Uno de ellos corresponde al uso del procesador, con dos matices especiales; uso del procesador en modo usuario y el otro en modo kernel. El otro contador corresponde a la cantidad de requerimientos encolados del procesador.


Como ya hemos visto en otros posts, Windows utiliza dos modos de ejecución. Uno de ellos es el modo usuario, donde corren las aplicaciones que nosotros ejecutamos y el otro es el modo Kernel, en el cual corre el kernel del sistema operativo.


¿Por qué es importante esta explicación?


Si logramos identificar quién está consumiendo el procesador, podremos inferir y tener mejores pistas para encontrar al culpable del consumo de éste.


Si te preguntas ¿para qué necesitas saber el modo (usuario o kernel) que está consumiendo el procesador, si mirando el task manager sabrás fácilmente qué aplicación es responsable?


Bueno. Hay un par de razones. Veamos cada una de ellas.


1.- Task manager te dirá la aplicación que está consumiendo el procesador, pero no te dirá más. Solo la aplicación. Perfmon por otra parte te podrá entregar más información. [procexp] de Sysinternals es una excelente herramienta.
2.- Task manager no muestra siempre el consumo del kernel.


El segundo punto no está tan relacionado con aplicaciones, pero vale la pena aclarar que hay ciertas actividades del kernel que no son cuantificadas por Task Manager, o son cuantificadas pero no desplegadas. Recordemos que el kernel corresponde al proceso llamado System, siempre con el PID 4. Entonces puede parecer que tu sistema tiene bajo uso del procesador, pero el kernel está trabajando activamente. Para mejores resultados en el despliegue de los recursos consumidos, puedes utilizar [procexp].


Resumiendo


Podemos decir que para una aplicación específica:


El porcentaje de consumo en modo kernel corresponde a las actividades que debe realizar el kernel para cumplir con los requerimientos de la aplicación, como por ejemplo, trabajar con archivos, mover memoria, escribir en sockets, pintar ventanas, etc.


El porcentaje de consumo en modo usuario corresponde a la ejecución de código sin interacción con el kernel, es decir, algo así como “el código sin acceso a recursos externos.” Si estuviésemos utilizando un profiler, diríamos entonces que corresponde a lo que se llama “exclusive time.”


Contadores


La siguiente es la lista de contadores de perfmon a utilizar, que como podrán notar, es muy breve, repetitiva y casi no requiere mayor explicación.




  • Objeto System, contador Processor Queue Length: muestra la cantidad de requerimientos encolados para el procesador.



  • Objeto Processor, contador % Privileged Time: muestra el porcentaje de tiempo utilizado por el kernel, para todo el procesador



  • Objeto Processor, contador % User Time: similar al anterior, pero para modo usuario



  • Objeto Process, contador % Privileged Time, instancia específica. muestra el porcentaje de tiempo utilizado por el kernel, para la aplicación seleccionada en el campo instancia



  • Objeto Process, contador % User Time, instancia específica: similar al anterior, pero para modo usuario



  • Objeto Thread, contador % Privileged Time, instancia específica: muestra el porcentaje de tiempo utilizado por el kernel, para un thread especifico de la aplicación seleccionada



  • Objeto Thread, contador % User Time, instancia especifica: similar al anterior, pero para modo usuario


Probablemente te podrás estar preguntando la utilidad de la información de los últimos 2 contadores. ¿de que te serviría saber cuál es el thread responsable si no se puede hacer mucho con esa información?


Si se puede hacer bastante. Puedes determinar si es un único thread que está ocupando mucho el procesador o es un conjunto de ellos en que todos aportan. Te sirve para obtener pistas.


Podría ser el thread o los threads del GC, el thread del Finalizador, un thread que se quedó pegado en un ciclo, etc. [procexp] entrega mucha información referente a tipos de procesos e informacón de los threads.


Ok, ya tengo todos los datos…


¿Contra qué comparar la información?


Salvo para el primer contador, no hay punto de comparación. Si el uso del procesador llega a 80% o más, probablemente sea hora de proveer alivio al servidor, ya sea corrigiendo el código o haciendo un upgrade del hardware.


El primer contador no debe ser mayor a 2 multiplicado por la cantidad de procesadores (cores) de tu servidor. Para el caso de HT (Hyper Threading), no estoy seguro, pero creo que no se deben contabilizar.


Ya vendrá una tercera entrega relacionada con el disco duro.


Saludos,
Patrick

Signos vitales de un servidor: Parte I (memoria)

¿Qué sucede si uno no se siente bien?; en condiciones normales, uno visita al doctor para saber qué sucede y éste hace una revisión general. Si el doctor encuentra algo interesante, te pide hacer unos exámenes para saber con mayor detalle qué está sucediendo.


Llevando este mismo ejemplo al área de la informática, si tienes un servidor y presientes (o tienes total certeza) que algo no funciona como debiera, un buen punto de partida es determinar si los signos vitales de éste están bien.


El objetivo de este post es dar una pauta general para determinar cuando un servidor Windows 2003, cualquier versión, puede tener algún problema que afecte sus signos vitales. Hoy veremos los signos vitales de la memoria. Los siguientes posts abordarán otras áreas.


Un poco de contexto


Si has escuchado muchas veces la palabra contadores, y no tienes claro a que se refieren cuando lo escuchas, esta sección debiera aclararte un poco. Si tienes claro a que se refiere, puedes saltarte  esta sección.


Los contadores son entidades u objetos que mantienen información del funcionamiento de algún programa o sistema operativo (SO de ahora en adelante). Cada programa que utilizas —todos debieran, pero no siempre lo hacen— genera información con estadística y en tiempo real del funcionamiento de éste, la cual se le informa al SO. A su vez, el SO también genera información que almacena para que nosotros podamos consultarla y saber cómo está funcionando.


Los contadores se categorizan por objetos (por ejemplo, memoria, sistema, sql server, iis) y se pueden o no aplicar a instancias (procesos, sitios web, bases de datos, etc.)


Por ejemplo, un contador de importante SO es la cantidad de memoria disponible que le queda. Este se categoriza en el objeto Memory, y el contador se llama Available MBytes, que nos dice la cantidad de megabytes de memoria disponible. Los nombres de los contadores dependen del idioma del programa o SO. Si estás ejecutando un SO en español, el objeto podría llamarse Memoria y el contador, cantidad de MB disponibles.


La siguiente es una imagen de cómo se agrega este contador utilizando performance monitor (perfmon).



Luego, después de habar agregado los contadores, podrás ver una pantalla como ésta, donde la primera es la representación gráfica y la otra la textual, para la misma información.


 


 


Suponiendo que ya se entiende la idea, vamos entonces a ver la primera parte de cómo chequear la salud de un sistema operativo.


Grupos de contadores


Aunque los contadores ya están agrupados de acuerdo a los objetos a los cuales pertenecen, los volveremos a agrupar en clasificaciones más grandes, más generales. Tendremos entonces un grupo para colocar los contadores de memoria, uno para los del procesador, otro para el disco duro, otro para la tarjeta de red y otro para el sistema operativo. En otra oportunidad veremos los asociados a productos específicos como SQL, IIS o .NET.


Contadores de memoria


Los contadores de memoria son variados. Podemos encontrar desde la memoria utilizada por una aplicación, la memoria libre del servidor y la memoria utilizada por los componentes del Kernel del SO. Todas estas son importantes y están de alguna forma relacionadas.


Veamos cada uno de los contadores mencionados y contra qué compararlos (cuando aplica).


Memoria del sistema (incluido kernel)


La memoria libre del sistema la entrega el contador Available MBytes, del objeto Memory. El mismo contador existe retornando el valor en otras unidades como KBytes y Bytes. De acuerdo a la cantidad de memoria que hoy en día tiene un servidor, medir en bytes o kilobytes puede no ser necesario.


Diversa documentación podrán encontrar referente a este contador. Algunos dirán que con 10 MB disponibles, es momento de preocuparse. Otros dirán que 25 MB es una señal de preocupación. En general, menos del 10% de la memoria total instalada ya es reflejo de que al sistema le falta memoria. Por ejemplo, si mi servidor tiene 2GB instalados y le quedan 150 MB disponibles, significa que estoy consumiendo el 92,5% de la memoria. Sin embargo, otros contadores te ayudarán a determinar si esto es cierto o no (si hay o no un problema). Puede ser que tengas mucha memoria ocupada, pero el sistema funcione bien.


Otro contador importante es Pages/sec del objeto Memory. Este refleja la tasa de lecturas y escrituras a disco para resolver problemas de páginas no encontradas (hard faults) en la memoria física, y por ende, tiene que ir al archivo de paginación a buscarla (y guardar otra para hacer espacio).


La información disponible públicamente es muy variada. Desde sitios que dicen que valores de más de 100 son preocupantes. Otros dicen que más de 5 de forma constante es preocupante. Lamentablemente ninguna de esas medidas es agnóstica y depende de otros factores (como el disco). Una medida agnóstica se obtiene al multiplicar este contador por Avg. Disk sec/Transfer del objeto Physical Disk, en la instancia (disco) donde está el archivo de paginación. El resultado de multiplicar ambos corresponde al porcentaje de tiempo de acceso a disco. Si es mayor a 10%, está paginando mucho, lo que puede ser producto de falta de memoria.


Los contadores de memoria del kernel se deben comparar contra valores en unas tablas específicas. Estos son Pool Paged Bytes y Pool Nonpaged Bytes del objeto Memory. Las tablas se encuentran a continuación, tanto para Windows 2000 como Windows 2003 (clic para agrandar).







Windows 2003

Windows 2000 SP4

Los valores obtenidos para cada unos de los contadores, tanto para paged como nonpaged, corresponden a la cantidad de bytes de cada pool que ha sido utilizado. Si es menor al 50% del pool, todo está bien. Mayor a 50% y menor a 80%, es hora de preocuparse. Si es mayor a 80%, hay problemas.


¿Cómo se mide?


Si obtuviste un valor de 56 MB en el contador de paged pool bytes y tu sistema es Windows 2003 con 4 GB y estás usando /3GB, el tamaño total del paged pool bytes es de 258 MB (revisa la tabla). La tasa de uso (56/258) es cercana al 21%, lo que nos dice que nuestro sistema está bien. Lo mismo para nonpaged pool bytes, pero considerando el otro valor en la tabla.


El último contador relacionado con la memoria en el kernel corresponde a Free System Page Table Entries (PTEs), del objeto Memory. Los PTEs son estructuras internas utilizadas por el componente del kernel llamado Memory Manager y que tiene como objetivo administrar la memoria.


Si este contador se reduce mucho (menos de 15 mil o 10 mil), habrá problemas. Usualmente este contador se ve afectado por la utilización de la opción /3GB sin utilizar la opción /userva. Esta anécdota ocurrió en un caso donde no se utilizó /userva.


Después de haber visto las opciones para el sistema y kernel, podemos ver las opciones de memoria disponibles para las aplicaciones.


Memoria de aplicaciones


Para medir la memoria de una aplicación específica, tengo que agregar los siguientes contadores, y asociarlos con el proceso que quiero medir (instancia en la parte derecha de la ventana):




  • Objeto Process, contador Private Bytes



  • Objeto Process, contador Virtual Bytes



  • Objeto Process, contador Working Set


El primer contador corresponde a la memoria privada el proceso. El segundo contador, a la memoria virtual del proceso. Para un mejor entendimiento de los tipos de memoria que miden, te recomiendo la lectura de estos posts:



El working set corresponde a la cantidad de memoria privada (sumada a la memoria usada por dlls y otras estructuras) que está cargada en memoria real (física). A grosso modo, la suma de los working set de todos los procesos no podrá superar la memora física de tu máquina.


Lamentablemente no hay valores contra qué compararlos, pero de acuerdo a lo que vimos en el segundo post de la lista de más arriba, la memoria virtual no podrá superar los 2 GB en un sistema de 32 bits.


Si virtual bytes es muy elevado en comparación con private bytes, podría haber un problema de fragmentación de memoria. ¿Qué significa muy elevado?; tampoco hay una respuesta, pero unas 10 veces debiera ser preocupante, como también es preocupante acercarse a los 2 GB. Claro que las 10 veces dependerá de la memoria que use nuestro proceso. Si un proceso utiliza 10 MB de memoria privada, es normal que utilice 100 MB de memoria virtual.


Hay muchas cosas relativas, pero lo importante es monitorearlos en el tiempo.


Si virtual bytes sube y sube sin descender (o descendiendo menos de lo que aumenta), habrá un problema de fragmentación de memoria, aunque dependerá de cómo aumente private bytes también.


Si private bytes sube y sube sin descender (o descendiendo menos de lo que aumenta), tendrás un problema de pérdida de memoria o memory leak. Private bytes no podrá ser superior al máximo de memoria virtual de un proceso (2GB).


Otros contadores


Sin duda, hay muchos más contadores, pero para determinar signos vitales, con éstos basta.


Desde Santiago de Chile,
Patrick

Desmitificando la Encriptación (ex MTJ.NET)

Aclaración

El siguiente contenido apareció publicado en la revista MTJ.NET en MSDN en español en el año 2005, en partes I y II. Debido a que la revista MTJ.NET no está al aire hoy, he optado por lanzarlo al aire nuevamente desde mi blog en un sólo documento.

A los suscriptores RSS les pido disculpas de antemano por haber publicado diferentes versiones.

Código fuente disponible aquí.

Introducción

Cuando es necesario darle seguridad a ciertos datos de nuestra aplicación, y buscamos información en el Web que nos permita lograrlo, es normal que hablemos (o leamos) de encriptación, y que irremediablemente salgan a la luz palabras tales como algoritmos simétricos, asimétricos y Hash. Estos términos pueden generar temor o dar la sensación de ser complejos. La motivación de este documento es mostrar qué es la encriptación sin necesidad de profundizar en cómo funciona un algoritmo internamente; y también indicar las malas prácticas que se realizan popularmente. Se revisará además la forma correcta de utilizar los algoritmos de encriptación, dónde se deben usar y dónde no.

Debido a lo extenso del tema en cuestión y a que es preferible realizar todas las explicaciones correspondientes, fue necesario dividir el documento en dos partes. Lo que contendrá cada parte está detallado en el siguiente índice.

Agradecimientos

Antes de comenzar deseo agradecer tanto el apoyo de Mike Giagnocavo como el de la información disponible en su blog (http://www.atrevido.net/). Sin sus consejos y correcciones, los ejemplos de este documento y la demostración no habrían quedado de excelente calidad y listos para utilizar.

Introducción, alcance y preguntas iniciales

La seguridad de los datos de cualquier sistema es algo muy importante, y no siempre recibe la atención y dedicación necesarias. Podemos formular las siguientes preguntas básicas y es probable que encontremos falencias de seguridad que pueden comprometer a nuestro sistema:

  • ¿Se está almacenando la información crítica de forma segura?
  • ¿Qué entendemos por seguro?
  • Si se está encriptando, ¿Qué algoritmo se está utilizando?
  • ¿Se permite la desencriptación de datos que no debieran poder desencriptarse?
  • ¿Cómo está realizándose la encriptación?

Adicionalmente, podemos complementar con preguntas más complejas:

  • ¿Dónde están almacenadas la o las llaves?
  • ¿Con qué frecuencia se cambian las llaves?
  • ¿Cómo se enfrenta el problema de cambiar las llaves con tanta frecuencia?
  • ¿Con qué criterio se definen las nuevas llaves?

Es probable que algunas de las preguntas básicas no sean respondidas adecuadamente. Si esto es así, sería recomendable hacer una revisión de los criterios y metodologías que se están utilizando, y determinar si es necesario hacer modificaciones o rehacer esas funcionalidades. Si el pensamiento es que esto no genera utilidad, cabe hacerse las siguientes preguntas: ¿Qué le sucedería al negocio si alguien accediese a cualquiera de los datos privados del sistema? ¿Se verá comprometida la seguridad con hechos como éste? De seguro, en ese momento el pensamiento sería que valdría la pena. Entonces, manos a la obra.

El alcance de este documento es dar a conocer los distintos métodos que existen para proteger tanto el almacenamiento como el envío de la información. Este documento NO tiene como intención ser un manual de seguridad. Esto es lo mínimo que debe tener una organización para no estar al descubierto en sus procesos. Se debe entender que existen libros dedicados a la seguridad. Aquí se está danto el puntapié inicial para salir del desconocimiento que existe sobre este tema. Es responsabilidad tanto de los administradores  de sistemas operativos como del arquitecto de soluciones definir las reglas, dónde almacenar sus llaves, los criterios de cambio de llaves y también los lugares donde almacenará la información.

Habiendo introducido el tema y definido el alcance, comencemos con la revisión de los algoritmos y los distintos tipos que existen.

Definición de algoritmos

Como se mencionó con anterioridad, existen distintos tipos de algoritmos de encriptación, pero algo que no se ha mencionado hasta ahora son los “algoritmos” que se cree que encriptan, pero que no hacen nada más que transformar texto (o información). El objetivo de hacer esta distinción es mostrar algunas malas prácticas que se cometen, pensando que cualquier algoritmo que cambia de cierta forma un texto, está realizando encriptación de datos. Estos algoritmos podremos llamarlos simplemente “Transformaciones”.

Como se está hablando de encriptación, se revisarán también los algoritmos que han sido construidos con distintos fines y que están agrupados en distintas categorías. ¿Cuál es la funcionalidad de los algoritmos dentro de cada uno de estos grupos? Cada uno tiene un objetivo y un problema que atacar, y por lo mismo hay casos en los que debe utilizarse uno y sólo uno de estos algoritmos, como también casos en los que debe utilizarse algoritmos de más de un grupo.

Antes de revisar los escenarios, se debe aclarar el concepto de llave. La llave de encriptación es una serie de carácteres, de determinado largo, que se utiliza para encriptar y desencriptar la información que se quiere proteger. El largo de la llave depende del algoritmo. Existen llaves privadas y públicas, que serán detalladas más adelante. Veamos los siguientes escenarios.

  • Escenario 1: Necesito almacenar información crítica que deberá poder descifrarse, y seré yo el único que haga todo el proceso. Nadie más tendrá acceso a la llave con que se encriptará y desencriptará la información.
  • Escenario 2: Necesito que me envíen información crítica que yo desencriptaré posteriormente, pero necesito que las personas que me van a enviar información pueden encriptarla libremente, pero no desencriptarla. En este caso, se deja disponible una llave pública para que ellos encripten y yo tendré mi llave privada de encriptación en forma segura.
  • Escenario 3: Necesito almacenar o enviar información crítica de forma segura, pero que no requerirá ser desencriptada para su validación, o que es extremadamente importante verificar que no haya sido modificada en el camino.

Estos son los tres escenarios más comunes. Es probable que te preguntes para qué puede haber casos como el 3. Lo veremos más adelante con ejemplos, destacando ventajas y desventajas. Los tres escenarios calzan con los algoritmos listados a continuación:

  • Escenario 1: Encriptación simétrica
  • Escenario 2: Encriptación asimétrica
  • Escenario 3: Hash de información

Antes de comenzar a revisar cada uno de estos algoritmos, revisemos una muy mala práctica que se utiliza más de lo que debiera, que corresponde a la utilización de “algoritmos” o mejor dicho, transformaciones del contenido.

Malas prácticas o transformaciones

Antes mencionamos que es común escuchar o ver que se utilizan ciertas transformaciones como métodos de protección. Como la comparación se hace con el ojo humano, si se tiene una cadena de texto y se decide transformar aplicándole un XOR o desplazando los bytes, el resultado de esa transformación nos produce el efecto de que está encriptado, por que no lo podemos entender (nuestro cerebro no lo entiende). Debido a esto, no vamos a hacer el ejemplo transformando texto sino que lo haremos transformando una imagen. Se aplicarán los dos efectos mencionados recién, XOR y Desplazamiento, y se verá que es efectivamente lo que hacen. El motivo de utilizar una imagen es para que nuestro ojo no se engañe con las apariencias.

Nuestro ejemplo nos presenta una pantalla como la que se muestra en la Figura 1, donde tenemos dos botones; uno para cargar la imagen, otro para aplicarle las transformaciones XOR, y otro para desplazar los bytes:



Figura 1.

Si se presiona Transformar, el resultado será el que se muestra en la figura 2:


Figura 2: Resultado de las transformaciones.

Como se puede apreciar, no se produce una encriptación sino sólo una transformación de los datos (más adelante verás cómo queda la imagen realmente encriptada). Este tipo de transformaciones no protege nuestra información. Hagamos ahora las siguientes preguntas: ¿Cuánta seguridad existe con estas transformaciones? ¿Cuánto demorará un hacker en retransformar esto? El hacker obtiene la respuesta en menos de lo que te toma a ti leer sólo esta línea. Hacerle modificaciones a este “algoritmo” no sirve. La encriptación no es trivial de hacer, pero es mucho más fácil de usar. Más adelante conocerás  la cantidad de años que estuvo estudiándose el algoritmo Rijndael o AES, para ser declarado vencedor.

El código que utilizamos para realizar la transformación XOR es el siguiente. El código de desplazamiento es muy similar. Este, al igual que todo el código del documento, está disponible en el archivo adjunto con este artículo. Por motivos que no corresponde explicar aquí, ya que no entran en el objetivo del documento, con la imagen que se está trabajando es necesario comenzar desde la posición 34, ya que si se comienza desde el byte 0 transformando, el archivo BMP ya no es posible visualizarlo.

Lo único que hace este “algoritmo” es aplicar XOR a un arreglo de bytes. Para aplicarlo sobre texto lo único que hay que hacer son las transformaciones de bytes a texto y viceversa. Como explicación sencilla de este proceso, podemos decir que lo que hace es tomar cada uno de los bytes y aplicarle un XOR (representado por el símbolo “^” en el código). Un XOR corresponde al resultado de la comparación bit a bit de dos bytes. En este caso, un byte viene del texto a transformar y el otro del número 87 elegido al azar.

public class TransformacionXOR
{
      
private static int _XOR = 87;

       public static byte[] Codificar(byte[] bytACodificar)
       {
             
for (int _intPos = 34; _intPos < bytACodificar.Length; _intPos++)
                    
bytACodificar[_intPos] = (byte) (_XOR ^ bytACodificar[_intPos]);
             
return bytACodificar;
       }

       public static byte[] DeCodificar(byte[] bytADecodificar)
       {
             
for (int _intPos = 34; _intPos < bytADecodificar.Length; _intPos++)
                    
bytADecodificar[_intPos] = (byte) (_XOR ^ bytADecodificar[_intPos]);
              return bytADecodificar;
       }

}

Como ven, este algoritmo tan lineal no nos da ninguna seguridad; y por lo mismo, jamás debe utilizarse para proteger información. No debe creerse que es XOR el responsable de esto. XOR es muy útil en otros contextos, pero no se puede utilizar de esta forma.

Nota: Antes de continuar, es muy importante aclarar lo siguiente. Si se va a proteger información, el proceso DEBE realizarse a conciencia y con algoritmos probados. Además, se debe utilizar el algoritmo de la forma en que fue concebido. Implementaciones a medias, son tan inútiles como las transformaciones recién vistas o como la no utilización de ésta. O el proceso se hace bien, o no se hace.

Veamos ahora los algoritmos reales de encriptación.

Encriptación simétrica

Cuando hablamos de encriptación y no de transformación, ya estamos adentrándonos en temas de mayor protección, de algoritmos conocidos y seguridad real. El proceso de realizar una encriptación es complejo para ser entendido por nosotros mismos, pero no es limitante para conocer cuáles son los pasos para utilizarlos y qué errores no se deben cometer.

Dentro de los algoritmos de encriptación simétrica podemos encontrar los siguientes, algunos más seguros que otros.

DES (Digital Encryption Standard)
Creado en 1975 con ayuda de la NSA (National Security Agency); en 1982 se convirtió en un estándar. Utiliza una llave de 56 bit. En 1999 logró ser quebrado (violado) en menos de 24 horas por un servidor dedicado a eso. Esto lo calificó como un algoritmo inseguro y con falencias reconocidas.

3DES (Three DES o Triple DES)
Antes de ser quebrado el DES, ya se trabajaba en un nuevo algoritmo basado en el anterior. Este funciona aplicando tres veces el proceso con tres llaves diferentes de 56
bits. La importancia de esto es que si alguien puede descifrar una llave, es casi imposible poder descifrar las tres y utilizarlas en el orden adecuado. Hoy en día es uno de los algoritmos simétricos más seguros.

IDEA (International Data Encryption Algorithm)
Más conocido como un componente de PGP (encriptación de
mails), trabaja con llaves de 128 bits. Realiza procesos de shift y copiado y pegado de los 128 bits, dejando un total de 52 sub llaves de 16 bits cada una. Es un algoritmo más rápido que el DES, pero al ser nuevo, aún no es aceptado como un estándar, aunque no se le han encontrado debilidades aún.

AES (Advanced Encryption Standard)
Este fue el ganador del primer concurso de algoritmos de encriptación realizado por la NIST (
National Institute of Standards and Technology) en 1997. Después de 3 años de estudio y habiendo descartado a 14 candidatos, este algoritmo, también conocido como Rijndael por Vincent Rijmen y Joan Daemen, fue elegido como ganador. Aún no es un estándar, pero es de amplia aceptación a nivel mundial.

En nuestra demo utilizaremos el algoritmo AES. .NET provee implementaciones de AES, DES y TripleDES entre otros.

El algoritmo más seguro hoy el AES, aunque 3DES también es muy seguro. Este último se utiliza cuando hay necesidad de compatibilidad. AES 128 es aproximadamente 15% más rápido que DES, y AES 256 sigue siendo más rápido que DES.

Cualquiera de estos algoritmos utiliza los siguientes dos elementos; ninguno de los dos debe pasarse por alto ni subestimar su importancia:

IV (Vector de inicialización)
Esta cadena se utiliza para empezar cada proceso de encriptación. Un error común es utilizar la misma cadena de inicialización en todas las encriptaciones. En ese caso, el resultado de las encriptaciones es similar, pudiendo ahorrarle mucho trabajo a un
hacker en el desciframiento de los datos. Tiene 16 bytes de largo. Puedes encontrar más información acerca de IV en http://www.atrevido.net/blog/PermaLink.aspx?guid=6136ce81-9fa1-4995-bb3e-15bc5f1f5899.

Key (llave)
Esta es la principal información para encriptar y desencriptar en los algoritmos simétricos. Toda la seguridad del sistema depende de dónde esté esta llave, cómo esté compuesta y quién tiene acceso. El largo de las llaves depende del algoritmo. Al final del documento se darán algunas recomendaciones para el almacenamiento, generación y rotación de llaves.

Algoritmo
Largos válidos (bits)
Largo por defecto (bits)
DES 64 64 (8 bytes)
TripleDES 128, 192 192 (24 bytes)
Rijndael 128, 192, 256 256 (32 bytes)

Veamos ahora nuestra aplicación y cómo funciona para encriptación y desencriptación con algoritmos simétricos, específicamente Rijndael (Ver figura 3):


Figura 3: Resultado de la encriptación con Rijndael aplicado sobre una imagen y texto.

Lo primero que debemos hacer es especificar una llave de encriptación. Una cadena de texto se utiliza en el ejemplo. En la caja de más abajo se puede ingresar el texto que se desea encriptar. Luego de presionar “Encriptar” se obtiene el resultado de la encriptación en la caja del medio, y como es de esperarse, presionando “DesEncriptar” se obtiene el texto desencriptado en la última caja de texto.

Si revisamos el código fuente que realiza la encriptación y desencriptación, encontraremos algo de mayor complejidad con respecto las transformaciones anteriormente vistas.

public class MiRijndael
{
      
public static byte[] Encriptar(string strEncriptar, byte[] bytPK)
       {
              Rijndael miRijndael = Rijndael.Create();
             
byte[] encrypted = null;
             
byte[] returnValue = null;

              try
              {
                     miRijndael.Key =  bytPK;
                     miRijndael.GenerateIV();

                     byte[] toEncrypt =  System.Text.Encoding.UTF8.GetBytes(strEncriptar);
                     encrypted = (miRijndael.CreateEncryptor()).TransformFinalBlock(toEncrypt, 0, toEncrypt.Length);

                     returnValue = new byte[miRijndael.IV.Length + encrypted.Length];
                     miRijndael.IV.CopyTo(returnValue, 0);
                     encrypted.CopyTo(returnValue, miRijndael.IV.Length);
              }
             
catch { }
             
finally { miRijndael.Clear(); }

              return returnValue;
       }

       public static string Desencriptar(byte[] bytDesEncriptar, byte[] bytPK)
       {
              Rijndael miRijndael = Rijndael.Create();
             
byte[] tempArray = new byte[miRijndael.IV.Length];
             
byte[] encrypted = new byte[bytDesEncriptar.Length – miRijndael.IV.Length];
             
string returnValue = string.Empty;

              try
              {
                     miRijndael.Key =  bytPK;

                     Array.Copy(bytDesEncriptar, tempArray, tempArray.Length);
                     Array.Copy(bytDesEncriptar, tempArray.Length, encrypted, 0, encrypted.Length);
                     miRijndael.IV = tempArray;

 

                     returnValue =  System.Text.Encoding.UTF8.GetString((miRijndael.CreateDecryptor()).TransformFinalBlock(encrypted, 0, encrypted.Length));

              }
             
catch { }
             
finally { miRijndael.Clear(); }

              return returnValue;
       }

       public static byte[] Encriptar(string strEncriptar, string strPK)
       {
             
return Encriptar(strEncriptar, (new PasswordDeriveBytes(strPK, null)).GetBytes(32));
       }

       public static string Desencriptar(byte[] bytDesEncriptar, string strPK)
       {
             
return Desencriptar(bytDesEncriptar, (new PasswordDeriveBytes(strPK, null)).GetBytes(32));
       }
}

Explicar el funcionamiento del algoritmo no entra dentro del alcance de este documento y, siendo sincero, desconozco cómo funciona. Tampoco creo que sea necesario que nosotros sepamos cómo funcionan los algoritmos de encriptación. Lo importante es utilizarlos bien.

Nota: Para obtener la llave del algoritmo, dado que una cadena de carácteres generada por un humano en un formulario es considerada poco variada debido a que los carácteres utilizados son pocos (a->z, A->Z, 0->9), se le aplica un proceso de variación con la clase PasswordDeriveBytes y se le pide un resultado de 32 bytes. Esto nos retorna una llave con carácteres variados, ideal para la encriptación. PasswordDeriveBytes utiliza Hash para generar la salida. Hash se verá más adelante.

Volviendo al ejemplo, si se presiona “Encriptar” nuevamente, las veces que se desee, se verá que el resultado de la encriptación varía. ¿Por qué sucede esto, si el resultado de la encriptación debiera ser el mismo? La variación en el resultado de una encriptación se produce por dos variantes en el proceso, el Key (o llave de encriptación) y el IV (o vector de inicialización). En este caso se está utilizando el mismo Key (explícito en la caja de texto) pero el IV está variando todas las veces. ¿Porqué varía el IV? ¿Qué es el IV?

El IV o vector de inicialización es un concepto no entendido del todo, por que algunos creen que se puede encriptar “sin él”. El IV cumple un rol pequeño, pero muy importante. El único motivo por el cual existe es para apoyar el proceso de encriptación, permitiendo exactamente que ocurra lo que se observó en la caja de texto, es decir,  la variación del resultado del proceso de encriptación. El IV no debe considerarse como una segunda llave, por que no lo es. Es una ayuda y por lo mismo, no es un dato que haya que esconder, si no que basta con almacenarlo para que cuando se vaya a desencriptar se utilice el mismo IV que se utilizó en la encriptación. En el ejemplo, el IV se retorna como parte del resultado de la encriptación y se genera cada vez que se realiza un encriptación. Su largo es de 16 bytes para el algoritmo de Rijndael.

Nota: Utilizar siempre un mismo IV para no tener que almacenarlo es equivalente en seguridad a no utilizar encriptación. La seguridad mal implementada es más insegura que no implementarla. ¿Por que? Porque da la creencia de tener seguridad, y produce una confianza que es nociva. Si no tienes seguridad en tu aplicación, eso lo sabes y estás preocupado, pero si se tiene una seguridad mal implementada se cree que se tiene, pero no es así.

Con la implementación entregada en la demo, el IV está correctamente utilizado. En las imágenes de la segunda parte se aplica el mismo algoritmo de encriptación, pero utilizando variaciones en el modo de ciframiento. En la segunda imagen se aplica el modo ECB y en la tercera CBC. Estos modos especifican al algoritmo cómo debe funcionar. El modo más seguro, que también es el modo por defecto es CBC.

El modo de ciframiento ECB no utiliza IV, por lo mismo no debe usarse como modo de ciframiento, a menos que sea necesario por compatibilidad. Esta es otra muestra de porqué es útil el IV y porqué debe usarse, y que sucede con la encriptación cuando el IV no varía nunca.

Es importante considerar que en los algoritmos simétricos se utiliza la misma llave para encriptar y desencriptar. En los algoritmos asimétricos se verá que existen dos llaves, una pública para encriptar y una privada para desencriptar.

Encriptación Asimétrica

La encriptación asimétrica permite que dos personas puedan enviarse información encriptada, sin necesidad de compartir la llave de encriptación. Se utiliza una llave pública para encriptar el texto y una llave privada para desencriptar. A pesar de que puede sonar extraño que se encripte con la llave pública y desencripte con la privada, el motivo para hacerlo es el siguiente: si alguien necesita que le envíen la información encriptada, deja disponible la llave pública para que quienes le desean enviar algo lo encripten. Nadie puede desencriptar algo con la misma llave pública. El único que puede desencriptar es quien posea la llave privada, quien justamente es el que recibe la información encriptada.

Los algoritmos de encriptación asimétrica más conocidos son:

  • RSA (Rivest, Shamir, Adleman)
    Creado en 1978, hoy es el algoritmo de mayor uso en encriptación asimétrica. Tiene dificultades para encriptar grandes volúmenes de información, por lo que es usado por lo general en conjunto con algoritmos simétricos.
  • Diffie-Hellman (& Merkle)
    No es precisamente un algoritmo de encriptación sino un algoritmo para generar llaves públicas y privadas en ambientes inseguros.
  • ECC (Elliptical Curve Cryptography)
    Es un algoritmo que se utiliza poco, pero tiene importancia cuando es necesario encriptar grandes volúmenes de información.

El tamaño de la llave es el siguiente:

Algoritmo
Largos válidos (bits)
Largo por defecto (bits)
RSA 384 a 16.384 (incrementos de a 8) 1.024

Mientras más larga sea la llave, más seguro será. La relación con los algoritmos simétricos no es directa. En este caso, una llave de 1024 bits de RSA es equivalente en seguridad a una de 75 bits de un algoritmo simétrico.

Como era de esperar, en nuestra demostración utilizaremos el algoritmo de mayor uso, RSA. Debido a que el funcionamiento es diferente comparado con el algoritmo anterior, la pantalla varía levemente. Existirá una zona donde se desplegará la llave pública generada, dejándole el almacenamiento de la llave privada a la clase miRSA.

La idea de esto es que exista una clase que provea una llave pública, para que el cliente encripte la información y luego éste le envíe lo encriptado a la misma clase, para que con la llave privada la desencripte. En este caso, se ha dejado el método desencriptar como público, por que se necesita para pintar el resultado en el formulario. Este no debiera ser un método público en una implementación real.

Antes de ver la demo, es necesario revisar el siguiente capítulo, debido a que la encriptación con RSA no es eficiente y tiene limitaciones de tamaño.

Encriptación Asimétrica + Simétrica

Debido a que la encriptación asimétrica es casi 1000 veces más lenta que la simétrica, cuando la información a encriptar es mucha, se utiliza una combinación de algoritmos. El algoritmo simétrico se utiliza para encriptar la información y el asimétrico para encriptar la llave del algoritmo simétrico con que se encriptó la información. Entonces, el proceso es mucho más rápido. Esta técnica se utiliza hoy en SSL en la negociación entre el navegador del cliente y el servidor. En cada ida y vuelta al servidor se generan nuevas llaves y se realiza todo el proceso. También es utilizada por Windows en la encriptación de los archivos.

Otro motivo para utilizar esta encriptación combinada es la necesidad de encriptar textos largos. La encriptación asimétrica además de ser ineficiente en tiempo, tiene limitaciones de tamaño. El tamaño máximo depende del largo de la llave. En la demostración, puedes probar colocando mucho texto y verás que se cae.

Estas limitantes nos obligan a utilizar el método combinado.

Nuestra demostración implementa ambos métodos, y se decide cuál utilizar con la selección del check Utilizar Simétrico. La aplicación se ve como se muestra en la figura 4 y figura 5. Antes de hacer cada demo, se debe seleccionar si se utilizará en conjunto con encriptación simétrica.

Figura 4: Encriptación utilizando solamente RSA.


Figura 5: Encriptación utilizando RSA con Rijndael.

El resultado de la encriptación es notoriamente más largo.

Como vemos, si generamos una llave y luego procedemos con los siguientes pasos, la encriptación se produce en un lado y la desencriptación en otro. Incluso .NET provee el método ToXmlString() de RSACryptoServiceProvider que retorna la llave en formato XML, ideal para ser enviada por algún medio público (Web Service, Archivo XML, etc.).

Dependiendo de si se utiliza en conjunto con algún algoritmo simétrico, la encriptación y desencriptación es diferente. Como algoritmo simétrico se utiliza AES ó Rijndael.

El código de nuestra clase miRSA es el siguiente:

public class miRSA
{
       private RSACryptoServiceProvider _objRSA = null;

       public miRSA()
       {
              this._objRSA = new RSACryptoServiceProvider(1024);
       }

       public string ObtenerLlavePublica()
       {
              return this._objRSA.ToXmlString(false);
       }

       public string DesEncriptar(byte[] bytEncriptado, bool blnConSimetrico)
       {
              if (blnConSimetrico)
              {
                     byte[] keyArray = new byte[_objRSA.KeySize/8];
                     byte[] encrypted = new byte[bytEncriptado.Length – keyArray.Length];
                     Array.Copy(bytEncriptado, 0, keyArray, 0, keyArray.Length);
                     Array.Copy(bytEncriptado, keyArray.Length, encrypted, 0, encrypted.Length);

                     byte[] simKey = this._objRSA.Decrypt(keyArray, false);

                     return MiRijndael.Desencriptar(encrypted, simKey);
              }
              else
              {
                     return System.Text.Encoding.UTF8.GetString(this._objRSA.Decrypt(bytEncriptado, false));
              }
       }
}

Y el código del cliente público es el siguiente (declarado en algún lugar del formulario):

private miRSA _objKey  = null;
 
public Form1()
{
       InitializeComponent();
       _objKey = new miRSA();
}

private void btnAsimEncriptar_Click(object sender, System.EventArgs e)
{
       byte[] _bytEncriptado = null;

       //Creamos una instancia del encritador publico
       RSACryptoServiceProvider _objEncriptadorPublico = new RSACryptoServiceProvider();
       //Le asignamos la llave genarada
       _objEncriptadorPublico.FromXmlString(this.txtAsimLlavePublica.Text);

       if (this.chkSimetrica.Checked)
       {
              //Se declara la memoria para almacenar la llave utilizada por nuestro Rijndael personalizado
              byte[] _bytKey = (Rijndael.Create()).Key;

              //Se encripta el texto y se obtiene la llave que se utilizó para la encriptación
              byte[] _bytEncriptadoSimetrico = TransEncripHash.MiRijndael.Encriptar(this.txtAsimAEncriptar.Text, _bytKey);

              //Se encripta la llave con el algoritmo RSA
              byte[] _bytEncriptadoLlave = _objEncriptadorPublico.Encrypt(_bytKey, false);

              //Se copia en un arreglo la llave encriptada y el encriptado de Rijndael
              _bytEncriptado = new byte[_bytEncriptadoLlave.Length + _bytEncriptadoSimetrico.Length];
              _bytEncriptadoLlave.CopyTo(_bytEncriptado,0);
              _bytEncriptadoSimetrico.CopyTo(_bytEncriptado, _bytEncriptadoLlave.Length);
       }
       else
       {
              _bytEncriptado = _objEncriptadorPublico.Encrypt(System.Text.Encoding.UTF8.GetBytes(this.txtAsimAEncriptar.Text), false);
       }
       this.txtAsimEncriptado.Text = Convert.ToBase64String(_bytEncriptado);
}

private void btnAsimDesEncriptar_Click(object sender, System.EventArgs e)
{
       this.txtAsimDesEncriptado.Text = this._objKey.DesEncriptar(Convert.FromBase64String(this.txtAsimEncriptado.Text), this.chkSimetrica.Checked);
}

private void btnAsimGenerar_Click(object sender, System.EventArgs e)
{
       this.txtAsimLlavePublica.Text = this._objKey.ObtenerLlavePublica();
}

En este ejemplo, la encriptación se produce en un lugar, luego de solicitar la clave pública a la clase (pero que perfectamente puede ser un servicio Web), la información encriptada se puede enviar a este servicio Web sin problemas de violación de seguridad, y sin necesidad de conocer ambas llaves. Si bien es cierto que la clase o servicio conoce ambas llaves, la llave pública no le es de ninguna utilidad.

Antes de finalizar con este algoritmo, cabe mencionar que donde puede haber problemas es en la generación de la llave. Por eso, Diffie-Hellman (& Merkle) se especializa en mejorar ese proceso, ya que es de vital importancia la generación de llaves.

Nota: Existe cierta codificación que puede haber pasado desapercibida hasta ahora. Esta es la utilización de distintos métodos para transformar textos en arreglos de bytes, y viceversa. Los métodos reales para hacerlo son los que están implementados en los encodings de .NET, como por ejemplo System.Text.Encoding.UTF8.GetString(bytes) y System.Text.Encoding.UTF8.GetBytes(string). En las demos se ha cambiado algunos deliberadamente por Convert.ToBase64String(bytes) y Convert.FromBase64String(string) para poder desplegarlo en los controles del formulario Windows. El resultado de una encriptación genera carácteres visibles y no visibles. Si no se transforman a Base64 y desde Base64 mientras son almacenados en un control, se pierde información. Si el resultado de la encriptación no va a ser desplegado sino que será almacenado en algún medio, los bytes de éste pueden escribirse sin problemas en el medio que sea.

Hash

Por último, nos quedan los algoritmos de tipo Hash. Estos son algoritmos del tipo de los que se conocen como de sólo ida, ya que no es posible desencriptar lo que se ha encriptado. Puede ser que a primera vista no se le vea la utilidad, pero en los siguientes dos escenarios éste es el tipo de algoritmo necesario para realizar el proceso. El primero de ellos está dirigido a proteger información muy valiosa encriptando el contenido; y el segundo busca validar que no se modifique la información que se está enviando desde un lugar a otro.

  • Escenario 1: Almacenar contraseñas de un sistema. Las contraseñas de cualquier sistema serio jamás debieran poder desencriptarse. El motivo es simple. Si se pueden desencriptar, el riesgo de que alguien conozca la llave y tenga acceso a todo el sistema con todos los usuarios es muy grande. Entonces, si no puedo desencriptar una contraseña, ¿Cómo puedo saber entonces si una contraseña entregada es válida? La respuesta es muy simple. Se encripta la contraseña entregada y se compara el resultado de la encriptación con la contraseña previamente almacenada.
  • Escenario 2: Validar que cierta información no se haya modificado. Si se desea enviar un bloque de datos y se quiere estar seguro de que nadie lo ha modificado durante el camino, lo que se hace es enviar el bloque de información sin encriptar y además se envía un Hash/Digest de este mismo bloque de datos. Entonces, nuestro receptor al tener la información sin encriptar, le aplica un Hash y con el mismo criterio del Escenario 1, determina si ambos bloques son iguales. Si lo son, el bloque que no está con Hash es el original. ¿Porqué no usar un algoritmo de encriptación reversible? Porque éste podría ser desencriptado, modificado y vuelto a encriptar y el receptor jamás sabría que fue modificado en el camino. Como el Hash no es reversible, esto último no podrá suceder jamás.

Los algoritmos para hacer Hash mas conocidos son los siguientes:

Algoritmo
Tamaño del Hash (bits)
MD5
128
SHA-1
160
SHA-256
256
SHA-384
384
SHA-512
512

La robustez de un algoritmo de Hash es comparable a la mitad del largo del resultado en bits. Entonces, pensar que con MD5 se está seguro puede ser un error. SHA-256 es el indicado para obtener seguridad de 128 bits. Es un hecho que SHA-256 es mucho más lento, pero como Bruce Schneier dice “Ya existen suficientes sistemas rápidos e inseguros”.

Pero a pesar de parecer muy efectivo, existe un problema con el Hash para cada uno de los escenarios indicados anteriormente. Como son problemas conocidos, existen soluciones conocidas también. Revisemos ahora cada uno de los problemas y sus soluciones.

Problemas Escenario 1
Si bien el problema no está tan relacionado con el
Hash, debido a la utilización de contraseñas de escasa dificultad, haciendo un ataque de fuerza bruta o por diccionario se podrían encontrar valores de Hash que coinciden con el Hash de las contraseñas almacenadas. Para esto se pueden ejecutar dos planes de acción independientes, pero obteniéndose el mejor resultado con la utilización de ambos.

La Solución 1 consiste en realizar varias pasadas de Hash sobre los datos, aplicándole el Hash al resultado del Hash anterior. Realizando esto varios cientos de veces (idealmente mil veces) podemos dificultar más el trabajo de un hacker. Más información de porqué se debe iterar muchas veces y las consecuencias de no hacerlo, la puedes encontrar en http://www.atrevido.net/blog/PermaLink.aspx?guid=12b1338b-171e-4692-850e-8f59ed12f24a .

La Solución 2 requiere agregar un texto adicional a la contraseña. Esto se conoce como Salt. Entonces, cada vez que un usuario cambie o esté validando una contraseña, hay que concatenarle a la contraseña este Salt y de ahí generar el Hash. De esta forma le agrega variación a las contraseñas pobremente definidas. Esta solución requiere un trabajo adicional ya que es necesario además almacenar el texto del Salt junto a la información del usuario y la contraseña encriptada. Este texto se debe crear con carácteres generados al azar.

En el ejemplo se mostrará la combinación de ambas soluciones. Como encriptación de Hash se usará SHA256. Los otros algoritmos mencionados con anterioridad están disponibles también en .NET.

El resultado de la encriptación 1000 veces de cierto texto con un Salt definido, es el que se muestra en la figura 6:


Figura 6.

El código para realizar esto es muy simple y es el que se muestra a continuación:

public class HashContraseñas
{
       public static byte[] Encriptar(string strPassword, string strSalt, int intIteraciones)
       {
                               
string _strPasswordSalt = strPassword + strSalt;
                                SHA256Managed _objSha256 = new SHA256Managed();
                                byte[] _objTemporal = null;

                                try
                                {
                                                _objTemporal = System.Text.Encoding.UTF8.GetBytes(_strPasswordSalt);
                                                for (int i = 0; i <= intIteraciones – 1; i++)
                                                               
_objTemporal = _objSha256.ComputeHash(_objTemporal);
                                }
                                finally { _objSha256.Clear(); }

                                return _objTemporal;
       }
}

Qué cosas se deben clarificar en este proceso
El resultado de este
Hash (Sha256) siempre retorna 256 bits, es decir, 32 bytes. Cuidado con confundir con el largo del string resultante de la conversión a Base64. Esta conversión genera más carácteres. Para verificar, puedes medir el largo del arreglo colocando un punto de interrupción en el retorno (return), y notarás que son 32 bytes.

Además, las funciones de hash no tienen limitante para el tamaño del texto de entrada. Sea cual fuere el tamaño, el largo de la salida es el mismo y se procesan todos los carácteres del string de entrada.

Para un sistema de almacenamiento de contraseñas de usuario seguro, hay que hacer algunas modificaciones a esta función, pero son mínimas. Las modificaciones no pasan por cambios en el algoritmo, sino más bien por crear una función para que genere Salt randómicos y crear la función para la comparación de las contraseñas.

Probablemente te preguntes porqué hay 2 cajas de texto con el mismo texto. Como este proceso es común, o al menos debiera serlo, .NET provee una clase llamada PasswordDeriveBytes que sirve para realizar de forma automática todo lo anteriormente visto. En el evento click, además de la llamada a la función detallada de más arriba (HashContraseñas.Encriptar), se crea una instancia de esta clase (PasswordDeriveBytes) y se le especifican los mismos parámetros que estamos utilizando en nuestro algoritmo de Hash (la contraseña, el Salt, el algoritmo SHA256 y la cantidad de iteraciones). Esto hace que en sólo pocas líneas se pueda realizar el mismo proceso.

El código ejecutado es el siguiente:

PasswordDeriveBytes _objPdb = new PasswordDeriveBytes(this.txtHashEncriptar1.Text, System.Text.Encoding.UTF8.GetBytes(this.txtSalt.Text), “SHA256”, Convert.ToInt32(this.txtCantidad.Text));

this.txtHashEncriptado2.Text = Convert.ToBase64String(_objPdb.GetBytes(32));

Para comparar arreglos de bytes, se verá más adelante una función con tal objetivo.

Problemas Escenario 2: Si alguien puede ejecutar un Hash sobre un conjunto de datos y obtener el resultado, ¿Porqué no podría modificar el contenido del documento que estamos enviando y generar el Hash nuevamente, reemplazando el contenido del Hash anterior? ¿Cómo se puede diferenciar mi proceso de Hash del proceso de Hash impostor? No es posible a menos que haya alguna llave de por medio.

Debido a lo anterior, se han inventado los siguientes algoritmos de Hash, que utilizan llaves para realizar el proceso. En este caso, si el resultado se sobrescribe, al obtener el Hash con la llave, el resultado jamás coincidirá con el que viene en el mensaje.

Estos algoritmos y sus llaves son los siguientes:

Algoritmo
Tamaño del Hash (bits)
Tamaño de llave (bits)
HMAC-SHA1 160
64
MAC-3DES-CBC 64
8, 16, 24

Para realizar la demo, debemos primero generar una llave. Hasta ahora, las llaves se han creado manualmente, pero ahora vamos a utilizar herramientas del Framework para generarlas. Como la llave de 64 bits es del mismo largo que la que utiliza el algoritmo DES, se utilizará la clase de éste para generar una. Existe el botón “Generar Key” que es el encargado de generar y almacenar una llave. Esta se genera en la función del botón, y se almacena en una variable de formulario. El código es el siguiente:

privatevoid btnHashKey_Click(object sender, System.EventArgs e)
{
      this._objHashKey = (new System.Security.Cryptography.DESCryptoServiceProvider()).Key;
      this.lblSalt.Text = “Key : ” + Convert.ToBase64String(this._objHashKey);
}

Con la primer sentencia se crea la instancia del proveedor de DES y se obtiene el Key almacenándolo en _objHashKey, una variable de formulario. Con la segunda línea, se copia el contenido de la Key en una etiqueta del formulario, previamente transformándolo en Base64. Cabe mencionar que la transformación a Base64 genera una cadena más larga que la cantidad de bytes que está recibiendo, por eso si cuentas la cantidad de carácteres del Key, no cumple con ser de 64 bits (8 bytes).

La validación es muy simple de realizar. Recuerda que lo que se quiere hacer es verificar que la información que se está enviando no se haya modificado en el camino. Entonces lo que se debe hacer es generar un nuevo Hash con el texto en cuestión y la contraseña, y comparar el resultado del Hash generado con el que se envió inicialmente. La comparación de Hash se realiza con una función para comparar arreglos (Ver figura 7):

Figura 7: Resultado de presionar Validar Hash sin hacer ninguna modificación.

En cambio, si se modifica levemente el texto, agregando la palabra “me” (o cualquiera) en el círculo rojo remarcado, y se trata de validar, el resultado es erróneo inmediatamente. Con esta funcionalidad se valida que nadie haya modificado el contenido de lo que se está enviando (Ver figura 8):


Figura 8: Resultado de presionar
Validar Hash con una modificación.

Además, si se genera una nueva Key (con el botón Generar Key) y se valida, el resultado es fallido, como era de esperarse. Existe otro método para realizar esta misma validación pero sin compartir las llaves. La idea es muy similar a la estudiada en los algoritmos asimétricos, en donde una de las partes tiene una llave para encriptar y otra parte tiene otra llave para encriptar también. Algo similar a esto es lo que se utiliza para la firma digital, pero por ser un tema tan amplio, no cabe en este artículo y merece un artículo completo para su explicación. Algunas claves para poder conocer como funciona esto, las siguientes clases del Framework poseen funcionalidad para lograrlo. Estas son RSACryptoServiceProvider y DSACryptoServiceProvider. Por último, algo de código. Para generar un Hash con un Key y para comparar arreglos de bytes se utilizan los siguientes bloques de código:

public class HashValidacion
{
       public static byte[] Encriptar(string strTexto, byte[] objKey)
       {
              HMACSHA1 _objHMACSHA = new HMACSHA1(objKey);
              try
              {
                     return _objHMACSHA.ComputeHash(Encoding.UTF8.GetBytes(strTexto));
              }
              finally
              {
                     _objHMACSHA.Clear();
              }
       }

       public static bool CompareByteArray(Byte[] arrayA, Byte[] arrayB)
       {
              if (arrayA.Length != arrayB.Length)
                     return false;

              for (int i = 0; i <= arrayA.Length – 1; i++)
                     if (!arrayA[i].Equals(arrayB[i]))
                           return false;
              return true;
       }
}

Conclusiones

A través de las dos partes de este artículo se han explorado los métodos más comunes de encriptación, revisándose cada escenario donde se aplican. Cada escenario se ha complementado con ejemplos y código 100% funcional. Se ha  desmitificado la encriptación como un proceso complicado y nebuloso, mostrándose de forma clara cuándo y cómo aplicar cada algoritmo, además de la forma correcta de hacerlo.

Respuestas a las Preguntas

Revisemos entonces algunas de las preguntas inicialmente planteadas y veamos cómo podemos responderlas ahora.

  • ¿Se está almacenando la información crítica de forma segura?
    Esta pregunta la puede respnder el lector.
  • ¿Qué entendemos por seguro?
    Se ha mostrado la diferencia entre algoritmos que transforman texto con respecto a otros que realmente encriptan.
  • Si se está encriptando, ¿Qué algoritmo se está utilizando?
    Se revisaron los algoritmos más conocidos y se mostraron sus ventajas y falencias. Sea cual fuere, el algoritmo debe implementarse adecuadamente.
  • ¿Se permite la desencriptación de datos que no debieran poder desencriptarse?
    Esta pregunta la puede responder el lector, pero la respuesta debe ser NO.
  • ¿Cómo está realizándose la encriptación?
    Se revisó la forma correcta de encriptar con cada algoritmo, como también las malas prácticas y los riesgos que se corren al no hacer el proceso como debe ser.
  • ¿Dónde están almacenadas la o las llaves?
    Esta es una muy buena pregunta y la respuesta escapa al alcance de este artículo, pero como sugerencia se puede mencionar lo siguiente: se puede utilizar DPAPI o Certificate Storage. Para implementar DPAPI tanto desde aplicaciones Windows como web, puedes visitar los siguientes enlaces:

    DPAPI: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/SecNetHT07.asp
    DPAPI en
    Web: http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnnetsec/html/SecNetHT09.asp

    Pensamientos como el siguiente no son seguros: “si almaceno la llave en un directorio escondido, es difícil que un hacker la encuentre”, como así también “A un hacker le va a tomar mucho tiempo encontrarla.” Si hay algo que le sobra a un hacker es tiempo y ganas.

  • ¿Con qué frecuencia se cambian las llaves?
    La respuesta es depende, lamentablemente. Depende de la criticidad del sistema, como también del uso de las llaves. Para una conexión de transferencia, se crean las llaves, se utilizan y se descartan. En un sistema crítico tal como el de un banco, el cambio de llaves debería hacerse cada una hora, o como máximo, cada un día. Para información que va a tener mayor duración, el tiempo puede ser mayor.
  • ¿Cómo enfrento el problema de cambiar las llaves con tanta frecuencia?
    Como cambiar las llaves no es una tarea simple en recursos y tiempo, se emplea el concepto de llave maestra y llaves hijas. La información a encriptar se encripta con las llaves hijas y luego  con la llave maestra se encriptan las llaves hijas y se almacenan.  Entonces, al momento de aplicar cambio de llave, se cambia sólo la llave maestra, procediéndose a desencriptar todas las llaves hijas con la antigua y a encriptar todas con la llave nueva. Esto disminuye el tiempo a factor de N (cantidad de llaves) y no de la cantidad de bytes de información encriptada. Esto no agrega seguridad, sólo ayuda a realizar un proceso de forma eficiente.
  • ¿Con qué criterio se definen las nuevas llaves?
    Para definir una llave se puede ejecutar la función GenerateKey() que todo algoritmo simétrico posee. Esto genera una llave randómica.

Contadores de rendimiento de aplicaciones de 32 bits en sistemas de 64 bits

Durante el análisis de Microsoft.VisualBasic.dll tuve problemas para poder ver los contadores de rendimiento de la aplicación desarrollada con Visual Studio 2003 y que se ejecutaba sobre el Framework 1.1, en un sistema XP 64 bits.


En ese momento inferí que podría deberse a que la aplicación estaba compilada para 32 bits y se estaba ejecutando en un sistema de 64 bits. Es conveniente recalcar que el Framework 1.1 compila ensamblados sólo para 32 bits.


Hoy, después de un tiempo, me tope con el KB 922775. Para mi sorpresa, tiene un punto dedicado al problema que se me presentó (You cannot monitor 32-bit managed programs in the 64-bit version of Perfmon). En éste, se presenta un workaround para poder monitorear contadores de rendimiento de aplicaciones de 32 bits corriendo en sistemas de 64 bits.


Además tiene información relevante para reconstruir los contadores si alguna vez se corrompen.


Saludos,
Patrick

Predicando con el ejemplo

Predicar con el ejemplo. Hemos escuchado eso desde chicos. A veces es difícil lograrlo, pero el hacerlo trae beneficios de diferente índole.


El tema de esta oportunidad está relacionado con los mensajes de error. Uno de los tantos posibles problemas de seguridad de las aplicaciones es la revelación de más información de la necesaria cuando se produce un error.


Lo anterior significa que si la aplicación se cae, le informe al usuario pero no le dé mayor información al respecto, información que además es inútil para el usuario ordinario, pero útil para quien te quiera atacar.


Más información de esta vulnerabilidad podrán encontrar en el sitio del proyecto OWASP, en el siguiente link.


Hace un par de días estaba utilizando OWA de la empresa donde trabajo y de pronto, me topo con esta ventana. Haz clic en ella para verla completa.



Mal, mal. A predicar con el ejemplo. OWA no debiera mostrar este tipo de ventanas de error.


Buenos ejemplos 


Veamos algunas pantallas de error donde sí se han tomado las precauciones.


Esta es la de la aplicación que utilizamos en el blog de msmvps.com. Muy divertida, aunque el administrador podría sentirse ofendido.



Google también cumple con las buenas prácticas.



 


Malos ejemplos 


El típico error de asp.net, cuando el desarrollador y administrador no han tomado las medidas necesarias para que no le llegue al usuario final.


 


y para el final, el peor de todos, cuando se cae leyendo el archivo de configuración y se muestra el contenido de éste.



Corrección


¿Cómo corregirlo? Muy fácil.


Para que no se muestre el error en ASP.NET se puede utilizar el elemento Error en la configuración. Más información en este link: http://msdn2.microsoft.com/en-us/library/s2f4e3e7.aspx.


Para modificar IIS para que no muestre el contenido en los errores no controlados se pueden seguir los pasos detallados en este KB: http://support.microsoft.com/kb/302570/en-us


Saludos,
Patrick