jueves, 11 de agosto de 2011

Cifrando un ejecutable en C


Cifrando un ejecutable a mano
Cifrando un ejecutable a mano, dos variaciones

En las dos entradas anteriores hemos visto como cifrar un ejecutable a mano y algunas posibles variaciones a la hora de hacerlo. Conociendo el proceso para realizarlo a mano hacer un programa que haga lo mismo es cuestión de paciencia.

Aquí os dejo el código fuente en C del programa que he realizado.
#include <stdio.h>
#include <windows.h>

//Rutina encargada de descifrar la sección cifrada
char uncryptroutine[] =		  //21 bytes
	"\xB8\x00\x10\x40\x00"  //B8 00104000  MOV EAX, 401000    Section Start  ||Este valor será modificado
	"\x80\x30\xBB"          //8030 BB      XOR BYTE[EAX], BB  Key
	"\x40"                  //40           INC EAX
	"\x3D\x00\x45\x40\x00"  //3D 00454000  CMP EAX, 404500    Section End  ||Este valor será modificado
	"\x75\xF5"              //75 F5        JNZ SHORT -11      (Complemento a dos de 11 = F5)
	"\xB8\x00\x20\x40\x00"  //B8 00204000  MOV EAX, 402000    Pone en EAX el OEP || Este valor será modificado
	"\xFF\xE0";				//FFE0         JMP EAX			  Salta al OEP

//Posiciones de la rutina que tienen que ser corregidas
const int sectionStartPos = 1;
const int sectionEndPos = 10;
const int OEPPos = 17;

//Dado un numero y un alineamiento, devuelve el numero alineado superior mas cercano
DWORD align(DWORD number, DWORD alignment)
{
  if(number % alignment == 0)
    return number;
  else
    return (number / alignment) * alignment + alignment;
}

