ReverseEngineering
1.25K subscribers
41 photos
10 videos
55 files
666 links
Download Telegram
🧠 شبیه‌سازی ساده‌ی VMProtect با یک ماشین مجازی دست‌ساز در C



🎯 هدف:

درک پایه‌ای از اینکه VM-Based Obfuscation مثل VMProtect چطور کار میکنه ما یک زبان خیلی ساده اختراع می‌کنیم، یه ماشین مجازی خیلی سبک می‌نویسیم و کدمون رو به دستوراتی از این ماشین ترجمه میکنیم



🧠 مفهوم کلی VM-Based Obfuscation

VMProtect، Themida و شبیه‌های اون‌ها بخش‌هایی از کد رو از زبان اسمبلی واقعی تبدیل می‌کنن به یکسری دستور مخصوص که فقط ماشین مجازی خودشون می‌فهمه.
🔒 این کار باعث می‌شه تحلیل‌گر نتونه مستقیم توابع اصلی رو ببینه




🛠️ قدم اول: تعریف یک زبان بسیار ساده

فرض کنیم زبان ما ۳ دستور داره:

کد دستور عملکرد

01 PUSH_VAL
مقدار رو روی استک بذار
02 ADD
دو مقدار از استک بردار و جمع بزن
03 PRINT
مقدار بالا رو پرینت کن





📦 قدم دوم: تعریف کد Bytecode برنامه

ما می‌خوایم 10 + 20 رو محاسبه و چاپ کنیم.
کد Bytecode اون به‌صورت باینری:

01 0A ; PUSH_VAL 10
01 14 ; PUSH_VAL 20
02 ; ADD
03 ; PRINT




🧰 قدم سوم: پیاده‌سازی ماشین مجازی در C

#include <stdio.h>
#include <stdlib.h>

#define PUSH_VAL 0x01
#define ADD 0x02
#define PRINT 0x03

unsigned char bytecode[] = {
0x01, 0x0A, // PUSH 10
0x01, 0x14, // PUSH 20
0x02, // ADD
0x03 // PRINT
};

int stack[100];
int sp = -1;

void push(int val) {
stack[++sp] = val;
}

int pop() {
return stack[sp--];
}

void run_vm() {
int pc = 0;
while (pc < sizeof(bytecode)) {
unsigned char op = bytecode[pc++];
switch (op) {
case PUSH_VAL:
push(bytecode[pc++]);
break;
case ADD: {
int b = pop();
int a = pop();
push(a + b);
break;
}
case PRINT:
printf("Result: %d\n", pop());
break;
default:
printf("Invalid opcode: %02x\n", op);
return;
}
}
}

int main() {
run_vm();
return 0;
}





🧨 خروجی:

Result: 30




🤯 نکته مهم:

کدی که در این VM اجرا میشه از دید دی‌کامپایلرها مثل Ghidra یک بلاک غیر قابل درک به نظر میرسه چون توابع اصلی برنامه داخل VM مخفی شدن

🧠 Simple VMProtect Simulation with a Custom Virtual Machine in C




🎯 Goal:
Understand the basics of how VM-Based Obfuscation (like VMProtect) works.
We’ll invent a super simple language, write a minimal virtual machine, and translate our code into that VM's instructions.




🧠 General Concept of VM-Based Obfuscation

Tools like VMProtect, Themida, and similar ones convert parts of the code from real assembly into custom instructions that only their own virtual machine understands.

🔒 This makes it much harder for reverse engineers to directly analyze key functions.





🛠️ Step 1: Define a Very Simple Language

Let’s say our language supports just 3 instructions:

Code Instruction Action

01 PUSH_VAL Push a value onto the stack
02 ADD Pop two values, add them, push result
03 PRINT Print the value on top of the stack





📦 Step 2: Define the Bytecode of Our Program

We want to compute and print 10 + 20.

The binary bytecode would be:

01 0A ; PUSH_VAL 10
01 14 ; PUSH_VAL 20
02 ; ADD
03 ; PRINT





