Lua Virtualizer — Wirtualizacja online

Wirtualizuj, obfuskuj i chroń skrypty Lua dzięki wirtualizacji kodu, transformacjom skończonego automatu stanów (FSM), zaawansowanej obfuskacji, kontroli integralności oraz antydebugowaniu. Lua Virtualizer był już użyty 195 razy!

Kod źródłowy skrypt Lua (.lua)

Prześlij skrypt .lua

Strategie wirtualizacji

Dialekt źródła

Automatycznie próbuje źródła jako 5.1, potem 5.2, 5.3, 5.4, 5.5, a następnie Luau i zostawia najstarszy dialekt, który się parsuje. LuaJIT nie jest wnioskowany. Skrypt, którego jedyną nowszą cechą jest goto, jest wykrywany jako 5.2, bo goto pojawiło się w Lua 5.2. Wybierz LuaJIT jawnie, gdy skrypt ma być traktowany jako LuaJIT.

local a = 5
a = a + 1
print(a)
local __ins = __code[__pc]
__op, __a, __b, __c, __bx = __ins[1], __ins[2], __ins[3], __ins[4], __ins[5]
__pc = __pc + 1
__hs[__map[__op]]()

Kompiluje każdą funkcję do zaszyfrowanego kodu rejestrowego i wykonuje go interpreterem handlerów. Kodowanie opcode i wygenerowane nazwy zależą od ziarna. Suma kontrolna kodu jest sprawdzeniem integralności.

local a = 10
local b = a + 5
print(b)
repeat
  if __op < 10 then __band0()
  else if __op < 20 then __band1()
  end end
until __halt == 1

Wykonuje ten kod jako skończony automat stanów. Kody operacji są pogrupowane w stany i wykonywane z tych stanów. Kodowanie i nazwy zależą od ziarna.

local n = 1
if n > 0 then
    n = n + 2
end
print(n)
local __w = __tape[__ip]
__tape[__ip] = 0
__tape[0] = __w[__fall]
__arith(__w)

Wykonuje ten kod na jednej zmiennej taśmie. Superoperatory przesuwają taśmę i zapisują ją w trakcie wykonania. Kodowanie i nazwy zależą od ziarna.

local x = 1
local y = 2
local z = x + y
print(z)
while __block ~= 0 do
  if __block == 1 then
    __take()
    __block = __sched[__op]
  end
  if __block == 12 then
    __block = 1
  end
end

Spłaszcza dyspozycję do harmonogramu numerowanych bloków. Bieżący blok wybiera następny. Kodowanie i nazwy zależą od ziarna.

Ochrona

local function get_sum(a, b)
    return a + b
end

local x = "hello"
get_sum(1, 2)
local _g_gkq_tg = 0
local _g_vjf = 0

local function _probe()
    local info = debug.getinfo(1, "S")
    local src = (info and info.source) or ""
    local live = 0
    for i = 1, #src do
        live = (live * 33 + string.byte(src, i)) % 65536
    end
    if live ~= 0xA91C then
        _g_gkq_tg = 51
        os.exit(1)
    end
    _g_vjf = live
end

_probe()

Ostatnia faza ochrony wstawia sondę integralności, która odciska załadowany chunk. Klucze deszyfrowania i pułapek pochodzą z tego tokenu — edycja skryptu truuje późniejsze transformacje (śmieci zamiast jawnego tekstu). Opcjonalna ponowna weryfikacja haszuje ten sam chunk. Sonda nie potrzebuje osobnej kopii skryptu na dysku.

local function get_sum(a, b, c)
    return a + b
end

get_sum(1, 2, 3)
local _g_bd = 375103

local function _gom(x, y)
end

if not _g_vjf then
    pcall(get_sum, 655, 353, 571)
    pcall(_gom, -249233, -249516)
end

local function get_sum(a, b, c)
    return a + b
end

get_sum(1, 2, 3)

Późna faza łącząca helpery: honeypotowe resolvery, fałszywe wywołania z losowymi argumentami i pułapki na płytkim stosie aktywne przed bootstrapem. Normalne uruchomienie bez zmian; wyciągnięte fragmenty pozostają hałaśliwe.

Zmiana nazw

local user_count = 10
local path = "C:\\data"
print(user_count)
local _gt6188mys = 10
local _o_vd1a362a_d = "C:\\data"
print(_gt6188mys)

Zmienne lokalne i globalne otrzymują losowe identyfikatory; wszystkie odwołania są automatycznie poprawiane. Zasięg zmiennych pozostaje zachowany.

