Ir al contenido


Foto

TerminateMD5_Process, a la caza de un virus...


  • Por favor identifícate para responder
8 respuestas en este tema

#1 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.111 mensajes
  • LocationMadrid - España

Escrito 27 abril 2012 - 12:36

En ocasiones nos vemos en la necesidad de matar una aplicación desde código. Ya existen en el foro ejemplos de como hacerlo, bien por su nombre su id de proceso o el puerto que usa una App en red. En esta ocasión me vi en la necesidad de matar un proceso especial, se trataba de un virus de pendrive cuyo nombre variaba y no así el archivo. La solución fue usar el Hash md5 para localizarlo.

La filosofía es recorrer los procesos en ejecución, encontrar la ruta del archivo ejecutable y calcular su hash md5 para compararlo con el que buscamos y matar dicho proceso. Eventualmente, también podemos borrar el archivo ejecutable y desinstalarlo del registro de Windows.

La desinstalación del registro se realiza buscando el proceso en la clave:

delphi
  1. HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\run\

en el ejemplo se muestra como recorrer el registro y puede variarse para buscar en HKEY_LOCAL_MACHINE u otras. En mi caso bastó con lo expuesto.

Dejo una aplicación de consola que realiza la tarea. Usa como parámetro la cadena MD5 del archivo buscado. En ella está la función para calcular el hash md5 con la API de Windows, la función para encontrar y terminar el proceso y la función para recorrer el registro. Dicho código no encierra grandes complicaciones y puede ser útil para fines como el que me llevó a escribirlo.

El código original lo escribí en C/C++ y en realidad es algo mas complejo del que presento pues incluía un Hook a la API CreateProcessInternalW para, además, evitar la ejecución del proceso viral (o el que se tercie). En este aspecto quisiera recordar el hilo que abrió enecumene Prevenir que aplicación se ejecute
 

