利用外部应用程序进行加密·进阶篇
🔐

利用外部应用程序进行加密·进阶篇

(2/3)· 当许可证要在开发者、销售、交易者三方工具间流转,终端原生加解密远远不够

实战向进阶 第 2/3 篇
很多人在 MQL 程序里硬塞加密逻辑,结果既拖慢终端又没解决真实问题。许可证要在不同语言写的模块间流转时,闭门造车只会造成解密端对不上的尴尬。先把跨语言互通的边界想清楚,再动手写代码。

「EA授权数据的序列化与加解密落地」

下面这段 MQL5 把用户授权头和若干 EA 配置压进同一个 uchar 数组,用结构体与字节数组互转来避免文件读写时的字段错位。dest 就是承载全部信息的底层缓冲,iSizeUserlic 与 iSizeEauser 分别是头部和单条 EA 记录的固定字节长。 AddEA 先取当前数组长度 c,若 c 为 0 直接返回 0 表示未初始化;StructToCharArray 把传入的 ea 结构拍平到 tmp,再 ArrayInsert 接到 dest 尾部,返回新总长度。GetUser 要求 dest 至少要有 iSizeUserlic 字节,否则返回 false,够长才用 CharArrayToStruct 还原头部。 GetEA 按 0 基序号定位:index = iSizeUserlic + num * iSizeEauser,越界就 false。注意它从 dest 的 index 偏移处直接还原结构体,说明记录是紧挨头部顺序排的。 加解密走 Encode / Decode 双函数。两者都先调 IsKeyCorrect 校验密钥长度:AES128 要 16 字符、AES256 要 32、DES 要 7,不符返回 0。Encode 把 dest 用 CryptEncode 输出到 buffer;Decode 会先 Reset 清空 dest,再 CryptDecode 把 buffer 解回 dest。外汇与贵金属 EA 涉及实盘信号,这类本地授权方案仍有被逆向的高风险,密钥硬编码在 mqh 里并不保险。 IsKeyCorrect 用 switch 硬匹配长度,DEBUG 宏下还能打日志。Reset 只是 ArrayResize(dest,0),不释放内存句柄,频繁 Decode 不会堆碎。

MQL5 / C++
class="type">int AddEA(class="kw">const ea_user& ea) {
   class="type">int c = ArraySize(dest);
   if (c == class="num">0) class="kw">return class="num">0;
   class="type">uchar tmp[];
   if (!StructToCharArray(ea, tmp) ) class="kw">return class="num">0;
   ArrayInsert(dest, tmp, c);
   class="kw">return ArraySize(dest);
}class=class="str">"cmt">//class="type">int AddEA(ea_user& ea)

class="type">bool GetUser(user_lic& header) class="kw">const {
   if (ArraySize(dest) < iSizeUserlic) class="kw">return class="kw">false;
   class="kw">return CharArrayToStruct(header, dest);
}class=class="str">"cmt">//class="type">bool GetUser(user_lic& header)

class=class="str">"cmt">//num - class="num">0 based
class="type">bool GetEA(class="type">int num, ea_user& ea) class="kw">const {
   class="type">int index = iSizeUserlic + num * iSizeEauser;
   if (ArraySize(dest) < index + iSizeEauser) class="kw">return class="kw">false;
   class="kw">return CharArrayToStruct(ea, dest, index);
}class=class="str">"cmt">//class="type">bool GetEA(class="type">int num, ea_user& ea)

class="type">int Encode(ENUM_CRYPT_METHOD method, class="type">class="kw">string key, class="type">uchar& buffer[]) class="kw">const {
   if (ArraySize(dest) < iSizeUserlic) class="kw">return class="num">0;
   if(!IsKeyCorrect(method, key) ) class="kw">return class="num">0;
   class="type">uchar k[];
   StringToCharArray(key, k);
   class="kw">return CryptEncode(method, dest, k, buffer);
}

