PinkyVM Девиртуализация (Kaspersky Crackme)
Ниже будет представлен метод девиртуализации виртуальной машины от Лаборатории Касперского 2020г.
Программа запрашивает почту и пароль, проверят их и выводит либо Fail!
, либо Success!
C:\Users\****\Desktop>Pinky.exe
.uuu
z@#"%c .uuzm**"""""*%mu.. z*"` .e@#N
@!!!R. #c .z*" ^*c z dT!!!!!>
'!!!!!!N "i u*" #s :" @?!!!!!!!R
t!!!!!!!#u "i .@ ^$ :R!!!!!!!!!X
'!!!!!!!!!#c "i:# ?> R!!!!!!!!!!X
'!!!!!!!!!!!N @ 4W!!!!!!!!!!!>
'!!!!!!!!!!!!Ru" ?!!!!!!!!!!X
'X!!!!!!!!!!!9~ . . 'X!!!!!!!!!6
R!!!!!!!!!!tF z$#` h &!!UR!!!!!F
?!!!!!$X!!!$ .@ X $WTR!!!!!X
M!!!!!i#U!E . @F ! FdR!!!!!!f
'X!!!!!#c'?u@#"*$N. :$ F'9!!!!!!!@
9!!!!!!!?NM ^*c dF ' @!!!!!!!X>
R!!!!!!!!& "e d < E!!!!!!X"
t!!!!!!!# ^N :" .e$"^ Fn!!!!!XP
#X!!!!!!ML *c z" .e$$$$$ M'!!!!W*
"*UX!!X@t ^%u. ""**#).zd$$#$$$$$$$ <*@**"
'N 4$$$$$@$$$)$$#$$k4$$$$$$$$$E :$
?> "$$$$$$":$$$W$$$ "$$$$$$$$ %
:" ? ^#*" S "$$$$$ ?
F L d$L X
& t$i @$$$ f
* $$$$$$$$$$& @
'*. W'$$$$$$$$FM h u#
^*muz* % $$$$$$": `"
# ^**" d
"***"
What is the password, Pinky?
Email: testtest
Password: testtest
Fail!
Анализ
Первым делом после вывода ascii арта, программа запрашивает почту и пароль. Почта остается неизменной, а вот пароль переводится по 8 символов из hex строки в число и сохраняется в массив v7
После чего расшифровывается 219 байт данных и делается прыжок по ним:
j__printf("What is the password, Pinky?\n");
j__memset(v9, 0, 0x64u);
j__printf("Email: ");
v0 = get_stdin();
j__fgets(v9, 90, v0);
v9[j__strcspn(v9, "\r\n")] = 0;
v9[j__strcspn(v9, "\r\n")] = 0;
j__printf("Password: ");
v1 = get_stdin();
j__fgets(v8, 25, v1);
for ( i = 0; i < 3; ++i )
{
j__memmove(&v5, &v8[8 * i], 8u);
v6 = 0;
v7[i] = j__strtoul(&v5, 0, 16);
}
for ( j = 0; j < 219; ++j )
byte_44B000[j] ^= 0xDBu;
VirtualProtect(byte_44B000, 0xDBu, 0x40u, 0);
if ( (*(int (char *, int *))byte_44B000)(v9, v7) )
При выполнении первой инструкции расшифрованных данных происходит ошибка и исполнение переходит на зарегистрированный обработчик в SEH-chain
. В этом обработчике инциализируется контекст виртуальной машины и ее дальнейшее выполнение, а байткодом служит только что расшифрованные данные. Можно предположить, что таким образом авторы облегчили себе получение контекста программы.
int Exceptions(PEXCEPTION_RECORD ExceptionRecord, int EstablisherFrame, PCONTEXT ContextRecord)
{
struct VMCTX_t *vctx;
vctx = (struct VMCTX_t *)VCTX_init(ContextRecord, 9999);
VCTX_init_opcodes(vctx);
VCTX_setVIP(vctx, sub_416744);
VMEnter(vctx);
ContextRecord->Ebp = vctx->_ebp;
ContextRecord->Eax = vctx->_eax;
ContextRecord->Ecx = vctx->_ecx;
ContextRecord->Edx = vctx->_edx;
ContextRecord->Ebx = vctx->_ebx;
ContextRecord->Esi = vctx->_esi;
ContextRecord->Edi = vctx->_edi;
ContextRecord->Eip = vctx->_eip;
ContextRecord->Esp = vctx->saved_esp;
VCTX_free(vctx);
return 0;
}
Структура виртуальной машины
Виртуальная машина представляет собой регистровый тип вм, где виртуальным регистрам соотвествуют реальные.
PVMCTX VCTX_init(PCONTEXT ContextRecord, unsigned int value)
{
PVMCTX vctx;
if ( !ContextRecord || !value || value > 65535 )
return 0;
vctx = (PVMCTX)j__malloc(0x454u);
if ( !vctx )
return 0;
j__memset(vctx, 0, 0x454u);
vctx->v_eip = 0;
vctx->_eip = ContextRecord->Eip;
LOWORD(vctx->vmexit) = 1;
vctx->v_ebp_reg_or_mem = 0;
vctx->_ebp = ContextRecord->Ebp;
vctx->v_eax_reg_or_mem = 0;
vctx->_eax = ContextRecord->Eax;
vctx->v_ecx_reg_or_mem = 0;
vctx->_ecx = ContextRecord->Ecx;
vctx->v_edx_reg_or_mem = 0;
vctx->_edx = ContextRecord->Edx;
vctx->v_ebx_reg_or_mem = 0;
vctx->_ebx = ContextRecord->Ebx;
vctx->v_esi_reg_or_mem = 0;
vctx->_esi = ContextRecord->Esi;
vctx->v_edi_reg_or_mem = 0;
vctx->_edi = ContextRecord->Edi;
vctx->v_esp_reg_or_mem = 0;
vctx->_esp = ContextRecord->Esp;
vctx->_zf = 0;
vctx->saved_esp = ContextRecord->Esp;
VCTX_init_opcodes(vctx);
return vctx;
}
В данном случае reg_or_mem
обозначает является ли виртуальный регистр значением или ссылкой. ВМ имеет 256 обработчиков, 21 из них делают какие-то полезные действия, все остальные - nop
. Обработчики хранятся в виртуальном контексте программы в виде массива и вызываются по номеру опкода.
Пример обработчика xor
:
void __stdcall sub_419FD0(PVMCTX ctx)
{
int v1;
int v2;
int v3;
unsigned int v4;
out_reg = (unsigned __int8)GetNextByte(ctx);
if ( out_reg >= 8 )
crash(ctx, 1);
v3 = (unsigned __int8)GetNextByte(ctx);
if ( out_reg >= 8 )
crash(ctx, 1);
v2 = (unsigned __int8)GetNextByte(ctx);
if ( out_reg >= 8 )
crash(ctx, 1);
if ( *(&ctx->v_ebp_reg_or_mem + 2 * out_reg) == 1 && *(&ctx->_ebp + 2 * out_reg) )
j__free(*((void **)&ctx->_ebp + 2 * out_reg));
v1 = GetRegisterValue((int)ctx, v3);
*(&ctx->_ebp + 2 * out_reg) = GetRegisterValue((int)ctx, v2) ^ v1;
*(&ctx->v_ebp_reg_or_mem + 2 * out_reg) = 0;
if ( *(&ctx->_ebp + 2 * out_reg) )
ctx->_zf = 0;
else
ctx->_zf = 1;
++ctx->_eip;
}
Концепт во всех обработчиках одинаковый:
- Берется байт с текущего адреса инструкции
eip
. - Если это номер регистра, то сверяются границы.
- Если обработчик это математическая операция, то выставляется
ZF
zero flag.
Кстати в этом обработчике можно заметить ошибку: out_reg
проверяется 3 раза, хотя должны проверяться другие регистры v3
и v2
.
reg = bs(l=8, cls=(pinky_reg, ))
reg_deref = bs(l=8, cls=(pinky_reg_deref,))
imm8 = bs(l=8, cls=(pinky_imm8, pinky_arg))
imm16 = bs(l=16, cls=(pinky_imm16, pinky_arg))
imm32 = bs(l=32, cls=(pinky_imm32, pinky_arg))
addop("MOV", [bs("00000001"), reg, imm16]) # 1
addop("JMP", [bs("00010000"), imm16]) # 16
addop("JE" , [bs("00010001"), imm16]) # 17
addop("JNE", [bs("00010010"), imm16]) # 18
addop("XOR", [bs("00100000"), reg, reg, reg]) # 32
addop("ADD", [bs("00100001"), reg, reg, reg]) # 33
addop("SUB", [bs("00100010"), reg, reg, reg]) # 34
addop("MUL", [bs("00100011"), reg, reg, reg]) # 35
addop("DIV", [bs("00100100"), reg, reg, reg]) # 36
addop("INC", [bs("00100101"), reg]) # 37
addop("DEC", [bs("00100110"), reg]) # 38
addop("AND", [bs("00100111"), reg, reg, reg]) # 39
addop("OR", [bs("00101000"), reg, reg, reg]) # 40
addop("CMP", [bs("01000000"), reg, reg]) # 64
addop("CMP", [bs("01000001"), reg, imm16]) # 65
addop("CMP", [bs("01000010"), reg, imm32]) # 66
addop("MOV1", [bs("01010001"), reg, reg]) # 81 # REG TO REG OR MEM TO REG
addop("MOV2", [bs("01100000"), reg, reg_deref]) # 96 # REG TO REG
addop("MOV3", [bs("01100001"), reg, reg_deref]) # 97 # MEMORY
addop("RET", [bs("01110010")]) # 114
addop("NOP", [bs("11000100")]) # 196
Пару вещей, все таки, стоит сказать. Поскольку вм может работать как с 16-битными числами, так и с 32-битными, необходимо было конвертировать порядок байт. Это делается в одном из классов, описывающих аргумент, функцией decodeval
:
class pinky_imm16(imm_noarg, pinky_arg):
"""Generic pinky immediate
"""
intsize = 16
intmask = (1 << intsize) - 1
parser = base_expr
def decodeval(self, v):
return swap_sint(self.l, v) & self.intmask
def encodeval(self, v):
return swap_sint(self.l, v) & self.intmask
Так же у вм имеется возможность записывать в память. Чтобы это было заметно в дизассемблере, необходимо переводить выражение в ExprMem
:
class pinky_reg_deref(pinky_arg):
parser = deref_reg
reg_info = gpr_infos # the list of pinky registers defined in regs.py
def decode(self, v):
v = v & self.lmask
if v >= len(self.reg_info.expr):
return False
self.expr = self.reg_info.expr[v]
self.expr = ExprMem(self.expr, self.expr.size)
return True
Что получилось
Как выглядит девиртуализированная функция:
Инструкции очень похожи на x86
и при небольшом изменении, могут быть полностью перекомпилированы в эту архитектуру.
Miasm IR после SSA преобразования:
Пишем keygen
Судя по алгоритму, введенная почта записывается сначала на стэк по 4 байта с помощью ксора (0 xor val = val
) 3 раза. Затем к этим 4 байтам прибавляются 4 байта введенного ключа.
В конечном счете алгоритм будет такой:
int virtualized_function(int* mail, int* password) {
int ret = 1;
if (mail[0] + password[0] != 0x8b987160)
ret = 0;
if (mail[1] + password[1] != 0x8b987160)
ret = 0;
if (mail[2] + password[2] != 0x8b987160)
ret = 0;
}
return ret;
}
Для написания keygen’а будем использовать z3
решатель:
from z3 import *
from struct import pack
magic = [0x8b987160, 0x9b854771, 0xa0bea5d7]
s = Solver()
inp = []
for i in range(3):
m = BitVec('mail_%d' % i, 32)
p = BitVec('passwd_%d' % i, 32)
for j in range(0, 32, 8):
v = (m & (0xff << j)) >> j
s.add(v > 51, v < 124)
for j in range(0, 32, 4):
v = (m & (0xf << j)) >> j
s.add(v >= 0, v < 16)
s.add(m + p == magic[i])
inp.append((m, p))
if s.check() == sat:
model = s.model()
mail = b''
password = ''
for i in range(3):
v1 = model[inp[i][0]].as_long()
v2 = model[inp[i][1]].as_long()
v1 = pack("<I", v1)
mail += v1
password += hex(v2)[2:]
print(mail)
print(password)
Запускаем:
$ python3 solve.py
b'84484448@448'
53643d286351133d688a7197
Пробуем:
What is the password, Pinky?
Email: 84484448@448
Password: 53643d286351133d688a7197
Success!
Конец :)