12 Temmuz 2019 Cuma

Dinamik Olarak Oluşturan Kodu Çalıştırmak ve JIT mimarisine Düşük Seviyeli Bir Bakış

Hafıza yönetim API'lerinden olan VirtualAlloc fonksiyonunun dokümantasyonunda aşağıdaki gibi açıklama var.

To execute dynamically generated code, use VirtualAlloc to allocate memory and the VirtualProtect function to grant PAGE_EXECUTE access.

Burada hepimizin anlayacağı üzere diyor ki;
Dinamik olarak oluşturulan kodu çalıştırmak için, VirtualAlloc fonksiyonu ile hafıza da tahsis yap ve VirtualProtect fonksiyonu ile de PAGE_EXECUTE yetkisi ver.

Tamam da burada dinamik olarak oluşturulan kod nasıl bir kod olmalı? Java , Delphi, C, C++, C# yoksa Javascript ya da başka bir programlama dili kodu mu?
Elbette hiçbiri değil, CPU'nun anlayacağı kodlar olmalı.

Öyle ise; CPU'nun anlayacağı kodları oluşturup, çalıştırmaya çalışalım. 5 + 4 işlemini CPU'nun anlayacağı şekle dönüştürüp, çalıştırmasını isteyeceğiz.

mov eax,5
add eax,4
ret
Yukarıdaki kodda eax register'a 5 değerini yazıyoruz.Daha sonra ise add komutu ile de üzerine 4 ekliyoruz. ret komutu ile de eax register'daki son değeri geriye döndürüyoruz.



Yanlız buraya kadar yazdığımız kodlar yine CPU'nun anlayabileceği kodlar değil. Yukarıda yazdığımız mov eax,5 ... gibi sembolik kodları VirtualAlloc fonksiyonu ile çalıştırmak istersek yine çalıştıramayacağız.

77C3DBA8 | B8 05 00 00 00           | mov eax,5                               |
77C3DBAD | 83 C0 04                 | add eax,4                               |
77C3DBB0 | C3                       | ret                                     |

Delphi Byte Array

Array [1..9] of Byte = (
$B8, $05, $00, $00, $00, $83, $C0, $04, $C3
);

C/C++ Byte Array

{
0xB8, 0x05, 0x00, 0x00, 0x00, 0x83, 0xC0, 0x04, 0xC3
};
O nedenle yukarıdaki görselde görüleceği üzere asm kodları yerine, byte karşılığını alıp, VirtualAlloc fonksiyonu ile hafıza da tahsis ettiğimiz alana yerleştireceğiz, sonrasında flProtect parametresine de Memory Protection Constants değerlerinden PAGE_EXECUTE ile başlayan değerlerden birini parametre olarak geçeceğiz. O zaman bu cek-caklı vaatlerimizi bir kenara bırakıp, kodlamaya başlayalım.

Aşağıda işletim sistemi api'lerinin kullanımına olanak veren, bildiğim dillerde hazırladığım örnekleri inceleyebilirsiniz.

C#

using System;
using System.IO;
using System.Net;
using System.Runtime.InteropServices;

unsafe class Program
{
    // winnt.h    
    const int MEM_COMMIT = 0x00001000;
    const int MEM_RESERVE = 0x00002000;
    const int MEM_RELEASE = 0x00008000;
    const int PAGE_EXECUTE_READWRITE = 0x40;

    [DllImport("kernel32.dll", SetLastError = true)]
    unsafe static extern IntPtr VirtualAlloc(void* lpAddress, UIntPtr dwSize, int flAllocationType, int flProtect);

    [DllImport("kernel32.dll", SetLastError = true)]
    unsafe static extern bool VirtualFree(IntPtr lpAddress, UIntPtr dwSize, int dwFreeType);


    public delegate int FunctionPtr();
    static byte[] ByteCode = new byte[] { 0xB8, 0x05, 0x00, 0x00, 0x00, 0x83, 0xC0, 0x04, 0xC3 };

    const string FileName = "ByteCode.bin";
    const string ByteCodeURL = "https://github.com/ismailkocacan/Experiment/tree/master/JIT_ExecuteDynamicallyGeneratedCode/tests/" + FileName;