cpp
  1. #include <windows.h>
  2. #include <Shlwapi.h>
  3. #include <Tlhelp32.h>
  4. #include <stdio.h>
  5. #include <conio.h>
  6.  
  7. #pragma hdrstop
  8.  
  9. //---------------------------------------------------------------------------
  10. #define MD5LEN  16
  11. #define BUFSIZE 1024
  12. typedef CHAR TMD5[33];
  13.  
  14.  
  15. void GetMD5FromFile(CHAR* FileName, CHAR *MD5)
  16. {
  17.   HANDLE hFile = 0;
  18.   HCRYPTPROV hProv = 0;
  19.   HCRYPTHASH hHash = 0;
  20.   BYTE Hash[MD5LEN];
  21.   DWORD bHash = 0;
  22.   DWORD bRead = 0;
  23.   BYTE* Buffer = (BYTE*)VirtualAlloc(0, BUFSIZE, MEM_COMMIT, PAGE_READWRITE);
  24.   hFile = CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN,
  25.  
  26. NULL);
  27.   if(hFile != INVALID_HANDLE_VALUE){
  28.     if(CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)){
  29.       if(CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)){
  30.         while (1) {
  31.           ReadFile(hFile, Buffer, BUFSIZE, &bRead, NULL);
  32.           if(!bRead) break;
  33.           CryptHashData(hHash, Buffer, bRead, 0);
  34.         }
  35.         bHash = MD5LEN;
  36.         if(CryptGetHashParam(hHash, HP_HASHVAL, Hash, &bHash, 0)){
  37.           for(int i=0; i<2*MD5LEN; i++){
  38.             MD5[i] = (0x0F & Hash[i/2] >> 4*((i+1)%2)) + 48;
  39.             if(MD5[i] > '9') MD5[i] += 7;
  40.           }
  41.           MD5[32] = 0;
  42.         }
  43.       }
  44.       CryptDestroyHash(hHash);
  45.     }
  46.     CryptReleaseContext(hProv, 0);
  47.   }
  48.   CloseHandle(hFile);
  49.   VirtualFree(Buffer, 0, MEM_RELEASE);
  50. }
  51.  
  52.  
  53. //---------------------------------------------------------------------------
  54. // Desinstala una App de la clave HKEY_CURRENT_USER\...\run
  55. bool RegDesinstall(CHAR *App)
  56. {
  57.   CHAR* RunKey = "Software\\Microsoft\\Windows\\CurrentVersion\\run\\";
  58.   HKEY hKey  = 0;
  59.   CHAR* Value = 0;
  60.   CHAR  ValueName[256];
  61.   DWORD dwValueName = sizeof(ValueName);
  62.   DWORD Type;
  63.   CHAR  Data[256];
  64.   DWORD dwData = sizeof(Data);
  65.   int Index;
  66.   bool Result = false;
  67.  
  68.   // Si es un Path a una sunblave enumeramos los valores
  69.   Index = 0;
  70.   if(RegOpenKeyEx(HKEY_CURRENT_USER, RunKey, 0, KEY_READ | KEY_SET_VALUE, &hKey) == ERROR_SUCCESS){
  71.     int ErrorCode = RegEnumValue(hKey, Index++, ValueName, &dwValueName, NULL, &Type, (PBYTE)Data, &dwData);
  72.     while(ErrorCode != ERROR_NO_MORE_ITEMS){
  73.       if(StrStrI(Data, App)){
  74.         Result = (RegDeleteValue(hKey, ValueName) == ERROR_SUCCESS);
  75.         break;
  76.       }
  77.       DWORD dwValueName = sizeof(ValueName);
  78.       DWORD dwData = sizeof(Data);
  79.       ErrorCode = RegEnumValue(hKey, Index++, ValueName, &dwValueName, NULL, &Type, (PBYTE)Data, &dwData);
  80.     }
  81.     RegCloseKey(hKey);
  82.   }
  83.   return Result;
  84. }
  85.  
  86. //---------------------------------------------------------------------------
  87. // Termina los procesos conociendo el nombre del exe o su Hash MD5
  88. void TerminateMD5_Process(TMD5 FileHash, bool Terminate = true, bool DeleteProcess = false)
  89. {
  90.   TMD5 MD5;
  91.   HANDLE Process = 0;
  92.   PROCESSENTRY32 proc = { sizeof(proc) };
  93.  
  94.   // Nos Damos privilegios debug
  95.   HANDLE hToken;
  96.   TOKEN_PRIVILEGES priv = {1, {0, 0, SE_PRIVILEGE_ENABLED}};
  97.   LookupPrivilegeValue(0, SE_DEBUG_NAME, &priv.Privileges[0].Luid);
  98.   OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken);
  99.   AdjustTokenPrivileges (hToken, FALSE, &priv, sizeof priv, 0, 0);
  100.  
  101.   HANDLE hSysSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  102.   if(hSysSnapshot != INVALID_HANDLE_VALUE && Process32First(hSysSnapshot, &proc)){
  103.     do{
  104.       HANDLE hSnapshot;
  105.       MODULEENTRY32 ModuleEntry = {sizeof(MODULEENTRY32)};
  106.       hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, proc.th32ProcessID);
  107.       if(hSnapshot != (HANDLE)-1){
  108.         if(Module32First(hSnapshot, &ModuleEntry)){
  109.           GetMD5FromFile(ModuleEntry.szExePath, MD5);
  110.           if(!stricmp(FileHash, MD5)){
  111.             Process = OpenProcess(PROCESS_TERMINATE, false, proc.th32ProcessID);
  112.             if(Terminate && Process){
  113.               if(TerminateProcess(Process, 0))
  114.                 printf(ModuleEntry.szExePath); printf("\n");
  115.               CloseHandle(Process);
  116.             }
  117.             if(DeleteProcess){
  118.               SetFileAttributes(ModuleEntry.szExePath, FILE_ATTRIBUTE_ARCHIVE);
  119.               DeleteFile(ModuleEntry.szExePath);
  120.               RegDesinstall(ModuleEntry.szExePath);
  121.             } 
  122.           }
  123.         }
  124.         CloseHandle(hSnapshot);
  125.       }
  126.     }while(Process32Next(hSysSnapshot, &proc));
  127.   }
  128.   CloseHandle(hSysSnapshot);
  129.  
  130.   // Retirar los privilegios debug
  131.   priv.Privileges[0].Attributes = 0;
  132.   AdjustTokenPrivileges (hToken, FALSE, &priv, sizeof priv, 0, 0);
  133.   CloseHandle (hToken);
  134. }
  135.  
  136.  
  137. #pragma argsused
  138. int main(int argc, char* argv[])
  139. {
  140.   if(argc > 1){
  141.     printf("Terminando procesos...\n");
  142.     TerminateMD5_Process(argv[1], true, false);
  143.   }else
  144.     printf("Se necesita un parámetro MD5 que identifique un archivo.\n");
  145.  
  146.   printf("Pulse una tecla para terminar.");
  147.   getch();
  148.   return 0;
  149. }
  150. //---------------------------------------------------------------------------