local function get_tag(name)
    return name
end

get_tag("x")
local function get_tag(_gt6188mys)
    return _gt6188mys
end

get_tag("x")

Nazwy parametrów i powiązania w ciele funkcji zastąpione zostaną losowymi ciągami znakowymi.

local function invoke_work()
    return 1
end

invoke_work()
local function _gt6188myssr_v()
    return 1
end

_gt6188myssr_v()

Nazwy funkcji zmienione, a wszystkie wywołania przekierowane; metametody takie jak __index i wbudowane funkcje bibliotek pozostają nietknięte.

local function work()
    return 1
end

work()
local function work()
    return 1
end

local _calls = { a1 = work }
_calls.a1()

Bezpośrednie wywołania są przepisywane tak, aby cel był wyszukiwany w tabeli nazw/dyspozycji zamiast bezpośredniego identyfikatora.

function alpha()
    return beta()
end

function beta()
    return 1
end

alpha()
local function beta()
    return 1
end

local function alpha()
    return beta()
end

alpha()

Permutuje definicje funkcji najwyższego poziomu, gdy kolejność wykonania nadal jest poprawna.

local MAX_RETRIES = 5
for i = 1, MAX_RETRIES do
    print(i)
end
for i = 1, 5 do
    print(i)
end

Stałe zdefiniowane na poziomie modułu są wstawiane bezpośrednio w miejscu użycia, a nieużywane deklaracje usuwane.

Wybiera schemat nazewnictwa używany przez wszystkie strategie zmiany nazw (zmienne, parametry, funkcje).

Ciągi znakowe

local msg = "SecretKey"
print(msg)
local msg = "Se" .. "cr" .. "etK" .. "ey"
print(msg)

Literały tekstowe zostaną podzielone i składane w trakcie działania z mniejszych fragmentów.

local msg = "Secret"
print(msg)
local src, msg = "Tfdsfu", ""
for i = 1, #src do
    msg = msg .. string.char(string.byte(src, i) - 1)
end
print(msg)

Literały zostaną zapisane jako przesunięte znaki, odtwarzane niewielką transformacją w czasie działania.

local x = "payload"
print(x)
local function _gt6188myssr_v()
    local d = {257, 278, 190, 269, 272, 278, 277}
    local r = ""
    for i = 1, #d do
        local v = (d[i] - 189) % 256
        r = r .. string.char(v)
    end
    return r
end

local x = _gt6188myssr_v()
print(x)

Każde uruchomienie generuje unikalny deszyfrator ciągów z losowo dobranymi pętlami bitowymi — żadne dwa wyniki nie mają tego samego kształtu algorytmu.

local v = "Vault"
print(v)
local v = string.char(86, 97, 117, 108, 116)
print(v)

Łańcuchy odtwarzane z kodów znaków zamiast tekstu w cudzysłowie.

Wartości numeryczne

local k = 42
local r = k + 10
print(r)
local k = (2 * 21)
local r = k + (-370068 + 370078)
print(r)

Proste liczby całkowite zastępowane równoważnymi wyrażeniami arytmetycznymi i bitowymi.

local pi = 3.14
print(pi)
local pi = math.pow(10, math.log(3.14 * 0.3453511647258002 * 2.89560338038522) / math.log(10))
print(pi)

Liczby zmiennoprzecinkowe zostaną zapisane równoważnymi wyrażeniami biblioteki math, więc stałe nie są już widoczne jako zwykłe wartości.

local x = 10
print(x)
local x = math.floor(math.ceil((10.0 + 31.866664 - 31.866664) - 0.3803732855154077))
print(x)

Liczby całkowite odtwarzane z wyrażeń zmiennoprzecinkowych biblioteki math i rzutowane przez math.floor.

local n = 100
local m = 200
print(n + m)
local _gt6188mys = {{7648, 8688, 7405}, {-9849, 200, 100}}
local n = _gt6188mys[2][3]
local m = _gt6188mys[2][2]
print(n + m)

Wartości trafiają do tablic i są odczytywane po indeksie, razem z celowo zaciemnionymi wpisami.

local a = 2.5
local b = 1.25
print(a + b)
local _gt6188mys = {{7649.9344, 7405.0075, -703.1368}, {9032.1081, 1.25, 2.5}}
local a = _gt6188mys[2][3]
local b = _gt6188mys[2][2]
print(a + b)

Liczby zmiennoprzecinkowe zostaną przeniesione do tablic razem z celowo zaciemnionymi wartościami pomiędzy.

local v = 255
print(v)
local v = _bxor(_bxor(255, 3832), 3832)
print(v)

