Kopiuj
xor eax,eax
sub eax,eax
and eax,0
lea eax,[0]
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)
Kopiuj
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:
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:
Kopiuj
>>> 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:)
Kopiuj
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:
Kopiuj
#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))))))))));
}
Kopiuj
$ gcc test.c -O3
$ ./a.exe
666
Wynik kompilacji:
Kopiuj
mov [esp+10h+var_C], 29Ah
mov [esp+10h+var_10], offset aD
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)
Kopiuj
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:
Kopiuj
#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;
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;
printf("%d", result);
}
Kopiuj
$ gcc test.c -O3
$ ./a.exe
666
I wynik:
Kopiuj
mov [esp+10h+var_C], 29Ah
mov [esp+10h+var_10], offset aD
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).
Shalomvolatile
do zmiennych. nie ma prawa zoptymalizować.