Espero que este ejemplo sea de utilidad.


Saludos.

Edito para arreglar etiquetas de código y resubir adjunto perdido.

Archivos adjuntos


  • 0

#2 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 14.460 mensajes
  • LocationMéxico

Escrito 27 abril 2012 - 12:42

Muy interesante,

La pregunta obligada, como puedo saber el MD5 que voy a buscar y que supongo estás pasando en argv[1].

Además de interesante me parece una aportación excelente como sueles regalarnos amigo (y)

Saludos
  • 0

#3 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.111 mensajes
  • LocationMadrid - España

Escrito 27 abril 2012 - 12:50

La pregunta obligada, como puedo saber el MD5 que voy a buscar y que supongo estás pasando en argv[1].


En la red existen numerosas utilidades que nos dan el MD5 de un archivo, sin ir muy lejos, Microsoft tiene una llamada fciv.exe.
En el código que dejo escribo una función (GetMD5FromFile) que lo calcula con las librerías criptográficas de la API de Windows.

Pero estoy terminando de traducir el mismo código a delphi, con lo que te será sencillo usar la función GetMD5FromFile para calcularlo tu mismo.  ;)

Saludos.


  • 0

#4 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.111 mensajes
  • LocationMadrid - España

Escrito 27 abril 2012 - 01:06

Como suelo hacer en muchas ocasiones, he traducido el código a delphi de forma que los que no uséis el C/C++ lo tengáis disponible para vuestra herramienta favorita. Lo explicado arriba sirve para la versión delphi.

La función GetMD5FromFile os va a servir para calcular el Hash MD5 de cualquier archivo. Debéis tener en cuenta que el parámetro MD5 debe ser una cadena de tamaño mínimo 33 caracteres (32 para el Hash y uno para el nulo final, es un PAnsiChar). Os preguntareis porque no uso un String, pues el motivo es para no usar las funciones de cadena y comprimir el tamaño del ejecutable final. Y así sólo uso API… :p

Bueno, este es el código:

 