    static void Main(string[] args)
    {
        IntPtr P  = VirtualAlloc(null, (UIntPtr)ByteCode.Length, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
        Marshal.Copy(ByteCode, 0, P, ByteCode.Length);
        FunctionPtr functionPtr = (FunctionPtr)Marshal.GetDelegateForFunctionPointer(P, typeof(FunctionPtr));
        int result = functionPtr();
        VirtualFree(P, (UIntPtr)ByteCode.Length, MEM_RELEASE);
    }

    static byte[] GetByteCode()
    {
        WebClient client = new WebClient();
        byte[] codeData = client.DownloadData(ByteCodeURL);
        return codeData;
    }

    static void SaveToFile()
    {
        File.WriteAllBytes(FileName, ByteCode);
    }
}


C

#include <Windows.h>

typedef int(*FunctionPtr)();

byte BYTE_CODE[] = { 0xB8, 0x04, 0x00, 0x00, 0x00, 0x83, 0xC0, 0x03, 0xC3 };
const int CODE_SIZE = sizeof BYTE_CODE;

int main()
{
 PVOID P = VirtualAlloc(NULL, CODE_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 memcpy(P, BYTE_CODE, CODE_SIZE);
 FunctionPtr functionPtr = (FunctionPtr)P;
 int result = functionPtr();
 VirtualFree(P, CODE_SIZE, MEM_RELEASE);
}

Delphi

program Project2;
 
{$APPTYPE CONSOLE}
{$R *.res}
 
uses
 Winapi.Windows,
 System.SysUtils;
 
type
 TFunctionPtr = function: Integer;
 
const
 BYTE_CODE: Array [1 .. 9] of Byte = ($B8, $05, $00, $00, $00, $83, $C0, $04, $C3);
 CODE_SIZE = sizeof(BYTE_CODE);
 
var
 P: Pointer;
 Result: Integer;
 FunctionPtr: TFunctionPtr;
begin
 P := VirtualAlloc(nil, CODE_SIZE, MEM_COMMIT, PAGE_EXECUTE_READWRITE);
 CopyMemory(P, @BYTE_CODE, CODE_SIZE);
 FunctionPtr := TFunctionPtr(P);
 Result := FunctionPtr();
 VirtualFree(P, CODE_SIZE, MEM_RELEASE);
end.

Yukarıda yaptıklarımızı kısaca açıklayacak olursak;
- VirtualAlloc ile hafızada CODE_SIZE array'in boyu kadar yer açtık.

- PAGE_EXECUTE_READWRITE değerini geçerek bu hafıza bloğunun çalıştırabilir, okunup , yazılabilir olduğunu belirttik.

- CopyMemory ile çalıştırmak istediğimiz kodları hafızaya yerleştirdik.

- VirtualAlloc bize hafızada tahsis ettiği bloğunun başlagıç pointer'ını döndürmüştü.O adreside bir fonksiyon pointer'ına cast edip, çağırdık.

Konunun başlığı "Dinamik Olarak Oluşturan Kodu Çalıştırmak ve JIT mimarisine Düşük Seviyeli Bir Bakış" yazıyor ama ben debugger'da kodu kendim manuel yazdım.
Bu nasıl dinamik oluşturma diyebilirsiniz.Evet haklısınız.
Şimdi kodu dinamik olarak bize oluşturan bir Assembler'a ihtiyacımız var...
"Burada biz daha çok çalıştırma kısmını incelemiş olduk."

Dahası; .Net ve Java gibi run-time bağlı dillerde kullanılan bir teknik. Derlenmiş MSIL ya da Java Byte kodu tekrar CPU'nun anlayacağı koda dönüştürüp çalıştırma işi, işte bu şekilde gerçekleşiyor.Başka bir deyişle "JIT Compile" diye ifade edilmekte...

Şuanda aklıma gelen, gelmeyen pek çok farklı amaç için kullanılabilir.
Injection, dağıtık kod, sanal makine vs.
Kaynak kodlara buradan ulaşabilirsiniz.