Odwracalne maski bitowe nie zmieniają wartości liczbowych. Helper jest dobierany do wybranego dialektu.

local r = a + b
local m = a - b
local r = (a + b + 0) - ((a - a) * (b - b))
local m = (a + (0 - b))

Operatory binarne zastępowane są równoważnymi tożsamościami mieszanymi boolowsko-arytmetycznymi, ukrywając oryginalny operator przed statycznym dopasowywaniem wzorców.

Szyfrowane literały tablicowe

local ports = {21, 80, 443}
print(ports[1])
local ports = {43668, 43701, 43874}
for i = 1, #ports do
    ports[i] = _bxor(ports[i], 0xAAAA)
end
print(ports[1])

Zawartość tablic liczb całkowitych jest przechowywana w formie zaszyfrowanej i dekodowana w trakcie działania przed użyciem.

local token = {"A", "P", "I"}
print(token[3])
local token = {0x1A, 0x0B, 0x12}
for i = 1, #token do
    token[i] = string.char(_bxor(token[i], 0x5B))
end
print(token[3])

Literały tablic znaków zastępowane są zaszyfrowanymi wartościami i odtwarzane tylko przez wygenerowany kod dekodujący.

local scale = {1.5, 2.75}
print(scale[2])
local data = {4609434218613702656, 4613374868287651840}
local scale = {}
for i = 1, #data do
    scale[i] = _decode_double(_bxor(data[i], 0x55AA))
end
print(scale[2])

Stałe tablic zmiennoprzecinkowych są ukrywane jako zakodowane wzorce bitowe i odtwarzane w trakcie działania programu.

local roles = {"admin", "guest"}
print(roles[1])
local roles = {"\71\112\004\0\7", "\71\14\12\26\27"}
for i = 1, #roles do
    local o = ""
    for j = 1, #roles[i] do
        o = o .. string.char(_bxor(string.byte(roles[i], j), 7))
    end
    roles[i] = o
end
print(roles[1])

Elementy tablic tekstowych są szyfrowane niezależnie i odtwarzane na żądanie przez wygenerowany kod w trakcie działania.

Pule wartości-przynęt

local x = 1
print(x)
local _pool_a = {93817, 205, -4471, 88123}
local _pool_b = {"q1w2", "z9x8"}
local x = 1
print(x)

Włącza pule szumu na poziomie modułu, kontrolowane przez poniższe opcje.

local x = 1
local _pool_ints = {93817, 205, -4471, 88123, 620}
local x = 1

Nieużywane przynęty liczbowe dodawane do puli.

local x = 1
local _pool_arrays = {{1, 2, 3}, {7, 8}, {}}
local x = 1

Nieużywane przynęty w postaci tablic dodawane do puli.

local x = 1
local function _pool_fn_a()
    return 0
end

local x = 1

Nieużywane funkcje-przynęty dodawane do puli.

local x = 1
local _pool_chars = {"q", "z", "#", "9"}
local x = 1

Nieużywane przynęty znakowe dodawane do puli.

local x = 1
local _pool_rx = {"(a|b)+", "[0-9]{3,}\\.lua"}
local x = 1

Literały o kształcie wyrażeń regularnych mające dezorientować naiwne skanery sygnaturowe.

local x = 1
local _pool_paths = {"~/.config/app/run", "/etc/xdg/autostart/app.desktop"}
local x = 1

Przynęty tekstowe przypominające ścieżki autostartu/trwałości, nigdy nie używane przez działający kod.

Nieprzezroczyste predykaty i szum

if flag then
    x = 1
else
    x = 2
end
x = flag and 1 or 2

Proste rozgałęzienia są zwijane w wyrażenia warunkowe, łącząc logikę przypisania w jedną nieprzezroczystą linię.

if true then
    print(1)
end
if not (1 == 0) then
    print(1)
end

Proste testy true/false zastępowane dłuższymi, równoważnymi warunkami.

local s = 0
s = s + 1
s = s + 2
s = s + 4
print(s)
local s = 0
s = s + 1
if #"HP7ZVzxW" < 7 then
    s = s - 99
end
s = s + 2
if math.floor(3.14159) == 4 then
    s = 0
end
s = s + 4
print(s)

Gałęzie zawsze fałszywe wyglądające jak prawdziwy przepływ — zwiększają złożoność grafu.

local function total(a, b)
    return a + b
end
local function _qx_rf7mz(acc, s)
    return acc + ((s % 32768) + 1)
end

