Gắn chương trình với các thuộc tính thời gian chạy
Để minh họa việc làm việc với các thuộc tính đã mô tả trong các phần trước, chúng ta hãy xem xét nhiệm vụ phổ biến là gắn một chương trình MQL vào môi trường phần cứng nhằm bảo vệ nó khỏi việc sao chép. Khi chương trình được phân phối qua MQL5 Market, việc gắn kết được chính dịch vụ này cung cấp. Tuy nhiên, nếu chương trình được phát triển theo yêu cầu riêng, nó có thể được liên kết với số tài khoản, tên của khách hàng, hoặc các thuộc tính có sẵn của terminal (máy tính). Cách đầu tiên không phải lúc nào cũng tiện lợi, vì nhiều nhà giao dịch có nhiều tài khoản thực (có thể với các nhà môi giới khác nhau), chưa kể đến các tài khoản demo với thời hạn sử dụng hạn chế. Cách thứ hai có thể là hư cấu hoặc quá phổ biến. Do đó, chúng ta sẽ triển khai một thuật toán mẫu để gắn chương trình vào một tập hợp các thuộc tính môi trường được chọn. Các phương thức bảo mật nghiêm túc hơn có thể sử dụng DLL và đọc trực tiếp nhãn phần cứng thiết bị từ Windows, nhưng không phải mọi khách hàng sẽ đồng ý chạy các thư viện tiềm ẩn nguy cơ không an toàn.
Phương thức bảo vệ của chúng ta được trình bày trong script EnvSignature.mq5
. Script này tính toán các giá trị băm từ các thuộc tính môi trường đã cho và tạo ra một chữ ký duy nhất (dấu ấn) dựa trên chúng.
Băm (hashing) là một quá trình xử lý đặc biệt đối với thông tin bất kỳ, kết quả là tạo ra một khối dữ liệu mới có các đặc điểm sau (được đảm bảo bởi thuật toán sử dụng):
- Giá trị băm trùng khớp cho hai tập dữ liệu gốc có nghĩa là, với xác suất gần 100%, dữ liệu đó giống hệt nhau (xác suất trùng khớp ngẫu nhiên là không đáng kể).
- Nếu dữ liệu gốc thay đổi, giá trị băm của chúng cũng sẽ thay đổi.
- Không thể khôi phục toán học dữ liệu gốc từ giá trị băm (chúng vẫn bí mật) trừ khi thực hiện liệt kê hoàn toàn các giá trị ban đầu có thể có (nếu kích thước ban đầu tăng lên và không có thông tin về cấu trúc của chúng, vấn đề này không thể giải quyết trong tương lai gần).
- Kích thước của băm là cố định (không phụ thuộc vào lượng dữ liệu ban đầu).
Giả sử một trong những thuộc tính môi trường được mô tả bởi chuỗi: "TERMINAL_LANGUAGE=German". Nó có thể được lấy bằng câu lệnh đơn giản như sau (được đơn giản hóa):
string language = EnumToString(TERMINAL_LANGUAGE) + "=" + TerminalInfoString(TERMINAL_LANGUAGE);
Ngôn ngữ thực tế sẽ khớp với cài đặt. Với một hàm giả định Hash
, chúng ta có thể tính toán chữ ký.
string signature = Hash(language);
Khi có nhiều thuộc tính hơn, chúng ta chỉ cần lặp lại quy trình cho tất cả chúng, hoặc yêu cầu băm từ các chuỗi kết hợp (cho đến nay đây là mã giả, không phải là một phần của chương trình thực tế).
string properties[];
// điền các dòng thuộc tính theo ý muốn
// ...
string signature;
for(int i = 0; i < ArraySize(properties); ++i)
{
signature += properties[i];
}
return Hash(signature);
2
3
4
5
6
7
8
9
Chữ ký nhận được có thể được người dùng báo cáo cho nhà phát triển chương trình, người sẽ "ký" nó theo cách đặc biệt, sau khi nhận được một chuỗi xác thực chỉ phù hợp với chữ ký này. Chữ ký cũng dựa trên băm và yêu cầu kiến thức về một bí mật (cụm mật khẩu), chỉ nhà phát triển biết và được mã hóa cứng vào chương trình (cho giai đoạn xác minh).
Nhà phát triển sẽ chuyển chuỗi xác thực cho người dùng, sau đó người dùng sẽ có thể chạy chương trình bằng cách chỉ định chuỗi này trong các tham số.
Khi khởi chạy mà không có chuỗi xác thực, chương trình sẽ tạo một chữ ký mới cho môi trường hiện tại, in nó vào nhật ký và thoát (thông tin này nên được chuyển cho nhà phát triển). Với một chuỗi xác thực không hợp lệ, chương trình sẽ hiển thị thông báo lỗi và thoát.
Một số chế độ khởi chạy có thể được cung cấp cho chính nhà phát triển: với chữ ký nhưng không có chuỗi xác thực (để tạo chuỗi cuối cùng), hoặc với chữ ký và chuỗi xác thực (ở đây chương trình sẽ ký lại chữ ký và so sánh nó với chuỗi xác thực được chỉ định chỉ để kiểm tra).
Hãy đánh giá mức độ chọn lọc của biện pháp bảo vệ này. Sau cùng, việc gắn kết ở đây không được thực hiện với một định danh duy nhất của bất cứ thứ gì.
Bảng sau cung cấp số liệu thống kê về hai đặc điểm: kích thước màn hình và RAM. Rõ ràng, các giá trị sẽ thay đổi theo thời gian, nhưng phân bố gần đúng sẽ vẫn như cũ: một vài giá trị đặc trưng sẽ phổ biến nhất, trong khi một số giá trị "mới" tiên tiến và "cũ" đang dần không còn được sử dụng sẽ tạo thành các "đuôi" giảm dần.
Màn hình | 1920x1080 | 1536x864 | 1440x900 | 1366x768 | 800x600 |
---|---|---|---|---|---|
RAM | 21% | 7% | 5% | 10% | 4% |
4Gb 20% | 4.20 | 1.40 | 1.00 | 2.0 | 0.8 |
8Gb 20% | 4.20 | 1.40 | 1.00 | 2.0 | 0.8 |
16Gb 15% | 3.15 | 1.05 | 0.75 | 1.5 | 0.6 |
32Gb 10% | 2.10 | 0.70 | 0.50 | 1.0 | 0.4 |
64Gb 5% | 1.05 | 0.35 | 0.25 | 0.5 | 0.2 |
Hãy chú ý đến các ô có giá trị lớn nhất, vì chúng có nghĩa là cùng một chữ ký (trừ khi chúng ta thêm một yếu tố ngẫu nhiên vào chúng, điều này sẽ được thảo luận dưới đây). Trong trường hợp này, hai tổ hợp đặc điểm ở góc trên bên trái có khả năng cao nhất, mỗi tổ hợp chiếm 4.2%. Nhưng đây chỉ là hai đặc điểm. Nếu bạn thêm ngôn ngữ giao diện, múi giờ, số lõi, và đường dẫn dữ liệu làm việc (tốt nhất là đường dẫn chung, vì nó chứa tên người dùng Windows) vào môi trường được đánh giá, thì số lượng trùng khớp tiềm năng sẽ giảm đáng kể.
Để băm, chúng ta sử dụng hàm tích hợp CryptEncode
(sẽ được mô tả trong phần Mã hóa) hỗ trợ phương thức băm SHA256. Như tên gọi của nó, nó tạo ra một giá trị băm dài 256 bit, tức là 32 byte. Nếu cần hiển thị nó cho người dùng, chúng ta sẽ chuyển nó thành văn bản ở dạng biểu diễn thập lục phân và nhận được chuỗi dài 64 ký tự.
Để làm cho chữ ký ngắn hơn, chúng ta sẽ chuyển đổi nó bằng mã hóa Base64 (cũng được hỗ trợ bởi hàm CryptEncode
và hàm đối ứng của nó là CryptDecode
), điều này sẽ cho ra chuỗi dài 44 ký tự. Không giống như thao tác băm một chiều, mã hóa Base64 có thể đảo ngược, tức là dữ liệu gốc có thể được khôi phục từ nó.
Các thao tác chính được triển khai bởi lớp EnvSignature
. Nó định nghĩa chuỗi data
cần tích lũy các đoạn mô tả môi trường. Giao diện công khai bao gồm một số phiên bản quá tải của hàm append
để thêm các chuỗi với thuộc tính môi trường. Về cơ bản, chúng nối tên của thuộc tính được yêu cầu và giá trị của nó bằng một phần tử trừu tượng được trả về bởi phương thức ảo pepper
làm liên kết. Lớp dẫn xuất sẽ định nghĩa nó dưới dạng một chuỗi cụ thể (nhưng nó có thể để trống).
class EnvSignature
{
private:
string data;
protected:
virtual string pepper() = 0;
public:
bool append(const ENUM_TERMINAL_INFO_STRING e)
{
return append(EnumToString(e) + pepper() + TerminalInfoString(e));
}
bool append(const ENUM_MQL_INFO_STRING e)
{
return append(EnumToString(e) + pepper() + MQLInfoString(e));
}
bool append(const ENUM_TERMINAL_INFO_INTEGER e)
{
return append(EnumToString(e) + pepper() + StringFormat("%d", TerminalInfoInteger(e)));
}
bool append(const ENUM_MQL_INFO_INTEGER e)
{
return append(EnumToString(e) + pepper() + StringFormat("%d", MQLInfoInteger(e)));
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
Để thêm một chuỗi bất kỳ vào một đối tượng, có một phương thức chung append
, được gọi trong các phương thức trên.
bool append(const string s)
{
data += s;
return true;
}
2
3
4
5
Tùy chọn, nhà phát triển có thể thêm cái gọi là "muối" (salt) vào dữ liệu được băm. Đây là một mảng với dữ liệu được tạo ngẫu nhiên, làm phức tạp thêm việc đảo ngược băm. Mỗi lần tạo chữ ký sẽ khác với lần trước, ngay cả khi môi trường không đổi. Việc triển khai tính năng này cũng như các khía cạnh bảo vệ cụ thể hơn (như sử dụng mã hóa đối xứng và tính toán động bí mật) được để lại cho việc tự nghiên cứu.
Vì môi trường bao gồm các thuộc tính nổi tiếng (danh sách của chúng bị giới hạn bởi các hằng số API MQL5), và không phải tất cả đều đủ độc đáo, biện pháp bảo vệ của chúng ta, như đã tính toán, có thể tạo ra các chữ ký giống nhau cho các người dùng khác nhau nếu chúng ta không sử dụng muối. Sự trùng khớp chữ ký sẽ không cho phép xác định nguồn rò rỉ giấy phép nếu điều đó xảy ra.
Do đó, bạn có thể tăng hiệu quả bảo vệ bằng cách thay đổi phương thức trình bày thuộc tính trước khi băm cho mỗi khách hàng. Tất nhiên, chính phương thức này không nên được tiết lộ. Trong ví dụ được xem xét, điều này ngụ ý thay đổi nội dung của phương thức pepper
và biên dịch lại sản phẩm. Điều này có thể tốn kém, nhưng nó cho phép tránh sử dụng muối ngẫu nhiên.
Với chuỗi thuộc tính đã được điền, chúng ta có thể tạo chữ ký. Điều này được thực hiện bằng phương thức emit
.
string emit() const
{
uchar pack[];
if(StringToCharArray(data + secret(), pack, 0, StringLen(data) + StringLen(secret()), CP_UTF8) <= 0) return NULL;
uchar key[], result[];
if(CryptEncode(CRYPT_HASH_SHA256, pack, key, result) <= 0) return NULL;
Print("Hash bytes:");
ArrayPrint(result);
uchar text[];
CryptEncode(CRYPT_BASE64, result, key, text);
return CharArrayToString(text);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Phương thức này thêm một bí mật nhất định (chuỗi byte chỉ nhà phát triển biết và nằm trong chương trình) vào dữ liệu và tính toán băm cho chuỗi chung. Bí mật được lấy từ phương thức ảo secret
, cũng sẽ được định nghĩa bởi lớp dẫn xuất.
Mảng byte kết quả với băm được mã hóa thành chuỗi bằng Base64.
Bây giờ đến hàm quan trọng nhất của lớp: check
. Chính hàm này thực hiện việc ký từ nhà phát triển và kiểm tra từ người dùng.
bool check(const string sig, string &validation)
{
uchar bytes[];
const int n = StringToCharArray(sig + secret(), bytes, 0, StringLen(sig) + StringLen(secret()), CP_UTF8);
if(n <= 0) return false;
uchar key[], result1[], result2[];
if(CryptEncode(CRYPT_HASH_SHA256, bytes, key, result1) <= 0) return false;
/*
WARNING
The following code should only be present in the developer utility.
The program supplied to the user must compile without this if.
*/
#ifdef I_AM_DEVELOPER
if(StringLen(validation) == 0)
{
if(CryptEncode(CRYPT_BASE64, result1, key, result2) <= 0) return false;
validation = CharArrayToString(result2);
return true;
}
#endif
uchar values[];
// the exact length is needed to not append terminating '0'
if(StringToCharArray(validation, valuesすごく, 0, StringLen(validation)) <= 0) return false;
if(CryptDecode(CRYPT_BASE64, values, key, result2) <= 0) return false;
return ArrayCompare(result1, result2) == 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Trong hoạt động bình thường (đối với người dùng), phương thức tính toán băm từ chữ ký nhận được, được bổ sung bởi bí mật, và so sánh nó với giá trị từ chuỗi xác thực (nó phải được giải mã từ Base64 thành biểu diễn nhị phân thô của băm trước). Nếu hai giá trị băm khớp nhau, việc xác thực thành công: chuỗi xác thực khớp với tập hợp thuộc tính. Rõ ràng, một chuỗi xác thực trống (hoặc chuỗi được nhập ngẫu nhiên) sẽ không vượt qua bài kiểm tra.
Trên máy của nhà phát triển, macro I_AM_DEVELOPER
phải được định nghĩa trong mã nguồn cho tiện ích chữ ký, dẫn đến việc xử lý chuỗi xác thực trống khác đi. Trong trường hợp này, băm kết quả được mã hóa Base64, và chuỗi này được truyền ra ngoài qua tham số validation
. Do đó, tiện ích sẽ có thể hiển thị chuỗi xác thực sẵn sàng cho chữ ký đã cho cho nhà phát triển.
Để tạo một đối tượng, bạn cần một lớp dẫn xuất nhất định định nghĩa các chuỗi với bí mật và pepper
.
// WARNING: change the macro to your own set of random bytes
#define PROGRAM_SPECIFIC_SECRET "<PROGRAM-SPECIFIC-SECRET>"
// WARNING: choose your characters to link in pairs name'='value
#define INSTANCE_SPECIFIC_PEPPER "=" // obvious single sign is selected for demo
// WARNING: the following macro needs to be disabled in the real product,
// it should only be in the signature utility
#define I_AM_DEVELOPER
#ifdef I_AM_DEVELOPER
#define INPUT input
#else
#define INPUT const
#endif
INPUT string Signature = "";
INPUT string Secret = PROGRAM_SPECIFIC_SECRET;
INPUT string Pepper = INSTANCE_SPECIFIC_PEPPER;
class MyEnvSignature : public EnvSignature
{
protected:
virtual string secret() override
{
return Secret;
}
virtual string pepper() override
{
return Pepper;
}
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
Hãy nhanh chóng chọn một vài thuộc tính để điền vào chữ ký.
void FillEnvironment(EnvSignature &env)
{
// thứ tự không quan trọng, bạn có thể trộn lẫn
env.append(TERMINAL_LANGUAGE);
env.append(TERMINAL_COMMONDATA_PATH);
env.append(TERMINAL_CPU_CORES);
env.append(TERMINAL_MEMORY_PHYSICAL);
env.append(TERMINAL_SCREEN_DPI);
env.append(TERMINAL_SCREEN_WIDTH);
env.append(TERMINAL_SCREEN_HEIGHT);
env.append(TERMINAL_VPS);
env.append(MQL_PROGRAM_TYPE);
}
2
3
4
5
6
7
8
9
10
11
12
13
Bây giờ mọi thứ đã sẵn sàng để kiểm tra phương thức bảo vệ của chúng ta trong hàm OnStart
. Nhưng trước tiên, hãy xem xét các biến đầu vào. Vì cùng một chương trình sẽ được biên dịch thành hai phiên bản, cho người dùng cuối và cho nhà phát triển, có hai tập hợp biến đầu vào: để nhập dữ liệu đăng ký bởi người dùng và để tạo dữ liệu này dựa trên chữ ký của nhà phát triển. Các biến đầu vào dành cho nhà phát triển đã được mô tả ở trên bằng macro INPUT
. Chỉ chuỗi xác thực là có sẵn cho người dùng.
input string Validation = "";
Khi chuỗi trống, chương trình sẽ thu thập dữ liệu môi trường, tạo chữ ký mới và in nó vào nhật ký. Điều này hoàn thành công việc của script vì quyền truy cập vào mã hữu ích chưa được xác nhận.
void OnStart()
{
MyEnvSignature env;
string signature;
if(StringLen(Signature) > 0)
{
// ... đây sẽ là mã cần được tác giả ký
}
else
{
FillEnvironment(env);
signature = env.emit();
}
if(StringLen(Validation) == 0)
{
Print("Validation string from developer is required to run this script");
Print("Environment Signature is generated for current state...");
Print("Signature:", signature);
return;
}
else
{
// ... kiểm tra chuỗi xác thực ở đây
}
Print("The script is validated and running normally");
// ... mã làm việc thực tế ở đây
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
Nếu biến Validation
được điền, chúng ta kiểm tra sự tuân thủ của nó với chữ ký và kết thúc công việc trong trường hợp thất bại.
if(StringLen(Validation) == 0)
{
...
}
else
{
validation = Validation; // cần một đối số không const
const bool accessGranted = env.check(Signature, validation);
if(!accessGranted)
{
Print("Wrong validation string, terminating");
return;
}
// thành công
}
Print("The script is validated and running normally");
// ... mã làm việc thực tế ở đây
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Nếu không có sự khác biệt, thuật toán sẽ chuyển sang mã làm việc của chương trình.
Ở phía nhà phát triển (trong phiên bản chương trình được xây dựng với macro I_AM_DEVELOPER
), một chữ ký có thể được đưa vào. Chúng ta khôi phục trạng thái của đối tượng MyEnvSignature
bằng chữ ký và tính toán chuỗi xác thực.
void OnStart()
{
...
if(StringLen(Signature) > 0)
{
#ifdef I_AM_DEVELOPER
if(StringLen(Validation) == 0)
{
string validation;
if(env.check(Signature, validation))
Print("Validation:", validation);
return;
}
signature = Signature;
#endif
}
...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Nhà phát triển không chỉ có thể chỉ định chữ ký mà còn xác thực nó: trong trường hợp này, việc thực thi mã sẽ tiếp tục ở chế độ người dùng (cho mục đích gỡ lỗi).
Nếu muốn, bạn có thể mô phỏng một thay đổi trong môi trường, ví dụ, như sau:
FillEnvironment(env);
// cố tình thay đổi môi trường (thêm múi giờ)
// env.append("Dummy" + (string)(TimeGMTOffset() - TimeDaylightSavings()));
const string update = env.emit();
if(update != signature)
{
Print("Signature and environment mismatch");
return;
}
2
3
4
5
6
7
8
9
Hãy xem một vài nhật ký thử nghiệm.
Khi chạy script EnvSignature.mq5
lần đầu tiên, "người dùng" sẽ thấy một nhật ký tương tự như sau (các giá trị sẽ thay đổi do sự khác biệt trong môi trường):
Hash bytes:
4 249 194 161 242 28 43 60 180 195 54 254 97 223 144 247 216 103 238 245 244 224 7 68 101 253 248 134 27 102 202 153
Validation string from developer is required to run this script
Environment Signature is generated for current state...
Signature:BPnCofIcKzy0wzb+Yd+Q99hn7vX04AdEZf34hhtmypk=
2
3
4
5
Nó gửi chữ ký được tạo ra cho "nhà phát triển" (không có người dùng thực tế trong quá trình thử nghiệm, vì vậy tất cả vai trò của "người dùng" và "nhà phát triển" đều được đặt trong dấu nháy), người nhập nó vào tiện ích ký (được biên dịch với macro I_AM_DEVELOPER
), trong tham số Signature
. Kết quả là, chương trình sẽ tạo ra một chuỗi xác thực:
Validation:YBpYpQ0tLIpUhBslIw+AsPhtPG48b0qut9igJ+Tk1fQ=
"Nhà phát triển" gửi nó lại cho "người dùng", và "người dùng", bằng cách nhập nó vào tham số Validation
, sẽ nhận được script đã kích hoạt:
Hash bytes:
4 249 194 161 242 28 43 60 180 195 54 254 97 223 144 247 216 103 238 245 244 224 7 68 101 253 248 134 27 102 202 153
The script is validated and running normally
2
3
Để chứng minh hiệu quả của biện pháp bảo vệ, hãy sao chép script dưới dạng dịch vụ: để làm điều này, hãy sao chép tệp vào thư mục MQL5/Services/MQL5Book/p4/
và thay thế dòng sau trong mã nguồn:
#property script_show_inputs
bằng dòng sau:
#property service
Hãy biên dịch dịch vụ, tạo và chạy một phiên bản của nó, và chỉ định chuỗi xác thực đã nhận trước đó trong các tham số đầu vào. Kết quả là, dịch vụ sẽ bị hủy bỏ (trước khi đến các câu lệnh với mã cần thiết) với thông báo sau:
Hash bytes:
147 131 69 39 29 254 83 141 90 102 216 180 229 111 2 246 245 19 35 205 223 145 194 245 67 129 32 108 178 187 232 113
Wrong validation string, terminating
2
3
Điểm mấu chốt là trong số các thuộc tính của môi trường, chúng ta đã sử dụng chuỗi MQL_PROGRAM_TYPE
. Do đó, giấy phép đã cấp cho một loại chương trình sẽ không hoạt động cho một loại chương trình khác, ngay cả khi nó chạy trên máy tính của cùng một người dùng.