Ejemplo de virtualización de código con CPU imaginaria

Iniciado por BloodSharp, 27 Octubre 2021, 17:18 PM

0 Miembros y 1 Visitante están viendo este tema.

BloodSharp

Buenas gente del foro, hice este código simple en un par de horas en C++ para 32 bits en Windows y lo quería compartir para que pueda servirle a alguién o quién quiera colaborar en este proyecto y podemos hacerlo completo... :P

La idea de virtualización de código es hacer complicado el análisis de ingeniería inversa de cualquier programa, creando una CPU imaginaria con sus propios conjunto de instrucciones y reemplazando el código original de assembler de la CPU real por la CPU imaginaria.

Código (cpp) [Seleccionar]
#include <iostream>
#include "BloodVM.h"

int __declspec(naked) __stdcall Sumar()
{
    __asm
    {
        call BloodVM_Init;

        //MOV_R_N => mov eax, 4;
        _emit 0x02; //OPCODE::MOV_R_N
        _emit 0x05; //EAX offset
        _emit 0x04; //number 4 bytes
        _emit 0x00;
        _emit 0x00;
        _emit 0x00;
        //ADD_R_N => add eax, 5;
        _emit 0x04; //OPCODE::ADD_R_N
        _emit 0x05; //EAX offset
        _emit 0x05; //number 4 bytes
        _emit 0x00;
        _emit 0x00;
        _emit 0x00;
        //ADD_R_R => add eax, eax;
        _emit 0x03; //OPCODE::ADD_R_R
        _emit 0x05; //EAX offset
        _emit 0x05; //EAX offset
        //QUIT => Exit VM
        _emit 0x00;

        call BloodVM_End;
        ret;
    }
}

int main()
{
    std::cout << "Suma virtualizada (4 + 5) + (4 + 5) = " << Sumar() << std::endl;
    std::cin.ignore();
}




El código de ejemplo tiene apenas 5 instrucciones pero se podría implementar más. Al verse en un desamblador la función virtualizada se vería algo como esto:



Lo cuál forzaría al que quiera ver como funciona realmente el programa a analizar toda la CPU imaginaria siendo usualmente una tarea bastante compleja. Se podría crear también un programa que detecte las llamadas del inicio y fin de la virtualización y reemplazar el código real por el imaginario pero esa es una tarea bastante compleja que llevaría mucho tiempo.

Dejo el resto del código por si a alguien le interesa:

BloodVM.cpp
Código (cpp) [Seleccionar]
#include "BloodVM.h"

BloodVM gBloodVM;

uint32_t __declspec(naked) __stdcall GetCaller()//uint32_t dwESP)
{
    //return *(uint32_t*)(dwESP+4);
    __asm
    {
        mov eax, [esp + 4];
        ret;
    }
}

void __declspec(naked) __stdcall BloodVM_Init()
{
    __asm
    {
        pushad;
        pop gBloodVM.EDI;
        pop gBloodVM.ESI;
        pop gBloodVM.EBP;
        pop gBloodVM.ESP;
        pop gBloodVM.EBX;
        pop gBloodVM.EDX;
        pop gBloodVM.ECX;
        pop gBloodVM.EAX;
       
        //push gBloodVM.ESP;
        call GetCaller;
        mov gBloodVM.EIP, eax;
        lea ecx, gBloodVM;
        call BloodVM::RunVirtualMachine;
        mov eax, gBloodVM.EIP;
        mov [esp], eax;
        ret;
    }
}

void __declspec(naked) __stdcall BloodVM_End()
{
    __asm
    {
        mov edi, gBloodVM.EDI;
        mov esi, gBloodVM.ESI;
        mov ebx, gBloodVM.EBX;
        mov edx, gBloodVM.EDX;
        mov ecx, gBloodVM.ECX;
        mov eax, gBloodVM.EAX;
        mov ebp, gBloodVM.EBP;
        mov esp, gBloodVM.ESP;
        ret;
    }
}

void BloodVM::RunVirtualMachine()
{
    do
    {
        this->currentOpcode = (uint8_t)(*((uint8_t*)this->EIP));
        uint32_t nextEIP = this->vInstructions[this->currentOpcode].sizeOfInstruction;
        (this->*vInstructions[this->currentOpcode].operate)();
        this->EIP += nextEIP;
    } while (this->currentOpcode != OPCODE::QUIT);
}

void BloodVM::QUIT()
{

}

void BloodVM::MOV()
{
    uint32_t* firstRegister;
    uint32_t* secondRegister;
    uint32_t firstNumber;
    switch (this->currentOpcode)
    {
        case OPCODE::MOV_R_R:
            firstRegister = &this->EDI + *(uint8_t*)(this->EIP + 1);
            secondRegister = &this->EDI + *(uint8_t*)(this->EIP + 2);
            *firstRegister = *secondRegister;
            break;
        case OPCODE::MOV_R_N:
            firstRegister = &this->EDI + *(uint8_t*)(this->EIP + 1);
            firstNumber = *(uint32_t*)(this->EIP + 2);
            *firstRegister = firstNumber;
            break;
    }
}

void BloodVM::ADD()
{
    uint32_t* firstRegister;
    uint32_t* secondRegister;
    uint32_t firstNumber;
    switch (this->currentOpcode)
    {
        case OPCODE::ADD_R_R:
            firstRegister = &this->EDI + *(uint8_t*)(this->EIP + 1);
            secondRegister = &this->EDI + *(uint8_t*)(this->EIP + 2);
            *firstRegister += *secondRegister;
            break;
        case OPCODE::ADD_R_N:
            firstRegister = &this->EDI + *(uint8_t*)(this->EIP + 1);
            firstNumber = *(uint32_t*)(this->EIP + 2);
            *firstRegister += firstNumber;
            break;
    }
}


BloodVM.h
Código (cpp) [Seleccionar]
#pragma once
#include <cstdint>
#include <vector>

enum OPCODE
{
QUIT,
MOV_R_R,
MOV_R_N,
ADD_R_R,
ADD_R_N,
};

class BloodVM
{
public:
BloodVM()
{
vInstructions =
{
/*
All opcodes sizes are always 1 byte
All registers (R) sizes are also 1 byte
All numbers (N) sizes are always 4 bytes
*/
{OPCODE::QUIT, 1, &BloodVM::QUIT},
{OPCODE::MOV_R_R, 3, &BloodVM::MOV},
{OPCODE::MOV_R_N, 6, &BloodVM::MOV},
{OPCODE::ADD_R_R, 3, &BloodVM::ADD},
{OPCODE::ADD_R_N, 6, &BloodVM::ADD},
};
}
        uint32_t EDI, ESI, EBX, EDX, ECX, EAX, EBP, EIP, ESP;
uint8_t currentOpcode;
void RunVirtualMachine();

void MOV();
void ADD();
void QUIT();

struct INSTRUCTION
{
uint8_t opcode;
uint8_t sizeOfInstruction;
void (BloodVM::* operate)() = nullptr;
};

std::vector<INSTRUCTION> vInstructions;
};

void __stdcall BloodVM_Init();
void __stdcall BloodVM_End();



B#



@XSStringManolo


MAFUS

Esto me recuerda un proyecto llamado CHIP-8. Un chip virtual con sus especificaciones y con juegos diseñados para él.

Un poco de info sobre ese chip: https://github.com/mattmikolay/chip-8/wiki/