class="type">int Decode(ENUM_CRYPT_METHOD method, class="type">class="kw">string key, class="type">uchar& buffer[]) {
   Reset();
   if(!IsKeyCorrect(method, key) ) class="kw">return class="num">0;
   class="type">uchar k[];
   StringToCharArray(key, k);
   class="kw">return CryptDecode(method, buffer, k, dest);
}
class="kw">protected:
   class="type">void Reset() {ArrayResize(dest, class="num">0);}

   class="type">bool IsKeyCorrect(ENUM_CRYPT_METHOD method, class="type">class="kw">string key) class="kw">const {
      class="type">int len = StringLen(key);
      class="kw">switch (method) {
         case CRYPT_AES128:
            if (len == class="num">16) class="kw">return true;
            class="kw">break;
         case CRYPT_AES256:
            if (len == class="num">32) class="kw">return true;
            class="kw">break;
         case CRYPT_DES:
            if (len == class="num">7) class="kw">return true;
            class="kw">break;
      }
class="macro">#ifdef __DEBUG_USERMQH__

◍ 授权文件的落盘与回读实现

在 EA 授权类里,生成与读取许可文件是两个独立函数,分别负责把加密后的字节流写进本地 bin 文件、以及反向加载校验。CreateLic 先调用 li.Encode 得到密文数组 cd,若返回长度为 0 则直接放弃,说明密钥或加密方法可能不匹配。 写文件用 FileOpen(licname, FILE_WRITE|FILE_BIN) 拿句柄,失败会打印 "File create failed"(仅 __DEBUG_USERMQH__ 宏开启时)。随后 FileWriteArray(h, cd) 一次性落盘,FileClose 收尾;调试宏打开时还会顺手调 li.SaveArray() 把明文存下来供比对。 ReadLic 走相反路径:以 FILE_READ|FILE_BIN 打开,若句柄为 INVALID_HANDLE 则退出。重点在 ArraySize(cd) < CLic::iSizeUserlic 这一道闸门——CLic::iSizeUserlic 是静态成员,等于 sizeof(user_lic),通常几十字节;文件小于这个尺寸直接判为损坏或伪造。 .decode 之前先确认数组规模,能避免把残缺文件喂进解密逻辑导致越界。开 MT5 把这段代码塞进自己的许可类,改个 licname 跑一遍,看 FileReadArray 读回的数组大小是否正好等于 iSizeUserlic,外汇与贵金属品种测试时请注意脚本权限与文件目录的高风险操作。

MQL5 / C++
  Print("Key length is incorrect: ",len);
class="macro">#ifdef __DEBUG_USERMQH__
    class="kw">return class="kw">false;
  }
class=class="str">"cmt">//class="type">bool IsKeyCorrect(ENUM_CRYPT_METHOD method, class="type">class="kw">string key)
  
class="kw">private:
  class="type">uchar dest[];
};
class=class="str">"cmt">//class CLic
  class="kw">static class="type">int CLic::iSizeEauser  = class="kw">sizeof(ea_user);
  class="kw">static class="type">int CLic::iSizeUserlic = class="kw">sizeof(user_lic);
class="type">bool CreateLic(ENUM_CRYPT_METHOD method, class="type">class="kw">string key, CLic& li, class="type">class="kw">string licname) {
  class="type">uchar cd[];
  if (li.Encode(method, key, cd) == class="num">0) class="kw">return class="kw">false;
  class="type">int h = FileOpen(licname, FILE_WRITE | FILE_BIN);
  if (h == INVALID_HANDLE) {
class="macro">#ifdef __DEBUG_USERMQH__
    Print("File create failed: ",licname);
class="macro">#endif
    class="kw">return class="kw">false;
  }
  FileWriteArray(h, cd);
  FileClose(h);  
class="macro">#ifdef __DEBUG_USERMQH__
  li.SaveArray();
class="macro">#endif
  class="kw">return true;
}
class=class="str">"cmt">// class="type">bool CreateLic(ENUM_CRYPT_METHOD method, class="type">class="kw">string key, class="kw">const CLic& li, class="type">class="kw">string licname)
class="type">bool ReadLic(ENUM_CRYPT_METHOD method, class="type">class="kw">string key, CLic& li, class="type">class="kw">string licname) {
  class="type">int h = FileOpen(licname, FILE_READ | FILE_BIN);
  if (h == INVALID_HANDLE) {
class="macro">#ifdef __DEBUG_USERMQH__
    Print("File open failed: ",licname);
class="macro">#endif
    class="kw">return class="kw">false;
  }
  class="type">uchar cd[];
  FileReadArray(h,cd);
  if (ArraySize(cd) < CLic::iSizeUserlic) {
class="macro">#ifdef __DEBUG_USERMQH__
    Print("File too small: ",licname);
class="macro">#endif
    class="kw">return class="kw">false;
  }
  li.Decode(method, key, cd);
  FileClose(h);
  class="kw">return true;
}
class=class="str">"cmt">// class="type">bool ReadLic(ENUM_CRYPT_METHOD method, class="type">class="kw">string key, CLic& li, class="type">class="kw">string licname)

