直接调用OPcache生成之some.php.bin中的函数

  • A+
所属分类:安全开发

创建: 2021-09-27 10:17
更新:
http://scz.617.cn:8/web/202109271017.txt

参看

《围观0CTF2018之ezDoor》
http://scz.617.cn:8/web/202109261107.txt

flag.php.bin中的encrypt()是个简单的异或算法,对称加密,decrypt()实际上完全同encrypt()。LyleMi、zsx手工分析encrypt()的Opcode,用PHP实现decrypt()。我是用反编译器得到encrypt()的PHP伪代码,删掉其结尾处对encode()的调用,以此实现decrypt()。

考虑一种更普遍的场景,encrypt()不是简单的异或算法,其内部实现很复杂,通过其他技术手段判断其可能是一种对称加密算法,加解密都可以用encrypt()完成。此时,手工分析encrypt()的Opcode异常艰辛,反编译器也不见得精准输出。怎么继续?

虽然不会PHP,也不搞WEB安全,但我干过二十多年的逆向工程啊,脑洞一直在线。前述场景在逆向工程领域不要太普遍,碰上时我会设法直接调用以二进制形式存在的encrypt(),并不逆向分析它,只关心它的in/out。既然encrypt()、decrypt()本质上一样,只要有办法调用encrypt(),就可以进行解密操作。当年hume和我就是这样调用Skype各种复杂算法函数的。

场景假设我们拿不到some.php,但能拿到OPcache生成之some.php.bin。问题暂时转换成,直接调用some.php.bin中的函数。

在7.0.33中用LyleMi提供的CTF_ezDoor.php生成CTF_ezDoor.php.bin,确保后者已在OPcache中就位。接下来为了逼真,做如下操作

rm CTF_ezDoor.php
touch CTF_ezDoor.php
ls -l CTF_ezDoor.php

确保CTF_ezDoor.php已经是个空文件,为空,但必须存在。检验CTF_ezDoor.php.bin可用

php70 
-d opcache.enable_cli=1 -d opcache.file_cache="/home/scz/src/opcache" 
-d opcache.validate_timestamps=0 -d opcache.file_cache_consistency_checks=0 
-f CTF_ezDoor.php

应该输出"Wrong Answer",表示在没有CTF_ezDoor.php内容的前提下,仍然执行了CTF_ezDoor.php.bin。

OPcache从文件缓存加载some.php.bin时有一些检查,参看

/*
 * php-7.0.33extopcachezend_file_cache.c
 */

zend_persistent_script *zend_file_cache_script_load(zend_file_handle *file_handle)
{
...
    /* verify header */
    if (memcmp(info.magic, "OPCACHE"8) != 0) {
...
        return NULL;
    }
    if (memcmp(info.system_id, ZCG(system_id), 32) != 0) {
...
        return NULL;
    }

    /* verify timestamp */
    if (ZCG(accel_directives).validate_timestamps &&
        zend_get_file_handle_timestamp(file_handle, NULL) != info.timestamp) {
...
        unlink(filename);
...
        return NULL;
    }
...
    /* verify checksum */
    if (ZCG(accel_directives).file_cache_consistency_checks &&
        zend_adler32(ADLER32_INIT, mem, info.mem_size + info.str_size) != info.checksum) {
...
        unlink(filename);
...
        return NULL;
    }
...
    return script;
}

执行CTF_ezDoor.php.bin时指定了两个OPcache参数

opcache.validate_timestamps=0
opcache.file_cache_consistency_checks=0

前者关闭时间戳检查,后者关闭校验和检查。关闭这两个检查后,可以Patch CTF_ezDoor.php.bin中的Opcode并使之生效,后面我会演示一下,暂且略过。

已经可以执行CTF_ezDoor.php.bin,当include_once("CTF_ezDoor.php")时实际生效的是CTF_ezDoor.php.bin,理论上就可以调用其中的encrypt()了。

但是,CTF_ezDoor.php有main(),main()结尾有exit(),只是include的话,没机会调其中的encrypt()就退出了。

LyleMi就exit()这事提到一个链接

How to override built-in PHP function(s)
https:
//stackoverflow.com/questions/15230883/how-to-override-built-in-php-functions

这我哪看得懂啊。rename_function/override_function好像要依赖别的啥,缺省没它们,namespace那招我也用不来。试过php.ini中"disable_functions =",对付不了exit()。试过uopz_allow_exit(false),也要依赖别的啥,缺省用不了。命苦,没心情为这事去装其他PHP组件,我就一过路的妖怪,犯得着费这劲嘛。最后用LyleMi提到的register_shutdown_function(),设法在exit()时执行指定代码。下列代码同时演示了利用析构函数在exit()时执行指定代码。

vi CTF_ezDoor_call_0.php

<?php

function decode ( $string )
{
    $ret = "";

    for ( $i = 0x0; $i < strlen( $string ); $i+=2 )
    {
        $ret .= chr( intval( $string[$i].$string[$i+1], 16 ) );
    }

    return $ret;
}

//
//////////////////////////////////////////////////////////////////////////
//

//
// https://www.php.net/manual/zh/function.exit.php
//
// 底下讨论了一些exit()时执行代码的技巧
//

function shutdown ()
{
    echo 'Shutdown: ' . __FUNCTION__ . '()' . PHP_EOL;

    printf
    (
        "[0] %sn",
        decode
        (
            encrypt
            (
                "this_is_a_very_secret_key",
                decode( "af8b20dc63d276caf90064976e4e6cabb5495f989ae6a24a0603cc2632ec95e603fa66348c" )
            )
        )
    );
}

