Obliczanie stałych i odpowiedniki instrukcji

0

Rozwijam generator metamorficzny do swojego oprogramowania, jest to biblioteka, która bierze na wejściu binarny kod x86, analizuje go i tworzy zmutowaną (polimorficzną) kopię tego kodu, która w domyśle ma utrudniać jego analizę w deasemblerach oraz debuggerach. Efekty pracy są naprawdę ciekawe, z jednej instrukcji assemblera potrafi wygenerować nawet 1 MB wyjściowy kod o dokładnie takiej samej funkcjonalności.

Często szukam interesujących rozwiązań, które mógłbym dodać do biblioteki. Jednym z elementów silnika jest zamiana konkretnych instrukcji na ich odpowiedniki.

Przykład - zerowanie rejestru procesora x86

; proste rozwiazania
xor eax,eax

sub eax,eax

and eax,0

lea eax,[0]

; bardziej rozbudowane
or   eax,-1
not eax

or   eax,-1
xor eax,-1

and eax,7FFFFFFFh
sar  eax,31

or  eax,-1
inc eax

Czy znacie może jakieś interesujące algorytmy, które pozwalałyby uzyskiwać stałe wartości (0, 1, -1 etc.), właśnie przez zastępowanie oczywistych instrukcji serią innych, równoznacznych.

Druga rzecz czy znacie jakieś kreatywne metody na uzyskanie funkcjonalności podstawowych instrukcji:

  1. XOR A, B
  2. AND A, B
  3. OR A,B
  4. NOT A
  5. NEG A
  6. ADD A, B
  7. SUB A, B
  8. CMP A, B
  9. TEST A, B
  10. INC A
  11. DEC A

Mam całkiem dużo różnych kombinacji dla sporej gamy instrukcji, jednak jak pisałem wyżej, zawsze poszukuję nowych i interesujących rozwiązań.

6
; proste rozwiazania
xor eax,eax
 
sub eax,eax
 
and eax,0
 
lea eax,[0]
 
; bardziej rozbudowane
or   eax,-1
not eax
(...)

Nie wiem czy robisz specjalne checki na to, ale przecież w ogólności takie podstawienie jest niepoprawne:

  • NOT i LEA nie modyfikują flag
  • XOR i AND zmieniają CF, OF, PF, SF i ZF, a stan AF jest undefined
  • SUB zmienia AF, CF, OF, PF, SF, ZF jak leci.

W większości "typowego" kodu generowanego przez kompilator wszystko będzie ok bo większość rzeczy polega na samym ZF tuż przed skokiem, ale i tak ryzykownie.

Czy znacie może jakieś interesujące algorytmy, które pozwalałyby uzyskiwać stałe wartości (0, 1, -1 etc.), właśnie przez zastępowanie oczywistych instrukcji serią innych, równoznacznych.

Może rekurencyjnie? Skoro już masz kod do podmieniania normalnych wyrażeń, powinno się go dać zastosować elegancko również do generowania stałych.

(Uwaga: to poniżej to miał być 30-linijkowy POC. Nie wyszło, udało mi się powstrzymać dopiero po 130 linijkach. Jakoś tak szybko sie pisało)

import random
from numbers import Number

def add(a, b, t):
    t -= 1
    if t < 0:
        return 'add', a, b
    return sub(var(a, t), neg(var(b, t), t), t)

def sub(a, b, t):
    t -= 1
    if t < 0:
        return 'sub', a, b
    return add(var(a, t), neg(var(b, t), t), t)

def neg(a, t):
    t -= 1
    choice = random.randint(0, 2)
    if t < 0:
        return 'neg', a
    if choice == 0:
        return mul(var(a, t), -1, t)
    return sub(0, var(a, t), t)