用 C# 控制台复刻终端许可证加解密

在 Visual Studio 2017 里建一个面向 .NET Framework 的控制台应用,引用 System.Security 与 System.Security.Cryptography 命名空间,就能模拟 MT5 终端对 lic.txt 的解密、改字段、再加密流程。MQL5 与 C# 的时间格式不一致,可借用现成的 MtConverter 类做转换,不必自己造轮子。 先建 EaUser 和 UserLic 两个类,字段对齐 ea_user 与 user_lic 结构。核心动作是读二进制 lic.txt → AES 解密流 → 用 BinaryReader 填对象 → 改字段(示例里改用户 ID)→ 同模式加密写回 lic_C#.txt。项目里 EncryptToFile_Aes / DecryptFromFile_Aes 管文件,EncryptToArray_Aes / DecryptFromArray_Aes 管字节数组便于调试。 密钥行 "qwertyuiopasdfgh" 长度正好 16 字节,C# 中若超 16 会直接抛异常,所以 MQL5 侧生成密钥时也要卡死长度。ECB 模式不需要 IV,但代码仍建了 16 字节零数组占位,切到 CBC 等模式时直接喂随机值即可。 别把示例当成品 原文作者明说没做参数检查、异常捕获和流复用,只是为突出加解密主干。真要接实盘许可证系统,这些缺失可能让程序在畸形文件前直接崩,需自行补。 附件 CryptoC#.zip 含全部文件,开 VS 编译跑一遍,改 skey 或 IV 长度观察异常,比看文档直观。