class Foo
{
    public function __destruct ()
    
{
        echo 'Destruct: ' . __METHOD__ . '()' . PHP_EOL;

        printf
        (
            "[1] %sn",
            decode
            (
                encrypt
                (
                    "this_is_a_very_secret_key",
                    decode( "af8b20dc63d276caf90064976e4e6cabb5495f989ae6a24a0603cc2632ec95e603fa66348c" )
                )
            )
        );
    }
}

try
{
    register_shutdown_function( "shutdown" );
    $foo    = new Foo();
    include_once"CTF_ezDoor.php" );
}
catch ( Exception $e )
{
}

?>

decode()这个没办法,必须自己实现,CTF_ezDoor.php.bin中只有encode()。解码只是16进制表示转字符串,编码则是反过来。最重要的encrypt()/decrypt()不需要自己实现。

为了减少并不真正熟悉OPcache机制的读者的潜在困惑,执行CTF_ezDoor_call_0.php之前最好删一下潜在存在的CTF_ezDoor_call_0.php.bin。

rm /home/scz/src/opcache/888b1b2b3719b54e59f563400d7ce5f2/home/scz/src/php70/CTF_ezDoor_call*

php70 
-d opcache.enable_cli=1 -d opcache.file_cache="/home/scz/src/opcache" 
-d opcache.validate_timestamps=0 -d opcache.file_cache_consistency_checks=0 
-f CTF_ezDoor_call_0.php

应该看到输出

Wrong AnswerShutdown: shutdown()
[0] flag{0pc4che_b4ckd00r_is_4_g0o6_ide4}
Destruct: Foo::__destruct()
[1] flag{0pc4che_b4ckd00r_is_4_g0o6_ide4}

上例中,全局变量$foo的析构时间点比回调函数shutdown()要晚。

当encrypt()很复杂时,逆向分析有困难时,前面演示的技巧就会派上用场。演示环境是7.0.33,若是其他PHP版本,需将"af…8c"换成匹配值。

到这儿还没完。长期搞逆向工程的,对字节码有着挥之不去的迷恋。CTF_ezDoor.php有main(),能否Patch main(),让它直接return呢?这样include时干挠因素更少。答案是肯定的。

main()的第一条字节码是

[0] (27) = ASSIGN($flag,"input_your_flag_here")

ASSIGN的opcode是0x26(32),将之改成0x3e(62),这是RETURN的opcode

[0] (27) = RETURN($flag,"input_your_flag_here")

只改zend_op.opcode,无需同步修正op1、op2、result、handler等字段,PHP引擎有足够的容错能力。可以用010 Editor套着.bt模板改,找

struct zend_persistent_script persistent_script
  struct zend_op_array main_op_array
    struct zend_op opcodes[11]
      struct zend_op opcodes[0]
        uchar opcode

在我的7.0.33环境中

$ fc /b CTF_ezDoor.php.bin.orig CTF_ezDoor.php.bin
00000E7426 3E

改过后,相当于

main ()
{
    return;
}

此时include("CTF_ezDoor.php")只相当于导入一些库函数,原来的main()清空了。

vi CTF_ezDoor_call_1.php

<?php

function decode ( $string )
{
    $ret = "";

    for ( $i = 0x0; $i < strlen( $string ); $i+=2 )
    {
        $ret .= chr( intval( $string[$i].$string[$i+1], 16 ) );
    }

    return $ret;
}

//
//////////////////////////////////////////////////////////////////////////
//

include_once"CTF_ezDoor.php" );

printf
(
    "[2] %sn",
    decode
    (
        encrypt
        (
            "this_is_a_very_secret_key",
            decode( "af8b20dc63d276caf90064976e4e6cabb5495f989ae6a24a0603cc2632ec95e603fa66348c" )
        )
    )
);

?>
cp CTF_ezDoor.php.bin /home/scz/src/opcache/888b1b2b3719b54e59f563400d7ce5f2/home/scz/src/php70/CTF_ezDoor.php.bin

rm /home/scz/src/opcache/888b1b2b3719b54e59f563400d7ce5f2/home/scz/src/php70/CTF_ezDoor_call*

php70 
-d opcache.enable_cli=1 -d opcache.file_cache="/home/scz/src/opcache" 
-d opcache.validate_timestamps=0 -d opcache.file_cache_consistency_checks=0 
-f CTF_ezDoor_call_1.php

应该看到输出

PHP Notice:  Undefined variable: flag in /home/scz/src/php70/CTF_ezDoor.php on line 27
[2] flag{0pc4che_b4ckd00r_is_4_g0o6_ide4}

第一行提示是Patch main()带来的,不用理它,已成功调用CTF_ezDoor.php.bin中的encrypt()。

有人会说,上哪儿找这种理想环境去?这个不是说搞站,也不是说打CTF,而是告诉你,有这么个技术路线可用。至于能在何处用上?在沙坑边的单双杠上呗,这都不知道,傻缺!我给出那个Patch 7字节的Burp破解方案时,不也是类似脑洞的应用么。

对了,别跟我扯PHP,我是真不会,前面那些PHP写法大部分是临时放狗搜个片段抄一下。放狗,我在行。

相关推荐: Linux 黑话解释:什么是 sudo rm -rf?为什么如此危险? | Linux 中国

  导读:当你刚接触 Linux 时,你会经常遇到这样的建议:永远不要运行 sudo rm -rf /。在 Linux 世界里,更是围绕着 sudo rm -rf 有很多梗。 本文字数:2724,阅读时长大约:3分钟 https://linux.cn…

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: