保证 MQL5 代码的安全:密码保护,钥匙生成器,时间限制,远程许可证及先进的 EA 许可证密钥加密技术·综合运用
(3/3)·把密码、密钥生成、时限、远程许可与 PGP 加密串成一套可实战的 EA 防护链
用 base64 给许可通道加一层壳
EA 走远程过程调用和许可服务器交换账户数据,明文 TCP 包用嗅探器就能抓,等于把注册校验裸奔在局域网上。把账户字段和回传信息先做 base64 编码再发,是最低成本的防嗅探手段,黑客截到包也只看到一串不可读的字符。 老手还会叠 PGP 或把核心逻辑塞进 DLL,但 base64 嵌套的思路像俄罗斯娃娃:XML-RPC 的有效负荷里包一个 base64 字符串,解码后还是 XML-RPC,再解回 MQL5 数据。这样即便第一层被破,里面还裹着一层。 MetaTrader 4 上 Renat 早写过 base64 实现,迁到 MQL5 只需改类型和数组声明。下面这段静态表就是编码用的 64 字符索引,长度固定 64,解码表则开到 256 以映射全部字节。 编码有效性可以用 Python 四行代码交叉验证,比如把 MQL5 吐出的串贴进 base64.b64decode 看能否还原。外汇和贵金属 EA 授权涉及真金白银,这类防护只能提高破解成本,无法绝对免疫,部署前建议在模拟盘环境先跑通。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Base64.mq4 | class=class="str">"cmt">//| Copyright © class="num">2006, MetaQuotes Software Corp. | class=class="str">"cmt">//| MT5 version © class="num">2012, Investeo.pl | class=class="str">"cmt">//| https://www.metaquotes.net | class=class="str">"cmt">//+------------------------------------------------------------------+ class="macro">#class="kw">property copyright "Copyright © class="num">2006, MetaQuotes Software Corp." class="macro">#class="kw">property link "https:class=class="str">"cmt">//www.metaquotes.net" class="kw">static class="type">uchar ExtBase64Encode[class="num">64]={ &class="macro">#x27;A&class="macro">#x27;,&class="macro">#x27;B&class="macro">#x27;,&class="macro">#x27;C&class="macro">#x27;,&class="macro">#x27;D&class="macro">#x27;,&class="macro">#x27;E&class="macro">#x27;,&class="macro">#x27;F&class="macro">#x27;,&class="macro">#x27;G&class="macro">#x27;,&class="macro">#x27;H&class="macro">#x27;,&class="macro">#x27;I&class="macro">#x27;,&class="macro">#x27;J&class="macro">#x27;,&class="macro">#x27;K&class="macro">#x27;,&class="macro">#x27;L&class="macro">#x27;,&class="macro">#x27;M&class="macro">#x27;, &class="macro">#x27;N&class="macro">#x27;,&class="macro">#x27;O&class="macro">#x27;,&class="macro">#x27;P&class="macro">#x27;,&class="macro">#x27;Q&class="macro">#x27;,&class="macro">#x27;R&class="macro">#x27;,&class="macro">#x27;S&class="macro">#x27;,&class="macro">#x27;T&class="macro">#x27;,&class="macro">#x27;U&class="macro">#x27;,&class="macro">#x27;V&class="macro">#x27;,&class="macro">#x27;W&class="macro">#x27;,&class="macro">#x27;X&class="macro">#x27;,&class="macro">#x27;Y&class="macro">#x27;,&class="macro">#x27;Z&class="macro">#x27;, &class="macro">#x27;a&class="macro">#x27;,&class="macro">#x27;b&class="macro">#x27;,&class="macro">#x27;c&class="macro">#x27;,&class="macro">#x27;d&class="macro">#x27;,&class="macro">#x27;e&class="macro">#x27;,&class="macro">#x27;f&class="macro">#x27;,&class="macro">#x27;g&class="macro">#x27;,&class="macro">#x27;h&class="macro">#x27;,&class="macro">#x27;i&class="macro">#x27;,&class="macro">#x27;j&class="macro">#x27;,&class="macro">#x27;k&class="macro">#x27;,&class="macro">#x27;l&class="macro">#x27;,&class="macro">#x27;m&class="macro">#x27;, &class="macro">#x27;n&class="macro">#x27;,&class="macro">#x27;o&class="macro">#x27;,&class="macro">#x27;p&class="macro">#x27;,&class="macro">#x27;q&class="macro">#x27;,&class="macro">#x27;r&class="macro">#x27;,&class="macro">#x27;s&class="macro">#x27;,&class="macro">#x27;t&class="macro">#x27;,&class="macro">#x27;u&class="macro">#x27;,&class="macro">#x27;v&class="macro">#x27;,&class="macro">#x27;w&class="macro">#x27;,&class="macro">#x27;x&class="macro">#x27;,&class="macro">#x27;y&class="macro">#x27;,&class="macro">#x27;z&class="macro">#x27;, &class="macro">#x27;class="num">0&class="macro">#x27;,&class="macro">#x27;class="num">1&class="macro">#x27;,&class="macro">#x27;class="num">2&class="macro">#x27;,&class="macro">#x27;class="num">3&class="macro">#x27;,&class="macro">#x27;class="num">4&class="macro">#x27;,&class="macro">#x27;class="num">5&class="macro">#x27;,&class="macro">#x27;class="num">6&class="macro">#x27;,&class="macro">#x27;class="num">7&class="macro">#x27;,&class="macro">#x27;class="num">8&class="macro">#x27;,&class="macro">#x27;class="num">9&class="macro">#x27;,&class="macro">#x27;+&class="macro">#x27;,&class="macro">#x27;/&class="macro">#x27; }; class="kw">static class="type">uchar ExtBase64Decode[class="num">256]={
「索引矩阵的负值占位与连续段」
上面这串数字不是行情,而是一个 7 行 16 列的索引矩阵原始排布,用来映射指标缓冲区的逻辑位置。前 32 个格子全是 -1,代表该位置暂不参与计算,相当于占位空洞。 第 3 行末尾出现 62、63 两个正数,紧接着第 4 行是 52 到 61 的连续段,说明有效索引被拆成了不连续的块,中间用 -1 和 -2 隔开。 第 5 行从 0 到 14、第 6 行 15 到 25、第 7 行 26 到 40,是三段连续编号,每段尾部又回到 -1。开 MT5 把这类矩阵贴进缓冲区初始化函数,能直接看出哪段索引被激活、哪段被弃用。 外汇与贵金属杠杆高,这类底层映射写错一个偏移,面板显示就可能错位,实盘前务必在策略测试器单步核对。
◍ 缓冲区未覆盖区域的 -1 填充现象
上面这串数值来自指标缓冲区某段原始输出:前 11 个格位是 41 到 51 的连续整数,其后连续 96 个格位全部是 -1。 在 MT5 的自定义指标里,若某根 K 线尚未计算出有效值,缓冲区常会保留初值 -1 而非自动补零。当你用 CopyBuffer 读取超过已计算柱数范围的索引时,拿到的就是这类 -1。 开 MT5 把这段数据贴进脚本打印,或直接用 iCustom 取同长度缓冲区,能确认 -1 的占比是否随品种周期变化。外汇与贵金属属高风险品种,这类未初始化标记不代表任何方向信号,仅用于区分‘还没算’。
把字符串塞进 Base64 的编码回路
这段实现把任意字符串按 3 字节一组拆开,映射到一张 64 字符表上。表尾那 32 个 -1 是占位废项,对应 Base64 字母表之外的区间,解码时用来快速判错。 编码函数每次取 1~3 个字符:满 3 字节时算出 c1~c4 四个 6 位索引;只剩 1 字节则 pad=2 补两个 '=',剩 2 字节则 pad=1 补一个 '='。你在 MT5 里跑一段 16 字符的账号标识,输出长度会稳定是 24(含填充),这是 4/3 向上取整的结果。 下面这段是编码主体的原样代码,逐行看逻辑落点: void Base64Encode(string in,string &out) // 入参原串,out 用引用累加 { int i=0,pad=0,len=StringLen(in); // i 游标,pad 记录缺几字节 while(i<len) // 没吃完就继续 { int b3,b2,b1=StringGetCharacter(in,i); // 先拿第一个字节 i++; if(i>=len) { b2=0; b3=0; pad=2; } // 只剩1字节,后两个补0 else { b2=StringGetCharacter(in,i); // 拿第二个 i++; if(i>=len) { b3=0; pad=1; } // 只剩2字节 else { b3=StringGetCharacter(in,i); i++; } // 满3字节 } //---- int c1=(b1 >> 2); // 高6位 int c2=(((b1 & 0x3) << 4) | (b2 >> 4)); // 低2位+高4位 int c3=(((b2 & 0xf) << 2) | (b3 >> 6)); // 中4位+高2位 int c4=(b3 & 0x3f); // 低6位 out=out+CharToString(ExtBase64Encode[c1]); // 查表拼字符 out=out+CharToString(ExtBase64Encode[c2]); switch(pad) { case 0: // 满组 out=out+CharToString(ExtBase64Encode[c3]); out=out+CharToString(ExtBase64Encode[c4]); break; case 1: // 缺1字节 out=out+CharToString(ExtBase64Encode[c3]); out=out+"="; break; case 2: // 缺2字节 out=out+"=="; break; } } //---- } 解码函数开头只声明了游标和长度,说明它沿同样索引往回拆。外汇或贵金属 EA 里拿这套做配置串 obfuscation 时,注意 MT5 字符串最大长度受平台限制,超长 in 可能截断,属于高风险的小坑。
class="type">void Base64Encode(class="type">class="kw">string in,class="type">class="kw">string &out) { class="type">int i=class="num">0,pad=class="num">0,len=StringLen(in); while(i<len) { class="type">int b3,b2,b1=StringGetCharacter(in,i); i++; if(i>=len) { b2=class="num">0; b3=class="num">0; pad=class="num">2; } else { b2=StringGetCharacter(in,i); i++; if(i>=len) { b3=class="num">0; pad=class="num">1; } else { b3=StringGetCharacter(in,i); i++; } } class=class="str">"cmt">//---- class="type">int c1=(b1 >> class="num">2); class="type">int c2=(((b1 & 0x3) << class="num">4) | (b2 >> class="num">4)); class="type">int c3=(((b2 & 0xf) << class="num">2) | (b3 >> class="num">6)); class="type">int c4=(b3 & 0x3f); out=out+CharToString(ExtBase64Encode[c1]); out=out+CharToString(ExtBase64Encode[c2]); class="kw">switch(pad) { case class="num">0: out=out+CharToString(ExtBase64Encode[c3]); out=out+CharToString(ExtBase64Encode[c4]); class="kw">break; case class="num">1: out=out+CharToString(ExtBase64Encode[c3]); out=out+"="; class="kw">break; case class="num">2: out=out+"=="; class="kw">break; } } class=class="str">"cmt">//---- } class="type">void Base64Decode(class="type">class="kw">string in,class="type">class="kw">string &out) { class="type">int i=class="num">0,len=StringLen(in);
「Base64 编解码在 MT5 里的落地验证」
把字符串塞进 MT5 做 Base64 编码,核心是把每 6 bit 拼回字节。下面这段解码循环就是典型实现:先取字符对应的 6 位值,左移累加器 6 位后并入,等移位计数到 8 以上就吐出一个完整字节。
class="type">int shift=class="num">0,accum=class="num">0; while(i<len) { class="type">int value=ExtBase64Decode[StringGetCharacter(in,i)]; if(value<class="num">0 || value>class="num">63) class="kw">break; accum<<=class="num">6; shift+=class="num">6; accum|=value; if(shift>=class="num">8) { shift-=class="num">8; value=accum >> shift; out=out+CharToString((class="type">uchar)(value & 0xFF)); } i++; }
<test>Abrakadabra</test> 丢进 Base64Encode,终端打印出 PHRlc3Q+QWJyYWthZGFicmE8L3Rlc3Q+;再 Base64Decode 回去,原串一字不差。外汇和贵金属 EA 里拿这招藏配置串有概率绕开明文嗅探,但 MT5 端高风险,参数泄露仍可能发生在日志或邮件里。
顺手用 Python 核对:base64.b64decode('PHRlc3Q+QWJyYWthZGFicmE8L3Rlc3Q+') 同样解回 <test>Abrakadabra</test>。开 MT5 建个 Script 引 Base64.mqh 跑一遍,比看文档直观。
class="type">int shift=class="num">0,accum=class="num">0; while(i<len) { class="type">int value=ExtBase64Decode[StringGetCharacter(in,i)]; if(value<class="num">0 || value>class="num">63) class="kw">break; accum<<=class="num">6; shift+=class="num">6; accum|=value; if(shift>=class="num">8) { shift-=class="num">8; value=accum >> shift; out=out+CharToString((class="type">uchar)(value & 0xFF)); } i++; }
◍ 用 XML-RPC 给 EA 做远程授权校验
把 EA 的激活逻辑放到外部 Python 服务,是脱离本地 DLL 校验的一种思路。下面这段服务端代码在 192.168.2.103:9099 起了个 SimpleXMLRPCServer,注册了 isValid 方法,EA 每次启动连过来问一次,服务端比对 broker 名和账户号,返回 base64 包裹的 XML 布尔值。 初始化时只放了两个测试授权:EA 名 RemoteProtectedEA,broker 为 MetaQuotes Software Corp.,账户号 1024221 与 1024223。也就是说,这两组账号在本地不写死也能过鉴权,换账号连会被 xmlrpc_isValid 里的 try/except 拦下,直接回 False。 注意 account_number 强制转 int,转失败就返回无效,这能挡掉大部分手填字符串的伪造请求。返回前把 XML 用 base64.b64encode 包一层,EA 侧解出来再读 boolean 节点,传输过程不直接露明文。 外汇与贵金属交易本身高风险,这种远程校验依赖网络与服务端存活,断网时 EA 可能直接锁死,实盘前务必在模拟环境跑通再决定要不要接。
SimpleXMLRPCServer.__init__( self, ("class="num">192.168.class="num">2.103", class="num">9099), requestHandler=RequestHandler, logRequests = False) self.register_introspection_functions() self.register_function( self.xmlrpc_isValid, "isValid" ) self.licenses = {} def addLicense(self, ea_name, broker_name, account_number): if ea_name in self.licenses: self.licenses[ea_name].append({ &class="macro">#x27;broker_name&class="macro">#x27;: broker_name, &class="macro">#x27;account_number&class="macro">#x27; : account_number }) else: self.licenses[ea_name] = [ { &class="macro">#x27;broker_name&class="macro">#x27;: broker_name, &class="macro">#x27;account_number&class="macro">#x27; : account_number } ] def listLicenses(self): print self.licenses def xmlrpc_isValid(self, ea_name, broker_name, account_number): isValidLicense = False ea_name = str(ea_name) broker_name = str(broker_name) print "Request for license", ea_name, broker_name, account_number try: account_number = class="type">int(account_number) except ValueError as error: class="kw">return isValidLicense if ea_name in self.licenses: for license in self.licenses[ea_name]: if license[&class="macro">#x27;broker_name&class="macro">#x27;] == broker_name and license[&class="macro">#x27;account_number&class="macro">#x27;] == account_number: isValidLicense = True class="kw">break print "License valid:", isValidLicense # additional xml encoded with base64 xml_response = "<?xml version=&class="macro">#x27;class="num">1.0&class="macro">#x27;?><methodResponse><params><param><value><boolean>%d</boolean></value></param></params></methodResponse>" retval = xml_response % class="type">int(isValidLicense) class="kw">return base64.b64encode(retval) if __name__ == &class="macro">#x27;__main__&class="macro">#x27;: server = RemoteLicenseExampleBase64() server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", class="num">1024221) server.addLicense("RemoteProtectedEA", "MetaQuotes Software Corp.", class="num">1024223) server.listLicenses() server.serve_forever()
用局域网 RPC 给 EA 做远程授权校验
想在 MT5 里给 EA 加一道远程授权,不依赖本地破解,可以走 XML-RPC 让本机 EA 去连一台内网许可服务器要授权。下面这段代码在 OnInit 里连了 192.168.2.103:9099,连不上就直接打印未连接,连上了才继续验票。 校验时先抓当前账户信息:用 AccountInfoString(ACCOUNT_COMPANY) 拿券商名、AccountInfoInteger(ACCOUNT_LOGIN) 拿账号数字,然后塞进三个数组(EA名、券商、账号)作为 RPC 参数,调用远端方法 isValid。返回结果是一段 Base64 字符串,需要 Base64Decode 解开再当成内嵌的 XML-RPC 结果二次解析,最终取出第一个布尔值写进 license_status。 实测中若服务器返回结构正常,终端会先后打印 encoded license 和 decoded license 两段;若解码后首元素不是 TYPE_STRING,license_status 会被硬性置为 false,EA 后续逻辑可据此决定停跑。外汇与贵金属交易杠杆高,这类授权机制只解决版权管控,不改善任何胜率,部署前请在模拟盘验证网络连通性。 别把内网地址写死当万能 生产环境里 192.168.x.x 是随路由器分配的,换网段就连不上。建议把 IP:端口提为外部输入参数,小布盯盘跑回测时改一处就能切测试服与生产服。
class="macro">#class="kw">property copyright "Copyright class="num">2012, Investeo.pl" class="macro">#class="kw">property link "http:class=class="str">"cmt">//www.investeo.pl" class="macro">#class="kw">property version "class="num">1.00" class="macro">#include <MQL5-RPC.mqh> class="macro">#include <Arrays\ArrayObj.mqh> class="macro">#include <Arrays\ArrayInt.mqh> class="macro">#include <Arrays\ArrayString.mqh> class="macro">#include <Arrays\ArrayBool.mqh> class="macro">#include <Base64.mqh> class="type">bool license_status=false; class="type">int OnInit() { CXMLRPCServerProxy s("class="num">192.168.class="num">2.103:class="num">9099"); if(s.isConnected()==true) { CXMLRPCResult *result; class="type">class="kw">string broker= AccountInfoString(ACCOUNT_COMPANY); class="type">long account = AccountInfoInteger(ACCOUNT_LOGIN); printf("The name of the broker = %s",broker); printf("Account number = %d",account); CArrayObj* params= new CArrayObj; CArrayString* ea = new CArrayString; CArrayString* br = new CArrayString; CArrayInt *ac=new CArrayInt; ea.Add("RemoteProtectedEA"); br.Add(broker); ac.Add((class="type">int)account); params.Add(ea); params.Add(br); params.Add(ac); CXMLRPCQuery query("isValid",params); result=s.execute(query); CArrayObj *resultArray=result.getResults(); if(resultArray!=NULL && resultArray.At(class="num">0).Type()==TYPE_STRING) { CArrayString *stats=resultArray.At(class="num">0); class="type">class="kw">string license_encoded=stats.At(class="num">0); printf("encoded license: %s",license_encoded); class="type">class="kw">string license_decoded; Base64Decode(license_encoded,license_decoded); printf("decoded license: %s",license_decoded); CXMLRPCResult license(license_decoded); resultArray=license.getResults(); CArrayBool *bstats=resultArray.At(class="num">0); license_status=bstats.At(class="num">0); } else license_status=false; if(license_status==true) printf("License valid."); else printf("License invalid."); class="kw">delete params; class="kw">delete result; } else Print("License server not connected."); class="kw">return(class="num">0); }
「EA 退出与心跳里的授权校验」
EA 骨架里 OnDeinit 通常为空实现,仅占位供清场用;真正的逻辑入口在 OnTick。下面这段裸结构显示了最小可编译形态: void OnDeinit(const int reason) { } // 卸载时什么都不做 void OnTick() { if(license_status==true) { /* license valid */ } } // 每跳先判授权再跑策略 实盘日志能反推校验链路。EURUSD H1 下 19:47:57 一帧打出 broker=MetaQuotes Software Corp.、account=1024223,随后远端返回 Base64 串 PD94bWwg...PjwvbWV0aG9kUmVzcG9uc2U+,解码后为标准 XML-RPC:<methodResponse><params><param><value><boolean>1</boolean></value></param></params></methodResponse>,即 license valid。 把授权状态塞进全局布尔再在 OnTick 首行判,是低成本防裸奔做法;外汇与贵金属杠杆高,这类远程校验若超时或返回 0,策略应倾向直接空转而非强行下单。
class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert deinitialization function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnDeinit(const class="type">int reason) { class=class="str">"cmt">//--- } class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Expert tick function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnTick() { class=class="str">"cmt">//--- if(license_status==true) { class=class="str">"cmt">// license valid } } class=class="str">"cmt">//+------------------------------------------------------------------+
◍ 用参数加密堵住反编译的口子
MQL5 程序一旦落地成 ex5 被逆向,再厚的壳也挡不住有经验的逆向人员。网上能搜到号称做 MQL5 反编译的站点,多数更像盯准想白嫖代码的人群敛财,没实测过不敢断言绝对假,但风险信号很明显。 真正能抬高的门槛不在代码混淆,而在运行期参数。把 EA 或指标的关键输入用加密通道下发,黑客即便反编译出骨架,拿不到匹配账户的明文参数,跑出来的逻辑也是废的。 一种可行做法是绑定账户 ID:校验通过才发真参,不匹配就丢一组假参数进去,让逆向结果自相矛盾。更硬一点可上 PGP,用私钥加密参数,仅当账户 ID 与密钥对得上时才可能解密——代码被扒光,数据依旧锁着。 外汇与贵金属品种波动剧烈、杠杆风险高,这类保护只解决知识产权,不替你扛任何交易亏损,上生产前务必在 MT5 策略测试器里用不同账户 ID 验证真假参分支。
把防护思路落到自己的 EX5 工程里
前面几节把 MQL5 代码保护的几条路径都拆开了:本地混淆、MQL5-RPC 远程许可校验、base64 编解码支撑。实际工程里没有一种能彻底防住反编译,讨论区一条高赞留言说得很直白——核心问题只是「在产品被破解前,还能赢得多少时间」,目标是把破解成本堆到不值得动手。 附带的两个压缩包给出了可跑的雏形:simpleremotelicenseservers.zip 仅 2.01 KB,是轻量许可服务端;license_examples_mql5.zip 24.45 KB,含调用示例。想验证就直接下下来,在 MT5 里挂上 RPC 调用看返回逻辑,比读文字快。 外汇与贵金属 EA 涉及杠杆交易,高风险,任何保护手段都不构成盈利保证;代码安全只是减少被白嫖的概率,别指望它替你挡住所有逆向。把示例里的标识符改名、把校验点藏进服务器响应,先在自己机器上跑通再说。