🧰 Step 3: Implementing the Virtual Machine in C

#include <stdio.h>
#include <stdlib.h>

#define PUSH_VAL 0x01
#define ADD 0x02
#define PRINT 0x03

unsigned char bytecode[] = {
0x01, 0x0A, // PUSH 10
0x01, 0x14, // PUSH 20
0x02, // ADD
0x03 // PRINT
};

int stack[100];
int sp = -1;

void push(int val) {
stack[++sp] = val;
}

int pop() {
return stack[sp--];
}

void run_vm() {
int pc = 0;
while (pc < sizeof(bytecode)) {
unsigned char op = bytecode[pc++];
switch (op) {
case PUSH_VAL:
push(bytecode[pc++]);
break;
🔥2
🧠 ساخت یک VM پیچیده‌تر با Obfuscation داخلی




🎯 هدف:

ساخت نسخه دوم ماشین مجازی که به‌شدت تحلیل رو سخت‌تر می‌کنه طوری که تحلیل‌گر مجبور شه زمان زیادی صرف درک منطق کنه




💡 ایده‌هایی برای پیاده‌سازی VM پیشرفته‌تر:

1 اضافه‌کردن دستورات جعلی (Junk Instructions):

دستوراتی که کاری نمیکنن ولی باعث شلوغ‌شدن bytecode می‌شن



2 رمزگذاری Bytecode:

bytecode رو رمز میکنیم و VM اول decrypt
ش میکنه بعد اجرا



3 Control Flow Obfuscation:

اضافه‌کردن پرش‌های دروغین loopهای غیرواقعی و ترتیب اجرای غیرخطی.



4 ساخت Jump Table:

به‌جای switch ساده، از table برای نگه‌داری pointer هر دستور استفاده میکنیم







🧪 مثال عملی – نسخه دوم VM با دستورات جعلی

📦 Bytecode جدید (با دستورات جعلی)

FF ; NOP_FAKE_1
01 0A ; PUSH 10
AB ; NOP_FAKE_2
01 14 ; PUSH 20
FF ; NOP_FAKE_1
02 ; ADD
CD ; NOP_FAKE_3
03 ; PRINT

📚 دستورها:

کد دستور توضیح

01 PUSH_VAL مقدار روی استک بذار

02 ADD جمع دو عدد از استک

03 PRINT چاپ بالا‌ی استک

FF NOP_FAKE_1 هیچ کاری نمیکنه

AB NOP_FAKE_2 هیچ کاری نمیکنه

CD NOP_FAKE_3 هیچ کاری نمیکنه





🧠 پیاده‌سازی در C:

#define PUSH_VAL 0x01
#define ADD 0x02
#define PRINT 0x03
#define NOP1 0xFF
#define NOP2 0xAB
#define NOP3 0xCD

unsigned char bytecode[] = {
0xFF, // Fake NOP
0x01, 0x0A, // PUSH 10
0xAB, // Fake NOP
0x01, 0x14, // PUSH 20
0xFF, // Fake NOP
0x02, // ADD
0xCD, // Fake NOP
0x03 // PRINT
};

void run_vm() {
int pc = 0;
while (pc < sizeof(bytecode)) {
unsigned char op = bytecode[pc++];
switch (op) {
case PUSH_VAL:
push(bytecode[pc++]);
break;
case ADD:
push(pop() + pop());
break;
case PRINT:
printf("Result: %d\n", pop());
break;
case NOP1:
case NOP2:
case NOP3:
// Do nothing (fake instruction)
break;
default:
printf("Unknown opcode: %02X\n", op);
return;
}
}
}





🎯 تحلیل‌گر چه چیزی می‌بینه؟

در Ghidra همه چیز پیچیده‌تر به‌نظر می‌رسه چون:

bytecode قابل درک نیست (باید reverse بشه)

مسیر اجرای کد به خاطر NOP ها پخش و گیج‌کننده‌ست

کنترل جریان واضح نیست




🧠 Building a More Advanced Virtual Machine with Internal Obfuscation

🎯 Goal:
Create a second version of the VM that’s significantly harder to analyze—one that forces the reverse engineer to spend considerable time understanding its logic.




💡 Ideas for Implementing a More Complex VM:

1 Junk Instructions:
Add fake opcodes that do nothing but bloat the bytecode and confuse analysis.


2 Bytecode Encryption:
Encrypt the bytecode and have the VM decrypt it before execution.


3 Control Flow Obfuscation:
Insert fake jumps, fake loops, and non-linear execution to break down static analysis tools.


4 Jump Table Instead of Switch:
Use a function pointer jump table rather than a simple switch-case for opcode handling.






🧪 Practical Example – Version 2 of the VM (With Fake Instructions)

📦 Sample Bytecode with Junk Instructions:

FF ; NOP_FAKE_1
01 0A ; PUSH 10
AB ; NOP_FAKE_2
01 14 ; PUSH 20
FF ; NOP_FAKE_1
02 ; ADD
CD ; NOP_FAKE_3
03 ; PRINT




📚 Instruction Set:

Opcode Mnemonic Description

01 PUSH_VAL Push value onto the stack

02 ADD Add two top values on stack

03 PRINT Print the top of the stack

FF NOP_FAKE_1 Fake NOP, does nothing

AB NOP_FAKE_2 Another fake NOP

CD NOP_FAKE_3 Yet another fake

NOP








🧠 C Implementation:

#define PUSH_VAL 0x01
#define ADD 0x02
#define PRINT 0x03
#define NOP1 0xFF
#define NOP2 0xAB
#define NOP3 0xCD
🔥2
🔐 رمزگذاری Bytecode و اجرای آن داخل ماشین مجازی




🎯 هدف:

افزایش مقاومت VM در برابر تحلیل با رمزگذاری bytecode و decrypt کردن آن در زمان اجرا




🤔 چرا این کار رو می‌کنیم؟

وقتی bytecode ما به‌صورت plaintext داخل فایل باینری قرار داشته باشه حتی اگه پیچیده باشه با کمی بررسی می‌تونن به منطق برنامه پی ببرن

ولی اگر bytecode رمز شده باشه تا زمانی که decrypt نشه هیچکس نمی‌تونه بفهمه چی قراره اجرا بشه




🔒 رمزگذاری ساده با XOR

ما از یه کلید ساده برای XOR کردن bytecode استفاده می‌کنیم. مثلاً:

کلید: 0xAA


🔧 مثال رمزگذاری‌شده:

Bytecode قبل از رمزگذاری:

01 0A 01 14 02 03

رمز شده با XOR 0xAA:

AB A0 AB BE A8 A9

حالا اینو داخل باینری قرار می‌دیم




🧪 اجرای VM با Decryption داخلی

#define KEY 0xAA

unsigned char encrypted_bytecode[] = {
0xFF, // fake nop (همچنان باقی‌ست)
0xAB, 0xA0, // push 10 (رمز شده)
0xAB, // fake
0xAB, 0xBE, // push 20 (رمز شده)
0xFF, // fake
0xA8, // add (رمز شده)
0xCD, // fake
0xA9 // print (رمز شده)
};

unsigned char bytecode[sizeof(encrypted_bytecode)];

void decrypt_bytecode() {
for (int i = 0; i < sizeof(encrypted_bytecode); i++) {
switch (encrypted_bytecode[i]) {
case 0xFF: case 0xAB: case 0xCD:
bytecode[i] = encrypted_bytecode[i]; // fake ها رمز نشده‌ان
break;
default:
bytecode[i] = encrypted_bytecode[i] ^ KEY;
}
}
}





🧠 چرا این سخت می‌کنه؟

تحلیل‌گر داخل باینری هیچ نشانه‌ای از PUSH ADD یا PRINT نمیبینه چون bytecode رمز شده‌ست
فقط اگه کد decrypt رو درک کنه و bytecode رو در حافظه بعد از رمزگشایی بررسی کنه، متوجه میشه چه خبره


🔐 Bytecode Encryption & Execution Inside a Custom Virtual Machine (VM)




🎯 Goal:

To increase the resistance of your VM against reverse engineering by encrypting the bytecode and decrypting it only at runtime.




🤔 Why Encrypt the Bytecode?

If your bytecode is stored in plaintext inside the binary, no matter how complex your VM logic is, a skilled reverse engineer can eventually understand what the code does.

But if the bytecode is encrypted, it’s completely unreadable until it’s decrypted in memory during execution — making static analysis much harder.




🔒 Simple XOR Encryption

We’ll use a basic XOR scheme for encryption.

Key: 0xAA




🔧 Example

Original Bytecode:

01 0A 01 14 02 03

Encrypted using XOR 0xAA:

AB A0 AB BE A8 A9

This encrypted version is what gets embedded into your binary.




🧪 Runtime Decryption Inside the VM

#define KEY 0xAA

unsigned char encrypted_bytecode[] = {
0xFF, // fake nop (still there)
0xAB, 0xA0, // push 10 (encrypted)
0xAB, // fake
0xAB, 0xBE, // push 20 (encrypted)
0xFF, // fake
0xA8, // add (encrypted)
0xCD, // fake
0xA9 // print (encrypted)
};

unsigned char bytecode[sizeof(encrypted_bytecode)];

void decrypt_bytecode() {
for (int i = 0; i < sizeof(encrypted_bytecode); i++) {
switch (encrypted_bytecode[i]) {
case 0xFF: case 0xAB: case 0xCD:
bytecode[i] = encrypted_bytecode[i]; // leave fake ops as is
break;
default:
bytecode[i] = encrypted_bytecode[i] ^ KEY;
}
}
}




🧠 Why Is This Effective?

There's no trace of actual instructions like PUSH, ADD, or PRINT inside the binary.

Reverse engineers can't tell what will happen until the decryption logic runs

Even then, they’ll have to hook the VM at runtime or dump memory post-decryption to figure out the logic.


This adds a solid layer of obfuscation that slows down and frustrates reverse engineering efforts — especially when combined with junk instructions or virtualization tricks
2
ساخت یک باینری نیتیو (Win32) شبیه‌ سازی‌ شده که مکانیزم‌ های واقعی حفاظت آنتی‌ دیباگ ساده چک یکپارچگی تولید سریال بر اساس اسم  داخل داشته باشه تا بدون ریسک قانونی بتونید تمرین RE و عملیات کرک‌ سازی رو انجام بدید

ویژگی‌ها:

GUI
ساده با فیلدهای Name و Serial و دکمه Activate & Play

Anti-Debug: فراخوانی
IsDebuggerPresent() قبل از فعال‌سازی

Integrity (شبیه‌سازی):
بررسی ساده مسیر exe قابل ارتقا به checksum واقعی

الگوریتم سریال: تابع gen_expected_serial که روی نام کاربر s = s*31 + ch انجام میده و بعد سه گروه 16‌بیتی با XOR/شیفت میسازه خروجی در قالب AAAA-BBBB-CCCC

پیام موفق/ناموفق با MessageBoxA.


هشدار اخلاقی/قانونی: این کد فقط برای تمرین روی باینری‌ ایه که خودتون ساختید کرک و انتشار کرک نرم‌ افزارهای تجاری غیرقانونیه


کد کامل فایل hotspot_sim.c

کد رو دقیقا در یک فایل به اسم hotspot_sim.c ذخیره کنید

// hotspot_sim.c
یک برنامه شبیه‌ سازی‌ شده برای تمرین RE

#include <windows.h>
#include <stdio.h>
#include <string.h>

// شناساگر کنترل‌ها
#define IDC_NAME  1001
#define IDC_SERIAL 1002
#define IDC_BUTTON 1003

// یه تابع ساده برای ساختن سریال مورد انتظار بر اساس اسم کاربر
// فرمت خروجی: AAAA-BBBB-CCCC (اعداد هگز چهار رقمی)
void gen_expected_serial(const char *name, char *out, size_t outlen)
{
    unsigned long s = 0;
    // جمع مقادیر کاراکترها رو می‌گیریم
    for (size_t i = 0; i < strlen(name); ++i) {
        s = s * 31 + (unsigned char)name[i]; // یه ضرب و جمع ساده
    }
    // میایم سه گروه 16 بیتی می‌سازیم
    unsigned g1 = (unsigned)((s ^ 0xA5A5A5A5) & 0xFFFF);
    unsigned g2 = (unsigned)(((s >> 3) ^ 0x5A5A5A5A) & 0xFFFF);
    unsigned g3 = (unsigned)(((s << 7) ^ 0x3C3C3C3C) & 0xFFFF);
    // قالب‌بندی نهایی
    _snprintf_s(out, outlen, _TRUNCATE, "%04X-%04X-%04X", g1, g2, g3);
}

// تابعی که سریال ورودی رو با سریال تولیدشده مقایسه می‌کنه
int verify_serial(const char *name, const char *serial)
{
    char expected[64];
    gen_expected_serial(name, expected, sizeof(expected));
    // اینجا مقایسهٔ ساده میکنیم (حساس به حروف)
    if (_stricmp(expected, serial) == 0) return 1;
    return 0;
}

// تابعی که integrity ساده رو شبیه‌سازی می‌کنه
// اینجا فقط چک می‌کنیم که exe path طول مشخصی داشته باشه (نمونه‌ست)
int integrity_ok()
{
    char path[MAX_PATH];
    if (GetModuleFileNameA(NULL, path, MAX_PATH) == 0) return 0;
    size_t L = strlen(path);
    // شبیه‌سازی: اگه طول مسیر خیلی کوتاه باشه یا خیلی بلند باشه خطا بدیم
    if (L < 5) return 0;
    return 1;
}

// پنجره اصلی و پردازش پیام‌ها
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
    case WM_CREATE:
    {
        // یک لیبل و یک فیلد برای نام
        CreateWindowA("static", "Name:", WS_VISIBLE | WS_CHILD, 10, 10, 50, 20, hWnd, NULL, NULL, NULL);
        CreateWindowA("edit", "", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
                      70, 10, 240, 22, hWnd, (HMENU)IDC_NAME, NULL, NULL);

        // لیبل و فیلد سریال
        CreateWindowA("static", "Serial:", WS_VISIBLE | WS_CHILD, 10, 40, 50, 20, hWnd, NULL, NULL, NULL);
        CreateWindowA("edit", "", WS_CHILD | WS_VISIBLE | WS_BORDER | ES_AUTOHSCROLL,
                      70, 40, 240, 22, hWnd, (HMENU)IDC_SERIAL, NULL, NULL);

        // دکمه Activate
        CreateWindowA("button", "Activate & Play", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
                      110, 80, 120, 30, hWnd, (HMENU)IDC_BUTTON, NULL, NULL);
        break;
    }
    case WM_COMMAND:
    {
        if (LOWORD(wParam) == IDC_BUTTON)
        {
            // اگه دیباگر باشه لغو می‌کنیم
            if (IsDebuggerPresent()) {
                MessageBoxA(hWnd, "Activation failed: debugger detected.", "Error", MB_OK | MB_ICONERROR);
                break;
            }

            // integrity ساده
            if (!integrity_ok()) {
                MessageBoxA(hWnd, "Activation failed: integrity check failed.", "Error", MB_OK | MB_ICONERROR);
1
                break;
            }

// گرفتن متن از کنترل‌ ها
            char name[128] = {0};
            char serial[128] = {0};
            HWND hName = GetDlgItem(hWnd, IDC_NAME);
            HWND hSerial = GetDlgItem(hWnd, IDC_SERIAL);
            GetWindowTextA(hName, name, sizeof(name));
            GetWindowTextA(hSerial, serial, sizeof(serial));

            // پاک کردن فضای خالی ابتدای/انتهای رشته
            if (strlen(name) == 0) {
                MessageBoxA(hWnd, "Please enter your name first.", "Info", MB_OK | MB_ICONINFORMATION);
                break;
            }
            if (strlen(serial) == 0) {
                MessageBoxA(hWnd, "Please enter serial.", "Info", MB_OK | MB_ICONINFORMATION);
                break;
            }

            // بررسی سریال
            if (verify_serial(name, serial)) {
                MessageBoxA(hWnd, "Activation successful! Playing...", "OK", MB_OK | MB_ICONINFORMATION);
            } else {
                MessageBoxA(hWnd, "Invalid serial!", "Error", MB_OK | MB_ICONERROR);
            }
        }
        break;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProcA(hWnd, msg, wParam, lParam);
    }
    return 0;
}

// نقطهٔ ورود برنامه ویندوزی
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
    // ساخت کلاس پنجره
    WNDCLASSA wc = {0};
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.lpszClassName = "HotspotSimClass";
    wc.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1);
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);

    if (!RegisterClassA(&wc)) return -1;

    // ساخت پنجره
    HWND hWnd = CreateWindowA("HotspotSimClass", "Hotspot Player (Simulated - Native)",
                              WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX,
                              CW_USEDEFAULT, CW_USEDEFAULT, 340, 160, NULL, NULL, hInstance, NULL);
    if (!hWnd) return -1;

    ShowWindow(hWnd, nCmdShow);
    UpdateWindow(hWnd);

    // لوپ پیام ساده
    MSG msg;
    while (GetMessageA(&msg, NULL, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessageA(&msg);
    }

    return (int)msg.wParam;
}



