PHP代码混淆与去混淆:基于AST操作的防御与逆向工程
大家好,今天我们来深入探讨PHP代码混淆与去混淆技术,重点关注基于抽象语法树(AST)的操作。代码混淆旨在增加代码的复杂性,使其难以理解和逆向工程,从而保护知识产权。而相应的,去混淆则是逆向混淆的过程,试图恢复原始代码的可读性和逻辑。我们将从混淆技术入手,分析其原理和实现,然后讨论相应的去混淆策略,并结合代码示例进行说明。
代码混淆技术及其原理
代码混淆并非加密,它不会阻止代码执行,而是通过各种变换使代码更难阅读和理解。常用的混淆技术包括:
-
变量名和函数名替换: 将有意义的变量名和函数名替换为无意义的短字符串或随机字符串,降低代码的可读性。
-
字符串加密/编码: 对字符串进行加密或编码,使其在静态分析时不可见。运行时再进行解密/解码。
-
控制流平坦化: 将代码块的控制流打乱,使其不再按照线性顺序执行,增加代码逻辑的复杂性。
-
不透明谓词插入: 插入始终为真或始终为假的条件判断,扰乱代码的逻辑结构。
-
垃圾代码插入: 插入对程序执行没有影响的无用代码,增加代码量和复杂度。
-
指令替换: 将简单的操作替换为复杂的等效操作,例如将
$a + $b替换为$a - (-$b)。 -
资源混淆: 对图片,配置文件等资源进行混淆处理,增加逆向难度。
这些混淆技术可以单独使用,也可以组合使用,以达到更好的混淆效果。接下来,我们将针对其中几种常用的技术,结合AST操作进行详细讲解。
基于AST的变量名和函数名替换
AST(Abstract Syntax Tree,抽象语法树)是源代码的树形表示,它反映了代码的语法结构。通过操作AST,我们可以精确地修改代码的各个部分,而无需进行字符串级别的替换,从而避免引入语法错误。
PHP的nikic/php-parser库提供了一个强大的AST解析和操作工具。我们可以使用它来解析PHP代码,遍历AST,并替换变量名和函数名。
示例代码:
<?php
require 'vendor/autoload.php';
use PhpParserParserFactory;
use PhpParserNodeTraverser;
use PhpParserNodeVisitorAbstract;
use PhpParserNode;
use PhpParserPrettyPrinter;
// 自定义NodeVisitor,用于替换变量名
class VariableNameReplacer extends NodeVisitorAbstract {
private $variableMap = [];
public function enterNode(Node $node) {
if ($node instanceof NodeExprVariable) {
$originalName = $node->name;
// 检查是否已经替换过
if (isset($this->variableMap[$originalName])) {
$node->name = $this->variableMap[$originalName];
return null;
}
// 生成新的变量名
$newName = 'var_' . md5($originalName);
$this->variableMap[$originalName] = $newName;
$node->name = $newName;
return null;
}
if ($node instanceof NodeExprAssign) {
// 处理赋值语句中的变量,确保左侧和右侧的变量名一致
if ($node->var instanceof NodeExprVariable) {
$originalName = $node->var->name;
if (isset($this->variableMap[$originalName])) {
$node->var->name = $this->variableMap[$originalName];
} else {
$newName = 'var_' . md5($originalName);
$this->variableMap[$originalName] = $newName;
$node->var->name = $newName;
}
}
}
return null;
}
}
// 自定义NodeVisitor,用于替换函数名
class FunctionNameReplacer extends NodeVisitorAbstract {
private $functionMap = [];
public function enterNode(Node $node) {
if ($node instanceof NodeStmtFunction_) {
$originalName = $node->name->name;
// 检查是否已经替换过
if (isset($this->functionMap[$originalName])) {
return null; // 避免重复替换函数定义
}
// 生成新的函数名
$newName = 'func_' . md5($originalName);
$this->functionMap[$originalName] = $newName;
$node->name = new NodeIdentifier($newName);
return null;
}
if ($node instanceof NodeExprFuncCall) {
if ($node->name instanceof NodeName) {
$originalName = $node->name->toString();
if (isset($this->functionMap[$originalName])) {
$node->name = new NodeName($this->functionMap[$originalName]);
}
}
return null;
}
return null;
}
}
// 待混淆的PHP代码
$code = <<<'PHP'
<?php
function calculateSum($a, $b) {
$sum = $a + $b;
return $sum;
}
$x = 10;
$y = 20;
$result = calculateSum($x, $y);
echo "Result: " . $result;
?>
PHP;
// 解析PHP代码
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
// 创建NodeTraverser并添加NodeVisitor
$traverser = new NodeTraverser;
$traverser->addVisitor(new VariableNameReplacer);
$traverser->addVisitor(new FunctionNameReplacer);
// 遍历AST并进行替换
$ast = $traverser->traverse($ast);
// 将AST转换回PHP代码
$prettyPrinter = new PrettyPrinterStandard;
$obfuscatedCode = $prettyPrinter->prettyPrintFile($ast);
// 输出混淆后的代码
echo $obfuscatedCode;
?>
代码解释:
- 首先,我们使用
nikic/php-parser库解析PHP代码,将其转换为AST。 - 然后,我们创建两个自定义的
NodeVisitor,分别用于替换变量名和函数名。 VariableNameReplacer类遍历AST,找到NodeExprVariable类型的节点,将其name属性替换为新的变量名。 为了避免重复替换,使用$variableMap存储已经替换过的变量名。 同时处理NodeExprAssign节点,确保赋值语句中变量名一致。FunctionNameReplacer类遍历AST,找到NodeStmtFunction_类型的节点,将其name属性替换为新的函数名。同时,也处理NodeExprFuncCall,替换函数调用中的函数名。为了避免重复替换函数定义,使用$functionMap存储已经替换过的函数名。- 最后,我们使用
PrettyPrinterStandard类将AST转换回PHP代码,并输出混淆后的代码。
混淆效果:
原始代码:
<?php
function calculateSum($a, $b) {
$sum = $a + $b;
return $sum;
}
$x = 10;
$y = 20;
$result = calculateSum($x, $y);
echo "Result: " . $result;
?>
混淆后的代码:
<?php
function func_79132c5a9a68460b50a19c41c389c3f3(var_356a192b7913b04c54574d18c28d46e6, var_da4b9237bacccdf19c0760cab7aec4a8)
{
$var_a947e47047c1c0ad4e4216f3b1727495 = var_356a192b7913b04c54574d18c28d46e6 + var_da4b9237bacccdf19c0760cab7aec4a8;
return $var_a947e47047c1c0ad4e4216f3b1727495;
}
$var_6f39f657d9456ead1c47b8f9689a3290 = 10;
$var_d72aa81509f410a0b27c454469c13a56 = 20;
$var_65a555a6e258c61094e6d72a526d1c76 = func_79132c5a9a68460b50a19c41c389c3f3(var_6f39f657d9456ead1c47b8f9689a3290, var_d72aa81509f410a0b27c454469c13a56);
echo "Result: " . $var_65a555a6e258c61094e6d72a526d1c76;
可以看到,变量名和函数名都被替换成了无意义的字符串,降低了代码的可读性。
字符串加密/编码
字符串加密/编码是一种常用的混淆技术,它可以隐藏代码中的敏感信息,例如数据库密码、API密钥等。
示例代码:
<?php
require 'vendor/autoload.php';
use PhpParserParserFactory;
use PhpParserNodeTraverser;
use PhpParserNodeVisitorAbstract;
use PhpParserNode;
use PhpParserPrettyPrinter;
// 自定义NodeVisitor,用于加密字符串
class StringEncrypter extends NodeVisitorAbstract {
private $encryptionKey = 'my_secret_key';
public function enterNode(Node $node) {
if ($node instanceof NodeScalarString_) {
$originalString = $node->value;
$encryptedString = $this->encrypt($originalString, $this->encryptionKey);
// 将加密后的字符串替换为一个函数调用,该函数负责解密字符串
$node->value = 'ENCRYPTED_STRING_' . md5($originalString); // 使用唯一标识符,便于解密
return new NodeExprFuncCall(
new NodeName('decryptString'), // 假设存在一个名为 decryptString 的函数
[
new NodeArg(new NodeScalarString_($encryptedString)),
new NodeArg(new NodeScalarString_($this->encryptionKey))
]
);
}
return null;
}
private function encrypt($string, $key) {
$iv = openssl_random_pseudo_bytes(openssl_cipher_iv_length('aes-256-cbc'));
$encrypted = openssl_encrypt($string, 'aes-256-cbc', $key, 0, $iv);
return base64_encode($iv . $encrypted); // 使用 base64 编码,方便存储
}
}
// 待混淆的PHP代码
$code = <<<'PHP'
<?php
$username = "admin";
$password = "password123";
echo "Username: " . $username . "n";
echo "Password: " . $password . "n";
?>
PHP;
// 解析PHP代码
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
// 创建NodeTraverser并添加NodeVisitor
$traverser = new NodeTraverser;
$traverser->addVisitor(new StringEncrypter);
// 遍历AST并进行替换
$ast = $traverser->traverse($ast);
// 将AST转换回PHP代码
$prettyPrinter = new PrettyPrinterStandard;
$obfuscatedCode = $prettyPrinter->prettyPrintFile($ast);
// 输出混淆后的代码
echo $obfuscatedCode;
// 解密函数 (需要包含在混淆后的代码中,或者通过其他方式引入)
function decryptString($data, $key) {
$data = base64_decode($data);
$ivlen = openssl_cipher_iv_length('aes-256-cbc');
$iv = substr($data, 0, $ivlen);
$ciphertext = substr($data, $ivlen);
return openssl_decrypt($ciphertext, 'aes-256-cbc', $key, 0, $iv);
}
?>
代码解释:
- 我们创建了一个名为
StringEncrypter的NodeVisitor,它遍历AST,找到NodeScalarString_类型的节点,即字符串字面量。 - 对于每个字符串,我们使用
openssl_encrypt函数对其进行加密,并使用base64_encode函数对其进行编码,以便存储。 - 我们将加密后的字符串替换为一个函数调用,该函数负责解密字符串。在这个例子中,我们假设存在一个名为
decryptString的函数,它接受加密后的字符串和密钥作为参数,并返回解密后的字符串。 decryptString函数使用openssl_decrypt解密字符串,并返回原始字符串。
混淆效果:
原始代码:
<?php
$username = "admin";
$password = "password123";
echo "Username: " . $username . "n";
echo "Password: " . $password . "n";
?>
混淆后的代码:
<?php
$username = decryptString("...", "my_secret_key");
$password = decryptString("...", "my_secret_key");
echo "Username: " . $username . "n";
echo "Password: " . $password . "n";
function decryptString($data, $key) {
$data = base64_decode($data);
$ivlen = openssl_cipher_iv_length('aes-256-cbc');
$iv = substr($data, 0, $ivlen);
$ciphertext = substr($data, $ivlen);
return openssl_decrypt($ciphertext, 'aes-256-cbc', $key, 0, $iv);
}
可以看到,原始字符串被加密并替换为decryptString函数的调用,从而隐藏了敏感信息。
控制流平坦化
控制流平坦化是一种高级的混淆技术,它可以将代码块的控制流打乱,使其不再按照线性顺序执行,从而增加代码逻辑的复杂性。其核心思想是将所有的代码块放入一个大的switch语句中,并通过一个状态变量来控制代码块的执行顺序。
示例代码:
<?php
require 'vendor/autoload.php';
use PhpParserParserFactory;
use PhpParserNodeTraverser;
use PhpParserNodeVisitorAbstract;
use PhpParserNode;
use PhpParserPrettyPrinter;
class ControlFlowFlattener extends NodeVisitorAbstract {
private $stateVariable = 'state';
private $stateValue = 0;
private $cases = [];
private $defaultCase = null;
private $statements = [];
public function enterNode(Node $node) {
if ($node instanceof NodeStmtFunction_ || $node instanceof NodeStmtClassMethod) {
$this->statements = $node->getStmts(); // 获取函数或方法的语句块
$this->cases = [];
$this->stateValue = 0;
$this->defaultCase = null;
if (empty($this->statements)) {
return null; // 如果函数/方法为空,则不进行处理
}
// 创建 switch 语句的 case
foreach ($this->statements as $statement) {
$this->cases[] = new NodeStmtCase_(
new NodeScalarLNumber($this->stateValue++),
[$statement, new NodeStmtBreak_()]
);
}
// 添加 default case,使其回到第一个 case
$this->defaultCase = new NodeStmtCase_(null, [
new NodeExprAssign(
new NodeExprVariable($this->stateVariable),
new NodeScalarLNumber(0) // 设置 state 为初始值
),
new NodeStmtBreak_()
]);
$this->cases[] = $this->defaultCase;
// 创建 switch 语句
$switchStmt = new NodeStmtSwitch_(
new NodeExprVariable($this->stateVariable),
$this->cases
);
// 初始化 state 变量
$initState = new NodeExprAssign(
new NodeExprVariable($this->stateVariable),
new NodeScalarLNumber(0)
);
// 将新的语句块设置回函数或方法
$node->stmts = [$initState, $switchStmt];
}
return null;
}
}
// 待混淆的PHP代码
$code = <<<'PHP'
<?php
function calculateArea($width, $height) {
$area = $width * $height;
echo "Area: " . $area . "n";
if ($area > 100) {
echo "Area is large.n";
} else {
echo "Area is small.n";
}
}
calculateArea(10, 15);
?>
PHP;
// 解析PHP代码
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
// 创建NodeTraverser并添加NodeVisitor
$traverser = new NodeTraverser;
$traverser->addVisitor(new ControlFlowFlattener);
// 遍历AST并进行替换
$ast = $traverser->traverse($ast);
// 将AST转换回PHP代码
$prettyPrinter = new PrettyPrinterStandard;
$obfuscatedCode = $prettyPrinter->prettyPrintFile($ast);
// 输出混淆后的代码
echo $obfuscatedCode;
?>
代码解释:
- 我们创建了一个名为
ControlFlowFlattener的NodeVisitor,它遍历AST,找到NodeStmtFunction_类型的节点,即函数定义。 - 对于每个函数,我们将函数体内的所有语句提取出来,并将其放入一个大的
switch语句中。 switch语句使用一个状态变量$state来控制代码块的执行顺序。每个case对应一个原始的语句,并且在执行完该语句后,会使用break语句跳出switch语句。- 我们添加一个
defaultcase,使其回到第一个 case,从而实现循环执行。 - 最后,我们在函数体的开头添加一个初始化
$state变量的语句。
混淆效果:
原始代码:
<?php
function calculateArea($width, $height) {
$area = $width * $height;
echo "Area: " . $area . "n";
if ($area > 100) {
echo "Area is large.n";
} else {
echo "Area is small.n";
}
}
calculateArea(10, 15);
?>
混淆后的代码:
<?php
function calculateArea($width, $height)
{
$state = 0;
switch ($state) {
case 0:
$area = $width * $height;
break;
case 1:
echo "Area: " . $area . "n";
break;
case 2:
if ($area > 100) {
echo "Area is large.n";
} else {
echo "Area is small.n";
}
break;
case 3:
$state = 0;
break;
}
}
calculateArea(10, 15);
可以看到,原始的函数体被转换成了一个大的switch语句,代码的执行顺序不再是线性的,从而增加了代码逻辑的复杂性。请注意,这个例子简化了控制流平坦化的实现,实际应用中会更复杂,包括随机化 case 的顺序,增加不透明谓词,以及使用更复杂的控制流转移方式。
代码去混淆技术及其原理
代码去混淆是逆向混淆的过程,其目的是恢复原始代码的可读性和逻辑。去混淆技术通常包括:
- 静态分析: 分析代码的结构和逻辑,识别混淆模式。
- 动态分析: 运行代码,观察其行为,收集运行时信息。
- 符号执行: 使用符号值代替具体值,模拟代码的执行,推导代码的逻辑。
- 模式匹配: 识别已知的混淆模式,并应用相应的去混淆规则。
- 反编译: 将代码反编译成更高级的中间表示,例如伪代码,从而更容易理解代码的逻辑。
针对前面介绍的混淆技术,我们可以采取相应的去混淆策略。
变量名和函数名还原
对于变量名和函数名替换,我们可以通过以下方法进行还原:
- 代码分析: 观察变量和函数的使用情况,尝试推断其含义。
- 重命名: 根据变量和函数的含义,手动将其重命名为更有意义的名称。
- 自动化工具: 开发自动化工具,根据变量和函数的使用模式,自动推断其含义,并进行重命名。
例如,如果一个变量被频繁地用于存储数组的长度,那么我们可以将其重命名为arrayLength。
字符串解密/解码
对于字符串加密/编码,我们需要找到加密/编码算法和密钥,然后使用相应的解密/解码算法还原原始字符串。
- 查找解密函数: 在代码中查找解密函数,例如
decryptString。 - 分析解密函数: 分析解密函数的实现,了解其使用的加密算法和密钥。
- 提取密钥: 从代码中提取密钥。密钥可能被硬编码在代码中,也可能从配置文件或其他地方读取。
- 执行解密: 使用提取到的密钥和解密算法,解密加密后的字符串。
可以使用静态分析找到解密函数,然后使用动态分析或符号执行来提取密钥。 例如,如果解密函数使用了硬编码的密钥,那么我们可以直接从代码中提取密钥。
控制流解平坦化
控制流解平坦化是去混淆中最具挑战性的任务之一。我们需要恢复代码的原始控制流结构。
- 识别状态变量: 识别控制
switch语句的状态变量,例如$state。 - 构建控制流图: 根据
switch语句中的case,构建代码的控制流图。每个case对应图中的一个节点,break语句对应图中的一条边。 - 简化控制流图: 简化控制流图,例如删除不透明谓词,合并线性执行的节点。
- 重建代码结构: 根据简化后的控制流图,重建代码的原始控制流结构,例如将
switch语句转换回if-else语句或循环语句。
这个过程通常需要手动分析和干预,并且需要对代码的逻辑有深入的理解。自动化工具可以辅助完成一些重复性的任务,例如构建控制流图和简化控制流图。
基于AST的去混淆
与混淆类似,我们也可以使用AST操作来进行去混淆。例如,我们可以编写一个NodeVisitor来自动重命名变量和函数,或者解密加密后的字符串。
示例:基于AST的变量名还原
假设我们已经分析了混淆后的代码,并确定了变量的含义。我们可以编写一个NodeVisitor来自动重命名变量。
<?php
require 'vendor/autoload.php';
use PhpParserParserFactory;
use PhpParserNodeTraverser;
use PhpParserNodeVisitorAbstract;
use PhpParserNode;
use PhpParserPrettyPrinter;
// 自定义NodeVisitor,用于还原变量名
class VariableNameRestorer extends NodeVisitorAbstract {
private $variableMap = [
'var_6f39f657d9456ead1c47b8f9689a3290' => 'x',
'var_d72aa81509f410a0b27c454469c13a56' => 'y',
'var_65a555a6e258c61094e6d72a526d1c76' => 'result',
'var_356a192b7913b04c54574d18c28d46e6' => 'a',
'var_da4b9237bacccdf19c0760cab7aec4a8' => 'b',
'var_a947e47047c1c0ad4e4216f3b1727495' => 'sum'
];
public function enterNode(Node $node) {
if ($node instanceof NodeExprVariable) {
$originalName = $node->name;
if (isset($this->variableMap[$originalName])) {
$node->name = $this->variableMap[$originalName];
}
}
if ($node instanceof NodeExprAssign) {
// 处理赋值语句中的变量,确保左侧和右侧的变量名一致
if ($node->var instanceof NodeExprVariable) {
$originalName = $node->var->name;
if (isset($this->variableMap[$originalName])) {
$node->var->name = $this->variableMap[$originalName];
}
}
}
return null;
}
}
// 混淆后的PHP代码
$code = <<<'PHP'
<?php
function func_79132c5a9a68460b50a19c41c389c3f3(var_356a192b7913b04c54574d18c28d46e6, var_da4b9237bacccdf19c0760cab7aec4a8)
{
$var_a947e47047c1c0ad4e4216f3b1727495 = var_356a192b7913b04c54574d18c28d46e6 + var_da4b9237bacccdf19c0760cab7aec4a8;
return $var_a947e47047c1c0ad4e4216f3b1727495;
}
$var_6f39f657d9456ead1c47b8f9689a3290 = 10;
$var_d72aa81509f410a0b27c454469c13a56 = 20;
$var_65a555a6e258c61094e6d72a526d1c76 = func_79132c5a9a68460b50a19c41c389c3f3(var_6f39f657d9456ead1c47b8f9689a3290, var_d72aa81509f410a0b27c454469c13a56);
echo "Result: " . $var_65a555a6e258c61094e6d72a526d1c76;
?>
PHP;
// 解析PHP代码
$parser = (new ParserFactory)->create(ParserFactory::PREFER_PHP7);
$ast = $parser->parse($code);
// 创建NodeTraverser并添加NodeVisitor
$traverser = new NodeTraverser;
$traverser->addVisitor(new VariableNameRestorer);
// 遍历AST并进行替换
$ast = $traverser->traverse($ast);
// 将AST转换回PHP代码
$prettyPrinter = new PrettyPrinterStandard;
$deobfuscatedCode = $prettyPrinter->prettyPrintFile($ast);
// 输出去混淆后的代码
echo $deobfuscatedCode;
?>
在这个例子中,我们使用一个$variableMap来存储混淆后的变量名和原始变量名之间的映射关系。VariableNameRestorer类遍历AST,找到NodeExprVariable类型的节点,并根据$variableMap将其name属性替换为原始变量名。
去混淆效果:
混淆后的代码:
<?php
function func_79132c5a9a68460b50a19c41c389c3f3(var_356a192b7913b04c54574d18c28d46e6, var_da4b9237bacccdf19c0760cab7aec4a8)
{
$var_a947e47047c1c0ad4e4216f3b1727495 = var_356a192b7913b04c54574d18c28d46e6 + var_da4b9237bacccdf19c0760cab7aec4a8;
return $var_a947e47047c1c0ad4e4216f3b1727495;
}
$var_6f39f657d9456ead1c47b8f9689a3290 = 10;
$var_d72aa81509f410a0b27c454469c13a56 = 20;
$var_65a555a6e258c61094e6d72a526d1c76 = func_79132c5a9a68460b50a19c41c389c3f3(var_6f39f657d9456ead1c47b8f9689a3290, var_d72aa81509f410a0b27c454469c13a56);
echo "Result: " . $var_65a555a6e258c61094e6d72a526d1c76;
?>
去混淆后的代码:
<?php
function func_79132c5a9a68460b50a19c41c389c3f3(a, b)
{
$sum = a + b;
return $sum;
}
$x = 10;
$y = 20;
$result = func_79132c5a9a68460b50a19c41c389c3f3(x, y);
echo "Result: " . $result;
?>
可以看到,变量名被还原成了原始的名称,提高了代码的可读性。
案例分析:一个简单的混淆与去混淆的流程
假设我们有以下代码:
<?php
$apiKey = "YOUR_API_KEY";
function fetchData($url) {
// ...
return $data;
}
$data = fetchData("https://api.example.com?key=" . $apiKey);
echo "Data: " . $data;
?>
-
混淆:
- 替换变量名:
$apiKey->$a,$data->$b - 字符串加密:
YOUR_API_KEY使用base64编码eW91cl9hcGlfa2V5,https://api.example.com?key=也进行base64 编码aHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20/a2V5PQ==
混淆后的代码:
<?php $a = base64_decode("eW91cl9hcGlfa2V5"); function fetchData($url) { // ... return $data; } $b = fetchData(base64_decode("aHR0cHM6Ly9hcGkuZXhhbXBsZS5jb20/a2V5PQ==") . $a); echo "Data: " . $b; ?> - 替换变量名:
-
去混淆:
- 变量名还原:
$a->$apiKey,$b->$data - 字符串解密:使用base64解码还原字符串。
- 变量名还原:
还原后的代码:
<?php
$apiKey = "YOUR_API_KEY";
function fetchData($url) {
// ...
return $data;
}
$data = fetchData("https://api.example.com?key=" . $apiKey);
echo "Data: " . $data;
?>
PHP代码混淆与去混淆是一个持续对抗的过程
PHP代码混淆与去混淆是一个持续对抗的过程。混淆技术的不断发展推动着去混淆技术的发展,而去混淆技术的进步又反过来促进了混淆技术的改进。我们需要不断学习和探索新的混淆和去混淆技术,才能更好地保护和分析PHP代码。AST是实现PHP