MQL5 / C++
                class="kw">using (Aes aesAlg = Aes.Create())
                {
                    aesAlg.Key = Key;
                    aesAlg.IV = IV;
                    aesAlg.Mode = CipherMode.ECB;
                    aesAlg.Padding = PaddingMode.Zeros;
                    ..................................
                    class="type">class="kw">string skey = "qwertyuiopasdfgh";
                    byte[] Key  = Encoding.ASCII.GetBytes(s: skey);
byte[] iv   = new byte[class="num">16];

「用 C++ 和 OpenSSL 重写许可证加密」

在 Visual Studio 2017 里建一个控制台工程,箱体之外不碰任何加解密逻辑,直接挂 OpenSSL 函数库。装好安装包并把头文件、lib 连进项目后,才能调用现成的 AES 接口;官方文档残缺,只能靠试错把链接路径摸准。 C++ 版的结构比 C# 麻烦一点:账户数组字段用的是 int64_t[5],而 MQL 端头文件里是 long 型,本质都是 8 字节,但类型名不对齐就可能在序列化时写出难以捕获的越界 bug。好在这边不用做时间转换,std::time_t 直接复用。 密钥长度不对是常踩的坑,用下面 AES_NormalizeKey 把 appBuffer 截到 aSize 即可;再配 handleErrors 把 OpenSSL 的错误码打到 stderr,排错能省一半时间。 加密时不传初始化向量,自己开数组在调用点塞进去。填充必须设成“零”模式,库本身不帮你补位,所以加密前对象长度必须是 AES_BLOCK_SIZE = 16 字节的整数倍,否则 aes_encrypt 之前要先手动对齐。 跑完加密得到 lic_C++.txt,因为补齐了 16 字节的时钟块,文件大小会和 C# 版不同。解密、改账户、再加密这套流程和前一篇一致,源码都在 CryptoC++.zip 里,开 VS 挂 OpenSSL 就能复现。

MQL5 / C++
constexpr class="type">size_t PRODMAXLENGTH = class="num">255;
class="macro">#pragma pack(class="num">1)
typedef class="kw">struct EA_USER {
    EA_USER();
    EA_USER(std::class="type">class="kw">string name);
    EA_USER(EA_USER& eauser);
    std::time_t  expired;
    class="type">long  namelength;
    class="type">char eaname[PRODMAXLENGTH];
    std::class="type">class="kw">string GetName();
    class="type">void SetName(std::class="type">class="kw">string newName);
    std::class="type">class="kw">string GetTimeExpired();
    std::class="type">class="kw">string ToString();
    class="type">size_t ToArray(byte* pbyte);
    constexpr class="type">size_t GetSize() noexcept;
    friend std::ostream& class="kw">operator<< (std::ostream& out, EA_USER& eauser);
    friend std::istream& class="kw">operator>> (std::istream& in, EA_USER& eauser);
}EAUSER, *PEAUSER;
class="macro">#pragma pack()
constexpr class="type">size_t COUNTACC = class="num">5;
class="macro">#pragma pack(class="num">1)
typedef class="kw">struct USER_LIC
{
    class="kw">using PEAUNIC = std::unique_ptr<EAUSER>;
    USER_LIC();
    USER_LIC(USER_LIC&& ul);
    USER_LIC(class="kw">const byte* pbyte);
    int64_t uid;
    std::time_t expired;
    class="type">long log_count;
    int64_t logins[COUNTACC];
    class="type">long ea_count;
    std::vector<PEAUNIC> pEa;
    std::class="type">class="kw">string GetTimeExpired();
    std::class="type">class="kw">string ToString();
    class="type">size_t ToArray(byte* pbyte);
    class="type">void AddEA(EA_USER eau);
    class="type">bool AddAcc(class="type">long newAcc);
    class="type">size_t GetSize();
    friend std::ostream& class="kw">operator<< (std::ostream& out, USER_LIC& ul);
    friend std::istream& class="kw">operator>> (std::istream& in, USER_LIC& ul);
USER_LIC& class="kw">operator = (class="kw">const USER_LIC&) = class="kw">delete;
USER_LIC(class="kw">const USER_LIC&) = class="kw">delete;
} USERLIC, *PUSERLIC;
class="macro">#pragma pack()
std::class="type">class="kw">string AES_NormalizeKey(class="kw">const class="type">void *class="kw">const apBuffer, class="type">size_t aSize)
class="type">void handleErrors(class="type">void)
{
    ERR_print_errors_fp(stderr);
}
class="type">int aes_decrypt(class="kw">const byte* key, class="kw">const byte* iv, class="kw">const byte* pCtext, byte* pRtext, class="type">int iTextsz)

◍ AES 加密里那个 padding 开关到底怎么设

在 MT5 里做 AES 加密传输,padding 模式直接决定密文长度和后端能否解出来。上面这段 C++ 风格封装里连续调了两次 set_padding,第一次传 0,第二次传 EVP_PADDING_ZERO,等于把上下文的填充方式最终锁成零填充。 零填充不会像 PKCS7 那样补出额外字节,当明文长度恰好是块大小整数倍时,密文长度等于明文长度;若不足,尾部补 0x00 到块边界。外汇信号推送这类定长报文用零填充能少踩对接坑,但贵金属账户鉴权别默认它就安全,密钥泄露照样高风险。 实机验证时把两次 set_padding 注释掉任一个,用 16 字节明文跑一遍,对比输出密文尾字节,就能确认哪次调用真正生效。

MQL5 / C++
class="type">int aes_encrypt(class="kw">const byte* key, class="kw">const byte* iv, class="kw">const byte* pPtext, byte* pCtext, class="type">int iTextsz)
EVP_CIPHER_CTX_set_padding(ctx.get(), class="num">0);
EVP_CIPHER_CTX_set_padding(ctx.get(), EVP_PADDING_ZERO);
把跨端校验交给小布盯盘
这些诊断小布盯盘的 AIGC 已内置,打开对应品种页即可看到许可证状态与跨模块解密异常提示,把重复劳动交给小布,你专注决策。

常见问题

终端固定用 ECB 模式和零填充,而多数外部语言库默认 CBC 或 PKCS 填充,必须显式对齐参数才能互解。
AES128 密钥更长、被破解概率更低,且终端原生支持,倾向用它而非 DES 做轻量许可证保护。
可放用户标识、产品白名单、最大并发实例数等,解密后由 EA 或指标读取并做本地校验。
可以,小布盯盘的品种页已内置 AIGC 诊断,能标记解密异常并给出可能的原因倾向,减少人工查日志。
采用固定短块长度和明确错误码返回,避免外部模块因单次解析失败直接中断,概率上更稳。