def const(a, t):
    n = random.randint(0, 2**30)
    t -= 1
    choice = random.randint(0, 6)
    if t < 0:
        return a
    if choice == 0:
        return sub(a+n, n, t)
    if choice == 1:
        return add(a-n, n, t)
    if choice == 2 and a % 2 == 0:
        return add(a, a, t)
    if choice == 3:
        return xorb(a ^ n, n, t)
    if choice == 4:
        return orb(a & 0xAAAA, a & 5555, t)
    return neg(-a, t)

def var(a, t):
    if isinstance(a, Number) and random.choice([True, False]):
        return const(a, t)
    t -= 1
    choice = random.randint(0, 7)
    if t < 0:
        return a
    if choice == 0:
        return sub(a, 0, t)
    if choice == 1:
        return add(a, 0, t)
    if choice == 2:
        return mul(a, 1, t)
    if choice == 3:
        return xorb(a, 0, t)
    if choice == 4:
        return andb(a, a, t)
    if choice == 5:
        return orb(a, a, t)
    if choice == 6:
        return orb(a, 0, t)
    return neg(neg(a, t), t)
    
def mul(a, b, t):
    def expand(c, a, t):
        if c <= 0:
            return 0
        print c, a, t
        return add(a, expand(c-1, a, t-1), t-1)
    t -= 1
    if t < 0:
        return 'mul', a, b
    if isinstance(a, Number) and a < 10 and a > 2:
        return expand(a, b, t)
    if isinstance(b, Number) and b < 10 and b > 2:
        return expand(b, a, t)
    return 'mul', var(b, t), var(a, t)

def xorb(a, b, t):
    t -= 1
    if t < 0:
        return 'xor', a, b
    return andb(orb(var(a, t), var(b, t), t), notb(andb(var(a, t), var(b, t), t), t), t)

def andb(a, b, t):
    t -= 1
    choice = random.randint(0, 2)
    if t < 0:
        return 'and', a, b
    if choice == 0:
        return notb(orb(notb(var(a, t), t), notb(var(b, t), t), t), t)
    return andb(var(b, t), var(a, t), t)

def orb(a, b, t):
    t -= 1
    choice = random.randint(0, 2)
    if t < 0:
        return 'or', a, b
    if choice == 0:
        return notb(andb(notb(var(a, t), t), notb(var(b, t), t), t), t)
    return andb(var(b, t), var(a, t), t)

def notb(a, t):
    t -= 1
    choice = random.randint(0, 1)
    if t < 0:
        return 'not', a
    return xorb(var(a, t), 0xFFFFFFFF, t)
    
def generate(operation):
    if isinstance(operation, Number) or isinstance(operation, basestring):
        return [('push', str(operation))]
    result = []
    for operand in operation[1:]:
        result += generate(operand)
    if len(operation) == 2:
        result.append(('pop', 'eax'))
        result.append((operation[0], 'eax'))
    elif len(operation) == 3:
        result.append(('pop', 'eax'))
        result.append(('pop', 'ebx'))
        result.append((operation[0], 'eax', 'ebx'))
    else:  # hack, we do not support operations with > 2 operands
        raise Exception('not supported')
    return result

def dump(code):
    for opcode in generate(code):
        print ' '.join(opcode)

Najpierw rozwijamy każde wyrażenie rekurencyjnie, aż do osiągnięcia jakiegoś limitu (tutaj ograniczenie to zmienna t przekazywana ciągle, czyli maksymalne zagnieżdżenie rekurencji), a później za pomoca stosu wyliczamy jego wartość.

Przykładowo:

