Я раньше пользовался The Enigma Protector, сейчас по идее надо его обновить, вопрос насколько всё это сейчас актуально? Как сейчас на Западе с хакерами — их прижали, или наоборот они распоясались? Есть ли среди хакеров такие, которые продают наши программы за крипту?
Я наверно не смогу шифровать код Mac версии своей программы, поэтому такая мысль – может быть, довольно важно сделать кейген для Mac другой, с другим алгоритмом, чтобы хакеры не смогли, разобрав код Mac-программы, написать кейген для Windows-версии?
Какой шифратор лучше выбрать — Execryptor или есть что-то новое?
Не решена ли как-то проблема ложных срабатываний антивирусов на шифрованный код?
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Здравствуйте, Khimik, Вы писали:
K>Я раньше пользовался The Enigma Protector, сейчас по идее надо его обновить, вопрос насколько всё это сейчас актуально? Как сейчас на Западе с хакерами — их прижали, или наоборот они распоясались? Есть ли среди хакеров такие, которые продают наши программы за крипту? K>Я наверно не смогу шифровать код Mac версии своей программы, поэтому такая мысль – может быть, довольно важно сделать кейген для Mac другой, с другим алгоритмом, чтобы хакеры не смогли, разобрав код Mac-программы, написать кейген для Windows-версии? K>Какой шифратор лучше выбрать — Execryptor или есть что-то новое? K>Не решена ли как-то проблема ложных срабатываний антивирусов на шифрованный код?
Шифрование кода, в подавляющем числе случаем — это абсолютный overkill. Нормальный рабочий вариант — это внутренний проверки на модификацию кода, как статическую (патчи и кряки) так и динамическую (API hooking, DLL side loading, dynamic patching, etc.). Если таких проверок в коде, скажем, несколько десятков и они разнесены по времени, то вычистить их все крайне сложно и, главное, трудо- и времяемко. Это принципиальный момент, потому что хакерская сцена состоит на 99.9% из детей-любителей с соотвествующим уровнем скилзов, который хакают, чтобы выложить "релиз" и поразить камрадов своими l33t скилзами. Они могут снять подпись с exe и пропатчить `if (valid_license)`. Какая-то часть может посмотреть чуть дальше, но в общем и целом они ломают только то, что быстро ломается. Оставшиеся 0.1% — супер-квалифицированые товарищи, которые ломают в основном динамическими патчами. Сломать они сломают, но чистый 100% хак займет приличное количество времени. Им по-большому счету связываться с этим не имеет смысла, только если это не платный заказ или не какой-то high-profile софт.
Здравствуйте, Khimik, Вы писали: K>Я раньше пользовался The Enigma Protector, сейчас по идее надо его обновить, вопрос насколько всё это сейчас актуально? Как сейчас на Западе с хакерами — их прижали, или наоборот они распоясались? Есть ли среди хакеров такие, которые продают наши программы за крипту? K>Я наверно не смогу шифровать код Mac версии своей программы, поэтому такая мысль – может быть, довольно важно сделать кейген для Mac другой, с другим алгоритмом, чтобы хакеры не смогли, разобрав код Mac-программы, написать кейген для Windows-версии? K>Какой шифратор лучше выбрать — Execryptor или есть что-то новое? K>Не решена ли как-то проблема ложных срабатываний антивирусов на шифрованный код?
C кейгенами бороться очень просто: используй RSA c двумя ключами. Приватный ключ у девелопера, публичный в программе. Ключ шифруется приватным ключом при создании, а программа расшифровывает публичным. Не имея приватного ключа, создать валидный лицензионный ключ невозможно. Остаются патчи, но они скажем так менее привлекательны — пользователи боятся тк хз что там еще они поменяют и сколько троянов довесят.
По моему личному опыту, использовал VMProtect. Насколько я знаю один из немногих протекторов поддерживащих мак и линух. Протектор отличный, но у меня было три основные проблемы:
1) Размер исполняемых файлов удваивается как минимум за счет внедрения виртуальной машины. Собственно исполнение части кода на виртуальной машине и осложняет жизнь хацкерам, тк становится очень сложно понять что там происходит.
2) В связи с этим вместо кряков появляются покупки по краденным картам и чержбеки владельцев.
3) Антивирусы тоже не особо понимают код виртуальной машины, а следовательно на всякий случай задетектим его как вирус. Короче говоря ложных срабатываний полно, особенно у говно-антивирусов второго эшелона.
В итоге я решил от VMProtect отказаться, оставив однако схему лицензирования и генерации ключей. То есть используются точно такие же ключи, только проверку выполняет мой собственный код, а для пользователей ничего не изменилось.
Естественно появились патчи, но в целом продажи не изменились, и теперь фроды и борьба с антивирусниками занимают куда меньше времени. В итоге я пришел к выводу, что те кто не купят ваш софт в любом случае воспользутся или левой кредиткой или патчем. Да ну и пусть, c них все равно денег не получить. А те кто могут купить прогу, покупают ее как и раньше.
Поделюсь модулем для FPC/DELPHI который декодирует ключи VMProtect:
unit Licensing.VMProtect;
interface{$IFDEF FPC}
{$MODE DELPHI}
{$RANGECHECKS OFF}
{$OVERFLOWCHECKS OFF}
{$ELSE}
{$WEAKLINKRTTI ON}
{$RTTI EXPLICIT METHODS([]) PROPERTIES([]) FIELDS([])}
{$ENDIF}uses{$IFDEF FPC}
Base64, Classes, SysUtils, Sha1, Md5;
{$ELSE}
System.NetEncoding, System.Classes, System.SysUtils, System.WideStrUtils,
System.Hash;
{$ENDIF}const
BuildDate = {$I BuildDate.inc};
type
TVMProtectDate = packed record
wYear: Word;
bMonth: Byte;
bDay: Byte;
end;
PVMProtectSerialNumberData = ^TVMProtectSerialNumberData;
TVMProtectSerialNumberData = packed record
nState: Longword;
wUserName: array [0..255] of WideChar;
wEMail: array [0..255] of WideChar;
dtExpire: TVMProtectDate;
dtMaxBuild: TVMProtectDate;
bRunningTime: Longword;
nUserDataLength: Byte;
bUserData: array [0..254] of Byte;
end;
const
SERIAL_STATE_SUCCESS = 0;
SERIAL_STATE_FLAG_CORRUPTED = $00000001;
SERIAL_STATE_FLAG_INVALID = $00000002;
SERIAL_STATE_FLAG_BLACKLISTED = $00000004;
SERIAL_STATE_FLAG_DATE_EXPIRED = $00000008;
SERIAL_STATE_FLAG_RUNNING_TIME_OVER = $00000010;
SERIAL_STATE_FLAG_BAD_HWID = $00000020;
SERIAL_STATE_FLAG_MAX_BUILD_EXPIRED = $00000040;
type
LbIntBuf = packed record
dwLen: Integer;
pBuf: PByte;
end;
LbInteger = packed record
bSign: Boolean;
dwUsed: Integer;
IntBuf: LbIntBuf;
end;
{ TSerialDecoder }
TSerialDecoder = class
private
FSerialNum: TBytes;
FPublicExp: TBytes;
FPublicMod: TBytes;
FProductCode: UInt64;
FSerialNumberData: TVMProtectSerialNumberData;
procedure CheckSerialBlackList;
function GetDecodedBytes: TBytes;
function GetBytesFromBase64(const Value: string): TBytes;
function GetLargeIntFromBytes(const Bytes: TBytes): LbInteger;
function GetBytesFromLargeInt(const Value: LbInteger): TBytes;
protected
function GetBuildDate: TDate; virtual;
public
procedure SetPublicExp(const Value: string);
procedure SetPublicMod(const Value: string);
procedure SetSerialNum(const Value: string);
procedure SetProductCode(const Value: UInt64);
function VMProtectSetSerialNumber(const SerialNumber: string): LongWord;
function VMProtectGetSerialNumberData(Data: PVMProtectSerialNumberData;
DataSize: Integer): Boolean;
end;
implementation
const
cLESS_THAN = ShortInt(-1);
cEQUAL_TO = ShortInt(0);
cGREATER_THAN = ShortInt(1);
cPOSITIVE = True;
cNEGATIVE = False;
const
cBYTE_POSSIBLE_VALUES = 256;
cDEFAULT_PRECISION = 64;
cUSE_DEFAULT_PRECISION = 0;
cDEFAULT_SIGN = cPOSITIVE;
cDEFAULT_USED = 0;
cAPPEND_ARRAY = 0;
cPREPEND_ARRAY = 1;
const
sBIBufferNotAssigned = 'Buffer not assigned';
sBINoNumber = 'No number';
sBISubtractErr = 'Subtraction error';
sBIZeroDivide = 'Division by zero';
sBIQuotientErr = 'Quotient process error';
sBIZeroFactor = 'Factor is zero';
type
PBiByteArray = ^TBiByteArray;
TBiByteArray = array [0..Pred(MaxInt)] of Byte;
{$IFNDEF FPC}type
TBytesStream = class(System.Classes.TBytesStream)
public
function ReadByte: Byte;
function ReadWord: Word;
function ReadDWord: UInt32;
function ReadQWord: UInt64;
end;
{$ENDIF}procedure LbBiInit(out N1: LbInteger; const Precision: Integer);
begin
N1 := Default(LbInteger);
if Precision > 0 then
N1.IntBuf.dwLen := Precision
else
N1.IntBuf.dwLen := cDEFAULT_PRECISION;
N1.bSign := cDEFAULT_SIGN;
N1.dwUsed := cDEFAULT_USED;
N1.IntBuf.pBuf := PByte(AllocMem(N1.IntBuf.dwLen));
end;
procedure LbBiTrimSigZeros(var N1: LbInteger);
begin
if (not Assigned(N1.IntBuf.pBuf)) then
raise Exception.Create(sBIBufferNotAssigned);
while (PBiByteArray(N1.IntBuf.pBuf)[Pred(N1.dwUsed)] = 0) do
begin
Dec(N1.dwUsed);
if (N1.dwUsed <= 0) then
begin
N1.dwUsed := 1;
Exit;
end;
end;
end;
procedure LbBiRealloc(var N1: LbInteger; const Len: Integer);
var
TmpPtr: PByte;
begin
if (N1.dwUsed > Len) then
Exit;
TmpPtr := AllocMem(Len);
move(N1.IntBuf.pBuf^, TmpPtr^, N1.dwUsed);
FreeMem(N1.IntBuf.pBuf);
N1.IntBuf.dwLen := Len;
N1.IntBuf.pBuf := TmpPtr;
end;
procedure LbBiClear(var N1: LbInteger);
begin
N1.bSign := cDEFAULT_SIGN;
N1.dwUsed := cDEFAULT_USED;
FillChar(N1.IntBuf.pBuf^, N1.IntBuf.dwLen, $00);
end;
function LbBiIsZero(N1: LbInteger): Boolean;
begin
LbBiTrimSigZeros(N1);
Result := False;
if (N1.dwUsed = 1) and (PBiByteArray(N1.IntBuf.pBuf)[0] = 0) then
Result := True
end;
procedure LbBiAddByte(var N1: LbInteger; const Place: Integer;
const ByteValue: Byte);
begin
if (Place = cAPPEND_ARRAY) then
begin
if (Succ(N1.dwUsed) > N1.IntBuf.dwLen) then
LbBiRealloc(N1, Succ(N1.dwUsed));
PBiByteArray(N1.IntBuf.pBuf)[N1.dwUsed] := ByteValue;
Inc(N1.dwUsed);
end
else
begin
if (Place > N1.IntBuf.dwLen) then
LbBiRealloc(N1, Place);
PBiByteArray(N1.IntBuf.pBuf)[Pred(Place)] := ByteValue;
if (N1.dwUsed < Place) then
N1.dwUsed := Place;
end;
end;
function LbBiGetByteValue(const N1: LbInteger; const Place: Integer): Byte;
begin
if (N1.dwUsed < Place) then
begin
Result := 0;
Exit;
end;
Result := PBiByteArray(N1.IntBuf.pBuf)[Pred(Place)];
end;
function LbBiReverseBits(ByteValue: Byte): Byte;
var
I: Byte;
RBit: Byte;
begin
Result := 0;
RBit := $80;
for I := 1 to 8 do
begin
if ((ByteValue and $01) <> 0) then
Result := Result or RBit;
RBit := RBit shr 1;
ByteValue := ByteValue shr 1;
end;
end;
function LbBiIsOne(N1: LbInteger): Boolean;
begin
LbBiTrimSigZeros(N1);
Result := False;
if (N1.dwUsed = 1) and (PBiByteArray(N1.IntBuf.pBuf)[0] = 1) then
Result := True
end;
procedure LbBiCopy(var Dest: LbInteger; const Src: LbInteger;
const Len: Integer);
var
Ptr: PByte;
Size: Integer;
begin
FillChar(Dest.IntBuf.pBuf^, Dest.IntBuf.dwLen, $00);
Size := Integer(Len);
if Size > Dest.IntBuf.dwLen then
LbBiRealloc(Dest, Size);
Ptr := Dest.IntBuf.pBuf;
move(Src.IntBuf.pBuf^, Ptr^, Len);
if (Dest.dwUsed < Size) then
Dest.dwUsed := Size;
end;
function MultSpecialCase(const N1: LbInteger; const N2: LbInteger;
var Product: LbInteger): Boolean;
begin
Result := False;
if (LbBiIsZero(N1) or LbBiIsZero(N2)) then
begin
LbBiAddByte(Product, cPREPEND_ARRAY, $00);
Result := True;
Exit;
end;
if (LbBiIsOne(N1)) then
begin
Product.dwUsed := N2.dwUsed;
if (Product.IntBuf.dwLen < N2.IntBuf.dwLen) then
LbBiRealloc(Product, N2.IntBuf.dwLen);
LbBiCopy(Product, N2, N2.dwUsed);
Result := True;
Exit;
end;
if (LbBiIsOne(N2)) then
begin
Product.dwUsed := N1.dwUsed;
if (Product.IntBuf.dwLen < N1.IntBuf.dwLen) then
LbBiRealloc(Product, N1.IntBuf.dwLen);
LbBiCopy(Product, N1, N1.dwUsed);
Result := True;
end;
end;
procedure LbBiMultBase(N1: LbInteger; const N2: LbInteger;
var Product: LbInteger);
var
InxX: Integer;
InxY: Integer;
MaxX: Integer;
Carry: Integer;
Prd: Integer;
Plc: Integer;
TmpByte: Byte;
TmpInt: Integer;
begin
if (MultSpecialCase(N1, N2, Product)) then
Exit;
MaxX := Pred(N1.dwUsed);
TmpInt := Pred(N2.dwUsed);
for InxY := 0 to TmpInt do
begin
if PBiByteArray(N2.IntBuf.pBuf)[InxY] <> 0 then
begin
Carry := 0;
for InxX := 0 to MaxX do
begin
Plc := InxX + InxY;
Prd := PBiByteArray(N1.IntBuf.pBuf)[InxX];
Prd := Prd * PBiByteArray(N2.IntBuf.pBuf)[InxY];
if (Product.dwUsed < Plc) then
Prd := Prd + Carry
else
Prd := Prd + PBiByteArray(Product.IntBuf.pBuf)[Plc] + Carry;
TmpByte := Prd and $00FF;
Carry := Prd shr 8;
if (Succ(Plc) > Product.IntBuf.dwLen) then
LbBiRealloc(Product, Plc + 100);
PBiByteArray(Product.IntBuf.pBuf)[Plc] := TmpByte;
if (Product.dwUsed < Succ(Plc)) then
N1.dwUsed := Succ(Plc);
end;
LbBiAddByte(Product, (MaxX + InxY + 2), Carry);
end;
end;
LbBiTrimSigZeros(Product);
end;
procedure LbBiMult(const N1, N2: LbInteger; var Product: LbInteger);
begin
LbBiMultBase(N1, N2, Product);
if (N1.bSign = N2.bSign) then
Product.bSign := cPOSITIVE
else
Product.bSign := cNEGATIVE;
end;
procedure LbBiFree(var N1: LbInteger);
begin
if (Assigned(N1.IntBuf.pBuf)) then
FreeMem(N1.IntBuf.pBuf);
FillChar(N1, SizeOf(LbInteger), $00);
end;
procedure LbBiMultInPlace(var N1: LbInteger; const N2: LbInteger);
var
Product: LbInteger;
Precis: Integer;
begin
Precis := (N1.dwUsed + N2.dwUsed) * 2;
LbBiInit(Product, Precis);
LbBiMult(N1, N2, Product);
LbBiClear(N1);
N1.dwUsed := Product.dwUsed;
N1.bSign := Product.bSign;
if (N1.IntBuf.dwLen < Product.IntBuf.dwLen) then
LbBiRealloc(N1, Product.IntBuf.dwLen);
LbBiCopy(N1, Product, Product.dwUsed);
LbBiFree(Product);
end;
function LbBiFindFactor(B1: Byte): Byte;
begin
Result := 1;
while (B1 < $80) do
begin
B1 := (B1 shl 1);
Result := Result * 2;
end;
end;
procedure LbBiMulByDigitBase(N1: LbInteger; const N2: Byte;
var Product: LbInteger);
var
Count: Integer;
Carry: Byte;
Prd: WORD;
TmpByte: Byte;
TmpInt: Integer;
begin
if (N2 = 1) then
begin
if (Product.IntBuf.dwLen < N1.IntBuf.dwLen) then
LbBiRealloc(Product, N1.IntBuf.dwLen);
Product.dwUsed := N1.dwUsed;
Product.bSign := N1.bSign;
LbBiCopy(Product, N1, N1.dwUsed);
end;
if (N2 = 0) then
begin
Product.dwUsed := 1;
LbBiAddByte(Product, cPREPEND_ARRAY, 0);
end;
if LbBiIsOne(N1) then
begin
Product.dwUsed := 1;
LbBiAddByte(Product, cPREPEND_ARRAY, N2);
end;
if (N1.dwUsed = 1) and (PBiByteArray(N1.IntBuf.pBuf)[0] = 0) then
begin
Product.dwUsed := 1;
LbBiAddByte(Product, cPREPEND_ARRAY, 0);
end;
Carry := 0;
TmpInt := Pred(N1.dwUsed);
for Count := 0 to TmpInt do
begin
Prd := (PBiByteArray(N1.IntBuf.pBuf)[Count] * N2) + Carry;
TmpByte := Prd and $00FF;
Carry := Prd shr 8;
PBiByteArray(Product.IntBuf.pBuf)[Count] := TmpByte;
if (Product.dwUsed < Succ(Count)) then
N1.dwUsed := Succ(Count);
end;
LbBiAddByte(Product, Succ(N1.dwUsed), Carry);
LbBiTrimSigZeros(Product);
end;
procedure LbBiMulByDigit(const N1: LbInteger; const N2: Byte;
var Product: LbInteger);
begin
LbBiMulByDigitBase(N1, N2, Product);
Product.bSign := N1.bSign;
end;
procedure LbBiMulByDigitInPlace(var N1: LbInteger; const N2: Byte);
var
Product: LbInteger;
Precis: Integer;
begin
Precis := (N1.dwUsed + 1) * 2;
LbBiInit(Product, Precis);
try
LbBiMulByDigit(N1, N2, Product);
if (N1.IntBuf.dwLen < Product.IntBuf.dwLen) then
LbBiRealloc(N1, Product.IntBuf.dwLen);
N1.bSign := Product.bSign;
N1.dwUsed := Product.dwUsed;
LbBiCopy(N1, Product, Product.dwUsed);
finally
LbBiFree(Product);
end;
end;
procedure LbBiMove(var Fest: LbInteger; const Src: LbInteger;
const Place, Len: Integer);
var
Ptr: PByte;
Size: Integer;
begin
if (not Assigned(Fest.IntBuf.pBuf)) then
raise Exception.Create(sBIBufferNotAssigned);
if (Place = cAPPEND_ARRAY) then
begin
if ((Integer(Len) + Fest.dwUsed) > Fest.IntBuf.dwLen) then
LbBiRealloc(Fest, (Integer(Len) + Fest.dwUsed));
Ptr := Fest.IntBuf.pBuf;
Inc(Ptr, Fest.dwUsed);
move(Src.IntBuf.pBuf^, Ptr^, Len);
Inc(Fest.dwUsed, Len);
end
else
begin
Size := Pred(Place) + Integer(Len);
if Size > Fest.IntBuf.dwLen then
LbBiRealloc(Fest, Size);
Ptr := Fest.IntBuf.pBuf;
Inc(Ptr, Pred(Place));
move(Src.IntBuf.pBuf^, Ptr^, Len);
if (Fest.dwUsed < Size) then
Fest.dwUsed := Size;
end;
end;
function LbBiCompare(N1, N2: LbInteger): ShortInt;
var
Count: Integer;
begin
LbBiTrimSigZeros(N1);
LbBiTrimSigZeros(N2);
if (N1.bSign <> N2.bSign) then
begin
if (N1.bSign = cPOSITIVE) then
Result := cGREATER_THAN
else
Result := cLESS_THAN;
Exit;
end;
if (N1.dwUsed <> N2.dwUsed) then
begin
if (N1.dwUsed > N2.dwUsed) then
begin
Result := cGREATER_THAN;
Exit;
end
else
begin
Result := cLESS_THAN;
Exit;
end;
end;
Count := N1.dwUsed;
while PBiByteArray(N1.IntBuf.pBuf)[Pred(Count)] = PBiByteArray(N2.IntBuf.pBuf)
[Pred(Count)] do
begin
Dec(Count);
if (Count = 0) then
begin
Result := cEQUAL_TO;
Exit;
end;
end;
if PBiByteArray(N1.IntBuf.pBuf)[Pred(Count)] > PBiByteArray(N2.IntBuf.pBuf)
[Pred(Count)] then
Result := cGREATER_THAN
else
Result := cLESS_THAN;
end;
procedure LbBiFindLargestUsed(const N1, N2: LbInteger; out Count: Integer);
begin
if (N1.dwUsed >= N2.dwUsed) then
Count := N1.dwUsed
else
Count := N2.dwUsed;
end;
procedure LbBiAddBase(const N1, N2: LbInteger; var Sum: LbInteger);
var
Carry: Byte;
I: Integer;
Count: Integer;
TempWord: WORD;
TempByte: Byte;
begin
LbBiFindLargestUsed(N1, N2, Count);
if (LbBiIsZero(N1)) then
begin
LbBiCopy(Sum, N2, N2.dwUsed);
Exit;
end;
if (LbBiIsZero(N2)) then
begin
LbBiCopy(Sum, N1, N1.dwUsed);
Exit;
end;
Carry := 0;
if (Succ(Count) > Sum.dwUsed) then
LbBiRealloc(Sum, Succ(Count));
for I := 1 to Count do
begin
TempWord := LbBiGetByteValue(N1, I) + LbBiGetByteValue(N2, I) + Carry;
TempByte := TempWord and $00FF;
Carry := TempWord shr 8;
PBiByteArray(Sum.IntBuf.pBuf)[Sum.dwUsed] := TempByte;
Inc(Sum.dwUsed);
end;
LbBiAddByte(Sum, cAPPEND_ARRAY, Carry);
LbBiTrimSigZeros(Sum);
end;
function LbBiAbs(N1, N2: LbInteger): ShortInt;
var
Count: Integer;
begin
LbBiTrimSigZeros(N1);
LbBiTrimSigZeros(N2);
if (N1.dwUsed <> N2.dwUsed) then
begin
if (N1.dwUsed > N2.dwUsed) then
begin
Result := cGREATER_THAN;
Exit;
end
else
begin
Result := cLESS_THAN;
Exit;
end;
end;
Count := N1.dwUsed;
while PBiByteArray(N1.IntBuf.pBuf)[Pred(Count)] = PBiByteArray(N2.IntBuf.pBuf)
[Pred(Count)] do
begin
Dec(Count);
if (Count = 0) then
begin
Result := cEQUAL_TO;
Exit;
end;
end;
if PBiByteArray(N1.IntBuf.pBuf)[Pred(Count)] > PBiByteArray(N2.IntBuf.pBuf)
[Pred(Count)] then
Result := cGREATER_THAN
else
Result := cLESS_THAN;
end;
procedure LbBiSubBase(const N1, N2: LbInteger; var Diff: LbInteger);
var
TempInt: Integer;
Borrow: WORD;
Count: Integer;
I: Integer;
begin
if (LbBiIsZero(N1)) then
begin
LbBiCopy(Diff, N2, N2.dwUsed);
Exit;
end;
if (LbBiIsZero(N2)) then
begin
LbBiCopy(Diff, N1, N1.dwUsed);
Exit;
end;
Borrow := 0;
I := Pred(N1.dwUsed);
for Count := 0 to I do
begin
TempInt := PBiByteArray(N1.IntBuf.pBuf)[Count];
if (N2.dwUsed < Succ(Count)) then
TempInt := TempInt - Borrow
else
TempInt := TempInt - (PBiByteArray(N2.IntBuf.pBuf)[Count] + Borrow);
if (TempInt < 0) then
begin
Inc(TempInt, cBYTE_POSSIBLE_VALUES);
Borrow := 1;
end
else
Borrow := 0;
if (Succ(Diff.dwUsed) > Diff.IntBuf.dwLen) then
LbBiRealloc(Diff, Succ(Diff.dwUsed));
PBiByteArray(Diff.IntBuf.pBuf)[Diff.dwUsed] := TempInt;
Inc(Diff.dwUsed);
end;
if (Borrow <> 0) then
raise Exception.Create(sBISubtractErr);
LbBiTrimSigZeros(Diff);
end;
procedure LbBiAdd(const N1, N2: LbInteger; var Sum: LbInteger);
var
Value: ShortInt;
begin
if (N1.bSign = N2.bSign) then
begin
Sum.bSign := N1.bSign;
LbBiAddBase(N1, N2, Sum);
end
else
begin
Value := LbBiAbs(N1, N2);
if (Value = cEQUAL_TO) then
begin
LbBiAddByte(Sum, cPREPEND_ARRAY, $00);
Exit;
end
else if (Value = cGREATER_THAN) then
begin
Sum.bSign := N1.bSign;
LbBiSubBase(N1, N2, Sum);
end
else
begin
Sum.bSign := N2.bSign;
LbBiSubBase(N2, N1, Sum);
end;
end;
end;
procedure LbBiSub(const N1: LbInteger; N2: LbInteger; var Diff: LbInteger);
begin
N2.bSign := not N2.bSign;
LbBiAdd(N1, N2, Diff);
end;
procedure LbBiSubInPlace(var N1: LbInteger; const N2: LbInteger);
var
Difference: LbInteger;
Precision: Integer;
begin
if (N1.dwUsed > N2.dwUsed) then
Precision := Succ(N1.dwUsed)
else
Precision := Succ(N2.dwUsed);
LbBiInit(Difference, Precision);
try
LbBiSub(N1, N2, Difference);
LbBiClear(N1);
N1.dwUsed := Difference.dwUsed;
N1.bSign := Difference.bSign;
if (N1.IntBuf.dwLen < Difference.IntBuf.dwLen) then
LbBiRealloc(N1, Difference.IntBuf.dwLen);
LbBiCopy(N1, Difference, Difference.dwUsed);
finally
LbBiFree(Difference);
end;
end;
procedure LbBiDivByDigitBase(const N1: LbInteger; const N2: Byte;
var Quotient: LbInteger; var Remainder: Byte);
var
Factor: Byte;
I: Integer;
Temp: Integer;
SigDivd: LongInt;
LclQT: LongInt;
Carry: WORD;
Plc: Integer;
LclDVD: LbInteger;
Divisor: Byte;
begin
LbBiInit(LclDVD, N1.dwUsed);
Carry := 0;
try
if (LbBiIsZero(N1)) then
begin
LbBiAddByte(Quotient, cPREPEND_ARRAY, $00);
Exit;
end;
if (N2 = 1) then
begin
LbBiCopy(Quotient, N1, N1.dwUsed);
Exit;
end;
if (N2 = 0) then
raise Exception.Create(sBIZeroDivide);
LbBiCopy(LclDVD, N1, N1.dwUsed);
Divisor := N2;
Factor := LbBiFindFactor(N2);
if (Factor <> 1) then
begin
LbBiMulByDigitInPlace(LclDVD, Factor);
Divisor := Divisor * Factor;
end;
if PBiByteArray(LclDVD.IntBuf.pBuf)[Pred(LclDVD.dwUsed)] >= Divisor then
begin
LbBiAddByte(LclDVD, cAPPEND_ARRAY, $00);
end;
LbBiClear(Quotient);
Remainder := 0;
Plc := Pred(LclDVD.dwUsed);
if (LclDVD.dwUsed > Quotient.dwUsed) then
LbBiRealloc(Quotient, LclDVD.dwUsed);
Carry := 0;
Temp := Pred(LclDVD.dwUsed);
for I := Temp downto 0 do
begin
SigDivd := (Carry shl 8) or
(Integer(PBiByteArray(LclDVD.IntBuf.pBuf)[I]));
if (SigDivd < Divisor) then
begin
Carry := SigDivd;
Dec(Plc);
continue;
end;
LclQT := SigDivd div Divisor;
if (LclQT <> 0) then
begin
if (LclQT >= cBYTE_POSSIBLE_VALUES) then
LclQT := Pred(cBYTE_POSSIBLE_VALUES);
while SigDivd < (Divisor * LclQT) do
begin
Dec(LclQT);
if (LclQT = 0) then
raise Exception.Create(sBIQuotientErr);
end;
end;
if (LclQT <> 0) then
begin
PBiByteArray(Quotient.IntBuf.pBuf)[Plc] := LclQT;
if (Quotient.dwUsed < Succ(Plc)) then
Quotient.dwUsed := Succ(Plc);
Carry := SigDivd - (Divisor * LclQT);
end;
Dec(Plc);
end;
finally
Remainder := Carry;
if (Quotient.dwUsed = 0) then
LbBiAddByte(Quotient, cPREPEND_ARRAY, $00);
LbBiFree(LclDVD);
LbBiTrimSigZeros(Quotient);
end;
end;
procedure LbBiDivByDigit(const N1: LbInteger; const N2: Byte;
var Quotient: LbInteger; var Remainder: Byte);
begin
LbBiDivByDigitBase(N1, N2, Quotient, Remainder);
Quotient.bSign := N1.bSign;
end;
procedure LbBiDivByDigitInPlace(var N1: LbInteger; const N2: Byte;
var Remainder: Byte);
var
Temp: LbInteger;
Precision: Integer;
begin
Precision := (N1.dwUsed + 1) * 2;
LbBiInit(Temp, Precision);
try
LbBiDivByDigit(N1, N2, Temp, Remainder);
N1.dwUsed := Temp.dwUsed;
N1.bSign := Temp.bSign;
if (N1.IntBuf.dwLen < Temp.IntBuf.dwLen) then
LbBiRealloc(N1, Temp.IntBuf.dwLen);
LbBiCopy(N1, Temp, Temp.dwUsed);
finally
LbBiFree(Temp);
end;
end;
procedure LbBiDivBase(const N1, N2: LbInteger;
var Quotient, Remainder: LbInteger);
var
Factor: Byte;
InxQ: Integer;
InxX: Integer;
TmpByte: Byte;
TempInt: Integer;
SigDigit: Byte;
LclQT: LongInt;
LclDVD: LbInteger;
LclDSR: LbInteger;
TempDR: LbInteger;
TempBN: LbInteger;
SigDivd: LongInt;
begin
LbBiInit(LclDVD, N1.dwUsed);
LbBiInit(LclDSR, N1.dwUsed);
LbBiInit(TempDR, N1.dwUsed);
LbBiInit(TempBN, N1.dwUsed);
try
if (N1.dwUsed < 1) or (N2.dwUsed < 1) then
raise Exception.Create(sBINoNumber);
if LbBiIsZero(N1) then
begin
LbBiAddByte(Quotient, cPREPEND_ARRAY, $00);
LbBiAddByte(Remainder, cPREPEND_ARRAY, $00);
Exit;
end;
if LbBiIsOne(N2) then
begin
LbBiCopy(Quotient, N1, N1.dwUsed);
LbBiAddByte(Remainder, cPREPEND_ARRAY, $00);
Exit;
end;
if LbBiIsZero(N2) then
raise Exception.Create(sBIZeroDivide);
LbBiCopy(LclDVD, N1, N1.dwUsed);
LbBiCopy(LclDSR, N2, N2.dwUsed);
LbBiTrimSigZeros(LclDSR);
TmpByte := PBiByteArray(LclDSR.IntBuf.pBuf)[Pred(LclDSR.dwUsed)];
if (TmpByte = 0) then
raise Exception.Create(sBIZeroFactor);
Factor := LbBiFindFactor(TmpByte);
if (Factor <> 1) then
begin
LbBiMulByDigitInPlace(LclDVD, Factor);
LbBiMulByDigitInPlace(LclDSR, Factor);
end;
if PBiByteArray(LclDVD.IntBuf.pBuf)[Pred(LclDVD.dwUsed)] >=
PBiByteArray(LclDSR.IntBuf.pBuf)[Pred(LclDSR.dwUsed)] then
begin
LbBiAddByte(LclDVD, cAPPEND_ARRAY, $00);
end;
while (LclDVD.dwUsed < LclDSR.dwUsed) do
LbBiAddByte(LclDVD, cAPPEND_ARRAY, $00);
InxQ := LclDVD.dwUsed - LclDSR.dwUsed + 1;
InxX := LclDVD.dwUsed;
LbBiClear(Quotient);
LbBiClear(Remainder);
SigDigit := PBiByteArray(LclDSR.IntBuf.pBuf)[Pred(LclDSR.dwUsed)];
if (SigDigit = 0) then
begin
TempInt := Pred(LclDSR.dwUsed);
while SigDigit = 0 do
begin
SigDigit := PBiByteArray(LclDSR.IntBuf.pBuf)[TempInt];
Dec(TempInt);
if TempInt < 0 then
raise Exception.Create(sBIQuotientErr);
end;
end;
while InxQ >= 1 do
begin
if (LclDVD.dwUsed = 1) then
SigDivd := PBiByteArray(LclDVD.IntBuf.pBuf)[0]
else
SigDivd := Integer(PBiByteArray(LclDVD.IntBuf.pBuf)[InxX])
shl 8 + PBiByteArray(LclDVD.IntBuf.pBuf)[Pred(InxX)];
LclQT := SigDivd div SigDigit;
if (LclQT <> 0) then
begin
if (LclQT >= cBYTE_POSSIBLE_VALUES) then
LclQT := Pred(cBYTE_POSSIBLE_VALUES);
LbBiClear(TempDR);
LbBiMove(TempDR, LclDSR, InxQ, LclDSR.dwUsed);
LbBiMulByDigitInPlace(TempDR, LclQT);
while (LbBiCompare(LclDVD, TempDR) = cLESS_THAN) do
begin
Dec(LclQT);
if (LclQT = 0) then
break;
LbBiClear(TempDR);
LbBiMove(TempDR, LclDSR, InxQ, LclDSR.dwUsed);
LbBiMulByDigitInPlace(TempDR, LclQT);
end;
end;
if (LclQT <> 0) then
begin
LbBiAddByte(Quotient, InxQ, LclQT);
LbBiSubInPlace(LclDVD, TempDR);
end;
Dec(InxX);
Dec(InxQ);
end;
LbBiCopy(Remainder, LclDVD, LclDVD.dwUsed);
if (Factor <> 0) then
begin
if (Remainder.dwUsed > 1) then
begin
LbBiDivByDigitInPlace(Remainder, Factor, TmpByte);
end
else if (Remainder.dwUsed = 1) then
begin
TmpByte := PBiByteArray(Remainder.IntBuf.pBuf)[0];
TmpByte := TmpByte div Factor;
LbBiAddByte(Remainder, cPREPEND_ARRAY, TmpByte);
end;
end;
finally
LbBiFree(LclDVD);
LbBiFree(LclDSR);
LbBiFree(TempDR);
LbBiFree(TempBN);
if (Quotient.dwUsed = 0) then
LbBiAddByte(Quotient, cPREPEND_ARRAY, $00);
if (Remainder.dwUsed = 0) then
begin
LbBiAddByte(Remainder, cPREPEND_ARRAY, $00);
end;
LbBiTrimSigZeros(Quotient);
LbBiTrimSigZeros(Remainder);
end;
end;
procedure LbBiDiv(const N1, N2: LbInteger; var Quotient, Remainder: LbInteger);
begin
LbBiDivBase(N1, N2, Quotient, Remainder);
if (N1.bSign = N2.bSign) then
Quotient.bSign := cPOSITIVE
else
Quotient.bSign := cNEGATIVE;
end;
procedure LbBiMod(const N1, N2: LbInteger; var Remainder: LbInteger);
var
Quotient: LbInteger;
begin
LbBiInit(Quotient, N2.dwUsed);
LbBiDiv(N1, N2, Quotient, Remainder);
LbBiFree(Quotient);
end;
procedure LbBiModInPlace(var N1: LbInteger; const Modulas: LbInteger);
var
Remainder: LbInteger;
begin
LbBiInit(Remainder, Modulas.dwUsed);
LbBiMod(N1, Modulas, Remainder);
LbBiClear(N1);
N1.dwUsed := Remainder.dwUsed;
N1.bSign := Remainder.bSign;
if (N1.IntBuf.dwLen < Remainder.IntBuf.dwLen) then
LbBiRealloc(N1, Remainder.IntBuf.dwLen);
LbBiCopy(N1, Remainder, Remainder.dwUsed);
LbBiFree(Remainder);
end;
procedure LbBiPowerAndMod(const I1, Exponent, Modulus: LbInteger;
var Result: LbInteger);
var
BitCount: Integer;
I: Integer;
TempByte: Byte;
Hold: LbInteger;
begin
LbBiClear(Result);
if (LbBiIsZero(Exponent)) then
begin
LbBiAddByte(Result, cPREPEND_ARRAY, $01);
Exit;
end;
LbBiInit(Hold, cDEFAULT_PRECISION);
try
I := Exponent.dwUsed;
LbBiAddByte(Result, cPREPEND_ARRAY, $01);
while I > 0 do
begin
TempByte := LbBiGetByteValue(Exponent, I);
Dec(I);
BitCount := 8;
TempByte := LbBiReverseBits(TempByte);
while BitCount > 0 do
begin
LbBiMultInPlace(Result, Result);
LbBiModInPlace(Result, Modulus);
if Odd(TempByte) then
begin
LbBiMultInPlace(Result, I1);
LbBiModInPlace(Result, Modulus);
end;
TempByte := TempByte shr 1;
Dec(BitCount);
end;
end;
finally
LbBiFree(Hold);
end;
end;
procedure LbBiPowerAndModInPLace(var I1: LbInteger;
const Exponent, Modulus: LbInteger);
var
Result: LbInteger;
begin
LbBiInit(Result, cUSE_DEFAULT_PRECISION);
try
LbBiPowerAndMod(I1, Exponent, Modulus, Result);
LbBiClear(I1);
I1.dwUsed := Result.dwUsed;
I1.bSign := Result.bSign;
if (I1.IntBuf.dwLen < Result.IntBuf.dwLen) then
LbBiRealloc(I1, Result.IntBuf.dwLen);
LbBiCopy(I1, Result, Result.dwUsed);
finally
LbBiFree(Result);
end;
end;
{TSerialDecoder}function TSerialDecoder.GetDecodedBytes: TBytes;
var
Serial, Exponent, Modulas, Output: LbInteger;
begin
Serial := GetLargeIntFromBytes(FSerialNum);
try
Modulas := GetLargeIntFromBytes(FPublicMod);
try
Exponent := GetLargeIntFromBytes(FPublicExp);
try
LbBiInit(Output, cUSE_DEFAULT_PRECISION);
try
LbBiPowerAndMod(Serial, Exponent, Modulas, Output);
Result := GetBytesFromLargeInt(Output);
finally
LbBiFree(Output);
end;
finally
LbBiFree(Exponent);
end;
finally
LbBiFree(Modulas);
end;
finally
LbBiFree(Serial);
end;
end;
function TSerialDecoder.GetBytesFromBase64(const Value: string): TBytes;
{$IFDEF FPC}var
Input: TStringStream;
Output: TMemoryStream;
Decoder: TBase64DecodingStream;
{$ENDIF}begin{$IFDEF FPC}
Input := TStringStream.Create(Value);
try
Decoder := TBase64DecodingStream.Create(Input);
try
Output := TMemoryStream.Create;
try
Result := nil;
Output.CopyFrom(Decoder, Decoder.size);
SetLength(Result, Output.size);
Move(Output.Memory^, Result[0], Output.size);
finally
Output.Free;
end;
finally
Decoder.Free;
end;
finally
Input.Free;
end;
{$ELSE}
Result := TNetEncoding.Base64.DecodeStringToBytes(Value);
{$ENDIF}end;
function TSerialDecoder.GetLargeIntFromBytes(const Bytes: TBytes): LbInteger;
var
I: Integer;
begin
Result.bSign := cDEFAULT_SIGN;
Result.IntBuf.pBuf := AllocMem(Length(Bytes));
Result.IntBuf.dwLen := Length(Bytes);
Result.dwUsed := Length(Bytes);
for I := High(Bytes) downto Low(Bytes) do
Result.IntBuf.pBuf[High(Bytes) - I] := Bytes[I];
end;
function TSerialDecoder.GetBytesFromLargeInt(const Value: LbInteger): TBytes;
var
I: Integer;
begin
Result := nil;
SetLength(Result, Value.dwUsed);
for I := Low(Result) to High(Result) do
Result[High(Result) - I] := Value.IntBuf.pBuf[I];
end;
function TSerialDecoder.GetBuildDate: TDate;
begin{Constant is in format YYYY-MM-DD}
Result := EncodeDate(StrToInt(Copy(BuildDate, 1, 4)),
StrToInt(Copy(BuildDate, 6, 2)), StrToInt(Copy(BuildDate, 9, 2)));
end;
procedure TSerialDecoder.SetPublicExp(const Value: string);
begin
FPublicExp := GetBytesFromBase64(Value);
if Length(FPublicExp) >= 8 then
Assert(Length(FPublicExp) mod 8 = 0);
end;
procedure TSerialDecoder.SetPublicMod(const Value: string);
begin
FPublicMod := GetBytesFromBase64(Value);
if Length(FPublicMod) >= 8 then
Assert(Length(FPublicMod) mod 8 = 0);
end;
procedure TSerialDecoder.SetSerialNum(const Value: string);
begin
FSerialNum := GetBytesFromBase64(Value);
end;
procedure TSerialDecoder.SetProductCode(const Value: UInt64);
begin
FProductCode := Value;
end;
function TSerialDecoder.VMProtectSetSerialNumber(
const SerialNumber: string): LongWord;
var
HashData: PByte;
HashSize: Integer;
DataStart: Integer;
Digest: {$IFDEF FPC}TSHA1Digest{$ELSE}THashSHA1{$ENDIF};
BytesStream: TBytesStream;
Buffer: TBytes;
Len: Byte;
begin
try{Assign serial number}
SetSerialNum(SerialNumber);
{Check the black list}
CheckSerialBlackList;
{Decode license key}
BytesStream := TBytesStream.Create(GetDecodedBytes);
try{Find data section}while BytesStream.ReadByte <> 0 do
begin{Skip random data}end;
DataStart := BytesStream.Position;
while BytesStream.Position < BytesStream.Size do
begin
case BytesStream.ReadByte of
$01: {Version}begin
if BytesStream.ReadByte <> 1 then
Abort;
end;
$02: {User name}begin
Len := BytesStream.ReadByte;
SetLength(Buffer, Len);
BytesStream.ReadBuffer(Buffer[0], Length(Buffer));
{$IFDEF FPC}
FSerialNumberData.wUserName := TEncoding.UTF8.GetString(Buffer);
{$ELSE}
WStrPLCopy(@FSerialNumberData.wUserName, TEncoding.UTF8.GetString(
Buffer), Length(FSerialNumberData.wUserName) - 1);
{$ENDIF}end;
$03: {E-mail}begin
Len := BytesStream.ReadByte;
SetLength(Buffer, Len);
BytesStream.ReadBuffer(Buffer[0], Length(Buffer));
{$IFDEF FPC}
FSerialNumberData.wEMail := TEncoding.UTF8.GetString(Buffer);
{$ELSE}
WStrPLCopy(@FSerialNumberData.wEMail, TEncoding.UTF8.GetString(
Buffer), Length(FSerialNumberData.wEMail) - 1);
{$ENDIF}end;
$04: {Hardware ID}begin
BytesStream.Seek(BytesStream.ReadByte, soFromCurrent);
end;
$05: {License expiration date}begin
FSerialNumberData.dtExpire.bDay := BytesStream.ReadByte;
FSerialNumberData.dtExpire.bMonth := BytesStream.ReadByte;
FSerialNumberData.dtExpire.wYear := BytesStream.ReadWord;
end;
$06: {Maximum operation time}begin
FSerialNumberData.bRunningTime := BytesStream.ReadByte;
end;
$07: {Product code}begin
if BytesStream.ReadQWord <> FProductCode then
Abort;
end;
$08: {User data}begin
FSerialNumberData.nUserDataLength := BytesStream.ReadByte;
BytesStream.ReadBuffer(FSerialNumberData.bUserData,
FSerialNumberData.nUserDataLength);
end;
$09: {Maximum build date}begin
FSerialNumberData.dtMaxBuild.bDay := BytesStream.ReadByte;
FSerialNumberData.dtMaxBuild.bMonth := BytesStream.ReadByte;
FSerialNumberData.dtMaxBuild.wYear := BytesStream.ReadWord;
end;
$FF: {Checksum}begin
HashData := PByte(BytesStream.Memory) + DataStart;
HashSize := BytesStream.Position - DataStart - 1;
{$IFDEF FPC}
Digest := SHA1Buffer(HashData^, HashSize);
if NtoBE(BytesStream.ReadDWord) <> PLongWord(@Digest)^ then
Abort;
{$ELSE}
Digest := THashSHA1.Create;
Digest.Update(HashData^, HashSize);
if THash.ToBigEndian(BytesStream.ReadDWord) <> PLongWord(
@Digest.HashAsBytes[0])^ then
Abort;
{$ENDIF}
Break;
end
else
Abort;
end;
end;
finally
BytesStream.Free;
end;
{Loop finished and we check data}if (LongWord(FSerialNumberData.dtExpire) <> 0) and
(Date > EncodeDate(
FSerialNumberData.dtExpire.wYear,
FSerialNumberData.dtExpire.bMonth,
FSerialNumberData.dtExpire.bDay)) then
begin
FSerialNumberData.nState := SERIAL_STATE_FLAG_DATE_EXPIRED;
Exit(FSerialNumberData.nState);
end;
if (LongWord(FSerialNumberData.dtMaxBuild) <> 0) and
(GetBuildDate > EncodeDate(
FSerialNumberData.dtMaxBuild.wYear,
FSerialNumberData.dtMaxBuild.bMonth,
FSerialNumberData.dtMaxBuild.bDay)) then
begin
FSerialNumberData.nState := SERIAL_STATE_FLAG_MAX_BUILD_EXPIRED;
Exit(FSerialNumberData.nState);
end;
{Everything appears to be fine}
FSerialNumberData.nState := SERIAL_STATE_SUCCESS;
Result := FSerialNumberData.nState;
except
FSerialNumberData.nState := SERIAL_STATE_FLAG_INVALID;
Result := FSerialNumberData.nState;
end;
end;
function TSerialDecoder.VMProtectGetSerialNumberData(
Data: PVMProtectSerialNumberData; DataSize: Integer): Boolean;
begin
Result := SizeOf(FSerialNumberData) = DataSize;
if Result then
Move(FSerialNumberData, Data^, DataSize);
end;
procedure TSerialDecoder.CheckSerialBlackList;
const
BlackList: TArray<string> = [{$I BlackList.inc}];
var
Value, Hash: string;
Stream: TMemoryStream;
begin{$IFDEF FPC}
Hash := MD5Print(MD5Buffer(FSerialNum[0], Length(FSerialNum)));
{$ELSE}
Stream := TMemoryStream.Create;
try
Stream.WriteBuffer(FSerialNum[0], Length(FSerialNum));
Stream.Position := 0;
Hash := THashMD5.GetHashString(Stream);
{$ENDIF}for Value in BlackList do
if Value = Hash then
Abort;
{$IFNDEF FPC}finally
Stream.Free;
end;
{$ENDIF}end;
{$IFNDEF FPC}function TBytesStream.ReadByte: Byte;
begin
ReadBuffer(Result, SizeOf(Result));
end;
function TBytesStream.ReadWord: Word;
begin
ReadBuffer(Result, SizeOf(Result));
end;
function TBytesStream.ReadDWord: UInt32;
begin
ReadBuffer(Result, SizeOf(Result));
end;
function TBytesStream.ReadQWord: UInt64;
begin
ReadBuffer(Result, SizeOf(Result));
end;
{$ENDIF}end.
В файл BuildDate.inc идет дата сборки в YYYY-MM-DD, например '2023-01-05'. В BlackList.inc идут MD5 хеши ключей по чьим ордерам был рефанд или чаржбек. Система сборки запускает скрипт на моем сервере, который получает список таких ордеров от пайпро и вычисляет хеши таких ключей. Таким образом украденные и отмененные ключи блокируются автоматически при пересборке софта.
Использовать класс примерно так:
SerialDecoder := TSerialDecoder.Create;
SerialDecoder.SetProductCode(UInt64($XXXXXXXXXXXXXXX));//<- в VMP файле это Base64, его надо сконвертировать в число UInt64.
SerialDecoder.SetPublicExp('AAEAAQ==');
SerialDecoder.SetPublicMod('сюда публичный ключ в Base64');
Flags := SerialDecoder.VMProtectSetSerialNumber('текст ключа что ввел пользователь');
if Flags <> 0 then
begin//Что-то не так, проверяйте SERIAL_STATE_FLAG_***end
else
begin
Info := Default(TVMProtectSerialNumberData);
if SerialDecoder.VMProtectGetSerialNumberData(@Info, SizeOf(Info)) then//Все хорошо, достаем из ключа данные end;
ЧВ>3) Антивирусы тоже не особо понимают код виртуальной машины, а следовательно на всякий случай задетектим его как вирус. Короче говоря ложных срабатываний полно, особенно у говно-антивирусов второго эшелона.
Это, как я понимаю, главная проблема. Может быть, стоит купить протектор, но использовать его без функции шифрования кода? Что он ещё умеет?
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Здравствуйте, wantus, Вы писали:
W>Шифрование кода, в подавляющем числе случаем — это абсолютный overkill. Нормальный рабочий вариант — это внутренний проверки на модификацию кода, как статическую (патчи и кряки) так и динамическую (API hooking, DLL side loading, dynamic patching, etc.).
Я не очень понял, что это за проверки, поясните их принцип.
Полагаю, эти проверки устроены так, что в случае срабатывания отрубаются какие-то отдельные фичи программы? Так что программой в целом пользоваться по-прежнему можно, но будет мотивация всё-таки купить официальную версию. По идее, хакеры не разбираются во взламываемой программе и не будут проверять функциональность этих случайных фич.
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Здравствуйте, Черный 😈 Властелин, Вы писали:
ЧВ>В итоге я решил от VMProtect отказаться, оставив однако схему лицензирования и генерации ключей. То есть используются точно такие же ключи, только проверку выполняет мой собственный код, а для пользователей ничего не изменилось.
Интересно, схема генерации ключей точно ничем не защищена, не должна ли лицензироваться и т.д.
ЧВ>Поделюсь модулем для FPC/DELPHI который декодирует ключи VMProtect:
Коллеги, нет у кого-нибудь аналогичного кода для C#?
Здравствуйте, Khimik, Вы писали:
K>Здравствуйте, Черный 😈 Властелин, Вы писали:
ЧВ>>3) Антивирусы тоже не особо понимают код виртуальной машины, а следовательно на всякий случай задетектим его как вирус. Короче говоря ложных срабатываний полно, особенно у говно-антивирусов второго эшелона.
K>Это, как я понимаю, главная проблема. Может быть, стоит купить протектор, но использовать его без функции шифрования кода? Что он ещё умеет?
Причем тут шифрование? Проблема в другом: важное выделено жирным.
Антивирусы тоже не особо понимают код виртуальной машины
Хоть шифруй, хоть не шифруй — внутри все равно код виртуальной машины, которую анвири вкурить со смыслом не могут.
Но зло в другом!
Виртуальная машина или нет, шифрован код или нет — анвири все равно тупят, тупили и тупить будут.
История леденящая душу!
В одном из моих продуктов понадобилось мне, чтоб одна работающая копия могла завершить предыдущую, уже работающую.
Сказано-сделано!
Схема простая:
1) заводится именованный эвент, который ожидается в фоновом потоке в первом уже работающем экземпляре.
2) Второй экземпляр софтины, если запущен с определенным ключом командной строки (MySoft.exe /флаг_с_вещами_на_выход) открывает так называемый тьфу-млин, открывает вышеупомянутый эвент, сигналит, и тут же завершается (второй экземпляр)
3) первый (уже работающий) экземпляр, ловит в фоновом потоке событие, и как-то уже разруливается по делу, что мол пора завершаться: закрыть окна, сказать пользователю до свиданья, все дела.
Короче, проще пареной репы.
То бишь всю суть, делает этот именнованный эвент, ожидание и все такое. Фигня-война — написалось-причесалось за вечер со всеми делами (секурность, как закрываться в уже работающем экземпляре, что делать если закрываться пользователь все таки НЕ хочет — в общем все краевые ситуации).
Ну и прикрутилась к этой схеме простенькая отдельная exe-тулза для завершения работающего экземпляра приложения.
Совсем простенькая!
3 строчки кода(три, Карл! три): OpenEvent + SetEvent + CloseHandle.
И что тут блин началось!
Ага, млин да это мелварь, да всё такое, да она лезет в непонятные эвенты Виндовс.....
А я ж прихирел! Несколько раз перепроверял, пересобирал, еще раз перепроверял. И еще раз прихеривал, но уже сильнее...
Ан фиг! Мелварь и всё точка!
И ничего так, что основная софтина с десятком подобных эвентов дело имеет — с ней всё в порядке.
И опять же ничего так, что оная основная софтина шарится по чужим адресным пространствам, и какими только способами туда не пролезает. Моя софтина — в чужое адресное пространство "в дверь", а там заперто. Гуй с ним. Тогда через окно? Законопачено? Тады через чердак! И там не пущатЪ? Ну да фиг с Вами, тогда и по кул-хацки можно...
Ну и мало того, что лазиет по чужим адресным пространствам — дык это основная софтина, еще и данные оттуда, из-за забора к себе отгружает.
Казалось бы, вот на чтобы ругаться бы... Ан фиг! К основной софтине претензий нет.
А мелко-поделке в 3 строки кода (те самые OpenEvent+SetEvent...) — ПРЕТЕНЗИИ ТАКИ ЕСТЬ!!!
И это не говноантивирусы, это все таки был Avast — не самый поганый антивирус!
PS: зато родился лайф-хак как общаться в тех поддержкой Аваста на предмет false positive detection.
1) Начинать сходу по русски! Причем на русском-матерном! Пускай шекспирят на конференциях, да журналюгам очки втирают на аглицком. Здесь разговор по делу.
2) Начинать сие послание султанам-нах, нужно с эпической фразы: "Здорово щеглы, плятЪ!!!" (C).
3) Далее перечисление трех упомянутых функций, описание что и зачем, ссылка на официальную доку на официальном же сайте.
4-ое по списку но не менее важное: предложение выгнать нафиг студентов — рукожопых лоботрясов (C) аналитигоф первой линии!. Ну или хотя бы отправить в неоплачиваемый отпуск. А сэкомноленные средствА — всенепременно и заобязательно совместно — пропить. В хорошей компании, отличным чешским пивом.
Проверено!
Результат 100-процентный.
Забегали, засуеитились. Через секунду тикет висел уже в поддержке, через пару часов уже отписались в стиле "извини, колллега, хирню сморозили". Через два часа уже был именно что официальный ответ: ля-ля, ля-ля — наш косяк — в течении суток выложим исправления, и публичные базы обновим при ближайшем же ежедневном апдейте сигнатур.
Здравствуйте, sfsoft, Вы писали:
S>Здравствуйте, Черный 😈 Властелин, Вы писали:
S>Т.е. привязку к оборудованию ты не используешь? А как тогда отслеживается количество копий? Ключ можно всем друзьям раздать, например.
Есть такая штука — Valla — может быть чем-то поможет.
Здравствуйте, Khimik, Вы писали:
K>Здравствуйте, wantus, Вы писали:
W>>Шифрование кода, в подавляющем числе случаем — это абсолютный overkill. Нормальный рабочий вариант — это внутренний проверки на модификацию кода, как статическую (патчи и кряки) так и динамическую (API hooking, DLL side loading, dynamic patching, etc.).
K>Я не очень понял, что это за проверки, поясните их принцип. K>Полагаю, эти проверки устроены так, что в случае срабатывания отрубаются какие-то отдельные фичи программы? Так что программой в целом пользоваться по-прежнему можно, но будет мотивация всё-таки купить официальную версию. По идее, хакеры не разбираются во взламываемой программе и не будут проверять функциональность этих случайных фич.
Из простых вариантов
1. Добавить проверку подписи собственного exe (через WinVerifyTrust). По случайному таймеру, не сразу. Проверили, запомнили результат, запустили еще один таймер на несколько минут. Когда таймер сработал, покрасили background окна в красный цвет, или перевернули его кверх ногами, или еще чего-нибудь очевидно намеренное и кривое. Это уже достаточно противно патчить.
2. Добавить еще несколько копий этой проверки — полных копий всего кода, а не просто вызовов check_exe_signature() — и навешать их на разные события, 56-ой клик мышки или типа того. Опять же, все проверки и реакции на их результаты — отложенные, через таймеры, простые counters или чего там еще.
3. Могут запатчить сам WinVerifyTrust. На это добавляем вызовов WinVerifyTrust с кривыми параметрами и смотрим, что он таки возвращает ошибку.
Как бонус
4. Можно все эти проверки отключать, если похоже что программа бежит под дебагером — IsDebuggerPresent, PEB.BeingDebugged, NtQueryInformationProcess(..., 7, ...), etc. В принципе все хорошие дебагеры умеют притворяться, что их нет, но это простая в изготовлении какашка и подложить её не мешает.
5. Из той же серии в релиз билды можно нашпиговать NtSetInformationThread((HANDLE)-2, 0x11), что выключает дебаг текущего треда. Тоже обходится соответствующим add-on'ом к дебагеру, но далеко не все об этом знают.
Типа окопная партизанская война с целью измождения противника.
И как уже сказали — строго online licensing с public keys, никаких прошитых или алгоритмических ключей. Как побочный эффект, это помогает свести credit card fraud и chargebacks практически в ноль.
Здравствуйте, sfsoft, Вы писали:
S>Здравствуйте, Черный 😈 Властелин, Вы писали:
S>Т.е. привязку к оборудованию ты не используешь? А как тогда отслеживается количество копий? Ключ можно всем друзьям раздать, например.
Главное выделено жирным — привязка к оборудованию.
Нужно понимать его шире.
Привязка к оборудованию — это все таки просто некая сравнительно уникальная метка для конкретного пользователя\машины. Что это может быть на самом деле — да всё что угодно. А то упрутся в motherboard\hdd\что_еще серийный номер, и всё. Как будто ничего другого не бывает. И это не только, и главное, не обязательно именно железо.
Фантазируйте, коллеги, фантазируйте.
Всё равно всё сведется к банальному отношению (в математическом смысле). Сложность\надежность этого «замка» (hardware print) и ценность\важность\необходимость того функционала, который этот «замочек» защищает.
Ну, а «далее везде, со всеми остановками» (C)
Кто клиент (пользователь, компания)? Что делает код? «Где» это что-то делает ваш код (отдельный ПК или сервер)? Причем сервер в широком понимании: хошь веб-сервер, а хошь и нет — может это сервер печати бухгалтерского отдела конторы "Рога и Копыта"!?! Нужен ли по делу вашему приложению эти ваши интернеты?
Вот ответы на такие вопросы и дадут понимание, что нужно и что нет.
Примеры на вскидку: если веб для софтины нужен по делу, то тут не грех и онлайн-активацию приделать.
Примеру нумер Два: если ваш супер-пупер-сервер печати для бухгалтерского отдела конторы "Рога и копыта" начнётЪ печатитЪ вместо счетов фактур "Ойопта, нахЫр, наZ взломали!!!!" да еще и в псевдослучайном порядке, да еще и за день до сдачи балансового отчета...
Ну вы поняли... МарьВанна — она же главбух так называемого отдела, в момент подпишет платежку на ваш софт, и купит. Местный же кул-хацкер Пятя из IT-отела будет отметелен ногами в курилке если и не будет уволен, то увидит ближайшую премию только в случае скоропостижной кончины вышеозначенной МарьВанны, и то не факт.
А вот какую-нить онлайн активацию в эдаком (пусть и выдуманным) примере прикручивать может и не стОит. Ибо толковые и мнительные бухгалтерА (а толковые они заувсегда мнительные, и они в этом правы) могут подстремануться: а с какого перепуга это наш кул-супер-принтер в эти наши интернеты лезет? Чего он там забыл?
В общем размышлять следует в контексте и начиная со сценариев использования, а не с технической стороны: чего б такого бы шифрануть да захешить...
Здравствуйте, sfsoft, Вы писали: S>Здравствуйте, Черный 😈 Властелин, Вы писали: S>Т.е. привязку к оборудованию ты не используешь? А как тогда отслеживается количество копий? Ключ можно всем друзьям раздать, например.
Никак, все на доверии. Если кто-то будет использовать бесплатно, это не страшно, другие заплатят.
У меня несколько десятков ордеров в день, если переписываться с каждым у кого не работает ключ, поменялись железки или там закончились активации, когда тогда писать код?
Здравствуйте, Unhandled_Exception, Вы писали:
K>>Какой шифратор лучше выбрать — Execryptor или есть что-то новое?
U_E>Эх, это был первый проектор, который я использовал. Он уже очень много лет как не существует.
Сорри, я опечатался, хотел сказать The Enigma Protector.
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Здравствуйте, wantus, Вы писали:
W>1. Добавить проверку подписи собственного exe (через WinVerifyTrust). По случайному таймеру, не сразу. Проверили, запомнили результат, запустили еще один таймер на несколько минут. Когда таймер сработал, покрасили background окна в красный цвет, или перевернули его кверх ногами, или еще чего-нибудь очевидно намеренное и кривое. Это уже достаточно противно патчить.
Я сходу не нагуглил, что делает WinVerifyTrust, просьба пояснить. "запомнили результат" — какой результат, число возвращаемое WinVerifyTrust?
Я привык делать всё по-своему, мне легче изобрести велосипед, чем разбираться в чужих решениях. Можно подсчитать контрольную сумму exe файла собственной программы. Если хакер что-то в нём изменит, контрольная сумма поменяется, значит надо чтобы программа с временем это проверила, как вы написали.
Это не защитит от кейгена; по крайней мере, лично я рассылки ключей не боюсь (боюсь именно кейгена), поскольку моя программа требует регулярно обновлять ключи, чтобы ими можно было активировать (юзер может получить новый ключ на сайте).
Вот ещё пример простой уязвимости: если человек проинсталлирует программу, введёт ключ, а потом скопирует весь каталог с программой и перенесёт на другой компьютер. Значит надо использовать hardwareid, или хотя бы при активации запоминать, в каком каталоге хранится программа.
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Здравствуйте, sfsoft, Вы писали:
S>Т.е. привязку к оборудованию ты не используешь? А как тогда отслеживается количество копий? Ключ можно всем друзьям раздать, например.
Полагаю, любая лицензия подразумевает, что человек может хотя бы на минуту дать попользоваться друзьям, и тут в любом случае слишком сложно бороться. У меня ключ кодируется по данным покупателя (имя, фамилия и т.д.), и эти данные отображаются вверху главного окна программы.
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
Защита не нужна. Он-лайн активация, отзыв ключей (чтобы спокойно делать рефанды) и простая проверка ключа. Все.
Я в последнем большом обновлении всю защиту выкинул. Те два с половиной бедолаги, что в 2023 году все еще шарятся по помойкам в поисках кряка, все равно ничего не купят. Даже наличие кейгена не влияет на продажи по моему опыту.
Здравствуйте, Khimik, Вы писали:
K>Здравствуйте, wantus, Вы писали:
W>>1. Добавить проверку подписи собственного exe (через WinVerifyTrust). По случайному таймеру, не сразу. Проверили, запомнили результат, запустили еще один таймер на несколько минут. Когда таймер сработал, покрасили background окна в красный цвет, или перевернули его кверх ногами, или еще чего-нибудь очевидно намеренное и кривое. Это уже достаточно противно патчить.
K>Я сходу не нагуглил, что делает WinVerifyTrust, просьба пояснить.
2023 на дворе. Какие нафиг кейгены?
K>... юзер может получить новый ключ на сайте
То есть серверная часть уже есть? Это замечательно.
1. Напишите тривиальный HTTP/GET клиент на WinHTTP (пример есть на msdn), который из программы посылает на сайт MachineGUID и получает назад подписанную RSA ключом "лицензию" — [MachineGUID + срок годности + подпись]. Причем это можно даже по http делать, без https. На серверной стороне всё что требуется это вызвать "openssl rsautl -sign ..." из командной строки.
2. Зашейте public часть ключа в программу и периодически проверяйте наличие (а) лицензии (б) её подпись (в) соответствие MachineGUID.
3. Плюс добавьте отложенные по времени проверки целостности exe.
Этот комплект убирает риск 99% хаков.
И таки подписывайте ваши exe. Как минимум это делает очевидным, когда exe был модифицирован.