delphi
  1. program MD5Killer;
  2.  
  3. {$APPTYPE CONSOLE}
  4.  
  5. uses
  6.   Windows, Tlhelp32;
  7.  
  8.  
  9. function StrStrI(s1: PCHAR; s2: PCHAR): PCHAR; stdcall; external 'Shlwapi.dll' name 'StrStrIA';
  10. function _stricmp(s1: PCHAR; s2: PCHAR): DWORD; stdcall; external 'ntdll.dll' name '_stricmp';
  11. function CryptAcquireContext(phProv: PDWORD; pszContainer, pszProvider: PCHAR; dwProvType, dwFlags: DWORD): BOOL; stdcall; external ADVAPI32 name 'CryptAcquireContextA';
  12. function CryptCreateHash(hProv, Algid, hKey, dwFlags: DWORD; phHash: PDWORD): BOOL; stdcall; external  ADVAPI32 name 'CryptCreateHash';
  13. function CryptHashData(hHash: DWORD; pbData: PBYTE; dwDataLen, dwFlags: DWORD): BOOL; stdcall; external  ADVAPI32 name 'CryptHashData';
  14. function CryptGetHashParam(hHash, dwParam: DWORD; pbData: PBYTE; pdwDataLen: PDWORD; dwFlags: DWORD): BOOL; stdcall; external  ADVAPI32 name 'CryptGetHashParam';
  15. function CryptDestroyHash(hHash: DWORD): BOOL; stdcall; external  ADVAPI32 name 'CryptDestroyHash';
  16. function CryptReleaseContext(hProv, dwFlags: DWORD): BOOL; stdcall; external  ADVAPI32 name 'CryptReleaseContext';
  17.  
  18. const
  19. CALG_MD5 = ((4 shl 13) or 3);
  20.  
  21. type
  22. TMD5 = array [0..32] of char;
  23.  
  24. procedure GetMD5FromFile(FileName: PCHAR; var MD5: TMD5);
  25. const
  26.   BUFSIZE = 1024;
  27. var
  28.   hProv: DWORD; //HCRYPTPROV;
  29.   hHash: DWORD; //HCRYPTHASH;
  30.   Hash: array [0..15] of Byte;
  31.   bHash, bRead, hFile: DWORD;
  32.   Buffer: PBYTE;
  33.   i: integer;
  34. begin
  35.   Buffer:= VirtualAlloc(nil, BUFSIZE, MEM_COMMIT, PAGE_READWRITE);
  36.  
  37.   hFile:= CreateFile(FileName, GENERIC_READ, FILE_SHARE_READ, nil, OPEN_EXISTING, FILE_FLAG_SEQUENTIAL_SCAN, 0);
  38.   if hFile <> INVALID_HANDLE_VALUE then
  39.   begin
  40.     if CryptAcquireContext(@hProv, nil, nil, 1{PROV_RSA_FULL}, $F0000000{CRYPT_VERIFYCONTEXT}) then
  41.     begin
  42.       if CryptCreateHash(hProv, ((4 shl 13) or 3){CALG_MD5}, 0, 0, @hHash) then
  43.       begin
  44.         while true do
  45.         begin
  46.           ReadFile(hFile, Buffer^, BUFSIZE, bRead, nil);
  47.           if bRead = 0 then break;
  48.           CryptHashData(hHash, Buffer, bRead, 0);
  49.         end;
  50.         bHash:= 16;
  51.         if CryptGetHashParam(hHash, 2 {HP_HASHVAL}, @Hash[0], @bHash, 0) then
  52.         begin
  53.           for i:=0 to 31 do
  54.           begin
  55.             MD5[i]:= CHAR(($0F and (Hash[i div 2] shr (4*((i+1) mod 2)))) + 48);
  56.             if(MD5[i] > '9') then inc(MD5[i], 7);
  57.           end;
  58.           MD5[32]:= #0;
  59.         end;
  60.       end;
  61.       CryptDestroyHash(hHash);
  62.     end;
  63.     CryptReleaseContext(hProv, 0);
  64.   end;
  65.   CloseHandle(hFile);
  66.   VirtualFree(Buffer, 0, MEM_RELEASE);
  67. end;
  68.  
  69.  
  70. //---------------------------------------------------------------------------
  71. // Desinstala una App de la clave HKEY_CURRENT_USER\...\run
  72. function RegDesinstall(App: PCHAR): boolean;
  73. const
  74.   RunKey: PCHAR = 'Software\Microsoft\Windows\CurrentVersion\run\';
  75. var
  76.   Key: HKEY;
  77.   Value: PCHAR;
  78.   dwValueName, _Type, dwData, Index, ErrorCode: DWORD;
  79.   Data, ValueName: array [1..255] of CHAR;
  80. begin
  81.   Result:= false;
  82.   Key:= 0;
  83.   Value:= nil;
  84.   dwValueName:= sizeof(ValueName);
  85.   // Si es un Path a una sunblave enumeramos los valores
  86.   Index:= 0;
  87.   if RegOpenKeyEx(HKEY_CURRENT_USER, RunKey, 0, KEY_READ or KEY_SET_VALUE, Key) = ERROR_SUCCESS then
  88.   begin
  89.     ErrorCode:= RegEnumValue(Key, Index, @ValueName, dwValueName, nil, @_Type, @Data, @dwData);
  90.     Inc(Index);
  91.     while(ErrorCode <> ERROR_NO_MORE_ITEMS) do
  92.     begin
  93.       if(StrStrI(@Data, App) <> nil) then
  94.       begin
  95.         Result:= (RegDeleteValue(Key, @ValueName) = ERROR_SUCCESS);
  96.         break;
  97.       end;
  98.       dwValueName:= sizeof(ValueName);
  99.       dwData:= sizeof(Data);
  100.       ErrorCode:= RegEnumValue(Key, Index, @ValueName, dwValueName, nil, @_Type, @Data, @dwData);
  101.       inc(Index);
  102.     end;
  103.     RegCloseKey(Key);
  104.   end;
  105. end;
  106.  
  107. //---------------------------------------------------------------------------
  108. // Termina los procesos conociendo el nombre del exe o su Hash MD5
  109. procedure TerminateMD5_Process(FileHash: PCHAR; Terminate: boolean = true; DeleteProcess: boolean = false);
  110. var
  111.   MD5: TMD5;
  112.   hProcess, hToken: DWORD;
  113.   proc: TPROCESSENTRY32;
  114.   ModuleEntry: MODULEENTRY32;
  115.   hSysSnapshot, hSnapshot: DWORD;
  116.   priv: TOKEN_PRIVILEGES;
  117. begin
  118.   hProcess:= 0;
  119.   proc.dwSize:= sizeof(proc);
  120.   priv.PrivilegeCount:= 1;
  121.   priv.Privileges[0].Attributes:= SE_PRIVILEGE_ENABLED;
  122.  
  123.     // Nos Damos privilegios debug
  124.   LookupPrivilegeValue(nil, 'SeDebugPrivilege', Pint64(@(priv.Privileges[0].Luid))^);
  125.   OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, hToken);
  126.   AdjustTokenPrivileges (hToken, FALSE, priv, sizeof(priv), nil, PDWORD(0)^);
  127.  
  128.   hSysSnapshot:= CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
  129.   if boolean(hSysSnapshot) <>  (boolean(INVALID_HANDLE_VALUE) and boolean(Process32First(hSysSnapshot, proc))) then
  130.   begin
  131.     repeat
  132.       ModuleEntry.dwSize:= sizeof(MODULEENTRY32);
  133.       hSnapshot:= CreateToolhelp32Snapshot(TH32CS_SNAPMODULE, proc.th32ProcessID);
  134.       if hSnapshot <> -1 then
  135.       begin
  136.         if (Module32First(hSnapshot, ModuleEntry)) then
  137.         begin
  138.           GetMD5FromFile(ModuleEntry.szExePath, MD5);
  139.           if _stricmp(@FileHash[0], @MD5[0]) = 0 then
  140.           begin
  141.             hProcess:= OpenProcess(PROCESS_TERMINATE, false, proc.th32ProcessID);
  142.             if Terminate and boolean(hProcess) then
  143.             begin
  144.               if TerminateProcess(hProcess, 0) then
  145.                 writeln(ModuleEntry.szExePath);
  146.               CloseHandle(hProcess);
  147.             end;
  148.             if (DeleteProcess) then
  149.             begin
  150.               SetFileAttributes(ModuleEntry.szExePath, FILE_ATTRIBUTE_ARCHIVE);
  151.               DeleteFile(ModuleEntry.szExePath);
  152.               RegDesinstall(ModuleEntry.szExePath);
  153.             end;
  154.           end;
  155.         end;
  156.         CloseHandle(hSnapshot);
  157.       end;
  158.     until (not Process32Next(hSysSnapshot, proc));
  159.   end;
  160.   CloseHandle(hSysSnapshot);
  161.  
  162.   // Retirar los privilegios debug
  163.   priv.Privileges[0].Attributes:= 0;
  164.   AdjustTokenPrivileges (hToken, FALSE, priv, sizeof(priv), nil, PDWORD(0)^);
  165.   CloseHandle (hToken);
  166. end;
  167.  
  168. begin
  169.   if(ParamCount >= 1) then
  170.   begin
  171.     Writeln('Terminando procesos...' + #10);
  172.     TerminateMD5_Process(PCHAR(ParamStr(1)), true, true);
  173.   end
  174.   else
  175.       Writeln('Se necesita un parámetro MD5 que identifique un archivo.' + #10);
  176.  
  177.   Writeln('Pulse una tecla para terminar.');
  178.   Readln;
  179. end.

Espero que os sirva.



Saludos.

Edito para arreglar etiquetas de código y resubir adjunto perdido

Archivos adjuntos


  • 0

#5 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 14.460 mensajes
  • LocationMéxico

Escrito 27 abril 2012 - 01:15

Ya entiendo, lo que pasas es el nombre del archivo y se calcula el MD5 (y)

Saludos
  • 0

#6 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.111 mensajes
  • LocationMadrid - España

Escrito 27 abril 2012 - 01:18

Ya entiendo, lo que pasas es el nombre del archivo y se calcula el MD5 (y)

Exacto, se pasa la ruta completa y calcula el md5.

Subo una aplicación delphi de ejemplo recién salida del tostadero.


Saludos.
  • 0

#7 egostar

egostar

    missing my father, I love my mother.

  • Administrador
  • 14.460 mensajes
  • LocationMéxico

Escrito 27 abril 2012 - 01:23


Ya entiendo, lo que pasas es el nombre del archivo y se calcula el MD5 (y)

Exacto, se pasa la ruta completa y calcula el md5.

Subo una aplicación delphi de ejemplo recién salida del tostadero.


Saludos.


Genial, llegando a casa las descargo (y)

Saludos
  • 0

#8 seoane

seoane

    Advanced Member

  • Administrador
  • 1.259 mensajes
  • LocationEspaña

Escrito 27 abril 2012 - 05:41

Esta muy bien  (y) ... aunque lo que me parece mas interesante es lo de utilizarlo conjuntamente con hooks de las APIs, aunque me temo que en windows 7 debe ser bastante complicado ¿que SO estas usando?

Por otro lado me ha recordado a mi "chivato": http://delphi.jmrds.com/?q=node/47  ... viejos tiempos  :)

Saludos
  • 0

#9 escafandra

escafandra

    Advanced Member

  • Administrador
  • 4.111 mensajes
  • LocationMadrid - España

Escrito 27 abril 2012 - 06:10

Esta muy bien  (y) ... aunque lo que me parece mas interesante es lo de utilizarlo conjuntamente con hooks de las APIs, aunque me temo que en windows 7 debe ser bastante complicado ¿que SO estas usando?


Cracias por tu apreciación, seoane.

Si lo interesante es usarlo en un Hook. De hecho el programa original lo usa en la aplicación inyectora, para realizar una limpieza en el arranque, y luego la vigilancia es en un Hook a la API CreateProcessInternalW. Digamos que el sistema es mixto y en versión unicode.

El S.O. sobre el que está desarrollado es Windows XP profesional. Tienes toda la razón al puntualizar este aspecto. La inyección de dll 32 bits no funciona en procesos de 64 bits y, además tenemos que según la versión y/o actualización de un S.O., los primeros Bytes de una API pueden ser distintos de un PC a otro, lo que lleva a tener un método para desensamblar código y localizar los jmp y call para poder realizar el trampolín con éxito. Eso lo tengo para procesadores de 32 bits pero no en 64 bits, de esta forma, y por el momento, no puedo real izarlo para Win 7 64 bits. :(

Sabes que se puede hacer el Hook a través de la IAT de el PE ejecutable, evitando el problema de la sustitución de código en la API a la que queremos hacer el Hook, pero eso tiene el problema de que no funciona si la llamada a la API es dinámica con GetProcAdress y tampoco resuelve el tema de inyección de dll 32 en 64 bits para lo que precisamos un compilador de 64 bits.

Y si nos complicamos mas, lo mas efectivo es un Hook en el Kernel mediante un driver. No precisa dll, no precisa inyección. Pero con esto nos volvemos a enfrentar con las distintas versiones del S.O. (Win XP, Vista, Win7...)

...Por otro lado me ha recordado a mi "chivato": http://delphi.jmrds.com/?q=node/47  ... viejos tiempos  :)

Si, conocía tu viejo chivato. En realidad esos viejos tiempos si eran divertidos. Los constantes cambios en S.O. y convivencia de varios de ellos nos lo ponen cada vez un poco mas complicado, pero sigue siendo estimulante.  :)


Saludos.



  • 0




IP.Board spam blocked by CleanTalk.