标题: 直接调用OPcache生成之some.php.bin中的函数 创建: 2021-09-27 10:17 更新: 链接: https://scz.617.cn/web/202109271017.txt 参看 《围观0CTF2018之ezDoor》 https://scz.617.cn/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.33\ext\opcache\zend_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 -------------------------------------------------------------------------- -------------------------------------------------------------------------- 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 00000E74: 26 3E 改过后,相当于 main () { return; } 此时include("CTF_ezDoor.php")只相当于导入一些库函数,原来的main()清空了。 $ vi CTF_ezDoor_call_1.php -------------------------------------------------------------------------- -------------------------------------------------------------------------- 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写法大部分是临时放狗搜个片段抄一 下。放狗,我在行。