local function total(a, b)
    local v = _qx_rf7mz(0, -402184)
    if v == -8372144055286123915 then
        return 0
    end
    return a + b
end

Dodaje statyczne funkcje-miksery, wywołuje ich łańcuchy wewnątrz funkcji i wstawia zabezpieczenia, które nigdy się nie wykonają, zwiększając ilość kodu do przeanalizowania.

local x = 1
print(x)
local x = 1
local _lvl_x = 55 * 457 + 17
local _lvl_s = math.abs(_lvl_x - 791)
print(x)

Generowane fałszywe instrukcje pod zawsze-fałszywymi warunkami, niezmieniające działania skryptu, zwiększające rozmiar i utrudniające analizę.

local x = 1
print(x)
local x = 1
local function _finally()
    local _ = {}
end
pcall(function() end)
_finally()
print(x)

Lua nie ma try/finally, więc przebieg wstawia obojętne pcall i sprzątanie. Dodatkowe zagnieżdżenie nie zmienia wartości.

Przynęty

local function real_core()
    return 42
end

real_core()
local function _q6188myssr_v(sa, n)
    local bag = { bq_vp = 556, zv_nvf = n }
    return bag
end

local function real_core()
    return 42
end

real_core()

Wiarygodnie wyglądające funkcje nigdy nie wywoływane — szum utrudniający analizę.

local x = 1
print(x)
local _s88 = function()
    return (173 + 190) - (173 + 190) + 54
end
local x = 1
print(x)

Nieużywane funkcje anonimowe na początku — nigdy nie wywoływane w runtime.

local x = 1
print(x)
local _pad = [[
 disable_app_background_task get_kds_root_key stop_pcsv_device
]]
local _ = #_pad
local x = 1
print(x)

Duża stała tekstowa przesuwa numery linii i powiększa plik — logika jej nie czyta.

local x = 1
print(x)
-- require("cached_quay_19422")
-- local quarantine = require("nested.profile")
local x = 1
print(x)

Komentarze imitują dodatkowe wywołania require. Usuwane przy włączonym usuwaniu komentarzy.

local os = require("os")
os.clock()
local os = require("os")
os["cl" .. "ock"]()

Cele wywołań budowane są z fragmentów i rozwiązywane przez odczyt z tabeli — statyczne wyszukiwanie po nazwach funkcji już nie zadziała.

local cjson = require("cjson")
local cjson = _G["require"]("cj" .. "son")

Przepisuje require tak, aby nazwa modułu powstawała w czasie działania. Nazwy modułów zostają jako ciągi pod późniejsze szyfrowanie.

local x = 1
print(x)
if false then
    local function _on_tick(evt)
        return 1
    end
    local _handlers = { tick = _on_tick }
end
local x = 1
print(x)

Fragmenty przypominają rejestrację callbacków/handlerów, ale nigdy nie podpinają prawdziwego źródła zdarzeń.

Inne
Wykrywaj debuggery, wirtualne maszyny, piaskownice (tzw. sandbox) i emulatory CPU

  • Wykrywaj debuggery dołączone do procesu (debug.gethook(); Windows IsDebuggerPresent)
  • Sprawdzaj podejrzanie małą liczbę rdzeni procesora (wyjdź jeśli jest ich mniej niż 3)
  • Sandboxie (biblioteki DLL SbieDll / SbieHide, rejestr)
  • Joe Sandbox (procesy)
  • Host / użytkownik / ścieżki / zmienne środowiskowe typowe dla analizy (Cuckoo i podobne)

  • VMware (procesy, pliki, sterowniki, rejestr BIOS, MAC OUI, ciągi platformy)
  • Oracle VirtualBox (procesy, pliki, sterowniki, rejestr BIOS)
  • Parallels (procesy, pliki)
  • KVM (sterowniki)
  • Linux DMI (nazwa produktu / dostawcy), gdy nie na Windows
  • WINE (niekonsystencje w funkcjach API, biblioteki DLL, specjalne funkcje API)
  • Bochs / QEMU (procesy, rejestr BIOS)
  • XEN (procesy)
  • Znaczniki emulatorów CPU/trybu użytkownika i krótki test czasu

Dodany kod jest uruchamiany na początku skryptu. W przypadku pozytywnej detekcji proces zostanie po cichu zamknięty bez jakiegokolwiek komunikatu błędu.

-- secret plan
local x = 1 -- inline
print(x)
local x = 1
print(x)

Komentarze (--) usuwane z wygenerowanego skryptu.

Masz pytania?

Jeśli chcesz zapytać o Lua Virtualizer, napisz do mnie.