Build a simulated native (Win32) binary that has real anti-debug protection mechanisms, simple integrity check, serial generation based on the name inside, so you can practice RE and cracking operations without legal risk

Features:

Simple GUI
with Name and Serial fields and Activate & Play button

Anti-Debug: Call
IsDebuggerPresent() before activation

Integrity (simulation):
Simple check of exe path, upgradeable to real checksum

Serial algorithm: gen_expected_serial function that does s = s*31 + ch on the username and then creates three 16-bit groups with XOR/shift. Output in the format AAAA-BBBB-CCCC

Success/Failure message with MessageBoxA.

Ethical/Legal Warning: This code is for practice only on a binary that you have created yourself. Cracking and distributing commercial software is illegal

Full code of the hotspot_sim.c file

Save the code exactly in a file named hotspot_sim.c

// hotspot_sim.c
A simulated program for practicing RE
Simple and vernacular explanations and comments

#include <windows.h>
#include <stdio.h>
#include <string.h>

// Control identifiers
#define IDC_NAME 1001
#define IDC_SERIAL 1002
#define IDC_BUTTON 1003

// A simple function to generate the expected serial based on the user name
// Output format: AAAA-BBBB-CCCC (four-digit hex numbers)
void gen_expected_serial(const char *name, char *out, size_t outlen)
{
unsigned long s = 0;
// We take the sum of the character values
for (size_t i = 0; i < strlen(name); ++i) {
s = s * 31 + (unsigned char)name[i]; // A simple multiplication and addition
}
// Let's create three 16-bit groups
unsigned g1 = (unsigned)((s ^ 0xA5A5A5A5) & 0xFFFF);
unsigned g2 = (unsigned)(((s >> 3) ^ 0x5A5A5A5A) & 0xFFFF);
unsigned g3 = (unsigned)(((s << 7) ^ 0x3C3C3C3C) & 0xFFFF);
// Final formatting
_snprintf_s(out, outlen, _TRUNCATE, "%04X-%04X-%04X", g1, g2, g3);
}
1