>>> add('eax', 'ebx', 4)
('add', ('and', ('add', ('not', ('or', ('not', ('and', ('add', 0, 0), ('and', 'eax', 'eax'))), ('not', ('xor', ('not', ('or', ('not', ('xor', 'eax', 0)), ('not', ('or', 0, 0)))), 4294967295L)))), ('neg', 0)), ('add', ('not', ('or', ('not', ('and', ('add', 0, 0), ('and', 'eax', 'eax'))), ('not', ('xor', ('not', ('or', ('not', ('xor', 'eax', 0)), ('not', ('or', 0, 0)))), 4294967295L)))), ('neg', 0))), ('neg', ('sub', 0, ('add', ('add', 0, ('neg', ('or', ('sub', 0, ('sub', 0, ('sub', ('neg', 0), ('neg', ('sub', 0, ('mul', ('and', ('sub', ('or', 'ebx', 'ebx'), ('neg', ('sub', 0, ('sub', 0, 0)))), ('sub', ('or', 'ebx', 'ebx'), ('neg', ('sub', 0, ('sub', 0, 0))))), 1)))))), 0))), 0))))
>>> const(444, 4)
('add', ('and', ('sub', 0, -848737611), ('sub', 0, -848737611)), ('neg', ('sub', 0, ('sub', ('mul', -1, ('or', ('mul', 1, 848737167), ('mul', 1, 848737167))), 0))))
>>> const(444, 4)
('and', ('sub', ('and', ('sub', ('sub', -411900607, ('neg', 679890536)), 0), ('or', ('sub', 0, -267989525), 0)), 0), ('or', ('and', ('or', ('or', ('xor', ('not', ('and', ('not', ('xor', ('or', ('sub', -5011564, ('neg', 273001493)), 0), 4294967295L)), ('not', ('xor', ('mul', ('sub', 47352906, ('neg', 220636619)), 1), 4294967295L)))), 4294967295L), 0), 4294967295L), ('not', ('and', ('or', ('xor', ('not', ('and', ('not', ('xor', ('or', ('sub', -5011564, ('neg', 273001493)), 0), 4294967295L)), ('not', ('xor', ('mul', ('sub', 47352906, ('neg', 220636619)), 1), 4294967295L)))), 4294967295L), 0), 4294967295L))), ('and', ('or', ('or', ('xor', ('not', ('and', ('not', ('xor', ('or', ('sub', -5011564, ('neg', 273001493)), 0), 4294967295L)), ('not', ('xor', ('mul', ('sub', 47352906, ('neg', 220636619)), 1), 4294967295L)))), 4294967295L), 0), 4294967295L), ('not', ('and', ('or', ('xor', ('not', ('and', ('not', ('xor', ('or', ('sub', -5011564, ('neg', 273001493)), 0), 4294967295L)), ('not', ('xor', ('mul', ('sub', 47352906, ('neg', 220636619)), 1), 4294967295L)))), 4294967295L), 0), 4294967295L)))))
>>> dump(andb('eax', 'ebx', 2))
push eax
push 1
pop eax
pop ebx
mul ebx eax
push eax
push ebx
push 0
pop eax
pop ebx
or ebx eax
push eax
pop eax
pop ebx
and ebx eax
push eax
>>> dump(const(666, 3))
push -772936222
push 985105622
pop eax
pop ebx
add ebx eax
push eax
push 8202
push 4114
pop eax
pop ebx
or ebx eax
push eax
push -1
pop eax
pop ebx
mul ebx eax
push eax
pop eax
neg eax
push eax
pop eax
pop ebx
sub ebx eax
push eax

W ten sposób z prostego dodawania/stałej możemy zrobić dowolnie skomplikowany kod.

---KONIEC POSTA---

OFFTOPIC:
Podstawowy problem z takim zabezpieczeniem to na pewno to, że jest to jak najbardziej odwracalne. Wystarczy np. wczytać taki megabajtowy ASM, wyeksportować np. do asemblera llvm, uruchomić kompilator z maksymalną optymalizacja i odebrać gotowy wynik.

Z ciekawości na ile praktyczna jest taka deobfuskacja, zrobiłem sobie eksport do C (nie chciało mi się bawić z konwerowaniem wyjścia do LLVM więc eksport z poziomu wyrażenia:)

