├── requirements.txt ├── LICENSE ├── README.md └── mzphp2-deobfuscator.py /requirements.txt: -------------------------------------------------------------------------------- 1 | requests -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 zigzag2050 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | mzphp2-deobfuscator 2 | ====== 3 | 最近在学习某源码时遇到几个文件是经过混淆的。新学PHP,没有遇到过PHP的代码混淆,因此开动搜索引擎稍微研究了一下。 4 | 5 | 通过检测工具发现是代码采用**mzphp2**混淆的,在[这个网址](http://enphp.djunny.com/ "http://enphp.djunny.com/"),mzphp2的作者提供了在线的源码混淆服务,感谢作者的辛勤工作和无私奉献。 6 | 7 | ## 混淆原理 8 | 上传了几段PHP代码进行混淆,对比一下混淆前后的代码,大致可以推测出混淆的思路: 9 | 1. 收集代码中所有的函数名、变量名、字符和数值常量,整理成一个Array,这里我就命名为`var_list`; 10 | 2. 生成一小段随机字符`str_separater`,通过`implode`函数把`var_list`里的所有项连接成为一个很长的字符串`str_obfus`,各项之间使用`str_separater`连接; 11 | 3. 把`var_list`保存在`$GLOBALS`、`$_SERVER`或`$_GET`之类的超全局变量中,所用的key(我命名为`var_key`)为一串不可读随机字符,于是 12 | ` 13 | $GLOBALS{var_key} = var_list = explode(str_separater, str_obfus); 14 | ` 15 | 代码中原先可读的函数名,变量名,常量数值都被转化成了`$GLOBALS{var_key}[hex_id]`的形式; 16 | 17 | 4. 在每一个函数的实现代码段中,采用一个局部变量`$xxx`指向`$GLOBAL{var_key}`,局部变量`$xxx`的变量名通常也是一串不可读随机字符; 18 | 这样,在函数实现代码段中,函数名,变量名,常量数值等被转化成了`$xxx[hex_id]`的形式,而不再是`$GLOBALS{var_key}[hex_id]`的形式。然后,函数实现代码段中的所有其它局部变量名都被转化成随机不可读字符串; 19 | 5. 还有,代码中所有的`true`转化为`!0`,所有的`false`转化为`!1`; 20 | 6. 此外,代码中随机添加一些不可读随机字符组成的最后以“;”结尾的行,PHP在执行到这行时只会报告“未定义过的常量”错误,不影响原有代码逻辑的执行。在代码最前面添加`error_reporting(E_ALL^E_NOTICE);`防止报错停止PHP的执行; 21 | 7. 再是,代码头部添加`define(var_key, '随机字符串');`,估计是防止后面的`$GLOBALS{var_key}`报出变量未定义的错误; 22 | 8. 最后,去除所有的换行符。加上mzphp2的注释(可选)。 23 | 24 | 最终,原来的可读代码变成一堆天书。 25 | 26 | ## 解混淆方法 27 | 基本上就是上述原理的逆过程。 28 | 1. 去除所有注释,换行符。 29 | 2. 找到类似`$GLOBALS{var_key} = explode(str_separater, str_obfus);`这一句,提取出超全局变量的名字:`var_name`,所用的key:`var_key`,以及所有被替换的名字数组:`var_list`; 30 | 3. 依照索引`hex_id`,把代码中的所有`$GLOBALS{var_key}[hex_id]`的形式的文本替换为`var_list[hex_id]`的值; 31 | 4. 在每个函数实现代码段中寻找`$xxx=$GLOBALS{var_key};`这一句,保存变量名$xxx到`var_list_instance`数组中,把函数中所有`$xxx[hex_id]`形式的文本替换为原来的值; 32 | 5. 查找每段函数实现代码中的局部变量,这些变量的名字被替换成了不可读字符串。把它们找出来,依次用`$var_1`,`$var_2`...替换; 33 | 6. 最后,做一些格式整理,去除掉一些不需要的语句,还有替换回`true`和`false`等。 34 | 35 | ## 使用方法 36 | 程序用python语言实现,主要通过正则表达式的匹配和替换实现。运行需要python3,因为python2中str和unicode的不同使得对不可读字符的处理很麻烦,而python3中统一了str,从根本上支持unicode,避免了很多麻烦。 37 | 38 | 执行命令 39 | ``` 40 | python3 mzphp_deobfuscator.py mzphp_obfuscated.php 41 | ``` 42 | 输出结果到控制台,或 43 | ``` 44 | python3 mzphp_deobfuscator.py mzphp_obfuscated.php mzphp_deobfuscated.php 45 | ``` 46 | 输出结果到文件 mzphp_deobfuscated.php 。 47 | 48 | ## 几个注意的地方 49 | 1. 输出的文件没有格式美化,代码里没有换行,全都堆在一起。推荐使用[php-cs-fixer](https://github.com/FriendsOfPHP/PHP-CS-Fixer "https://github.com/FriendsOfPHP/PHP-CS-Fixer")美化一下; 50 | 2. 代码中的局部变量名被混淆了无法恢复,只能替换为$var_1,$var_2...格式; 51 | 52 | >我有个大胆的猜想,有没有可能mzphp2的作者把原始的局部变量名收集起来,经过处理/混淆/打散成不可读的字符串,这些字符串实际上包含了可以用来恢复变量名的信息。而混淆后代码中插入的不可读随机字符组成的最后以“;”结尾的行实际上就是这些字符串。目前以我的水平无法求证,不知道mzphp2的作者可不可能确认一下。 53 | 3. 不支持经过gz字符串压缩的mzphp混淆。其实这个特性实现起来不难,寻找`gzinflate(substr(gz_bytes, 0xa, -8));`这一句,取出`gz_bytes`,在python中`decompress`就行了。只不过懒癌发作,没什么挑战性的工作提不起劲来,谁有兴趣帮忙实现一下; 54 | 4. 输入的php代码必须是utf-8编码的,其他格式的编码不能保证程序能正常工作(没有测试过非utf-8编码的文件,但是现在应该没有谁会用非utf-8的编码格式写PHP了吧); 55 | 5. 程序只是通过正则表达式的匹配和替换来实现,不排除在某些特殊的情况下不能恢复原有代码逻辑,甚至可能破坏原始代码逻辑的情况; 56 | 6. 程序只是**能工作**,缺乏错误和异常处理,只是一个概念的实现,不能在生产环境中应用; 57 | 7. 在发布前,发现了[这个项目](https://github.com/FunnyStudio/mzphp2_decrypt "https://github.com/FunnyStudio/mzphp2_decrypt"),和本项目的目的基本一致,方法也差不多。他的程序结构比我的面条式代码要合理一些,于是借用了他的程序结构和变量命名,把我的程序重构了一下。感谢项目作者的卓越工作和伟大的开源精神。 58 | 59 | 最后,强调一下,这个程序只是一项技术研究和实现,请不要用来破解或修改他人程序。破解、修改他人版权的软件,这属于**违法行为**。对于这种行为,希望大家一起抵制。 -------------------------------------------------------------------------------- /mzphp2-deobfuscator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import re 3 | import argparse 4 | 5 | slash = '\\' 6 | 7 | 8 | def parse_code(file_name): 9 | # 按二进制读取文件内容 10 | file_content = open(file_name, 'rb').read() 11 | # 转换成文本字符串 12 | file_content = repr(file_content) 13 | 14 | # 字符串可能是双引号包围(“***”)或单引号包围(‘***') 15 | # 单引号包围的字符串中出现的单引号字符(’)要变为 "\'",在正则表达式中为 "\\'" 16 | if file_content[1] == '"': 17 | singlequate = "'" 18 | re_singlequate = "'" 19 | else: 20 | singlequate = r"\'" 21 | re_singlequate = r"\\'" 22 | 23 | # 去除注释 24 | file_content = re.sub(r"/\*(.+?)\*/", "", file_content) 25 | # 去除mzphp2自动加上的 error_reporting(E_ALL^E_NOTICE);语句 26 | file_content = re.sub(r"error_reporting\(E_ALL\^E_NOTICE\);", "", file_content) 27 | # 去除所有的回车换行符 28 | file_content = re.sub(r"(?:\\n)|(?:\\r)", "", file_content) 29 | # 去除所有mzphp2加上的随机不可读字符串 30 | file_content = re.sub(r";(?:\\x[a-f0-9]{2})+;", ";", file_content) 31 | # 所有 !0 变为 true 32 | file_content = re.sub("!0", "true", file_content) 33 | # 所有 !1 变为 false 34 | file_content = re.sub("!1", "false", file_content) 35 | 36 | # mzphp2把代码中的所有函数名(最顶层函数除外),变量名,字符串与数值常量都收集到一个大的数组中 37 | # 然后用一串随机字符作为间隔,将这个大数组连接成一个很长的字符串 38 | 39 | # 寻找混淆用的长字符串,提取出需要的信息,然后去除 40 | 41 | global var_name 42 | global var_key 43 | global var_list 44 | 45 | def get_var_list(repl): 46 | global var_name 47 | global var_key 48 | global var_list 49 | # 记录用哪一个超全局变量保存的 50 | var_name = repl.group(1) 51 | # 记录超全局变量中保存var_list用的key 52 | var_key = repl.group(2).replace(slash, slash * 2) 53 | # 记录所有被替换的函数名,变量名和常量值 54 | var_list = repl.group(4).split(repl.group(3)) 55 | return '' 56 | 57 | # 混淆前的字符串数组通常保存在GLOBALS,_SERVER或_GET这几个超全局变量中 58 | # 后面紧跟着一个explode函数调用 59 | file_content = re.sub(r"\$((?:GLOBALS)|(?:_SERVER)|(?:_GET))\[((?:\\x[a-f0-9]{2})+?)\] = explode\(\s*" + re_singlequate + r"(.+?)" + re_singlequate + r"\s*,\s*"+ re_singlequate + r"((?:[^'\\]|(?:\\')|(?:\\?))*)" + re_singlequate + r"\s*\);", get_var_list, file_content) 60 | 61 | # 去除mzphp2添加的 define(..., '...'); 语句 62 | file_content = re.sub(r"define\(\s*" + re_singlequate + var_key + re_singlequate + r"\s*,\s*" + re_singlequate + r"(?:\\x[a-f0-9]{2})+" + re_singlequate + r"\s*\);", "", file_content) 63 | 64 | # 把所有类似 {$GLOBALS{var_key}[hex_id]} 形式的语句替换为原来的变量名或函数名 65 | file_content = re.sub( 66 | r"{\$" + var_name + r"{" + var_key + r"}[\[\{]((?:0)|(?:0x[a-f0-9]+?))[\]\}]}", 67 | lambda x: var_list[int(x.group(1), 16)], 68 | file_content 69 | ) 70 | 71 | # 把所有类似 $GLOBALS{var_key}[hex_id]( var... 形式的语句替换为原来的函数名+( 72 | file_content = re.sub( 73 | r"\$" + var_name + r"{" + var_key + r"}[\[\{]((?:0)|(?:0x[a-f0-9]+?))[\]\}]\(", 74 | lambda x: var_list[int(x.group(1), 16)] + "(", 75 | file_content 76 | ) 77 | 78 | # 把所有类似 $GLOBALS{var_key}[hex_id] 形式的语句替换为原来的字符串,再加上首尾的单引号 79 | file_content = re.sub( 80 | r"\$" + var_name + r"{" + var_key + r"}[\[\{]((?:0)|(?:0x[a-f0-9]+?))[\]\}]", 81 | lambda x: singlequate + var_list[int(x.group(1), 16)] + singlequate, 82 | file_content 83 | ) 84 | 85 | # 在每一个函数里mzphp2会用一个本地变量指向混淆用的 $GLOBALS{var_key} 86 | # 即 $xxx = $GLOBALS{var_key}; 的形式 87 | # 在函数内部混淆后的变量不是 $GLOBALS{var_key}[hex_id] 样式 而是 $xxx[hex_id] 样式 88 | # 需要提取所有的函数内部的 $xxx 变量, 再分别替换 89 | 90 | global mnc 91 | global var_list_instance 92 | 93 | mnc = 0 94 | var_list_instance = [] 95 | 96 | def rp_var(repl): 97 | global var_list_instance 98 | if repl.group(1) not in var_list_instance: 99 | var_list_instance.append(repl.group(1)) 100 | return '' 101 | 102 | # 记录各个函数内的混淆用变量到 var_list_instance数组中 103 | file_content = re.sub(r"(\$(?:\\x[a-f0-9]{2})+)=&\$" + var_name + r"{" + var_key + r"};", rp_var, 104 | file_content) 105 | 106 | # 排序,不是必需的步骤,但是感觉会提高一些效率。纯感觉,无证据... 107 | var_list_instance.sort(key=lambda x: len(x), reverse=True) 108 | 109 | for instance in var_list_instance: 110 | # 同上面一样, 分 {$xxx[hex_id]}, $xxx[hex_id](, $xxx[hex_id] 三种情况分别替换 111 | file_content = re.sub(slash + repr(instance)[1:-1] + r"{[\[\{]((?:0)|(?:0x[a-f0-9]+?))[\]\}]}", 112 | lambda x: var_list[int(x.group(1), 16)], 113 | file_content) 114 | file_content = re.sub(slash + repr(instance)[1:-1] + r"[\[\{]((?:0)|(?:0x[a-f0-9]+?))[\]\}]\(", 115 | lambda x: var_list[int(x.group(1), 16)] + "(", 116 | file_content) 117 | file_content = re.sub(slash + repr(instance)[1:-1] + r"[\[\{]((?:0)|(?:0x[a-f0-9]+?))[\]\}]", 118 | lambda x: singlequate + var_list[int(x.group(1), 16)] + singlequate, 119 | file_content) 120 | 121 | mnc = 0 122 | var_list_instance = {} 123 | 124 | # 所有函数内部的局部变量名都被混淆成了不可读的字符 125 | # 下面就把这些变量名用 $var_1,$var_2...之类的名字来替换 126 | 127 | def fix_var(repl): 128 | global var_list_instance 129 | global mnc 130 | if repl.group(1) not in var_list_instance: 131 | var_list_instance[repl.group(1)] = "$_var_" + str(mnc) 132 | mnc += 1 133 | return var_list_instance[repl.group(1)] 134 | 135 | file_content = re.sub(r"(\$(?:\\x[a-f0-9]{2})+)", fix_var, file_content) 136 | 137 | # 把代码里的所有十六进制数转为十进制形式 138 | file_content = re.sub(r'(0x[0-9a-f]+)', lambda x: str(int(x.group(1), 16)), file_content) 139 | 140 | # 把字符串形式的文件恢复成二进制数据 141 | file_content = eval(file_content) 142 | 143 | return file_content 144 | 145 | if __name__ == '__main__': 146 | # 添加命令行参数处理 147 | parser = argparse.ArgumentParser(description='mzphp2 deobfuscator tool') 148 | parser.add_argument('f', metavar="mzphp2 obfuscated file", help='mzphp2 obfuscated file') 149 | parser.add_argument('o', metavar="Output deobfuscated file", help='output deobfuscated file') 150 | args = parser.parse_args() 151 | if not os.path.isfile(args.f): 152 | raise FileNotFoundError 153 | result = parse_code(args.f) 154 | 155 | # 如果不指定输出文件,输出结果到控制台 156 | if args.o is not None: 157 | with open(args.o, 'wb') as of: 158 | of.write(result) 159 | else: 160 | print(result) --------------------------------------------------------------------------------