int main(int argc, char *argv[])
{
	if(argc != 2)
	{
		printf("Pasame un archivo como argumento, ej: %s program.exe", *argv);
		return EXIT_FAILURE;
	}
	HANDLE file = CreateFile((LPCTSTR)argv[1], 
		GENERIC_READ | GENERIC_WRITE, 
		0, 
		NULL, 
		OPEN_EXISTING, 
		FILE_ATTRIBUTE_NORMAL, 
		NULL);
	if(file == INVALID_HANDLE_VALUE){
		fprintf(stderr, "No se pudo abrir el fichero: %s Error: %d\n", argv[1], GetLastError());
		return EXIT_FAILURE;
	}

	//Abrimos una vista de solo lectura para leer el fichero y saber cuanto ocupará el nuevo fichero cifrado
	HANDLE mappedFile = CreateFileMapping(file, 0, PAGE_READONLY, 0, 0, 0);
	if(mappedFile == INVALID_HANDLE_VALUE)
	{
		fprintf(stderr, "No se puede mapear el fichero en memoria. Error: %d\n" , GetLastError());
		CloseHandle(file);
		return EXIT_FAILURE;
	}
	byte *viewMappedFile = (byte*)MapViewOfFile(mappedFile, FILE_MAP_READ, 0, 0, 0);
	if(viewMappedFile == NULL)
	{
		fprintf(stderr, "No se puede mapear el fichero en memoria. Error: %d\n" , GetLastError());
		CloseHandle(mappedFile);
		CloseHandle(file);
		return EXIT_FAILURE;
	}
	PIMAGE_DOS_HEADER pIDH = (PIMAGE_DOS_HEADER)viewMappedFile;
	if(pIDH->e_magic != IMAGE_DOS_SIGNATURE) //MZ
	{
		fprintf(stderr, "No se encontro la cabecera MZ!\n");
		UnmapViewOfFile(viewMappedFile);
		CloseHandle(mappedFile);
		CloseHandle(file);
		return EXIT_FAILURE;
	}
	PIMAGE_NT_HEADERS pINH = (PIMAGE_NT_HEADERS)&viewMappedFile[pIDH->e_lfanew];

	//Ahora con pINH->OptionalHeader.FileAlignment ya podemos saber exactamente el tamaño del nuevo ejecutable cifrado
	DWORD fileAlignment = pINH->OptionalHeader.FileAlignment;
	//Reabrimos las vistas con el nuevo tamaño (el viejo tamaño + el tamaño de la sección nueva)
	UnmapViewOfFile(viewMappedFile);
	CloseHandle(mappedFile);
		
	DWORD size = GetFileSize(file, NULL);
	mappedFile = CreateFileMapping(file,
		0,
        PAGE_READWRITE,
        0,
		size + align(sizeof(uncryptroutine) ,fileAlignment),
        0);
	if(mappedFile == INVALID_HANDLE_VALUE)
	{
		fprintf(stderr, "No se puede mapear el fichero en memoria. Error: %d\n" , GetLastError());
		CloseHandle(file);
		return EXIT_FAILURE;
	}
	viewMappedFile = (byte*)MapViewOfFile(mappedFile, FILE_MAP_READ | FILE_MAP_WRITE, 0, 0, 0);
	
	pIDH = (PIMAGE_DOS_HEADER)viewMappedFile;
	pINH = (PIMAGE_NT_HEADERS)&viewMappedFile[pIDH->e_lfanew];
	if(pIDH->e_lfanew + sizeof(IMAGE_NT_HEADERS) + pINH->FileHeader.NumberOfSections * sizeof(IMAGE_SECTION_HEADER) > pINH->OptionalHeader.SizeOfHeaders)
	{
		fprintf(stderr, "No hay espacio en la cabecera PE para agregar una seccion y es tan lioso agrandarla, quizas en futuras versiones...");
		UnmapViewOfFile(viewMappedFile);
		CloseHandle(mappedFile);
		CloseHandle(file);
		return EXIT_FAILURE;
	}
	PIMAGE_SECTION_HEADER pISH;
	DWORD i;
	//Busca la sección donde apunta el EP, que es la que cifrará
	for(i = 0; i < pINH->FileHeader.NumberOfSections; i++)
	{
		pISH = (PIMAGE_SECTION_HEADER)&viewMappedFile[pIDH->e_lfanew + sizeof(IMAGE_NT_HEADERS) + i*sizeof(IMAGE_SECTION_HEADER)];
		if(pINH->OptionalHeader.AddressOfEntryPoint >=  pISH->VirtualAddress && 
		   pINH->OptionalHeader.AddressOfEntryPoint < pISH->VirtualAddress + pISH->Misc.VirtualSize)
		{
			printf("EP apunta a la seccion: %s\n", pISH->Name);
	        break;
		}
	}
	//Añadimos permisos de escritura a la sección que se va a encriptar
	pISH->Characteristics |= IMAGE_SCN_MEM_WRITE;

	//Ciframos la sección donde apunta el EP
	for(i = 0; i < pISH->Misc.VirtualSize; i++)
		viewMappedFile[pISH->PointerToRawData + i] ^= 0xBB;
	printf("Seccion %s cifrada\n", pISH->Name);

	//Obtenemos un puntero a la última sección
	PIMAGE_SECTION_HEADER pLastISH = (PIMAGE_SECTION_HEADER)&viewMappedFile[pIDH->e_lfanew + sizeof(IMAGE_NT_HEADERS) + (pINH->FileHeader.NumberOfSections - 1) * sizeof(IMAGE_SECTION_HEADER)];
	//Puntero a la nueva sección
	PIMAGE_SECTION_HEADER pNewISH = pLastISH + 1;
	memset(pNewISH, 0, sizeof(IMAGE_SECTION_HEADER));
	sprintf((char *)pNewISH->Name, ".crypt");
	pNewISH->Characteristics = IMAGE_SCN_MEM_EXECUTE | IMAGE_SCN_MEM_READ | IMAGE_SCN_CNT_CODE;
	pNewISH->VirtualAddress = align(pLastISH->VirtualAddress + pLastISH->Misc.VirtualSize, pINH->OptionalHeader.SectionAlignment);
	pNewISH->Misc.VirtualSize = sizeof(uncryptroutine);
	pNewISH->PointerToRawData = align(pLastISH->PointerToRawData + pLastISH->SizeOfRawData, pINH->OptionalHeader.FileAlignment);
	pNewISH->SizeOfRawData = align(sizeof(uncryptroutine), pINH->OptionalHeader.FileAlignment);

	//Corregimos los valores del PE para que tenga en cuenta la nueva sección
	pINH->FileHeader.NumberOfSections++;
	DWORD OEP = pINH->OptionalHeader.AddressOfEntryPoint;
	//El nuevo entry point estará al comienzo de la nueva sección
	pINH->OptionalHeader.AddressOfEntryPoint = pNewISH->VirtualAddress;
	pINH->OptionalHeader.SizeOfImage = align(pNewISH->VirtualAddress + pNewISH->Misc.VirtualSize, pINH->OptionalHeader.SectionAlignment);
	printf("Seccion %s agregada\n", pNewISH->Name);	

	//Modificamos la rutina de desencriptación
	DWORD VirtualStart = pINH->OptionalHeader.ImageBase + pISH->VirtualAddress;
	memcpy(&uncryptroutine[sectionStartPos], &VirtualStart, sizeof(DWORD));
	DWORD VirtualEnd = VirtualStart + pISH->Misc.VirtualSize;
	memcpy(&uncryptroutine[sectionEndPos], &VirtualEnd, sizeof(DWORD));
	OEP += pINH->OptionalHeader.ImageBase;
	memcpy(&uncryptroutine[OEPPos], &OEP, sizeof(DWORD));

	if (size != pNewISH->PointerToRawData)
	{
		printf("Existen datos EOF, se copiaran al final del fichero\n");
		//Los datos EOF se desplazan al final del fichero
		memcpy(&viewMappedFile[pNewISH->PointerToRawData + pNewISH->SizeOfRawData], &viewMappedFile[pNewISH->PointerToRawData], size - pNewISH->PointerToRawData);
	}
	//Escribimos la rutina de desencriptación al inicio de la nueva sección
	memcpy(&viewMappedFile[pNewISH->PointerToRawData], uncryptroutine, sizeof(uncryptroutine));
	//Rellenamos de 00s el resto de la sección
	memset(&viewMappedFile[pNewISH->PointerToRawData + sizeof(uncryptroutine)], 0, pNewISH->SizeOfRawData - sizeof(uncryptroutine));
	printf("Rutina de descifrado escrita\n");
	
	//Cerramos la vista mapeada
	UnmapViewOfFile(viewMappedFile);
	CloseHandle(mappedFile);
	CloseHandle(file);

	printf("Archivo encriptado!\n");
	return EXIT_SUCCESS;
}
El ejecutable conserva los datos EOF que puedan existir. La rutina de descifrar a sido cambiada levemente ("JMP dirección" por "MOV EAX, dirección; JMP EAX") para que sea mas fácil de modificar desde el código.

Al ejecutarlo muestra algo como esto:
image

Subo aquí el programa compilado.

Es posible volver a cifrar el ejecutable resultante. De modo que podemos llegar a tener 20 secciones “.crypt”, y una tras otra van descifrándose hasta que la última descifra el código original. Como curiosidad he probado ha cifrarlo 88 veces, obteniendo 91 secciones (inicialmente había 3) y funciona correctamente:
image

Este “crypter” no funciona con todos los ejecutables, de hecho da problemas con muchos de ellos, en siguientes entradas veremos los problemas que pueden surgir y como tratarlos.

Cualquier sugerencia será bienvenida. Saludos!

2 comentarios:

  1. Enhorabuena por las entradas sobre el formato PE, me han gustado muchísimo.
    En la función (línea 20): DWORD align(DWORD number, DWORD alignment) se debería comprobar también que el aligment no sea cero ;-)

    ResponderEliminar
  2. Muchas gracias!
    Tienes razón, si el alignment es 0 dará una excepción :( Lo corregiré.

    ResponderEliminar