def export_c(operation):
    if isinstance(operation, Number) or isinstance(operation, basestring):
        return str(operation)
    return '{0}({1})'.format(operation[0], ','.join(export_c(x) for x in operation[1:]))

export_c(const(666, 3))

Eksportujemy sobie dowolne wyrażenie do C, a później kompilujemy:

#include <stdint.h>
#include <stdio.h>

uint32_t add(uint32_t a, uint32_t b) { return a + b; }
uint32_t sub(uint32_t a, uint32_t b) { return a - b; }
uint32_t mul(uint32_t a, uint32_t b) { return a * b; }
uint32_t and(uint32_t a, uint32_t b) { return a & b; }
uint32_t or(uint32_t a, uint32_t b) { return a | b; }
uint32_t xor(uint32_t a, uint32_t b) { return a ^ b; }
uint32_t neg(uint32_t a) { return -a; }
uint32_t not(uint32_t a) { return ~a; }

int main() {
    printf("%d", sub(or(add(add(0,neg(neg(neg(add(0,neg(neg(neg(sub(or(0,0),neg(mul(or(and(0,-666),and(0,-666)),-1))))))))))),neg(0)),add(add(0,neg(neg(neg(add(0,neg(neg(neg(sub(or(0,0),neg(mul(or(and(0,-666),and(0,-666)),-1))))))))))),neg(0))),neg(sub(0,and(add(0,neg(or(add(sub(sub(0,0),neg(sub(0,neg(neg(add(sub(add(add(and(mul(0,-1),mul(0,-1)),neg(mul(sub(add(0,neg(or(sub(add(sub(sub(xor(sub(-881620716,neg(881621382)),0),neg(sub(0,mul(add(0,neg(and(and(or(0,0),not(and(0,0))),and(or(0,0),not(and(0,0)))))),1)))),0),neg(sub(0,or(0,0)))),neg(0)),sub(add(sub(sub(xor(sub(-881620716,neg(881621382)),0),neg(sub(0,mul(add(0,neg(and(and(or(0,0),not(and(0,0))),and(or(0,0),not(and(0,0)))))),1)))),0),neg(sub(0,or(0,0)))),neg(0))))),0),-1))),0),neg(sub(0,neg(0)))),neg(0))))))),neg(0)),add(sub(sub(0,0),neg(sub(0,neg(neg(add(sub(add(add(and(mul(0,-1),mul(0,-1)),neg(mul(sub(add(0,neg(or(sub(add(sub(sub(xor(sub(-881620716,neg(881621382)),0),neg(sub(0,mul(add(0,neg(and(and(or(0,0),not(and(0,0))),and(or(0,0),not(and(0,0)))))),1)))),0),neg(sub(0,or(0,0)))),neg(0)),sub(add(sub(sub(xor(sub(-881620716,neg(881621382)),0),neg(sub(0,mul(add(0,neg(and(and(or(0,0),not(and(0,0))),and(or(0,0),not(and(0,0)))))),1)))),0),neg(sub(0,or(0,0)))),neg(0))))),0),-1))),0),neg(sub(0,neg(0)))),neg(0))))))),neg(0))))),add(0,neg(or(add(sub(sub(0,0),neg(sub(0,neg(neg(add(sub(add(add(and(mul(0,-1),mul(0,-1)),neg(mul(sub(add(0,neg(or(sub(add(sub(sub(xor(sub(-881620716,neg(881621382)),0),neg(sub(0,mul(add(0,neg(and(and(or(0,0),not(and(0,0))),and(or(0,0),not(and(0,0)))))),1)))),0),neg(sub(0,or(0,0)))),neg(0)),sub(add(sub(sub(xor(sub(-881620716,neg(881621382)),0),neg(sub(0,mul(add(0,neg(and(and(or(0,0),not(and(0,0))),and(or(0,0),not(and(0,0)))))),1)))),0),neg(sub(0,or(0,0)))),neg(0))))),0),-1))),0),neg(sub(0,neg(0)))),neg(0))))))),neg(0)),add(sub(sub(0,0),neg(sub(0,neg(neg(add(sub(add(add(and(mul(0,-1),mul(0,-1)),neg(mul(sub(add(0,neg(or(sub(add(sub(sub(xor(sub(-881620716,neg(881621382)),0),neg(sub(0,mul(add(0,neg(and(and(or(0,0),not(and(0,0))),and(or(0,0),not(and(0,0)))))),1)))),0),neg(sub(0,or(0,0)))),neg(0)),sub(add(sub(sub(xor(sub(-881620716,neg(881621382)),0),neg(sub(0,mul(add(0,neg(and(and(or(0,0),not(and(0,0))),and(or(0,0),not(and(0,0)))))),1)))),0),neg(sub(0,or(0,0)))),neg(0))))),0),-1))),0),neg(sub(0,neg(0)))),neg(0))))))),neg(0))))))))));
}
$ gcc test.c -O3
$ ./a.exe
666

Wynik kompilacji:

mov     [esp+10h+var_C], 29Ah
mov     [esp+10h+var_10], offset aD ; "%d"
call    printf

:(.

Ok, ale to w sumie nie był poziom asemblera. Postanowiłem więc wskoczyć na poziom niżej, i przeprowadzić porządną podmianę asma na maszynę rejestrową. Dalej byłem zbyt leniwy na odpalanie LLVM, ale to również można w C zasymulować:
(ten kod to już kompletny hack, z "czystym kodem" nie ma wiele wspólnego. Ale kto powiedział że PoCe mają być ładne)

def export_c_opcodes(opcodes):
    stack = []
    eax, ebx = '', ''
    for i, opcode in enumerate(opcodes):
        val = None
        if opcode[0] == 'push':
            stack.append('var_' + str(i))
            val = opcode[1]
        elif opcode[0] == 'pop':
            val = stack.pop()
            if opcode[1] == 'eax':
                eax = val
            else:
                ebx = val
            continue
        else:
            val = '{0}({1})'.format(opcode[0], ','.join(str(x) for x in opcode[1:]))
        val = val.replace('eax', eax)
        val = val.replace('ebx', ebx)
        print 'uint32_t var_{0} = {1};'.format(i, val)
        eax = 'var_' + str(i)
    print 'uint32_t result = ' + stack.pop() + ';'

export_c_opcodes(generate(const(666, 4)))

(tutaj warto zauważyć że to co dostaje export_c_opcodes jest na poziomie abstrakcji asemblera, i dokonuje translacji do c).

export_c_opcodes generuje nam takie cudo:

#include <stdint.h>
#include <stdio.h>

uint32_t add(uint32_t a, uint32_t b) { return a + b; }
uint32_t sub(uint32_t a, uint32_t b) { return a - b; }
uint32_t mul(uint32_t a, uint32_t b) { return a * b; }
uint32_t and(uint32_t a, uint32_t b) { return a & b; }
uint32_t or(uint32_t a, uint32_t b) { return a | b; }
uint32_t xor(uint32_t a, uint32_t b) { return a ^ b; }
uint32_t neg(uint32_t a) { return -a; }
uint32_t not(uint32_t a) { return ~a; }

int main() {
    uint32_t eax, ebx;
    // <autogenerated>
    uint32_t var_0 = 694626396;
    uint32_t var_1 = 694626396;
    uint32_t var_4 = and(var_0,var_1);
    uint32_t var_5 = var_4;
    uint32_t var_6 = 1;
    uint32_t var_9 = mul(var_5,var_6);
    uint32_t var_10 = var_9;
    uint32_t var_11 = 0;
    uint32_t var_12 = 0;
    uint32_t var_13 = 694625730;
    uint32_t var_15 = not(var_13);
    uint32_t var_16 = var_15;
    uint32_t var_17 = 694625730;
    uint32_t var_19 = not(var_17);
    uint32_t var_20 = var_19;
    uint32_t var_23 = or(var_16,var_20);
    uint32_t var_24 = var_23;
    uint32_t var_26 = not(var_24);
    uint32_t var_27 = var_26;
    uint32_t var_28 = 0;
    uint32_t var_31 = add(var_27,var_28);
    uint32_t var_32 = var_31;
    uint32_t var_34 = neg(var_32);
    uint32_t var_35 = var_34;
    uint32_t var_38 = add(var_12,var_35);
    uint32_t var_39 = var_38;
    uint32_t var_40 = 0;
    uint32_t var_41 = 694625730;
    uint32_t var_43 = not(var_41);
    uint32_t var_44 = var_43;
    uint32_t var_45 = 694625730;
    uint32_t var_47 = not(var_45);
    uint32_t var_48 = var_47;
    uint32_t var_51 = or(var_44,var_48);
    uint32_t var_52 = var_51;
    uint32_t var_54 = not(var_52);
    uint32_t var_55 = var_54;
    uint32_t var_56 = 0;
    uint32_t var_59 = add(var_55,var_56);
    uint32_t var_60 = var_59;
    uint32_t var_62 = neg(var_60);
    uint32_t var_63 = var_62;
    uint32_t var_66 = add(var_40,var_63);
    uint32_t var_67 = var_66;
    uint32_t var_70 = and(var_39,var_67);
    uint32_t var_71 = var_70;
    uint32_t var_74 = sub(var_11,var_71);
    uint32_t var_75 = var_74;
    uint32_t var_77 = neg(var_75);
    uint32_t var_78 = var_77;
    uint32_t var_81 = add(var_10,var_78);
    uint32_t var_82 = var_81;
    uint32_t result = var_82;
    // </autogenerated>
    printf("%d", result);
}
$ gcc test.c -O3
$ ./a.exe
666

I wynik:

mov     [esp+10h+var_C], 29Ah
mov     [esp+10h+var_10], offset aD ; "%d"
call    printf

:>.

Więc jeśli chodzi o obfuskację, można z tego wyciągnąć wniosek, że z tak zmutowanym przez maszynę kodem, może poradzić sobie również maszyna (tzn. wrócić do bardziej cywilizowanej postaci). No chyba że uda się "ogłupić" kompilator rzeczami których nie będzie potrafił zoptymalizować (ale kompilatory z czasem coraz mądrzejsze).
Większą skuteczność takie rozwiązanie osiągnie pewnie przeciwko silnikom antywirusów, ale na heurystykę nie pomoże (a i tak już nic nie działa wyłacznie na podstawie patternów/fingerprintów).

0

Po pierwsze dziękuję za odpowiedź, mało kto chyba zajmuje się takimi problemami i rozumie specyfikę tego typu rozwiązań.

msm napisał(a):

W większości "typowego" kodu generowanego przez kompilator wszystko będzie ok bo większość rzeczy polega na samym ZF tuż przed skokiem, ale i tak ryzykownie.

Akurat ta kwestia jest inaczej rozwiązana, jednak flagi w tym wypadku można zachować (pushfd + popfd) lub wymusić ich ustawienie (or) przed wyjściem z bloku zmutownaych instrukcji.

msm napisał(a):

Najpierw rozwijamy każde wyrażenie rekurencyjnie, aż do osiągnięcia jakiegoś limitu (tutaj ograniczenie to zmienna t przekazywana ciągle, czyli maksymalne zagnieżdżenie rekurencji), a później za pomoca stosu wyliczamy jego wartość.

Bardzo fajne rozwiązanie, ja unikam instrukcji mul/div etc. ze względu na prędkość ich wykonywania. Stosowanie MUL/DIV można fajnie rozwiązać implementując zastępcze algorytmy do mnożenia i dzielenia bazujące na shiftach i instrukcjach LEA.

msm napisał(a):

Podstawowy problem z takim zabezpieczeniem to na pewno to, że jest to jak najbardziej odwracalne. Wystarczy np. wczytać taki megabajtowy ASM, wyeksportować np. do asemblera llvm, uruchomić kompilator z maksymalną optymalizacja i odebrać gotowy wynik.

To o czym pisałem, czyli rozwijanie pojedynczych instrukcji to tylko skrawek mojego zabezpieczenia. Dochodzi do tego generowanie pseudo-procedur dla bloków kodu, wielu ścieżek z różnymi mutacjami tych samych instrukcji czy instrukcje zaśmiecające korzystające z analizy danego bloku kodu, więc nie jest to robione na "pałę", do tego oczywiście instrukcje utrudniające debuggowanie (tzw. junki) i kilka innych metod utrudniających analizę.

msm napisał(a):

Z ciekawości na ile praktyczna jest taka deobfuskacja, zrobiłem sobie eksport do C (nie chciało mi się bawić z konwerowaniem wyjścia do LLVM więc eksport z poziomu wyrażenia:)
...
Ok, ale to w sumie nie był poziom asemblera. Postanowiłem więc wskoczyć na poziom niżej, i przeprowadzić porządną podmianę asma na maszynę rejestrową. Dalej byłem zbyt leniwy na odpalanie LLVM, ale to również można w C zasymulować:
(ten kod to już kompletny hack, z "czystym kodem" nie ma wiele wspólnego. Ale kto powiedział że PoCe mają być ładne)
...
Więc jeśli chodzi o obfuskację, można z tego wyciągnąć wniosek, że z tak zmutowanym przez maszynę kodem, może poradzić sobie również maszyna (tzn. wrócić do bardziej cywilizowanej postaci). No chyba że uda się "ogłupić" kompilator rzeczami których nie będzie potrafił zoptymalizować (ale kompilatory z czasem coraz mądrzejsze).
Większą skuteczność takie rozwiązanie osiągnie pewnie przeciwko silnikom antywirusów, ale na heurystykę nie pomoże (a i tak już nic nie działa wyłacznie na podstawie patternów/fingerprintów).

Masz rację co do tych optymalizacji, że kompilator w tym przypadku sobie poradzi.

Jednak to zupełnie inna bajka, jeśli engine mutujący jest stosowany jako element innych zabezpieczeń, dla przykładu używam go do mutacji kolejnych warstw silnika polimorficznego (czyli jest to funkcja szyfrująca dane/kod i jednocześnie generująca kod dekryptora), który działa tak, że docelowy zmutowany kod jest deszyfrowany np. przez 100 warstw (taka deszyfrująca matrioszka), jedna po drugiej i każda zmutowana, i prawie na samym końcu wykonywany jest ten ostateczny kod, który jest np. silnikiem VM, który dopiero ostatecznie wykonuje czy emuluje działanie kodu, który ma chronić.

Widziałem już kilka wtyczek dla OllyDbg czy IDA, czy samodzielnych "deobfuscatorów", jednak w starciu z rzeczywistością ich funkcjonowanie było bardzo mocno ograniczone, a wręcz kulawe. Silniki mutujące są też różne, np. niektóre bazują jedynie na operacjach na stosie i stworzenie poprawnego deobfuscatora jest po prostu cholernie trudne, a jeszcze jak masz to w zabezpieczonej aplikacji ze sprawdzaniem sum kontrolnych kodu (jakby np. ktoś wpadł na pomysł, żeby dynamicznie "oczyścić" kod i go śledzić), z metodami antydebug, z wielowątkowością warstw zabezpieczających (które współpracują ze sobą np. przez system zdarzeń) to można się załamać psychicznie :)

Jakbyś miał jakieś inne pomysły jak skomplikować ten proces lub widzisz w tym co napisałem jakieś niedociągnięcia to chętnie wysłucham.

Pozdrawiam

1 użytkowników online, w tym zalogowanych: 0, gości: 1